>  기사  >  백엔드 개발  >  Python의 Yield 및 Generator에 대한 자세한 소개

Python의 Yield 및 Generator에 대한 자세한 소개

Y2J
Y2J원래의
2017-04-27 11:55:381178검색

이 글은 주로 Python의 Yield와 Generator 관련 정보를 얕은 것부터 더 깊은 것까지 설명합니다. 글의 소개는 매우 자세하며 필요한 모든 사람이 참조할 수 있는 내용이 있습니다.

머리말

이 글에서는 어떤 제너레이터, 어떻게 생성하는지 등의 내용을 포함하여 간단한 것부터 깊은 것까지 Yield와 Generator를 자세히 소개합니다. 발전기 및 발전기 기능, 발전기의 기본 및 고급 응용 시나리오, 발전기 사용 시 주의 사항입니다. 이 글에는 Enhanced Generator나 pep342 관련 내용이 포함되어 있지 않습니다. 이 부분은 나중에 소개하겠습니다.

생성기 기본

파이썬의 함수 정의에서 항복 표현식이 나타나면 실제로 정의된 것은 생성기입니다. 함수이며, 이 generator function 호출의 반환 값은 생성기입니다. 이 일반적인 함수 호출은 다릅니다. 예:

def gen_generator():
 yield 1
def gen_value():
 return 1
 
if __name__ == '__main__':
 ret = gen_generator()
 print ret, type(ret) #<generator object gen_generator at 0x02645648> <type &#39;generator&#39;>
 ret = gen_value()
 print ret, type(ret) # 1 <type &#39;int&#39;>

위 코드에서 볼 수 있듯이 gen_generator 함수는 생성기 인스턴스를 반환합니다.

generator에는 다음과 같은 Special이 있습니다.

__iter__ 을 구현해야 하는 반복자 프로토콜을 따르고 다음 인터페이스

•여러 번 입력할 수 있으며 여러 번 반환하면 일시 중지될 수 있습니다. 함수 본문의 코드 실행

테스트 코드를 살펴보겠습니다.

>>> def gen_example():
... print &#39;before any yield&#39;
... yield &#39;first yield&#39;
... print &#39;between yields&#39;
... yield &#39;second yield&#39;
... print &#39;no yield anymore&#39;
... 
>>> gen = gen_example()
>>> gen.next()    # 第一次调用next
before any yield
&#39;first yield&#39;
>>> gen.next()    # 第二次调用next
between yields
&#39;second yield&#39;
>>> gen.next()    # 第三次调用next
no yield anymore
Traceback (most recent call last):
 File "<stdin>", line 1, in <module>
StopIteratio

gen 예제 메서드를 호출하면 아무것도 출력되지 않습니다. 본문 코드가 아직 실행을 시작하지 않았습니다. 제너레이터의 다음 메소드가 호출되면 제너레이터는 항복 표현식을 실행하고, 항복 표현식의 내용을 반환한 다음 이 위치에서 일시 중지(중지)하므로 next에 대한 첫 번째 호출은 첫 번째 문장을 인쇄하고 "를 반환합니다. 첫 번째 수확량". 일시 중지한다는 것은 다음 메서드 호출이 재개될 때까지 메서드의 지역 변수, 포인터 정보 및 실행 환경이 저장된다는 의미입니다. next를 두 번째로 호출한 후 마지막 항복에서 일시 중지됩니다. next() 메서드가 다시 호출되면 StopIteration 예외가 발생합니다.

for 문은 StopIteration 예외를 자동으로 포착할 수 있기 때문에 생성기(기본적으로 모든 반복자)의 더 일반적인 방법은 이를 루프에서 사용하는 것입니다.

def generator_example():
 yield 1
 yield 2

if __name__ == &#39;__main__&#39;:
 for e in generator_example():
 print e
 # output 1 2

생성기는 다음에 의해 생성됩니다. 제너레이터 함수는 일반 함수와 어떻게 다른가요?

(1) 함수는 매번 첫 번째 줄부터 실행되고, 제너레이터는 마지막 Yield 시작부터 실행됩니다.

(2) 함수 호출은 한 번에 하나의 값(세트)을 반환하는 반면, 생성기는

