Following system colour scheme Selected dark colour scheme Selected light colour scheme

Python 개선 제안 한국어 번역

PEP 289 – 제너레이터 표현식

Author:
Raymond Hettinger <python at rcn.com>
Status:
Final
Type:
Standards Track
Created:
30-Jan-2002
Python-Version:
2.4
Post-History:
22-Oct-2003

Table of Contents

번역·라이선스 안내

이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판

초록

이 PEP는 리스트 컴프리헨션 PEP 202 및 제너레이터 PEP 255를 고성능·메모리 효율적인 방식으로 일반화한 제너레이터 표현식을 도입합니다.

근거

리스트 컴프리헨션을 사용한 경험을 통해 리스트 컴프리헨션이 Python 전반에서 광범위하게 유용하다는 사실이 확인되었습니다. 그러나 많은 사용 사례에서는 전체 리스트를 메모리에 생성할 필요가 없습니다. 대신 요소를 한 번에 하나씩 순회하기만 하면 됩니다.

예를 들어 다음 합산 코드는 제곱 값의 전체 리스트를 메모리에 만들고, 해당 값을 순회한 다음, 참조가 더 이상 필요하지 않으면 리스트를 삭제합니다:

sum([x*x for x in range(10)])

대신 제너레이터 표현식을 사용하면 메모리를 절약할 수 있습니다:

sum(x*x for x in range(10))

컨테이너 객체의 생성자에도 이와 유사한 이점이 제공됩니다:

s = set(word  for line in page  for word in line.split())
d = dict( (k, func(k)) for k in keylist)

제너레이터 표현식은 이터러블 입력을 단일 값으로 축약하는 sum(), min(), max()와 같은 함수에서 특히 유용합니다:

max(len(line)  for line in file  if line.strip())

제너레이터 표현식은 람다로 작성된 함수형 코드의 일부 사례도 해결합니다:

reduce(lambda s, a: s + a.myattr, data, 0)
reduce(lambda s, a: s + a[3], data, 0)

이는 다음과 같이 단순화됩니다:

sum(a.myattr for a in data)
sum(a[3] for a in data)

리스트 컴프리헨션은 filter()와 map()의 필요성을 크게 줄였습니다. 마찬가지로 제너레이터 표현식은 itertools.ifilter()와 itertools.imap()의 필요성을 최소화할 것으로 예상됩니다. 이와 대조적으로 다른 itertools의 유용성은 제너레이터 표현식으로 향상될 것입니다:

dotproduct = sum(x*y for x,y in itertools.izip(x_vector, y_vector))

리스트 컴프리헨션과 유사한 구문을 사용하므로 애플리케이션을 확장할 때 기존 코드를 제너레이터 표현식으로 쉽게 변환할 수 있습니다.

초기 측정 결과 제너레이터가 리스트 컴프리헨션보다 상당한 성능 우위를 보였습니다. 그러나 리스트 컴프리헨션은 Py2.4에 맞게 고도로 최적화되었으며, 현재는 소규모에서 중간 규모의 데이터 집합에서 성능이 대략 비슷합니다. 데이터 양이 많아질수록 제너레이터 표현식이 캐시 메모리를 소진하지 않고 반복 사이에 Python이 객체를 재사용할 수 있도록 하므로 성능이 더 나은 경향이 있습니다.

BDFL의 선언

이 PEP는 Py2.4에 대해 ACCEPTED되었습니다.

세부 사항

