>  기사  >  백엔드 개발  >  Python의 반복자와 생성기의 고급 사용법

Python의 반복자와 생성기의 고급 사용법

高洛峰
高洛峰원래의
2017-03-01 14:09:141069검색

Iterator

반복자는 반복 프로토콜을 준수하는 객체입니다. 즉, 기본적으로 호출 시 Next 프로젝트 시퀀스를 반환하는 next 메소드가 있음을 의미합니다. 반환할 항목이 없으면 StopIteration 예외를 발생시킵니다.

반복 개체는 하나의 루프를 허용합니다. 단일 반복의 상태(위치)를 유지하거나 다른 관점에서 보면 시퀀스가 ​​반복될 때마다 반복 개체가 필요합니다. 이는 동일한 시퀀스를 두 번 이상 반복할 수 있음을 의미합니다. 반복 로직을 시퀀스에서 분리하면 더 많은 반복 방법이 제공됩니다.

반복자 객체를 생성하기 위해 컨테이너의 __iter__ 메서드를 호출하는 것은 반복자를 마스터하는 가장 직접적인 방법입니다. iter 기능을 사용하면 일부 키 입력을 절약할 수 있습니다.

>>> nums = [1,2,3]   # note that ... varies: these are different objects
>>> iter(nums)              
<listiterator object at ...>
>>> nums.__iter__()           
<listiterator object at ...>
>>> nums.__reversed__()         
<listreverseiterator object at ...>

>>> it = iter(nums)
>>> next(it)      # next(obj) simply calls obj.next()
1
>>> it.next()
2
>>> next(it)
3
>>> next(it)
Traceback (most recent call last):
 File "<stdin>", line 1, in <module>
StopIteration

루프 내에서 사용하면 StopIteration이 허용되고 루프가 중지됩니다. 그러나 명시적 호출을 사용하면 반복기 요소가 모두 소진되면 이에 액세스하면 예외가 발생한다는 것을 알 수 있습니다.

for...in 루프를 사용하고 __iter__ 메소드도 사용하세요. 이를 통해 시퀀스에 대한 반복을 투명하게 시작할 수 있습니다. 그러나 이미 반복자가 있는 경우 for 루프에서 유사하게 사용할 수 있기를 원합니다. 이를 달성하기 위해 반복자에는 next 외에도 반복자 자체(self)를 반환하는 __iter__ 메서드도 있습니다.

Python의 반복자 지원은 어디에나 있습니다. 표준 라이브러리의 모든 시퀀스와 순서가 지정되지 않은 컨테이너가 지원됩니다. 이 개념은 다른 것에도 확장되었습니다. 예를 들어 파일 객체는 줄 반복을 지원합니다.