을 여러 번 반환할 수 있습니다. (3) 함수는 수없이 반복적으로 호출될 수 있지만

함수에서 Yield를 사용한 후 함수를 호출하는 것은 생성기를 생성하는 방법입니다. 또 다른 일반적인 방법은 generator expression을 사용하는 것입니다. 예:

  >>> gen = (x * x for x in xrange(5))
  >>> print gen
  <generator object <genexpr> at 0x02655710>

발전기 애플리케이션

발전기 기본 애플리케이션

생성기를 사용하는 가장 중요한 이유는 모든 반환 값을 한 번에 생성하는 대신 요청에 따라 결과를 생성하고 "반환"할 수 있다는 것입니다. 게다가 때로는 "모든 반환 값"을 전혀 알 수 없습니다. .

예를 들어 다음 코드의 경우 

RANGE_NUM = 100
 for i in [x*x for x in range(RANGE_NUM)]: # 第一种方法:对列表进行迭代
 # do sth for example
 print i

 for i in (x*x for x in range(RANGE_NUM)): # 第二种方法:对generator进行迭代
 # do sth for example
 print i

위 코드에서 두 for 문의 출력은 동일합니다. 코드는 문자 그대로 대괄호와 소문자를 의미합니다. 괄호의 차이점. 그러나 이 차이점은 매우 다릅니다. 첫 번째 메서드는 목록을 반환하고 두 번째 메서드는 생성기 개체를 반환합니다. RANGE_NUM이 커질수록 첫 번째 메서드에서 반환되는 목록도 커지고 차지하는 메모리도 커지지만 두 번째 메서드에서는 차이가 없습니다.

무한 횟수로 "반환"할 수 있는 또 다른 예를 살펴보겠습니다.

def fib():
 a, b = 1, 1
 while True:
 yield a
 a, b = b, a+b

이 생성기는 셀 수 없이 많은 "반환 값"을 생성하는 기능이 있으며 사용자는 언제 중지할지 결정할 수 있습니다. 반복

Generator의 고급 응용

사용 시나리오 1:

Generator를 사용하여 다음 작업을 수행할 수 있습니다. 데이터 스트림 생성, 생성기 반환 값을 즉시 생성하지 않고 필요할 때까지 기다립니다. 예를 들어 로그 파일이 있고 각 줄에 대한 레코드가 생성됩니다. 각 기록마다 서로 다른 부서의 사람들이 이를 다르게 처리할 수 있지만 우리는 공통된 주문형 데이터 흐름을 제공할 수 있습니다.

def gen_data_from_file(file_name):
 for line in file(file_name):
 yield line

def gen_words(line):
 for word in (w for w in line.split() if w.strip()):
 yield word

def count_words(file_name):
 word_map = {}
 for line in gen_data_from_file(file_name):
 for word in gen_words(line):
  if word not in word_map:
  word_map[word] = 0
  word_map[word] += 1
 return word_map

def count_total_chars(file_name):
 total = 0
 for line in gen_data_from_file(file_name):
 total += len(line)
 return total
 