(화성에서 온 독자의 관점에서는 이 중 어느 것도 충분히 정확하지 않지만, 예제가 c.l.py에서 논의하기에 충분할 만큼 의도를 잘 전달하기를 바랍니다. Python 참조 매뉴얼에는 의미론적 및 구문론적 사양이 100% 정확하게 포함되어야 합니다.)

  1. 제너레이터 표현식의 의미는 이름 없는 제너레이터 함수를 생성하고 호출하는 것과 동등합니다. 예를 들어:
    g = (x**2 for x in range(10))
    print g.next()
    

    다음과 동등합니다:

    def __gen(exp):
        for x in exp:
            yield x**2
    g = __gen(iter(range(10)))
    print g.next()
    

    가장 바깥쪽의 for 표현식만 즉시 평가되고, 나머지 표현식은 제너레이터가 실행될 때까지 평가가 지연됩니다:

    g = (tgtexp  for var1 in exp1 if exp2 for var2 in exp3 if exp4)
    

    다음과 동등합니다:

    def __gen(bound_exp):
        for var1 in bound_exp:
            if exp2:
                for var2 in exp3:
                    if exp4:
                        yield tgtexp
    g = __gen(iter(exp1))
    del __gen
    
  2. 이 구문에서는 제너레이터 표현식이 항상 괄호 집합 바로 안에 있어야 하며 양쪽 어느 곳에도 쉼표를 둘 수 없습니다. CVS의 Grammar/Grammar 파일을 기준으로 보면 두 규칙이 변경됩니다:
    1. 규칙:
      atom: '(' [testlist] ')'
      

      다음과 같이 변경됩니다.:

      atom: '(' [testlist_gexp] ')'
      

      여기서 testlist_gexp는 listmaker와 거의 같지만, ‘for’ … ‘in’ 뒤에 단일 test만 허용합니다:

      testlist_gexp: test ( gen_for | (',' test)* [','] )
      
    2. arglist 규칙에도 유사한 변경이 필요합니다.

    이는 다음과 같이 작성할 수 있음을 의미합니다.:

    sum(x**2 for x in range(10))
    

    그러나 다음과 같이 작성해야 합니다.:

    reduce(operator.add, (x**2 for x in range(10)))
    

    또한 다음과 같이 작성해야 합니다.:

    g = (x**2 for x in range(10))
    

    즉, 함수 호출에 단일 위치 인자가 있는 경우에는 추가 괄호 없이 제너레이터 표현식으로 작성할 수 있지만, 그 밖의 모든 경우에는 괄호로 묶어야 합니다.

    정확한 세부 사항은 Grammar/Grammar 버전 1.49에 커밋되었습니다.

  3. 루프 변수(단순 변수이거나 단순 변수의 튜플인 경우)는 둘러싼 함수에 노출되지 않습니다. 이를 통해 구현이 용이해지고 일반적인 사용 사례의 신뢰성이 높아집니다. 향후 Python의 어느 버전에서는 리스트 컴프리헨션도 주변 코드에서 유도 변수를 숨기게 되며, (Py2.4에서는 유도 변수에 액세스하는 코드에 경고가 발행됩니다.)

    예를 들면 다음과 같습니다.:

    x = "hello"
    y = list(x for x in "abc")
    print x    # prints "hello", not "c"
    
  4. 리스트 컴프리헨션은 변경되지 않은 상태로 유지됩니다. 예를 들면 다음과 같습니다.:
    [x for x in S]    # This is a list comprehension.
    [(x for x in S)]  # This is a list containing one generator
                      # expression.
    

    안타깝게도 현재는 구문상 약간의 차이가 있습니다. 다음 표현식은:

    [x for x in 1, 2, 3]
    

    유효하며, 이는 다음을 의미합니다.:

    [x for x in (1, 2, 3)]
    

    그러나 제너레이터 표현식에서는 앞의 버전을 허용하지 않으므로:

    (x for x in 1, 2, 3)
    

    유효하지 않습니다.

    기존 리스트 컴프리헨션 구문은 Python 3.0에서 유효하지 않게 되며, Python 2.4 이후에는 사용 중단이 권고되어야 합니다.

    리스트 컴프리헨션은 루프 변수를 주변 스코프에 “누출”하기도 합니다. 이 또한 Python 3.0에서 변경되므로, Python 3.0에서 리스트 컴프리헨션의 의미론적 정의는 list(<제너레이터 표현식>)과 동등해집니다. Python 2.4 이후에는 리스트 컴프리헨션의 루프 변수가 바로 주변 스코프에서 사용되는 변수와 동일한 이름을 가질 경우 사용 중단 경고를 발행해야 합니다.

