함수형 프로그래밍 HOWTO
함수형 프로그래밍 HOWTO
절차적, 객체지향, 그리고 함수형. 프로그램을 나누는 방식은 여러 가지인데, 이 HOWTO에서는 파이썬을 함수형 스타일로 작성하는 데 적합한 기능들을 둘러봅니다. 함수형 프로그래밍의 개념을 먼저 가볍게 짚은 뒤, 이터레이터와 제너레이터 같은 언어 기능, 그리고 itertools와 functools 같은 관련 라이브러리 모듈을 차례대로 살펴볼게요.
소개 (Introduction)
프로그래밍 언어는 문제를 여러 가지 방식으로 분해하는 것을 지원합니다.
- 대부분의 프로그래밍 언어는 **절차적(procedural)**입니다. 프로그램은 프로그램의 입력으로 컴퓨터가 무엇을 할지 알려주는 명령들의 목록이죠. C, Pascal, 심지어 Unix 셸도 절차적 언어입니다.
- 선언적(declarative) 언어에서는 풀고자 하는 문제를 기술하는 명세를 작성하면, 언어 구현이 계산을 효율적으로 수행하는 방법을 알아서 찾아냅니다. SQL이 가장 친숙한 선언적 언어일 텐데요. SQL 쿼리는 당신이 검색하려는 데이터셋을 기술하고, SQL 엔진이 테이블을 스캔할지 인덱스를 쓸지, 어떤 하위절을 먼저 수행할지 등을 결정합니다.
- 객체지향(object-oriented) 프로그램은 객체의 모음(collection)을 조작합니다. 객체는 내부 상태를 가지며, 어떤 방식으로든 이 내부 상태를 조회하거나 수정하는 메서드를 지원해요. Smalltalk와 Java는 객체지향 언어입니다. C++와 파이썬은 객체지향 프로그래밍을 지원하지만 객체지향 기능의 사용을 강제하지는 않아요.
- 함수형(functional) 프로그래밍은 문제를 함수들의 집합으로 분해합니다. 이상적으로 함수는 입력을 받아 출력을 만들기만 하고, 주어진 입력에 대해 만들어지는 출력에 영향을 주는 내부 상태가 없어야 해요. 잘 알려진 함수형 언어로는 ML 계열(Standard ML, OCaml, 그리고 그 변형들)과 Haskell이 있습니다.
어떤 컴퓨터 언어의 설계자들은 프로그래밍의 특정 접근 방식을 강조하도록 선택합니다. 이 때문에 다른 접근 방식을 쓰는 프로그램을 작성하기가 어려워지기도 해요. 반면 다른 언어들은 여러 접근 방식을 지원하는 다중 패러다임(multi-paradigm) 언어입니다. Lisp, C++, 파이썬은 다중 패러다임이라서, 이들 모두에서 대체로 절차적·객체지향적·함수적인 프로그램이나 라이브러리를 작성할 수 있습니다. 큰 프로그램에서는 구역마다 다른 접근 방식을 써서 작성할 수 있어요. 예를 들어 GUI는 객체지향으로, 처리 로직은 절차적이거나 함수형으로 쓸 수 있죠.
함수형 프로그램에서는 입력이 함수들의 집합을 통해 흐릅니다. 각 함수는 자신의 입력에 대해 동작해서 어떤 출력을 만들어 냅니다. 함수형 스타일은 내부 상태를 수정하거나 함수의 반환 값에 보이지 않는 다른 변경을 만드는 부수 효과(side effect)가 있는 함수를 부적절하게 여깁니다. 부수 효과가 전혀 없는 함수를 **순수 함수형(purely functional)**이라고 불러요. 부수 효과를 피한다는 것은 프로그램이 실행되면서 갱신되는 데이터 구조를 쓰지 않는 것을 뜻합니다. 즉 모든 함수의 출력은 오직 자신의 입력에만 의존해야 해요.
일부 언어는 순수성에 대해 아주 엄격해서 a=3이나 c = a + b 같은 대입문조차 없지만, 화면에 출력하거나 디스크 파일에 쓰는 것 같은 모든 부수 효과를 피하는 것은 어렵습니다. 또 다른 예로는 print()나 time.sleep() 함수를 호출하는 경우인데, 둘 다 유용한 값을 돌려주지 않아요. 둘 다 화면에 텍스트를 보내거나 실행을 1초 멈추는 부수 효과를 위해서만 호출됩니다.
함수형 스타일로 작성된 파이썬 프로그램은 대개 모든 I/O나 모든 대입을 피하는 극단까지 가지는 않습니다. 대신 함수형처럼 보이는 인터페이스를 제공하면서 내부적으로는 비-함수형 기능을 쓸 거예요. 예를 들어 함수의 구현은 여전히 지역 변수에 대한 대입을 사용하되, 전역 변수를 수정하거나 다른 부수 효과를 가지지는 않습니다.
함수형 프로그래밍은 객체지향 프로그래밍의 반대라고 볼 수 있습니다. 객체는 일부 내부 상태와, 이 상태를 수정하게 해 주는 메서드 호출 모음이 들어 있는 작은 캡슐이고, 프로그램은 올바른 상태 변경의 집합을 만드는 것으로 구성됩니다. 함수형 프로그래밍은 상태 변경을 최대한 피하고 함수들 사이를 흐르는 데이터와 함께 동작하기를 원해요. 파이썬에서는 애플리케이션의 객체(이메일 메시지, 트랜잭션 등)를 나타내는 인스턴스를 받고 돌려주는 함수를 작성해서 두 접근을 결합할 수 있습니다.
함수형 설계는 작업하기에 이상한 제약처럼 보일 수 있어요. 왜 객체와 부수 효과를 피해야 할까요? 함수형 스타일에는 이론적·실용적 이점이 있습니다.
- 형식적 증명 가능성 (Formal provability)
- 모듈성 (Modularity)
- 합성 가능성 (Composability)
- 디버깅과 테스트의 용이성
형식적 증명 가능성 (Formal provability)
이론적 이점은 함수형 프로그램이 올바르다는 수학적 증명을 구성하기가 더 쉽다는 것입니다.
오랫동안 연구자들은 프로그램이 올바르다는 것을 수학적으로 증명하는 방법을 찾는 데 관심을 가져 왔습니다. 이것은 프로그램을 수많은 입력에 대해 테스트하고 출력이 대체로 올바르다고 결론짓거나, 프로그램의 소스 코드를 읽고 코드가 맞아 보인다고 결론짓는 것과는 달라요. 목표는 프로그램이 가능한 모든 입력에 대해 올바른 결과를 만든다는 엄밀한 증명입니다.
프로그램이 올바르다는 것을 증명하는 데 사용되는 기법은 **불변식(invariants)**을 적어 내려가는 것입니다. 불변식은 입력 데이터와 프로그램 변수의, 항상 참인 성질이에요. 각 코드 줄에 대해, 해당 줄이 실행되기 전에 불변식 X와 Y가 참이면 실행 후에 약간 다른 불변식 X'와 Y'가 참임을 보이면 됩니다. 이것을 프로그램 끝까지 계속하면, 그 지점에서 불변식은 프로그램 출력에 대한 원하는 조건과 일치해야 합니다.
함수형 프로그래밍이 대입을 피하게 된 까닭은 대입이 이 기법으로 다루기 어렵기 때문입니다. 대입은 대입 전에 참이던 불변식을 깨뜨릴 수 있으면서도 앞으로 전파될 수 있는 새 불변식을 만들어 내지는 못하거든요.
안타깝게도 프로그램이 올바르다는 것을 증명하는 것은 대체로 비실용적이고 파이썬 소프트웨어와는 관계가 없습니다. 사소한 프로그램조차 몇 페이지 길이의 증명이 필요하고, 중간 정도로 복잡한 프로그램의 정확성 증명은 엄청날 것입니다. 그리고 당신이 매일 쓰는 프로그램(파이썬 인터프리터, XML 파서, 웹 브라우저) 중 정확성이 증명될 수 있는 것은 거의 없어요. 설령 증명을 적거나 생성한다 해도, 그 증명을 검증하는 문제가 남습니다. 증명에 오류가 있을 수도 있고, 당신이 프로그램이 올바르다고 잘못 믿게 될 수도 있거든요.
모듈성 (Modularity)
함수형 프로그래밍의 더 실용적인 이점은 문제를 작은 조각으로 쪼개도록 강제한다는 것입니다. 그 결과 프로그램이 더 모듈화됩니다. 복잡한 변환을 수행하는 큰 함수보다 한 가지 일을 하는 작은 함수를 명세하고 작성하는 것이 더 쉬워요. 작은 함수는 읽기도, 오류를 검사하기도 더 쉽습니다.
디버깅과 테스트의 용이성 (Ease of debugging and testing)
함수형 스타일 프로그램의 테스트와 디버깅은 더 쉽습니다.
함수가 대체로 작고 명확하게 명세되어 있기 때문에 디버깅이 단순해집니다. 프로그램이 동작하지 않을 때, 각 함수는 데이터가 올바른지 확인할 수 있는 인터페이스 지점이에요. 중간 입력과 출력을 살펴서 버그의 원인이 되는 함수를 빠르게 격리할 수 있습니다.
각 함수가 단위 테스트의 잠재적 대상이기 때문에 테스트도 더 쉬워요. 함수는 테스트를 실행하기 전에 복제해야 하는 시스템 상태에 의존하지 않습니다. 올바른 입력을 합성한 다음 출력이 기대치와 일치하는지만 확인하면 되죠.
합성 가능성 (Composability)
함수형 스타일 프로그램을 작업하다 보면 입력과 출력이 다양한 여러 함수를 작성하게 됩니다. 이 중 일부는 불가피하게 특정 애플리케이션에 특화되지만, 다른 것들은 광범위한 프로그램에서 유용할 수 있어요. 예를 들어 디렉터리 경로를 받아 그 디렉터리의 모든 XML 파일을 돌려주는 함수나, 파일 이름을 받아 그 내용을 돌려주는 함수는 많은 상황에 적용할 수 있습니다.
시간이 지나면 개인 유틸리티 라이브러리가 형성됩니다. 자주 기존 함수들을 새 구성으로 배열하고 현재 작업에 특화된 함수 몇 개를 작성해서 새 프로그램을 조립하게 될 거예요.
이터레이터 (Iterators)
함수형 스타일 프로그램을 작성하는 데 중요한 토대가 되는 파이썬 언어 기능부터 시작하겠습니다. 바로 이터레이터입니다.
이터레이터는 데이터 스트림을 나타내는 객체로, 데이터를 한 번에 한 요소씩 돌려주어요. 파이썬 이터레이터는 인자를 받지 않고 항상 스트림의 다음 요소를 돌려주는 __next__()라는 메서드를 지원해야 합니다. 스트림에 더 이상 요소가 없으면 __next__()는 StopIteration 예외를 일으켜야 해요. 다만 이터레이터가 유한할 필요는 없습니다. 무한한 데이터 스트림을 만드는 이터레이터를 작성하는 것도 전적으로 합리적이에요.
내장 iter() 함수는 임의의 객체를 받아 그 객체의 내용이나 요소를 돌려줄 이터레이터를 반환하려 시도하며, 객체가 반복을 지원하지 않으면 TypeError를 일으킵니다. 파이썬의 몇몇 내장 데이터 타입은 반복을 지원하는데, 가장 흔한 것은 리스트와 딕셔너리입니다. 어떤 객체에 대해 이터레이터를 얻을 수 있다면 그 객체를 iterable이라고 부릅니다.
반복 인터페이스를 직접 실험해 볼 수 있어요.
>>> L = [1, 2, 3]
>>> it = iter(L)
>>> it
<...iterator object at ...>
>>> it.__next__() # same as next(it)
1
>>> next(it)
2
>>> next(it)
3
>>> next(it)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
>>>
파이썬은 여러 맥락에서 iterable 객체를 기대하는데, 그중 가장 중요한 것은 for 문입니다. for X in Y 문에서 Y는 이터레이터이거나 iter()가 이터레이터를 만들 수 있는 어떤 객체여야 합니다. 다음 두 문장은 동등합니다.
for i in iter(obj):
print(i)
for i in obj:
print(i)
이터레이터는 list() 또는 tuple() 생성자 함수를 사용해 리스트나 튜플로 구체화(materialize)할 수 있습니다.
>>> L = [1, 2, 3]
>>> iterator = iter(L)
>>> t = tuple(iterator)
>>> t
(1, 2, 3)
시퀀스 언패킹(sequence unpacking)도 이터레이터를 지원합니다. 이터레이터가 N개의 요소를 돌려줄 것임을 안다면, 그것들을 N-튜플로 언패킹할 수 있어요.
>>> L = [1, 2, 3]
>>> iterator = iter(L)
>>> a, b, c = iterator
>>> a, b, c
(1, 2, 3)
max()와 min() 같은 내장 함수는 단일 이터레이터 인자를 받아 가장 큰 또는 가장 작은 요소를 돌려줄 수 있습니다. "in"과 "not in" 연산자도 이터레이터를 지원해요. X in iterator는 X가 이터레이터가 돌려주는 스트림에서 발견되면 참입니다. 이터레이터가 무한하면 명백한 문제가 생깁니다. max(), min()은 절대 돌아오지 않고, 요소 X가 스트림에 나타나지 않으면 "in"과 "not in" 연산자도 반환되지 않아요.
이터레이터에서는 앞으로만 갈 수 있다는 점을 기억하세요. 이전 요소를 얻거나, 이터레이터를 재설정하거나, 복사하는 방법은 없습니다. 이터레이터 객체는 선택적으로 이런 추가 능력을 제공할 수는 있지만, 이터레이터 프로토콜은 __next__() 메서드만 명세합니다. 따라서 함수가 이터레이터의 모든 출력을 소비할 수 있고, 같은 스트림으로 다른 일을 해야 한다면 새 이터레이터를 만들어야 합니다.
이터레이터를 지원하는 데이터 타입 (Data Types That Support Iterators)
리스트와 튜플이 이터레이터를 어떻게 지원하는지는 이미 봤어요. 실제로 문자열 같은 어떤 파이썬 시퀀스 타입이든 자동으로 이터레이터의 생성을 지원합니다.
딕셔너리에 iter()를 호출하면 딕셔너리의 키를 반복할 이터레이터를 돌려줍니다.
>>> m = {'Jan': 1, 'Feb': 2, 'Mar': 3, 'Apr': 4, 'May': 5, 'Jun': 6,
... 'Jul': 7, 'Aug': 8, 'Sep': 9, 'Oct': 10, 'Nov': 11, 'Dec': 12}
>>> for key in m:
... print(key, m[key])
Jan 1
Feb 2
Mar 3
Apr 4
May 5
Jun 6
Jul 7
Aug 8
Sep 9
Oct 10
Nov 11
Dec 12
파이썬 3.7부터 딕셔너리 반복 순서는 삽입 순서와 같다고 보장됩니다. 더 이른 버전에서는 동작이 명세되어 있지 않아 구현 간에 달라질 수 있었어요.
딕셔너리에 iter()를 적용하면 항상 키를 반복하지만, 딕셔너리에는 다른 이터레이터를 돌려주는 메서드들이 있습니다. 값이나 키/값 쌍을 반복하고 싶으면 values() 또는 items() 메서드를 명시적으로 호출해 적절한 이터레이터를 얻을 수 있어요.
dict() 생성자는 유한한 (key, value) 튜플 스트림을 돌려주는 이터레이터를 받을 수 있습니다.
>>> L = [('Italy', 'Rome'), ('France', 'Paris'), ('US', 'Washington DC')]
>>> dict(iter(L))
{'Italy': 'Rome', 'France': 'Paris', 'US': 'Washington DC'}
파일도 파일에 줄이 더 없을 때까지 readline() 메서드를 호출하는 방식으로 반복을 지원합니다. 즉 파일의 각 줄을 이렇게 읽을 수 있어요.
for line in file:
# do something for each line
...
집합(set)은 iterable에서 내용을 가져오고 집합의 요소를 반복하게 해 줍니다.
>>> S = {2, 3, 5, 7, 11, 13}
>>> for i in S:
... print(i)
2
3
5
7
11
13
제너레이터 표현식과 리스트 컴프리헨션 (Generator expressions and list comprehensions)
이터레이터 출력에 대한 두 가지 흔한 연산은 1) 모든 요소에 어떤 연산을 수행하는 것, 2) 어떤 조건을 만족하는 요소의 부분집합을 선택하는 것입니다. 예를 들어 문자열 리스트가 주어졌을 때, 각 줄에서 끝의 공백을 잘라내거나 주어진 부분 문자열을 담고 있는 모든 문자열을 추출하고 싶을 수 있어요.
리스트 컴프리헨션과 제너레이터 표현식(줄여서 "listcomps"와 "genexps")은 함수형 프로그래밍 언어 Haskell에서 빌려 온, 이런 연산을 위한 간결한 표기입니다. 다음 코드로 문자열 스트림에서 모든 공백을 잘라낼 수 있어요.
>>> line_list = [' line 1\n', 'line 2 \n', ' \n', '']
>>>
>>> # Generator expression -- returns iterator
>>> stripped_iter = (line.strip() for line in line_list)
>>>
>>> # List comprehension -- returns list
>>> stripped_list = [line.strip() for line in line_list]
"if" 조건을 추가하면 특정 요소만 선택할 수 있습니다.
>>> stripped_list = [line.strip() for line in line_list
... if line != ""]
리스트 컴프리헨션으로는 파이썬 리스트를 돌려받습니다. stripped_list는 결과 줄을 담은 리스트이지 이터레이터가 아니에요. 제너레이터 표현식은 필요한 대로 값을 계산하는 이터레이터를 돌려주고, 값 전체를 한 번에 구체화할 필요가 없습니다. 즉 무한한 스트림이나 아주 큰 데이터를 돌려주는 이터레이터로 작업할 때는 리스트 컴프리헨션이 유용하지 않습니다. 이런 상황에서는 제너레이터 표현식이 더 바람직해요.
제너레이터 표현식은 괄호("()")로, 리스트 컴프리헨션은 대괄호("[]")로 둘러싸입니다. 제너레이터 표현식의 형태는 이렇습니다.
( expression for expr in sequence1
if condition1
for expr2 in sequence2
if condition2
for expr3 in sequence3
...
if condition3
for exprN in sequenceN
if conditionN )
리스트 컴프리헨션 역시 바깥쪽 괄호만 다릅니다(괄호 대신 대괄호).
생성된 출력의 요소는 expression의 연속적인 값입니다. if 절은 모두 선택적이며, 있으면 condition이 참일 때만 expression이 평가되어 결과에 추가됩니다.
제너레이터 표현식은 항상 괄호 안에 작성해야 하는데, 함수 호출을 알리는 괄호도 마찬가지로 취급됩니다. 즉시 함수에 전달될 이터레이터를 만들고 싶다면 이렇게 쓸 수 있어요.
obj_total = sum(obj.count for obj in list_all_objects())
for...in 절은 반복할 시퀀스들을 담고 있습니다. 시퀀스들은 같은 길이일 필요가 없는데, 왼쪽에서 오른쪽으로 반복되지 병렬로 반복되지 않기 때문입니다. sequence1의 각 요소에 대해 sequence2가 처음부터 반복됩니다. 그런 다음 sequence1과 sequence2에서 나온 각 요소 쌍에 대해 sequence3이 반복되죠.
다시 말하면, 리스트 컴프리헨션 또는 제너레이터 표현식은 다음 파이썬 코드와 동등합니다.
for expr1 in sequence1:
if not (condition1):
continue # Skip this element
for expr2 in sequence2:
if not (condition2):
continue # Skip this element
...
for exprN in sequenceN:
if not (conditionN):
continue # Skip this element
# Output the value of
# the expression.
즉 for...in 절이 여러 개이고 if 절이 없으면 결과 출력의 길이는 모든 시퀀스 길이의 곱과 같습니다. 길이 3의 리스트 두 개가 있다면 출력 리스트는 9개 요소입니다.
>>> seq1 = 'abc'
>>> seq2 = (1, 2, 3)
>>> [(x, y) for x in seq1 for y in seq2]
[('a', 1), ('a', 2), ('a', 3),
('b', 1), ('b', 2), ('b', 3),
('c', 1), ('c', 2), ('c', 3)]
파이썬 문법에 모호함을 도입하지 않기 위해, expression이 튜플을 만든다면 괄호로 둘러싸야 합니다. 아래 첫 번째 리스트 컴프리헨션은 문법 오류이고 두 번째가 올바릅니다.
# Syntax error
[x, y for x in seq1 for y in seq2]
# Correct
[(x, y) for x in seq1 for y in seq2]
제너레이터 (Generators)
제너레이터는 이터레이터 작성을 단순화하는 특별한 종류의 함수입니다. 일반 함수는 값을 계산해서 돌려주지만, 제너레이터는 값의 스트림을 돌려주는 이터레이터를 반환합니다.
파이썬이나 C에서 일반 함수 호출이 어떻게 동작하는지는 잘 알고 있을 거예요. 함수를 호출하면 지역 변수가 생성되는 사설 네임스페이스를 얻게 됩니다. 함수가 return 문에 도달하면 지역 변수는 파괴되고 값이 호출자에게 돌아갑니다. 나중에 같은 함수를 다시 호출하면 새 사설 네임스페이스와 새로운 지역 변수 집합이 만들어집니다. 하지만 함수를 빠져나갈 때 지역 변수를 버리지 않는다면 어떨까요? 나중에 함수가 중단된 지점에서 재개할 수 있다면 어떨까요? 이것이 바로 제너레이터가 제공하는 것입니다. 제너레이터는 재개 가능한 함수라고 생각할 수 있어요.
제너레이터 함수의 가장 간단한 예입니다.
>>> def generate_ints(N):
... for i in range(N):
... yield i
yield 키워드를 담고 있는 함수는 모두 제너레이터 함수입니다. 이를 파이썬의 바이트코드 컴파일러가 감지해서 그 결과로 함수를 특별히 컴파일합니다.
제너레이터 함수를 호출하면 단일 값을 돌려주지 않고, 이터레이터 프로토콜을 지원하는 제너레이터 객체를 돌려줍니다. yield 표현식을 실행하면 제너레이터는 return 문과 비슷하게 i의 값을 출력합니다. yield와 return 문의 큰 차이는 yield에 도달하면 제너레이터의 실행 상태가 중단되고 지역 변수가 보존된다는 점입니다. 제너레이터의 __next__() 메서드를 다음에 호출하면 함수가 실행을 재개합니다.
generate_ints() 제너레이터의 샘플 사용입니다.
>>> gen = generate_ints(3)
>>> gen
<generator object generate_ints at ...>
>>> next(gen)
0
>>> next(gen)
1
>>> next(gen)
2
>>> next(gen)
Traceback (most recent call last):
File "stdin", line 1, in <module>
File "stdin", line 2, in generate_ints
StopIteration
for i in generate_ints(5)라고 쓰거나 a, b, c = generate_ints(3)라고 쓰는 것도 동등합니다.
제너레이터 함수 안에서 return value는 __next__() 메서드에서 StopIteration(value)를 일으키게 합니다. 이것이 발생하거나 함수의 끝에 도달하면 값의 진행이 끝나고 제너레이터는 더 이상 값을 산출할 수 없어요.
자신만의 클래스를 작성하고 제너레이터의 모든 지역 변수를 인스턴스 변수로 저장하면 제너레이터의 효과를 수동으로 얻을 수도 있습니다. 예를 들어 self.count를 0으로 설정하고 __next__() 메서드가 self.count를 증가시켜 돌려주는 방식으로 정수 리스트를 돌려줄 수 있죠. 하지만 중간 정도로 복잡한 제너레이터라면 대응하는 클래스를 작성하는 것이 훨씬 지저분할 수 있습니다.
파이썬 라이브러리에 포함된 테스트 스위트인 Lib/test/test_generators.py에는 더 흥미로운 예시가 몇 가지 있습니다. 여기 제너레이터를 재귀적으로 사용해 트리의 중위 순회(in-order traversal)를 구현하는 제너레이터가 있어요.
# A recursive generator that generates Tree leaves in in-order.
def inorder(t):
if t:
for x in inorder(t.left):
yield x
yield t.label
for x in inorder(t.right):
yield x
test_generators.py의 다른 두 예시는 N-Queens 문제(NxN 체스판에 어떤 여왕도 서로 위협하지 않도록 N개의 여왕을 두는 문제)와 Knight's Tour(NxN 체스판의 모든 칸을 한 번도 방문하지 않고 나이트가 모든 칸을 지나는 경로 찾기)에 대한 해를 만들어 냅니다.
제너레이터에 값 전달하기 (Passing values into a generator)
파이썬 2.4 및 그 이전에서 제너레이터는 출력만 만들 수 있었습니다. 제너레이터의 코드가 이터레이터를 만들기 위해 한 번 호출된 뒤에는, 실행이 재개될 때 함수에 어떤 새 정보를 전달할 방법이 없었어요. 제너레이터가 전역 변수를 보게 하거나, 호출자가 수정하는 가변 객체를 넘겨줌으로써 이 능력을 억지로 만들어 낼 수는 있었지만, 이런 접근은 지저분합니다.
파이썬 2.5에는 제너레이터에 값을 전달하는 간단한 방법이 있습니다. yield가 표현식이 되어, 변수에 할당되거나 다른 방식으로 조작될 수 있는 값을 돌려주게 된 것이죠.
val = (yield i)
반환된 값으로 무언가를 할 때는 위 예시처럼 yield 표현식에 항상 괄호를 두는 것을 권장합니다. 괄호가 항상 필요한 건 아니지만, 언제 필요한지 기억해야 하는 대신 항상 넣는 편이 더 쉽거든요.
(PEP 342가 정확한 규칙을 설명하는데, yield-표현식은 대입의 오른쪽에 있는 최상위 표현식으로 나타날 때를 제외하고는 항상 괄호로 감싸야 합니다. 즉 val = yield i라고 쓸 수는 있지만 val = (yield i) + 12처럼 연산이 있을 때는 괄호를 써야 해요.)
값은 제너레이터의 send(value) 메서드를 호출해서 보냅니다. 이 메서드는 제너레이터의 코드를 재개하고 yield 표현식이 지정된 값을 돌려줍니다. 일반 __next__() 메서드를 호출하면 yield는 None을 돌려줘요.
다음은 1씩 증가하면서 내부 카운터의 값을 바꿀 수 있는 간단한 카운터입니다.
def counter(maximum):
i = 0
while i < maximum:
val = (yield i)
# If value provided, change counter
if val is not None:
i = val
else:
i += 1
카운터를 바꾸는 예시입니다.
>>> it = counter(10)
>>> next(it)
0
>>> next(it)
1
>>> it.send(8)
8
>>> next(it)
9
>>> next(it)
Traceback (most recent call last):
File "t.py", line 15, in <module>
it.next()
StopIteration
yield가 자주 None을 돌려줄 것이므로, 항상 이 경우를 확인해야 합니다. send() 메서드가 제너레이터 함수를 재개하는 데 쓰일 유일한 메서드임을 확신하지 않는 한, 표현식에서 그 값을 그냥 쓰지 마세요.
send()에 더해 제너레이터에는 메서드 두 개가 더 있습니다.
throw(value)는 제너레이터 안에서 예외를 일으키는 데 사용됩니다. 예외는 제너레이터의 실행이 멈춰 있는yield표현식에서 일어나요.close()는 반복을 종료시키기 위해GeneratorExit예외를 제너레이터에 보냅니다. 이 예외를 받으면 제너레이터 코드는GeneratorExit또는StopIteration을 반드시 일으켜야 합니다. 예외를 잡아서 다른 일을 하는 것은 불법이며RuntimeError를 촉발할 거예요.close()는 제너레이터가 가비지 컬렉션될 때 파이썬의 가비지 컬렉터가 호출하기도 합니다.
GeneratorExit가 발생할 때 정리 코드를 실행해야 한다면, GeneratorExit를 잡는 대신 try: ... finally: 구문을 사용할 것을 권장합니다.
이 변경들의 누적 효과는 제너레이터를 정보의 일방향 생산자에서 생산자이면서 소비자인 것으로 바꾸는 것입니다.
제너레이터는 또한 코루틴(coroutines), 즉 서브루틴의 더 일반화된 형태가 됩니다. 서브루틴은 한 지점(함수의 맨 위)에서 들어가고 다른 지점(return 문)에서 나오지만, 코루틴은 여러 다른 지점(yield 문들)에서 들어가고, 나오고, 재개될 수 있어요.
내장 함수 (Built-in functions)
이터레이터와 함께 자주 쓰이는 내장 함수들을 더 자세히 살펴볼게요.
파이썬의 내장 함수 두 개, map()과 filter()는 제너레이터 표현식의 기능을 복제합니다.
map(f, iterA, iterB, ...)는 시퀀스 f(iterA[0], iterB[0]), f(iterA[1], iterB[1]), f(iterA[2], iterB[2]), ...를 반복하는 이터레이터를 돌려줍니다.
>>> def upper(s):
... return s.upper()
>>> list(map(upper, ['sentence', 'fragment']))
['SENTENCE', 'FRAGMENT']
>>> [upper(s) for s in ['sentence', 'fragment']]
['SENTENCE', 'FRAGMENT']
물론 리스트 컴프리헨션으로 같은 효과를 얻을 수 있어요.
filter(predicate, iter)는 특정 조건을 만족하는 모든 시퀀스 요소를 반복하는 이터레이터를 돌려주며, 리스트 컴프리헨션으로 비슷하게 복제됩니다. predicate는 어떤 조건의 진릿값을 돌려주는 함수입니다. filter()에 쓰려면 predicate는 단일 값을 받아야 해요.
>>> def is_even(x):
... return (x % 2) == 0
>>> list(filter(is_even, range(10)))
[0, 2, 4, 6, 8]
이것도 리스트 컴프리헨션으로 쓸 수 있습니다.
>>> list(x for x in range(10) if is_even(x))
[0, 2, 4, 6, 8]
enumerate(iter, start=0)는 iterable의 요소를 세면서 (카운트(start부터), 각 요소)의 2-튜플을 돌려줍니다.
>>> for item in enumerate(['subject', 'verb', 'object']):
... print(item)
(0, 'subject')
(1, 'verb')
(2, 'object')
enumerate()는 리스트를 반복하면서 특정 조건이 충족되는 인덱스를 기록할 때 자주 쓰입니다.
f = open('data.txt', 'r')
for i, line in enumerate(f):
if line.strip() == '':
print('Blank line at line #%i' % i)
sorted(iterable, key=None, reverse=False)는 iterable의 모든 요소를 리스트에 모으고, 리스트를 정렬한 뒤 정렬된 결과를 돌려줍니다. key와 reverse 인자는 만들어진 리스트의 sort() 메서드에 전달됩니다.
>>> import random
>>> # Generate 8 random numbers between [0, 10000)
>>> rand_list = random.sample(range(10000), 8)
>>> rand_list
[769, 7953, 9828, 6431, 8442, 9878, 6213, 2207]
>>> sorted(rand_list)
[769, 2207, 6213, 6431, 7953, 8442, 9828, 9878]
>>> sorted(rand_list, reverse=True)
[9878, 9828, 8442, 7953, 6431, 6213, 2207, 769]
(정렬에 대한 더 자세한 논의는 Sorting Techniques 문서를 보세요.)
any(iter)와 all(iter) 내장은 iterable 내용의 진릿값을 살펴봅니다. any()는 iterable의 어떤 요소라도 참값이면 True를, all()은 모든 요소가 참값이면 True를 돌려줍니다.
>>> any([0, 1, 0])
True
>>> any([0, 0, 0])
False
>>> any([1, 1, 1])
True
>>> all([0, 1, 0])
False
>>> all([0, 0, 0])
False
>>> all([1, 1, 1])
True
zip(iterA, iterB, ...)는 각 iterable에서 요소 하나를 가져와 튜플로 돌려줍니다.
zip(['a', 'b', 'c'], (1, 2, 3)) =>
('a', 1), ('b', 2), ('c', 3)
메모리 안의 리스트를 구성하고 반환 전에 모든 입력 이터레이터를 소진하지는 않습니다. 대신 튜플은 요청될 때에만 구성되어 반환됩니다.(이 동작의 기술적 용어는 지연 평가 lazy evaluation입니다.)
이 이터레이터는 모두 같은 길이의 iterable과 함께 쓰도록 의도되었습니다. iterable의 길이가 다르면 결과 스트림은 가장 짧은 iterable과 같은 길이가 돼요.
zip(['a', 'b'], (1, 2, 3)) =>
('a', 1), ('b', 2)
다만 이것은 피해야 합니다. 더 긴 이터레이터에서 요소가 꺼내져 버려질 수 있기 때문이에요. 즉 버려진 요소를 건너뛰는 위험이 있어서 더 이상 이터레이터를 사용할 수 없게 됩니다.
itertools 모듈 (The itertools module)
itertools 모듈은 여러 흔히 쓰이는 이터레이터뿐 아니라 여러 이터레이터를 결합하는 함수들을 담고 있습니다. 이번 절에서는 작은 예시들을 보여주며 모듈의 내용을 소개할게요.
모듈의 함수는 몇 가지 큰 부류로 나뉩니다.
- 기존 이터레이터에 기반해 새 이터레이터를 만드는 함수.
- 이터레이터의 요소를 함수 인자로 취급하는 함수.
- 이터레이터 출력의 일부를 선택하는 함수.
- 이터레이터 출력을 그룹화하는 함수.
새 이터레이터 만들기 (Creating new iterators)
itertools.count(start, step)은 일정한 간격으로 떨어진 값들의 무한 스트림을 돌려줍니다. 시작 숫자(기본값 0)와 숫자 사이의 간격(기본값 1)을 선택적으로 제공할 수 있어요.
itertools.count() =>
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, ...
itertools.count(10) =>
10, 11, 12, 13, 14, 15, 16, 17, 18, 19, ...
itertools.count(10, 5) =>
10, 15, 20, 25, 30, 35, 40, 45, 50, 55, ...
itertools.cycle(iter)은 제공된 iterable 내용의 복사본을 저장하고, 그 요소를 처음부터 끝까지 돌려주는 새 이터레이터를 반환합니다. 새 이터레이터는 이 요소들을 무한히 반복할 거예요.
itertools.cycle([1, 2, 3, 4, 5]) =>
1, 2, 3, 4, 5, 1, 2, 3, 4, 5, ...
itertools.repeat(elem, [n])은 제공된 요소를 n번 돌려주거나, n이 제공되지 않으면 요소를 끝없이 돌려줍니다.
itertools.repeat('abc') =>
abc, abc, abc, abc, abc, abc, abc, abc, abc, abc, ...
itertools.repeat('abc', 5) =>
abc, abc, abc, abc, abc
itertools.chain(iterA, iterB, ...)은 임의 개수의 iterable을 입력으로 받아, 첫 번째 이터레이터의 모든 요소, 그다음 두 번째의 모든 요소를 식으로 모든 iterable이 소진될 때까지 돌려줍니다.
itertools.chain(['a', 'b', 'c'], (1, 2, 3)) =>
a, b, c, 1, 2, 3
itertools.islice(iter, [start], stop, [step])은 이터레이터의 슬라이스인 스트림을 돌려줍니다. 단일 stop 인자로는 처음 stop개의 요소를 돌려줍니다. 시작 인덱스를 제공하면 stop-start개의 요소를 얻고, step 값을 제공하면 그에 따라 요소가 건너뜁니다. 파이썬의 문자열·리스트 슬라이싱과 달리 start, stop, step에 음수 값을 쓸 수 없어요.
itertools.islice(range(10), 8) =>
0, 1, 2, 3, 4, 5, 6, 7
itertools.islice(range(10), 2, 8) =>
2, 3, 4, 5, 6, 7
itertools.islice(range(10), 2, 8, 2) =>
2, 4, 6
itertools.tee(iter, [n])은 이터레이터를 복제합니다. 모두 소스 이터레이터의 내용을 돌려줄 n개의 독립 이터레이터를 돌려주죠. n 값을 제공하지 않으면 기본값은 2입니다. 이터레이터 복제는 소스 이터레이터의 내용 일부를 저장해야 하므로, 이터레이터가 크고 새 이터레이터 하나가 다른 것보다 더 많이 소비되면 상당한 메모리를 소비할 수 있어요.
itertools.tee( itertools.count() ) =>
iterA, iterB
where iterA ->
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, ...
and iterB ->
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, ...
요소에 함수 호출하기 (Calling functions on elements)
operator 모듈은 파이썬의 연산자에 대응하는 함수 집합을 담고 있습니다. 몇 가지 예로 operator.add(a, b)(두 값을 더함), operator.ne(a, b)(a != b와 같음), operator.attrgetter('id')(.id 속성을 가져오는 callable을 돌려줌)가 있어요.
itertools.starmap(func, iter)은 iterable이 튜플의 스트림을 돌려줄 것이라고 가정하고, 이 튜플들을 인자로 사용해 func를 호출합니다.
itertools.starmap(os.path.join,
[('/bin', 'python'), ('/usr', 'bin', 'java'),
('/usr', 'bin', 'perl'), ('/usr', 'bin', 'ruby')])
=>
/bin/python, /usr/bin/java, /usr/bin/perl, /usr/bin/ruby
요소 선택하기 (Selecting elements)
또 다른 함수 그룹은 predicate에 기반해 이터레이터 요소의 부분집합을 선택합니다.
itertools.filterfalse(predicate, iter)은 filter()의 반대로, predicate가 거짓을 돌려주는 모든 요소를 돌려줍니다.
itertools.filterfalse(is_even, itertools.count()) =>
1, 3, 5, 7, 9, 11, 13, 15, ...
itertools.takewhile(predicate, iter)은 predicate가 참을 돌려주는 동안 요소를 돌려줍니다. predicate가 거짓을 돌려주면 이터레이터는 결과의 끝을 알립니다.
def less_than_10(x):
return x < 10
itertools.takewhile(less_than_10, itertools.count()) =>
0, 1, 2, 3, 4, 5, 6, 7, 8, 9
itertools.takewhile(is_even, itertools.count()) =>
0
itertools.dropwhile(predicate, iter)은 predicate가 참을 돌려주는 동안 요소를 버리고, 그다음 iterable 결과의 나머지를 돌려줍니다.
itertools.dropwhile(less_than_10, itertools.count()) =>
10, 11, 12, 13, 14, 15, 16, 17, 18, 19, ...
itertools.dropwhile(is_even, itertools.count()) =>
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...
itertools.compress(data, selectors)은 이터레이터 두 개를 받아, selectors의 대응 요소가 참인 data의 요소만 돌려주며, 둘 중 하나가 소진되면 멈춥니다.
itertools.compress([1, 2, 3, 4, 5], [True, True, False, False, True]) =>
1, 2, 5
조합 함수 (Combinatoric functions)
itertools.combinations(iterable, r)은 iterable에 담긴 요소의 가능한 모든 r-튜플 조합을 주는 이터레이터를 돌려줍니다.
itertools.combinations([1, 2, 3, 4, 5], 2) =>
(1, 2), (1, 3), (1, 4), (1, 5),
(2, 3), (2, 4), (2, 5),
(3, 4), (3, 5),
(4, 5)
itertools.combinations([1, 2, 3, 4, 5], 3) =>
(1, 2, 3), (1, 2, 4), (1, 2, 5), (1, 3, 4), (1, 3, 5), (1, 4, 5),
(2, 3, 4), (2, 3, 5), (2, 4, 5),
(3, 4, 5)
각 튜플 안의 요소는 iterable이 돌려준 것과 같은 순서를 유지합니다. 예를 들어 위 예시에서 숫자 1은 항상 2, 3, 4, 5보다 앞에 있어요. 비슷한 함수 itertools.permutations(iterable, r=None)은 순서에 대한 이 제약을 없애서 길이 r의 모든 가능한 배열을 돌려줍니다.
itertools.permutations([1, 2, 3, 4, 5], 2) =>
(1, 2), (1, 3), (1, 4), (1, 5),
(2, 1), (2, 3), (2, 4), (2, 5),
(3, 1), (3, 2), (3, 4), (3, 5),
(4, 1), (4, 2), (4, 3), (4, 5),
(5, 1), (5, 2), (5, 3), (5, 4)
itertools.permutations([1, 2, 3, 4, 5]) =>
(1, 2, 3, 4, 5), (1, 2, 3, 5, 4), (1, 2, 4, 3, 5),
...
(5, 4, 3, 2, 1)
r 값을 제공하지 않으면 iterable의 길이가 사용되어 모든 요소가 치환됩니다.
이 함수들은 위치에 의해 모든 가능한 조합을 만들며 iterable의 내용이 유일할 것을 요구하지 않는다는 점을 기억하세요.
itertools.permutations('aba', 3) =>
('a', 'b', 'a'), ('a', 'a', 'b'), ('b', 'a', 'a'),
('b', 'a', 'a'), ('a', 'a', 'b'), ('a', 'b', 'a')
동일한 튜플 ('a', 'a', 'b')가 두 번 나타나지만, 두 'a' 문자열은 서로 다른 위치에서 왔습니다.
itertools.combinations_with_replacement(iterable, r) 함수는 다른 제약을 완화합니다. 단일 튜플 안에서 요소가 반복될 수 있는 것이죠. 개념적으로 각 튜플의 첫 번째 위치에 요소가 선택된 다음, 두 번째 요소가 선택되기 전에 교체됩니다.
itertools.combinations_with_replacement([1, 2, 3, 4, 5], 2) =>
(1, 1), (1, 2), (1, 3), (1, 4), (1, 5),
(2, 2), (2, 3), (2, 4), (2, 5),
(3, 3), (3, 4), (3, 5),
(4, 4), (4, 5),
(5, 5)
요소 그룹화하기 (Grouping elements)
마지막으로 다룰 함수 itertools.groupby(iter, key_func=None)은 가장 복잡합니다. key_func(elem)은 iterable이 돌려주는 각 요소에 대해 키 값을 계산할 수 있는 함수입니다. 키 함수를 제공하지 않으면 키는 단순히 각 요소 그 자체입니다.
groupby()는 기본 iterable에서 같은 키 값을 가진 모든 연속 요소를 모으고, 키 값과 그 키를 가진 요소들에 대한 이터레이터를 담은 2-튜플의 스트림을 돌려줍니다.
city_list = [('Decatur', 'AL'), ('Huntsville', 'AL'), ('Selma', 'AL'),
('Anchorage', 'AK'), ('Nome', 'AK'),
('Flagstaff', 'AZ'), ('Phoenix', 'AZ'), ('Tucson', 'AZ'),
...
]
def get_state(city_state):
return city_state[1]
itertools.groupby(city_list, get_state) =>
('AL', iterator-1),
('AK', iterator-2),
('AZ', iterator-3), ...
where
iterator-1 =>
('Decatur', 'AL'), ('Huntsville', 'AL'), ('Selma', 'AL')
iterator-2 =>
('Anchorage', 'AK'), ('Nome', 'AK')
iterator-3 =>
('Flagstaff', 'AZ'), ('Phoenix', 'AZ'), ('Tucson', 'AZ')
groupby()는 기본 iterable의 내용이 이미 키에 기반해 정렬되어 있을 것이라고 가정합니다. 반환된 이터레이터들도 기본 iterable을 사용하므로, iterator-2와 그 대응 키를 요청하기 전에 iterator-1의 결과를 소비해야 한다는 점을 기억하세요.
functools 모듈 (The functools module)
functools 모듈은 몇 가지 고차 함수(higher-order function)를 담고 있습니다. 고차 함수는 함수 하나 이상을 입력으로 받아 새 함수를 돌려줍니다. 이 모듈에서 가장 유용한 도구는 functools.partial() 함수입니다.
함수형 스타일로 작성된 프로그램에서는 때로 매개변수 일부가 채워진 기존 함수의 변형을 만들고 싶을 때가 있습니다. 파이썬 함수 f(a, b, c)를 생각해 보세요. f(1, b, c)와 동등한 새 함수 g(b, c)를 만들고 싶을 수 있는데, 이는 f()의 매개변수 하나에 값을 채워 넣는 것입니다. 이것을 "부분 함수 적용(partial function application)"이라고 불러요.
partial()의 생성자는 (function, arg1, arg2, ..., kwarg1=value1, kwarg2=value2) 인자를 받습니다. 결과 객체는 호출 가능해서, 채워진 인자로 function을 호출하기 위해 그냥 호출하면 됩니다.
작지만 현실적인 예시입니다.
import functools
def log(message, subsystem):
"""Write the contents of 'message' to the specified subsystem."""
print('%s: %s' % (subsystem, message))
...
server_log = functools.partial(log, subsystem='server')
server_log('Unable to open socket')
functools.reduce(func, iter, [initial_value])은 iterable의 모든 요소에 대해 누적적으로 연산을 수행하므로 무한 iterable에는 적용할 수 없습니다. func는 두 요소를 받아 단일 값을 돌려주는 함수여야 해요. functools.reduce()는 이터레이터가 돌려준 처음 두 요소 A, B를 받아 func(A, B)를 계산합니다. 그다음 세 번째 요소 C를 요청해 func(func(A, B), C)를 계산하고, 이 결과를 네 번째 요소와 결합하며 iterable이 소진될 때까지 계속합니다. iterable이 아무 값도 돌려주지 않으면 TypeError 예외가 일어납니다. 초기 값이 제공되면 시작점으로 사용되고 func(initial_value, A)가 첫 번째 계산입니다.
>>> import operator, functools
>>> functools.reduce(operator.concat, ['A', 'BB', 'C'])
'ABBC'
>>> functools.reduce(operator.concat, [])
Traceback (most recent call last):
...
TypeError: reduce() of empty iterable with no initial value
>>> functools.reduce(operator.mul, [1, 2, 3], 1)
6
>>> functools.reduce(operator.mul, [], 1)
1
functools.reduce()와 함께 operator.add()를 쓰면 iterable의 모든 요소를 더하게 됩니다. 이 경우는 아주 흔해서 이것을 계산하는 특별한 내장 sum()이 따로 있어요.
>>> import functools, operator
>>> functools.reduce(operator.add, [1, 2, 3, 4], 0)
10
>>> sum([1, 2, 3, 4])
10
>>> sum([])
0
다만 functools.reduce()의 많은 사용에서, 명백한 for 루프를 그냥 쓰는 편이 더 명확할 수 있습니다.
import functools
# Instead of:
product = functools.reduce(operator.mul, [1, 2, 3], 1)
# You can write:
product = 1
for i in [1, 2, 3]:
product *= i
관련 함수로 itertools.accumulate(iterable, func=operator.add)이 있습니다. 같은 계산을 수행하지만 최종 결과만 돌려주는 대신, accumulate()는 각 부분 결과도 산출하는 이터레이터를 돌려줍니다.
itertools.accumulate([1, 2, 3, 4, 5]) =>
1, 3, 6, 10, 15
itertools.accumulate([1, 2, 3, 4, 5], operator.mul) =>
1, 2, 6, 24, 120
operator 모듈 (The operator module)
operator 모듈은 앞서 언급했어요. 파이썬의 연산자에 대응하는 함수 집합을 담고 있습니다. 이 함수들은 단일 연산을 수행하는 지루한 함수를 작성하는 수고를 덜어 주기 때문에 함수형 스타일 코드에서 자주 유용합니다.
이 모듈의 함수 몇 가지는:
- 수학 연산:
add(),sub(),mul(),floordiv(),abs(), … - 논리 연산:
not_(),truth(). - 비트 연산:
and_(),or_(),invert(). - 비교:
eq(),ne(),lt(),le(),gt(),ge(). - 객체 정체성:
is_(),is_not().
전체 목록은 operator 모듈 문서를 참고하세요.
작은 함수와 lambda 표현식 (Small functions and the lambda expression)
함수형 스타일 프로그램을 작성할 때 predicate 역할을 하거나 어떤 방식으로 요소를 결합하는 작은 함수가 자주 필요해집니다.
적합한 파이썬 내장 또는 모듈 함수가 있다면, 새 함수를 정의할 필요가 전혀 없어요.
stripped_lines = [line.strip() for line in lines]
existing_files = filter(os.path.exists, file_list)
필요한 함수가 없다면 직접 작성해야 합니다. 작은 함수를 쓰는 한 가지 방법은 lambda 표현식을 사용하는 것입니다. lambda는 여러 매개변수와 이 매개변수들을 결합하는 표현식을 받아, 그 표현식의 값을 돌려주는 익명 함수를 만듭니다.
adder = lambda x, y: x+y
print_assign = lambda name, value: name + '=' + str(value)
대안은 그냥 def 문을 사용해 평소 방식으로 함수를 정의하는 것입니다.
def adder(x, y):
return x + y
def print_assign(name, value):
return name + '=' + str(value)
어느 쪽이 더 나을까요? 그것은 스타일의 문제입니다. 제 평소 방침은 lambda 사용을 피하는 것입니다.
그런 선호의 한 가지 이유는 lambda가 정의할 수 있는 함수가 상당히 제한적이라는 것입니다. 결과가 단일 표현식으로 계산 가능해야 하므로, 다중 if... elif... else 비교나 try... except 문을 쓸 수 없어요. lambda 문에서 너무 많은 일을 하려 하면 읽기 어려운 지나치게 복잡한 표현식이 됩니다. 자, 다음 코드가 무엇을 하는지 빠르게 말해 보세요?
import functools
total = functools.reduce(lambda a, b: (0, a[1] + b[1]), items)[1]
알아낼 수는 있지만, 무슨 일이 벌어지는지 파악하기 위해 표현식을 풀어내는 데 시간이 걸립니다. 짧은 중첩 def 문을 쓰면 조금 나아집니다.
import functools
def combine(a, b):
return 0, a[1] + b[1]
total = functools.reduce(combine, items)[1]
하지만 그냥 for 루프를 썼다면 가장 좋았을 것입니다.
total = 0
for a, b in items:
total += b
또는 sum() 내장과 제너레이터 표현식으로요.
total = sum(b for a, b in items)
functools.reduce()의 많은 사용은 for 루프로 쓸 때 더 명확합니다.
Fredrik Lundh는 한때 lambda 사용을 리팩터링하기 위한 다음 규칙 집합을 제안했습니다.
- 람다 함수를 작성한다.
- 그 람다가 도대체 무엇을 하는지 설명하는 주석을 작성한다.
- 그 주석을 잠시 연구하고, 주석의 본질을 담아낼 이름을 생각한다.
- 그 이름을 사용해 람다를 def 문으로 변환한다.
- 주석을 제거한다.
저는 이 규칙들을 정말 좋아하지만, 이 무-람다 스타일이 더 낫다는 데 동의하지 않아도 자유입니다.
더 알아보기 (Learn more)
- Functional Programming HOWTO (원문)
- itertools 모듈 문서
- functools 모듈 문서
- operator 모듈 문서
- PEP 289: "Generator Expressions" — 제너레이터 표현식 제안서
- PEP 342: "Coroutines via Enhanced Generators" — 파이썬 2.5의 새 제너레이터 기능 설명