Python에서도 Lambda function이 존재합니다. 이번에는 람다 함수에 관한 글을 번역해서 포스팅하도록 하겠습니다.
원문은 Python Information and Examples의 Lambda functions 입니다. (시간 나는대로 다음 주제인 Dynamic typing, Structured typing, List comprehensions, Block indentation에 대해서 다루도록 하겠습니다.)
Python은 "람다"라는 구조체를 사용하여 anonymous한 함수 (이름에 구속되지 않는)들을 실행시간에서 생성할 수 있다. 이는 함수 프로그래밍에서 사용되는 람다와 정확히 같지는 않다. 하지만 이 컨셉은 아주 강력하며 Python에 잘 적용되었다. 종종 filter(), map(), reduce()와 같은 전통적인 함수의 컨셉과 결합하여 사용되어진다.
다음 코드는 일반적인 함수 정의 ("f")와 람다 함수 ("g")의 차이점을 보여준다.
>>> def f (x): return x**2 ... >>> print f(8) 64 >>> >>> g = lambda x: x**2 >>> >>> print g(8) 64
위와 같이 f()와 g()는 정확히 같은 역할을 하며 같은 방식으로 사용되어 진다. 여기서 람다의 정의법은 return을 포함하고 있지 않으며 항상 반환되는 표현을 포함하고 있다. 또한 우리는 함수가 필요되는 곳에서는 변수를 지정할 필요 없는 람다 정의를 사용할 수 있다.
다음의 코드는 람다 함수의 이용에 대한 예이다. 여기서 python은 nested scopes를 지원하기 위하여 2.2 이후의 버전이어야 한다.
>>> def make_incrementor (n): return lambda x: x + n >>> >>> f = make_incrementor(2) >>> g = make_incrementor(6) >>> >>> print f(42), g(42) 44 48 >>> >>> print make_incrementor(22)(33) 55
위의 코드는 anonymous한 함수를 생성하고 반환하는 "make_incrementor" 함수를 정의하고 있다. 반환되는 함수는 입력되는 argument를 생성될 때 구체화 시킨 값만큼 증가시켜준다.
이제 우리는 수많은 다른 종류의 증가 함수를 만들고 그것에 변수를 할당할 수 있게되었다. 또한 그들은 독립적으로 사용할 수 있다. 마지막 코드가 나타내는 것은 우리는 함수를 어디에도 할당하지 않아도 된다는 것이다. 다른 곳에 사용되지 않는다면 그냥 즉흥적으로 사용하고 잊어버릴 수 있는 것이다.
이제 한 걸음 더 나아가 보자
>>> foo = [2, 18, 9, 22, 17, 24, 8, 12, 27] >>> >>> print filter(lambda x: x % 3 == 0, foo) [18, 9, 24, 12, 27] >>> >>> print map(lambda x: x * 2 + 10, foo) [14, 46, 28, 54, 44, 58, 26, 34, 64] >>> >>> print reduce(lambda x, y: x + y, foo) 139
먼저 우리는 integer 값을 가진 간단한 리스트를 정의하였다. 그리고 표준 함수인 filter(), map(), reduce()에게 리스트에 관련된 다양한 일을 하도록 하였다. 세 가지 함수 모두 함수와 리스트를 입력으로 사용한다.
물론 우리는 다른 어떤 곳에 분리된 함수를 filter()라고 정의할 수도 있다. 또한 우리가 이 함수를 여러번 이용하거나 함수가 한 줄에 작성하기 너무 복잡하다면 이는 사실 좋은 생각이다. 하지만 만약 우리가 이 함수를 단 한번만 사용하고 한 줄로 작성할 수 있다면 (위와 같이) 람다 구조체를 이용하여 (임시의) anonymous 함수를 만들어 filter()로 즉시 보내는 것이 편리하다. 이는 아주 간결하고 이해하기 쉬운 코드이다.
첫 번째 예제는 filter()가 리스트의 모든 원소들에서 람다 함수를 불러 함수가 True를 반환한 원소들로 이루어진 새로운 리스트를 반환한다. 3으로 나눠 나머지가 0이 되는 3의 배수들이 리스트로 반환되었다.
두 번째 예제는 map()이 리스트를 변환시킨다. 모든 원소에 2을 곱하고 10을 더해서 새로운 리스트를 반환한다.
마지막 reduce()는 다소 특별하다. 이 "worker function"은 두 개의 arguments (x, y)를 받아야한다. 함수는 리스트의 첫 번째 두 원소를 가지고 불러지며, 그 결과 값과 세번 째 값이 이용된다. 계속 끝까지 나가 더 이상 부를 원소가 없을 때 끝나며 총 함수의 호출 횟수는 n 개의 원소로 구성된 리스트에서 n-1 번이다. 반환 값은 마지막 연산의 결과이며 위의 예는 단순히 arguments를 더한 결과이다. (내장 함수인 sum()이 같은 역할은 하면서 더 효율적이다.)
다음은 python에서 prime number(소수) 을 구하는 방법이다. (가장 효율적인 코드는 아니지만..)
[2부터 49까지의 숫자를 대상으로 하여 range(2, 8)을 한 것으로 생각된다.]
>>> nums = range(2, 50) >>> for i in range(2, 8): ... nums = filter(lambda x: x == i or x % i, nums) ... >>> print nums [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47]
어떤 원리일까? 먼저 2부터 49까지의 모든 숫자를 nums 리스트에 넣는다. 다음 for 루프가 모든 가능한 나누기를 반복한다. 다시 말해 i는 2부터 7까지의 값을 갖는다. 자연적으로 약수들의 배수인 모든 숫자들은 소수가 되지 못한다. 그래서 우리는 이것들을 리스트에서 제거하기 위하여 filter 함수를 사용하였다. (이 알고리즘을 "the sieve of Eratosthenes"라고 한다.)
위의 경우에는 filter 함수가 단순히 "원소가 i와 같다면 리스트에 남겨라" 혹은, "i로 나누어 0이 아닌 값을 나머지로 갖게 되면 리스트에 남겨라 (true인 것들)", "위 두 경우가 아니라면 리스트에서 제거해라."라고 말하고 있다. 필터링 루프가 종료되면 소수들만 리스트에 남겨져있을 것이다. 아마 같은 것을 간결하게 내장 특징이나 readable로 작성할 수도 있을지 모른다.
다음 예제는 문장이 단어로 나누어져 있는 리스트이며 이 리스트는 각 단어의 길이를 가지는 리스트를 만든다.
>>> sentence = 'It is raining cats and dogs' >>> words = sentence.split() >>> print words ['It', 'is', 'raining', 'cats', 'and', 'dogs'] >>> >>> lengths = map(lambda word: len(word), words) >>> print lengths [2, 2, 7, 4, 3, 4]
내 생각엔 더 이상의 설명은 필요 없을 듯하다. 코드가 자신을 설명중이니까
물론 이는 하나의 statement로 작성될 수 있다. 하지만 이는 이해하기 쉽지 않다 (많이는 아니더라도).
>>> print map(lambda w: len(w), 'It is raining cats and dogs'.split()) [2, 2, 7, 4, 3, 4]
이번 예제는 UNIX 스크립트에서 파일 시스템의 모든 mount 위치를 찾고 싶을 때의 경우다. 우리는 이를 위해 외부의 "mount" 명령을 실행하여 결과를 파싱할 것이다.
>>> import commands >>> >>> mount = commands.getoutput('mount -v') >>> lines = mount.splitlines() >>> points = map(lambda line: line.split()[2], lines) >>> >>> print points ['/', '/var', '/usr', '/usr/local', '/tmp', '/proc']
파이썬 기본 라이브러리의 커맨드 모듈의 getoutput 함수는 주어진 명령어를 실행시키고 결과를 하나의 스트링으로 리턴해준다. 그렇기에 우리는 리턴 값을 각각의 줄을 나누고 각 줄에서 단어로 나누고 "map"을 이용하여 3번째 값을 반환하는 람다 함수를 사용하는 것이다. 이 값은 시스템에서 마운트되는 mountpoint들이다.
또한 여기서도 하나의 statement로 작성이 가능하지만 높은 간결성은 가독성을 감소시킨다.
print map(lambda x: x.split()[2], commands.getoutput('mount -v').splitlines()) ['/', '/var', '/usr', '/usr/local', '/tmp', '/proc']
"real-world"의 스크립트를 작성할 때에는 복잡한 statement를 쪼개서 알아보기 쉽게 하는 것이 추천된다. 또한 이는 보수하는 것도 더 쉽다.
그러나 커맨드의 결과를 줄들의 리스트로 나누는 것은 아주 흔한 일이다. 우리는 외부의 커맨드의 결과를 파싱할 때 항상 사용하기 때문이다. 그러므로 getoutput 라인에 나누는 작업을 포함하는 것은 흔한 방법이다. 다음은 나머지 부분은 분리하여 간결성과 가독성을 잘 맞춘 사례이다.
>>> lines = commands.getoutput('mount -v').splitlines() >>> >>> points = map(lambda line: line.split()[2], lines) >>> print points ['/', '/var', '/usr', '/usr/local', '/tmp', '/proc']
아마 보다 나은 방법은 이 과정을 encapsulate(캡슐화) 시켜서 결과를 반환하는 작은 함수를 이용하는 것이다.
우리는 list comprehension라고 불리는 것을 이용하여 다른 리스트들로부터 리스트들을 구현할 수 있다. 가끔씩 효율이 좋으면서 가독성도 좋아 잘 이용된다. 위의 예는 다음과 같이 list comprehension으로 잘 구현될 수 있다.
>>> lines = commands.getoutput('mount -v').splitlines() >>> >>> points = [line.split()[2] for line in lines] >>> print points ['/', '/var', '/usr', '/usr/local', '/tmp', '/proc']
대부분 우리는 list comprehensions를 map() 혹은 filter() 대신에 사용할 수 있다. 우리의 상황에 따라 어떤 것을 사용할 것인지 결정된다.
Note: Python 2.x 버전에는 존재하지만 최근의 Python에서는 commands 모듈이 소멸되고 subprocess 모듈로 대체되었다. 따라서 Python 2.4 이후의 버전에서는 commands.getoutput()함수는 subprocess.che_output() 함수로 대체되어야 한다.