조기 바인딩과 지연 바인딩

많은 논의 끝에, 첫 번째(가장 바깥쪽) for 표현식은 즉시 평가하고 나머지 표현식은 제너레이터가 실행될 때 평가하기로 결정되었습니다.

첫 번째 표현식을 바인딩하는 이유를 요약해 달라는 요청에 Guido는 [1]을 제시했습니다.:

Consider sum(x for x in foo()). Now suppose there's a bug in foo()
that raises an exception, and a bug in sum() that raises an
exception before it starts iterating over its argument. Which
exception would you expect to see? I'd be surprised if the one in
sum() was raised rather the one in foo(), since the call to foo()
is part of the argument to sum(), and I expect arguments to be
processed before the function is called.

OTOH, in sum(bar(x) for x in foo()), where sum() and foo()
are bugfree, but bar() raises an exception, we have no choice but
to delay the call to bar() until sum() starts iterating -- that's
part of the contract of generators. (They do nothing until their
next() method is first called.)

제너레이터가 정의될 때 모든 자유 변수를 바인딩하는 다양한 사용 사례가 제안되었습니다. 또한 일부 지지자들은 결과 표현식을 즉시 바인딩하면 이해하고 디버깅하기가 더 쉬워질 것이라고 생각했습니다.

그러나 Python은 람다 표현식에 지연 바인딩 방식을 사용하며 자동 조기 바인딩의 전례가 없습니다. 새로운 패러다임을 도입하면 불필요하게 복잡성이 추가될 것이라고 판단했습니다.

여러 가능성을 탐구한 끝에, 바인딩 문제는 이해하기 어렵기 때문에 사용자에게는 인자를 즉시 소비하는 함수 내부에서 제너레이터 표현식을 사용하도록 강력히 권장해야 한다는 데 합의가 이루어졌습니다. 더 복잡한 응용에서는, 범위·수명·바인딩을 명확히 드러낸다는 점에서 완전한 제너레이터 정의가 항상 더 우수합니다 [2].

축약 함수

sum(), min(), max()와 같은 축소 함수와 결합하면 제너레이터 표현식의 유용성이 크게 향상됩니다. Python 2.4의 heapq 모듈에는 nlargest()와 nsmallest()라는 두 가지 새로운 축소 함수가 포함되어 있습니다. 둘 다 제너레이터 표현식과 잘 작동하며, 한 번에 n개를 초과하는 항목을 메모리에 유지하지 않습니다.

감사의 말

  • Raymond Hettinger는 2002년 1월에 “제너레이터 컴프리헨션”이라는 개념을 처음 제안했습니다.
  • Peter Norvig는 Accumulation Displays에 관한 자신의 제안에서 이 논의를 되살렸습니다.
  • Alex Martelli는 제너레이터 표현식의 성능 이점을 입증하는 중요한 측정 결과를 제공했습니다. 그는 또한 이것이 갖출 만한 가치가 있는 기능이라는 강력한 근거도 제시했습니다.
  • Phillip Eby는 이름으로 “이터레이터 표현식”을 제안했습니다.
  • 이후 Tim Peters가 “제너레이터 표현식”이라는 이름을 제안했습니다.
  • Armin Rigo, Tim Peters, Guido van Rossum, Samuele Pedroni, Hye-Shik Chang, Raymond Hettinger는 이른 바인딩과 늦은 바인딩을 둘러싼 문제들을 규명해냈습니다 [1].
  • Jiwon Seo는 CVS에 적재된 최종 버전을 포함하여 이 제안의 여러 버전을 홀로 구현했습니다. 그 과정에서 Hye-Shik Chang과 Raymond Hettinger가 정기적으로 코드 리뷰를 진행했습니다. Guido van Rossum은 Armin Rigo의 의견과 뉴스그룹 논의를 거쳐 핵심 설계 결정을 내렸습니다. Raymond Hettinger는 테스트 스위트, 문서, 튜토리얼, 예제를 제공했습니다 [2].

참고 문헌