>>> f = open(&#39;/etc/fstab&#39;)
>>> f is f.__iter__()
True

파일 자체는 반복자이며 해당 __iter__ 메서드는 별도의 개체를 생성하지 않습니다. 단일 스레드 순차 읽기만 허용됩니다.

표현식 생성
반복 가능한 객체를 생성하는 두 번째 방법은 목록 이해의 기초인 생성기 표현식을 사용하는 것입니다. 명확성을 높이기 위해 생성된 표현식은 항상 괄호나 표현식으로 묶입니다. 괄호를 사용하면 생성기 반복자가 생성됩니다. 대괄호의 경우 이 프로세스는 '단락'되어 목록을 얻습니다.

>>> (i for i in nums)          
<generator object <genexpr> at 0x...>
>>> [i for i in nums]
[1, 2, 3]
>>> list(i for i in nums)
[1, 2, 3]

Python 2.7 및 3.x에서는 목록 표현식 구문이 사전 및 집합 표현식으로 확장되었습니다. 생성된 표현식을 중괄호로 묶으면 세트가 생성됩니다. 표현식에 key:value:

>>> {i for i in range(3)}  
set([0, 1, 2])
>>> {i:i**2 for i in range(3)}  
{0: 0, 1: 1, 2: 4}

형식의 키-값 쌍이 포함된 경우 사전 사전 사전이 생성됩니다. Python 버전에서는 구문이 약간 나쁩니다.

>>> set(i for i in &#39;abc&#39;)
set([&#39;a&#39;, &#39;c&#39;, &#39;b&#39;])
>>> dict((i, ord(i)) for i in &#39;abc&#39;)
{&#39;a&#39;: 97, &#39;c&#39;: 99, &#39;b&#39;: 98}

식을 생성하는 것은 매우 간단하며 말할 필요도 없습니다. 언급할 가치가 있는 단 한 가지 문제가 있습니다: 인덱스 변수 (i)는 Python 3 미만 버전에서 누출됩니다.

생성기

생성기는 단일 값이 아닌 결과 목록을 생성하는 함수입니다.

반복 가능한 객체를 생성하는 세 번째 방법은 생성기 함수를 호출하는 것입니다. 제너레이터는 Yield 키워드를 포함하는 함수입니다. 이 키워드가 존재한다는 것만으로도 함수의 성격이 완전히 바뀌는 점은 주목할 가치가 있습니다. 즉, Yield 문을 호출할 필요도 없고 액세스할 필요도 없습니다. 하지만 함수가 생성자가 되도록 하세요. 함수가 호출되면 그 안에 있는 명령이 실행됩니다. 그리고 생성기가 호출되면 실행은 해당 생성기의 첫 번째 명령 이전에 중지됩니다. 생성기를 호출하면 반복 프로토콜에 연결된 생성기 개체가 생성됩니다. 일반 함수와 마찬가지로 동시 및 재귀 호출이 허용됩니다.
next가 호출되면 함수는 첫 번째 산출물까지 실행됩니다. Yield 문이 발생할 때마다 next로 반환되는 값을 가져옵니다. Yield 문이 실행된 후 함수 실행이 중지됩니다.

>>> def f():
...  yield 1
...  yield 2
>>> f()                  
<generator object f at 0x...>
>>> gen = f()
>>> gen.next()
1
>>> gen.next()
2
>>> gen.next()
Traceback (most recent call last):
 File "<stdin>", line 1, in <module>
StopIteration

단일 생성기 함수 호출의 전체 프로세스를 살펴보겠습니다.

>>> def f():
...  print("-- start --")
...  yield 3
...  print("-- middle --")
...  yield 4
...  print("-- finished --")
>>> gen = f()
>>> next(gen)
-- start --
3
>>> next(gen)
-- middle --
4
>>> next(gen)              
-- finished --
Traceback (most recent call last):
 ...
StopIteration

일반 함수에서 f()를 실행하여 즉시 print를 실행하는 것과 비교하면 함수 본문에 어떤 문장도 실행하지 않고 gen을 할당합니다. 다음에 gen.next()가 호출될 때만 첫 번째 Yield 부분까지의 명령문이 실행됩니다. 두 번째 문은 -- middle --을 인쇄하고 두 번째 항복이 발생하면 실행을 중지합니다. 세 번째 다음 인쇄가 완료되고 함수가 끝날 때까지 결과가 없으므로 예외가 발생합니다.

함수가 반환된 후 제어권이 호출자에게 반환되면 어떻게 되나요? 각 생성기의 상태는 생성기 개체에 저장됩니다. 이 시점에서 생성기 함수는 별도의 스레드에서 실행되는 것처럼 보이지만 이것은 단지 환상일 뿐입니다. 실행은 엄격하게 단일 스레드이지만 인터프리터는 다음 값 요청 사이에 상태를 유지하고 저장합니다.

생성기가 왜 유용한가요? 반복자 섹션에서 강조했듯이 생성기 함수는 반복 가능한 객체를 생성하는 또 다른 방법일 뿐입니다. Yield 문으로 완료할 수 있는 모든 작업은 다음 메서드로도 완료할 수 있습니다. 그러나 인터프리터가 마술처럼 반복자를 생성할 수 있도록 함수를 사용하면 이점이 있습니다. 함수는 next 및 __iter__ 메서드가 필요한 클래스 정의보다 훨씬 짧을 수 있습니다. 더 중요한 것은 생성기 작성자가 연속적인 next 호출 사이에 반복자 객체의 인스턴스 속성을 전달하는 것보다 지역 변수에 지역화된 명령문을 더 쉽게 이해할 수 있다는 것입니다.

还有问题是为何迭代器有用?当一个迭代器用来驱动循环,循环变得简单。迭代器代码初始化状态,决定是否循环结束,并且找到下一个被提取到不同地方的值。这凸显了循环体——最值得关注的部分。除此之外,可以在其它地方重用迭代器代码。

双向通信
每个yield语句将一个值传递给调用者。这就是为何PEP 255引入生成器(在Python2.2中实现)。但是相反方向的通信也很有用。一个明显的方式是一些外部(extern)语句,或者全局变量或共享可变对象。通过将先前无聊的yield语句变成表达式,直接通信因PEP 342成为现实(在2.5中实现)。当生成器在yield语句之后恢复执行时,调用者可以对生成器对象调用一个方法,或者传递一个值 给 生成器,然后通过yield语句返回,或者通过一个不同的方法向生成器注入异常。

第一个新方法是send(value),类似于next(),但是将value传递进作为yield表达式值的生成器中。事实上,g.next()和g.send(None)是等效的。

第二个新方法是throw(type, value=None, traceback=None),等效于在yield语句处

raise type, value, traceback

不像raise(从执行点立即引发异常),throw()首先恢复生成器,然后仅仅引发异常。选用单次throw就是因为它意味着把异常放到其它位置,并且在其它语言中与异常有关。

当生成器中的异常被引发时发生什么?它可以或者显式引发,当执行某些语句时可以通过throw()方法注入到yield语句中。任一情况中,异常都以标准方式传播:它可以被except和finally捕获,或者造成生成器的中止并传递给调用者。

因完整性缘故,值得提及生成器迭代器也有close()方法,该方法被用来让本可以提供更多值的生成器立即中止。它用生成器的__del__方法销毁保留生成器状态的对象。

让我们定义一个只打印出通过send和throw方法所传递东西的生成器。

>>> import itertools
>>> def g():
...   print &#39;--start--&#39;
...   for i in itertools.count():
...     print &#39;--yielding %i--&#39; % i
...     try:
...       ans = yield i
...     except GeneratorExit:
...       print &#39;--closing--&#39;
...       raise
...     except Exception as e:
...       print &#39;--yield raised %r--&#39; % e
...     else:
...       print &#39;--yield returned %s--&#39; % ans

>>> it = g()
>>> next(it)
--start--
--yielding 0--
0
>>> it.send(11)
--yield returned 11--
--yielding 1--
1
>>> it.throw(IndexError)
--yield raised IndexError()--
--yielding 2--
2
>>> it.close()
--closing--

注意: next还是__next__?

在Python 2.x中,接受下一个值的迭代器方法是next,它通过全局函数next显式调用,意即它应该调用__next__。就像全局函数iter调用__iter__。这种不一致在Python 3.x中被修复,it.next变成了it.__next__。对于其它生成器方法——send和throw情况更加复杂,因为它们不被解释器隐式调用。然而,有建议语法扩展让continue带一个将被传递给循环迭代器中send的参数。如果这个扩展被接受,可能gen.send会变成gen.__send__。最后一个生成器方法close显然被不正确的命名了,因为它已经被隐式调用。

链式生成器
注意: 这是PEP 380的预览(还未被实现,但已经被Python3.3接受)

比如说我们正写一个生成器,我们想要yield一个第二个生成器——一个子生成器(subgenerator)——生成的数。如果仅考虑产生(yield)的值,通过循环可以不费力的完成:

subgen = some_other_generator()
for v in subgen:
  yield v

然而,如果子生成器需要调用send()、throw()和close()和调用者适当交互的情况下,事情就复杂了。yield语句不得不通过类似于前一章节部分定义的try...except...finally结构来保证“调试”生成器函数。这种代码在PEP 380中提供,现在足够拿出将在Python 3.3中引入的新语法了:

yield from some_other_generator()

像上面的显式循环调用一样,重复从some_other_generator中产生值直到没有值可以产生,但是仍然向子生成器转发send、throw和close。

更多Python中的迭代器与生成器高级用法相关文章请关注PHP中文网!

성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.