if __name__ == &#39;__main__&#39;:
 print count_words(&#39;test.txt&#39;), count_total_chars(&#39;test.txt&#39;)

위의 예는 2008년 PyCon 강의에서 나온 것입니다. gen_words gen_data_from_file은 데이터 생산자이고 count_words count_total_chars는 데이터 소비자입니다. 보시다시피 데이터는 미리 준비되지 않고 필요할 때만 가져옵니다. 또한 gen_words의 (w for w in line.split() if w.strip()) 도 생성기

를 생성합니다. 사용 시나리오 2:

一些编程场景中,一件事情可能需要执行一部分逻辑,然后等待一段时间、或者等待某个异步的结果、或者等待某个状态,然后继续执行另一部分逻辑。比如微服务架构中,服务A执行了一段逻辑之后,去服务B请求一些数据,然后在服务A上继续执行。或者在游戏编程中,一个技能分成分多段,先执行一部分动作(效果),然后等待一段时间,然后再继续。对于这种需要等待、而又不希望阻塞的情况,我们一般使用回调(callback)的方式。下面举一个简单的例子:

 def do(a):
 print &#39;do&#39;, a
 CallBackMgr.callback(5, lambda a = a: post_do(a))
 
 def post_do(a):
 print &#39;post_do&#39;, a

这里的CallBackMgr注册了一个5s后的时间,5s之后再调用lambda函数,可见一段逻辑被分裂到两个函数,而且还需要上下文的传递(如这里的参数a)。我们用yield来修改一下这个例子,yield返回值代表等待的时间。

 @yield_dec
 def do(a):
 print &#39;do&#39;, a
 yield 5
 print &#39;post_do&#39;, a

这里需要实现一个YieldManager, 通过yield_dec这个decrator将do这个generator注册到YieldManager,并在5s后调用next方法。Yield版本实现了和回调一样的功能,但是看起来要清晰许多。

下面给出一个简单的实现以供参考:

# -*- coding:utf-8 -*-
import sys
# import Timer
import types
import time

class YieldManager(object):
 def __init__(self, tick_delta = 0.01):
 self.generator_dict = {}
 # self._tick_timer = Timer.addRepeatTimer(tick_delta, lambda: self.tick())

 def tick(self):
 cur = time.time()
 for gene, t in self.generator_dict.items():
  if cur >= t:
  self._do_resume_genetator(gene,cur)

 def _do_resume_genetator(self,gene, cur ):
 try:
  self.on_generator_excute(gene, cur)
 except StopIteration,e:
  self.remove_generator(gene)
 except Exception, e:
  print &#39;unexcepet error&#39;, type(e)
  self.remove_generator(gene)

 def add_generator(self, gen, deadline):
 self.generator_dict[gen] = deadline

 def remove_generator(self, gene):
 del self.generator_dict[gene]

 def on_generator_excute(self, gen, cur_time = None):
 t = gen.next()
 cur_time = cur_time or time.time()
 self.add_generator(gen, t + cur_time)

g_yield_mgr = YieldManager()

def yield_dec(func):
 def _inner_func(*args, **kwargs):
 gen = func(*args, **kwargs)
 if type(gen) is types.GeneratorType:
  g_yield_mgr.on_generator_excute(gen)

 return gen
 return _inner_func

@yield_dec
def do(a):
 print &#39;do&#39;, a
 yield 2.5
 print &#39;post_do&#39;, a
 yield 3
 print &#39;post_do again&#39;, a

if __name__ == &#39;__main__&#39;:
 do(1)
 for i in range(1, 10):
 print &#39;simulate a timer, %s seconds passed&#39; % i
 time.sleep(1)
 g_yield_mgr.tick()

注意事项:

(1)Yield是不能嵌套的!

def visit(data):
 for elem in data:
 if isinstance(elem, tuple) or isinstance(elem, list):
  visit(elem) # here value retuened is generator
 else:
  yield elem
  
if __name__ == &#39;__main__&#39;:
 for e in visit([1, 2, (3, 4), 5]):
 print e

上面的代码访问嵌套序列里面的每一个元素,我们期望的输出是1 2 3 4 5,而实际输出是1  2  5 。为什么呢,如注释所示,visit是一个generator function,所以第4行返回的是generator object,而代码也没这个generator实例迭代。那么改改代码,对这个临时的generator 进行迭代就行了。

def visit(data):
 for elem in data:
 if isinstance(elem, tuple) or isinstance(elem, list):
  for e in visit(elem):
  yield e
 else:
  yield elem

或者在python3.3中 可以使用yield from,这个语法是在pep380加入的

 def visit(data):
 for elem in data:
  if isinstance(elem, tuple) or isinstance(elem, list):
  yield from visit(elem)
  else:
  yield elem

(2)generator function中使用return

在python doc中,明确提到是可以使用return的,当generator执行到这里的时候抛出StopIteration异常。

def gen_with_return(range_num):
 if range_num < 0:
 return
 else:
 for i in xrange(range_num):
  yield i

if __name__ == &#39;__main__&#39;:
 print list(gen_with_return(-1))
 print list(gen_with_return(1))

但是,generator function中的return是不能带任何返回值的


 def gen_with_return(range_num):
 if range_num < 0:
  return 0
 else:
  for i in xrange(range_num):
  yield i

上面的代码会报错:SyntaxError: 'return' with argument inside generator

总结

위 내용은 Python의 Yield 및 Generator에 대한 자세한 소개의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

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