실제로 Python 3.3의 새로운 "yield from"구문에 대한 주요 용도는 무엇입니까?


407

나는 PEP 380 주위에 나의 두뇌를 감싸는 데 어려움을 겪고있다 .

  1. "yield from"이 유용한 상황은 무엇입니까?
  2. 기본 사용 사례는 무엇입니까?
  3. 왜 마이크로 스레드와 비교됩니까?

[업데이트]

이제 나는 어려움의 원인을 이해합니다. 나는 발전기를 사용했지만 실제로는 코 루틴을 사용하지 않았습니다 ( PEP-342에 의해 도입 됨 ). 몇 가지 유사점에도 불구하고 생성기와 코 루틴은 기본적으로 두 가지 다른 개념입니다. 새로운 구문을 이해하기 위해서는 코 루틴 (생성기뿐만 아니라)을 이해하는 것이 중요합니다.

IMHO 코 루틴은 가장 모호한 파이썬 기능 이며, 대부분의 책은 쓸모없고 흥미롭지 않게 보입니다.

큰 답변에 감사하지만 agfDavid Beazley 프레젠테이션 과 관련된 그의 의견에 특별히 감사드립니다 . 데이비드 바위.



7
David Beazley의 dabeaz.com/coroutines 프레젠테이션 비디오 : youtube.com/watch?v=Z_OAlIhXziw
jcugat

답변:


570

먼저 한 가지 방법을 찾아 보자. yield from g동등한 설명 은 모든 것에 대한 for v in g: yield v 정의시작하지도 않습니다yield from . 왜냐하면, 모든 yield from것이 for루프를 확장하는 것이라면 yield from언어에 추가하는 것을 보증하지 않으며 파이썬 2.x에서 구현되는 새로운 기능을 완전히 배제 하지 않습니다 .

어떤 yield from일은 그것이 호출 서브 발전기 사이에 투명 양방향 연결을 설정합니다 :

  • 연결은 생성되는 요소뿐만 아니라 모든 것을 올바르게 전파한다는 의미에서 "투명"합니다 (예 : 예외가 전파됨).

  • 연결 데이터가 모두 전송 될 수 있다는 의미에서 "양방향"인 발전기.

( 우리가 TCP에 관해 이야기했다면, yield from g"이제 클라이언트 소켓을 일시적으로 연결 해제했다가 다른 서버 소켓에 다시 연결하십시오"를 의미 할 수 있습니다. )

BTW, 데이터를 생성기로 전송하는 것이 무엇을 의미 하는지 확실하지 않은 경우 모든 것을 삭제하고 코 루틴 에 대해 먼저 읽어야 합니다. 매우 유용 하지만 ( 서브 루틴 과 대조적 이지만) 불행히도 Python에서는 잘 알려져 있지 않습니다. Coroutines에 관한 Dave Beazley의 Curious Course 는 훌륭한 출발입니다. 빠른 입문서를 위해 슬라이드 24-33읽으십시오 .

에서 산출량을 사용하여 발전기에서 데이터 읽기

def reader():
    """A generator that fakes a read from a file, socket, etc."""
    for i in range(4):
        yield '<< %s' % i

def reader_wrapper(g):
    # Manually iterate over data produced by reader
    for v in g:
        yield v

wrap = reader_wrapper(reader())
for i in wrap:
    print(i)

# Result
<< 0
<< 1
<< 2
<< 3

수동으로 반복하는 대신 reader()할 수 있습니다 yield from.

def reader_wrapper(g):
    yield from g

그것은 작동하고 한 줄의 코드를 제거했습니다. 그리고 아마도 의도가 조금 더 명확하거나 아닙니다. 그러나 인생은 변하지 않습니다.

-Part 1의 yield를 사용하여 생성기 (코 루틴)에 데이터 보내기

이제 더 흥미로운 것을 해봅시다. writer전송 된 데이터를 받아들이고 소켓, fd 등에 쓰는 코 루틴을 만들어 봅시다 .

def writer():
    """A coroutine that writes data *sent* to it to fd, socket, etc."""
    while True:
        w = (yield)
        print('>> ', w)

이제 문제는 어떻게 래퍼 함수의 핸들은 래퍼로 전송되는 모든 데이터가되고 그래서, 작가로 데이터를 전송해야한다 투명하게 받는 보내 writer()?

def writer_wrapper(coro):
    # TBD
    pass

w = writer()
wrap = writer_wrapper(w)
wrap.send(None)  # "prime" the coroutine
for i in range(4):
    wrap.send(i)

# Expected result
>>  0
>>  1
>>  2
>>  3

랩퍼 는 전송 된 데이터를 명백하게 받아 들여야StopIteration 하며 for 루프가 소진 될 때도 처리해야합니다 . 분명히 그냥 for x in coro: yield x하지 않을 것입니다. 다음은 작동하는 버전입니다.

def writer_wrapper(coro):
    coro.send(None)  # prime the coro
    while True:
        try:
            x = (yield)  # Capture the value that's sent
            coro.send(x)  # and pass it to the writer
        except StopIteration:
            pass

또는 우리는 이것을 할 수 있습니다.

def writer_wrapper(coro):
    yield from coro

그것은 6 줄의 코드를 절약하고 훨씬 더 읽기 쉽고 작동합니다. 마법!

-Part 2-예외 처리에서 제너레이터 수율로 데이터 전송

좀 더 복잡하게합시다. 작가가 예외를 처리해야하는 경우 어떻게해야합니까? writer핸들 a를 말하고 핸들 a를 만나면 SpamException인쇄 한다고 가정 해 봅시다 ***.

class SpamException(Exception):
    pass

def writer():
    while True:
        try:
            w = (yield)
        except SpamException:
            print('***')
        else:
            print('>> ', w)

변경하지 않으면 writer_wrapper어떻게됩니까? 작동합니까? 해보자

# writer_wrapper same as above

w = writer()
wrap = writer_wrapper(w)
wrap.send(None)  # "prime" the coroutine
for i in [0, 1, 2, 'spam', 4]:
    if i == 'spam':
        wrap.throw(SpamException)
    else:
        wrap.send(i)

# Expected Result
>>  0
>>  1
>>  2
***
>>  4

# Actual Result
>>  0
>>  1
>>  2
Traceback (most recent call last):
  ... redacted ...
  File ... in writer_wrapper
    x = (yield)
__main__.SpamException

음, x = (yield)그냥 예외를 제기하고 모든 것이 중단 되기 때문에 작동하지 않습니다 . 작동 시키지만 수동으로 예외를 처리하고 예외를 보내거나 하위 생성자 ( writer)에 던지도록합시다.

def writer_wrapper(coro):
    """Works. Manually catches exceptions and throws them"""
    coro.send(None)  # prime the coro
    while True:
        try:
            try:
                x = (yield)
            except Exception as e:   # This catches the SpamException
                coro.throw(e)
            else:
                coro.send(x)
        except StopIteration:
            pass

작동합니다.

# Result
>>  0
>>  1
>>  2
***
>>  4

그러나 이것도 마찬가지입니다!

def writer_wrapper(coro):
    yield from coro

yield from투명 핸들 값을 전송하거나 생성 부에 값을 던지고.

그래도 여전히 모든 코너 케이스를 다루지는 않습니다. 외부 발전기가 닫히면 어떻게됩니까? 하위 생성기가 값을 반환하는 경우 (예, Python 3.3 이상에서는 생성자가 값을 반환 할 수 있음) 반환 값을 어떻게 전파해야합니까? 그것은 yield from모든 코너 케이스를 투명하게 처리하는 것이 정말 인상적 입니다.yield from마술처럼 작동하고 모든 경우를 처리합니다.

개인적으로 yield from양방향 선택이 분명하지 않기 때문에 키워드 선택이 잘못되었다고 생각 합니다. 제안 된 다른 키워드가있었습니다 (예 : delegate언어에 새 키워드를 추가하는 것이 기존 키워드를 결합하는 것보다 훨씬 어렵 기 때문에 거부되었습니다).

요약하면 다음과 yield from같이 생각하는 것이 가장 좋습니다transparent two way channel 발신자 서브 발전기 사이.

참고 문헌 :

  1. PEP 380- 하위 생성기에 위임하기위한 구문 (유잉) [v3.3, 2009-02-13]
  2. PEP 342- 향상된 발전기 (GvR, Eby)를 통한 코 루틴 [v2.5, 2005-05-10]

3
@PraveenGollakota, 귀하의 질문의 두 번째 부분 에서-Part 1의 수익률을 사용하여 생성기 (코 루틴)에 데이터 보내기 , 수신 된 항목을 전달하기 위해 코 루틴 이상이 있다면 어떨까요? 예제에서 래퍼에 여러 코 루틴을 제공하는 브로드 캐스터 또는 가입자 시나리오와 같이 항목을 모두 또는 일부로 보내야합니까?
케빈 가부시

3
@PraveenGollakota, 큰 답변을위한 Kudos. 작은 예제를 통해 repl에서 시도해 볼 수 있습니다. Dave Beazley 코스에 대한 링크는 보너스였습니다!
BiGYaN

1
except StopIteration: passINSIDE를 수행 하면 while True:루프가 정확하게 표현 yield from coro되지 않습니다. 이는 무한 루프가 아니며 coro소진 된 후 (즉, StopIteration 발생) writer_wrapper다음 명령문을 실행합니다. 마지막 성명서 후 그것은 StopIteration지친 발전기로 자동 상승 할 것입니다 ...
Aprillion

1
... 대신에 writer포함 된 경우 인쇄 한 후에도 자동 올리기 가 수행되고 자동으로 처리 된 다음 자체적으로 자동 제기되며 블록 내부가 아니기 때문에 실제로이 시점에서 제기됩니다 ( 즉, traceback은 발전기 내부의 어떤 것도 아닌 라인 for _ in range(4)while True>> 3StopIterationyield fromwriter_wrapperStopIterationwrap.send(i)trywrap.send(i)
만보고합니다

3
" 정의를 시작하지도 않는다 "라는 말을 읽었을 때 , 나는 정답에 도달했다는 것을 안다. 좋은 설명 감사합니다!
Hot.PxL

89

"yield from"이 유용한 상황은 무엇입니까?

다음과 같은 루프가있는 모든 상황 :

for x in subgenerator:
  yield x

PEP에 설명 된 바와 같이, 이것은 subgenerator를 사용에서, 여러 측면을 누락 오히려 순 시도는 특히 적절한 처리이다 .throw()/ .send()/ .close()도입기구 PEP 342 . 이 작업을 제대로 수행하려면 다소 복잡한 코드가 필요합니다.

기본 사용 사례는 무엇입니까?

재귀 데이터 구조에서 정보를 추출하려고합니다. 트리에서 모든 리프 노드를 가져오고 싶다고합시다.

def traverse_tree(node):
  if not node.children:
    yield node
  for child in node.children:
    yield from traverse_tree(child)

더욱 중요한 것은, 이전까지 yield from생성기 코드를 리팩토링하는 간단한 방법이 없다는 사실입니다 . 다음과 같이 (무의미한) 생성기가 있다고 가정하십시오.

def get_list_values(lst):
  for item in lst:
    yield int(item)
  for item in lst:
    yield str(item)
  for item in lst:
    yield float(item)

이제이 루프를 별도의 발전기로 분리하기로 결정했습니다. 이 없으면 yield from실제로 추론하고 싶은지 두 번 생각할 때까지 추한 것입니다. 를 사용하면 yield from실제로 살펴 보는 것이 좋습니다.

def get_list_values(lst):
  for sub in [get_list_values_as_int, 
              get_list_values_as_str, 
              get_list_values_as_float]:
    yield from sub(lst)

왜 마이크로 스레드와 비교됩니까?

나는 무엇을 생각 PEP에이 섹션 에 대해 이야기하는 것은 모든 제너레이터가 자체 격리 된 실행 컨텍스트가 없다는 것입니다. 함께 실행 발전기 반복자 및 사용 호출자 절환된다는 사실로 yield하고 __next__(), 각각이 운영 체제가 실행 콘텍스트와 함께 수시로 실행 스레드 스위치 스레드 (스택 레지스터 유사 ...).

이것의 효과도 비슷합니다. 제너레이터-반복자와 호출자가 동시에 실행 상태에서 진행되면 실행이 인터리브됩니다. 예를 들어, 생성기가 어떤 종류의 계산을 수행하고 호출자가 결과를 인쇄하면 결과가 나 오자마자 표시됩니다. 이것은 동시성의 한 형태입니다.

그 비유는 특정 yield from적이지는 않지만 파이썬에서 생성기의 일반적인 속성입니다.


리팩토링 생성기는 오늘날 고통 스럽 습니다.
Josh Lee

1
나는 itertools를 생성기 (itertools.chain과 같은 것들)를 리팩토링하기 위해 많이 사용하는 경향이 있습니다. 나는 수확량을 좋아하지만 여전히 얼마나 혁명적인지 알지 못합니다. Guido가 그것에 대해 모두 미쳤 기 때문에 아마도 큰 그림이 누락되어야합니다. 리팩토링하기가 어렵 기 때문에 send ()에 유용하다고 생각하지만 자주 사용하지는 않습니다.
전자 좌석

나는 이것들 get_list_values_as_xxx이 하나의 라인을 가진 간단한 발전기 for x in input_param: yield int(x)이고 다른 하나 는 strand와float
madtyn

@NiklasB. "재귀 적 데이터 구조에서 정보를 추출합니다." 데이터를 위해 Py에 들어가고 있습니다. 이 Q에 찌를 수 있습니까?
alancalvitti

33

생성기 내에서 생성기를 호출 할 때마다 yield값 을 다시 지정 하려면 "펌프"가 필요합니다 for v in inner_generator: yield v. PEP가 지적했듯이, 대부분의 사람들이 무시하는 미묘한 복잡성이 있습니다. 로컬이 아닌 흐름 제어 throw()는 PEP에 주어진 예입니다. 새로운 구문 yield from inner_generatorfor이전에 명시 적 루프를 작성했을 때 사용됩니다 . 그것은 단지 구문 설탕이 아닙니다 : 그것은 for루프에 의해 무시되는 모든 코너 케이스를 처리합니다 . "설탕"은 사람들이 그것을 사용하여 올바른 행동을하도록 격려합니다.

토론 스레드에서이 메시지는 다음과 같은 복잡성에 대해 설명합니다 .

PEP 342에 도입 된 추가 생성기 기능을 사용하면 더 이상 그렇지 않습니다. Greg의 PEP에 설명 된 것처럼 단순 반복은 send () 및 throw ()를 올바르게 지원하지 않습니다. send () 및 throw ()를 지원해야하는 체조는 실제로 분해 할 때 그렇게 복잡하지는 않지만 사소한 것도 아닙니다.

제너레이터가 패러렐 리즘이라는 것을 관찰하는 것 외에는 마이크로 스레드와 비교할 수 없습니다 . 일시 중단 된 생성기를 yield소비자 스레드 로 값을 보내는 스레드로 간주 할 수 있습니다 . 실제 구현은 이와 같지 않을 수도 있으며 (실제 구현은 분명히 Python 개발자에게 큰 관심사입니다) 그러나 이것은 사용자와 관련이 없습니다.

새로운 yield from구문은 스레딩 측면에서 언어에 추가 기능을 추가하지 않고 기존 기능을보다 쉽게 ​​사용할 수있게합니다. 보다 정확하게 는 전문가 가 작성한 복잡한 내부 발전기 의 초보자 소비자가 복잡한 기능을 손상시키지 않고 해당 발전기를 쉽게 통과 할 수 있습니다.


23

간단한 예제는 yield from의 사용 사례 중 하나를 이해하는 데 도움이 됩니다. 다른 생성기에서 가치 얻기

def flatten(sequence):
    """flatten a multi level list or something
    >>> list(flatten([1, [2], 3]))
    [1, 2, 3]
    >>> list(flatten([1, [2], [3, [4]]]))
    [1, 2, 3, 4]
    """
    for element in sequence:
        if hasattr(element, '__iter__'):
            yield from flatten(element)
        else:
            yield element

print(list(flatten([1, [2], [3, [4]]])))

2
막바지로 인쇄하면 목록으로 변환하지 않고 조금 더 멋지게 보일 것이라고 제안하고 싶었습니다.print(*flatten([1, [2], [3, [4]]]))
yoniLavi

6

yield from 기본적으로 반복자를 효율적인 방식으로 연결합니다.

# chain from itertools:
def chain(*iters):
    for it in iters:
        for item in it:
            yield item

# with the new keyword
def chain(*iters):
    for it in iters:
        yield from it

보시다시피 하나의 순수한 파이썬 루프가 제거됩니다. 그것은 거의 모든 일이지만 체인 반복자는 Python에서 매우 일반적인 패턴입니다.

스레드는 기본적으로 완전히 임의의 지점에서 함수를 뛰어 넘어 다른 함수의 상태로 되돌아 갈 수있는 기능입니다. 스레드 관리자는이 작업을 매우 자주 수행하므로 프로그램은 이러한 모든 기능을 동시에 실행하는 것으로 보입니다. 문제는 점이 임의적이므로 관리자가 문제가있는 지점에서 기능을 중지하지 못하도록 잠금을 사용해야합니다.

제너레이터는 이러한 의미에서 스레드와 매우 유사합니다 yield. 즉, 점프 할 때마다 특정 포인트를 지정할 수 있습니다 (언제든지 ). 이런 식으로 사용하면 생성기를 코 루틴이라고합니다.

자세한 내용은 Python의 코 루틴에 대한이 훌륭한 자습서를 읽으십시오.


10
이 답변은 위에서 언급 한 것처럼 send () 및 throw () 지원과 같이 "yield from"의 두드러진 기능을 제거하기 때문에 오해의 소지가 있습니다.
저스틴 W

2
@ 저스틴 W : 코드를 단순화해야하기 때문에 분명히 올바르게 구현 해야하는 기능을 이해하지 못했기 때문에 이전에 읽은 내용이 실제로 오도 된 것 throw()/send()/close()같습니다 . 이러한 사소한 부분은 사용법과 아무 관련이 없습니다. yieldyield from
Jochen Ritzel

5
위의 벤 잭슨의 대답에 논란이 있습니까? 귀하의 답변에 대한 나의 독서는 본질적으로 귀하가 제공 한 코드 변환을 따르는 구문 설탕이라는 것입니다. 벤 잭슨의 대답은 특히 그 주장을 반박한다.
저스틴 W

@JochenRitzel 이미 존재 chain하기 때문에 자신의 함수 를 작성할 필요가 없습니다 itertools.chain. 사용하십시오 yield from itertools.chain(*iters).
Acumenus

3

에 적용 사용에 비동기 IO의 코 루틴 , yield from와 유사한 행위가 awaitA의 코 루틴 기능을 . 둘 다 코 루틴 실행을 일시 중단하는 데 사용됩니다.

Asyncio의 경우 이전 Python 버전 (예 :> 3.5)을 지원할 필요가없는 경우 async def/ await는 코 루틴을 정의하는 데 권장되는 구문입니다. 따라서 yield from코 루틴에서는 더 이상 필요하지 않습니다.

그러나 일반적으로 asyncio 외부 에서는 이전 답변에서 언급 한 것처럼 하위 생성기yield from <sub-generator>반복하는 데 여전히 다른 용도가 있습니다 .


1

이 코드는 fixed_sum_digits6 자리 숫자를 모두 열거하여 숫자의 합이 20이되도록 생성기를 반환 하는 함수를 정의합니다 .

def iter_fun(sum, deepness, myString, Total):
    if deepness == 0:
        if sum == Total:
            yield myString
    else:  
        for i in range(min(10, Total - sum + 1)):
            yield from iter_fun(sum + i,deepness - 1,myString + str(i),Total)

def fixed_sum_digits(digits, Tot):
    return iter_fun(0,digits,"",Tot) 

없이 작성하십시오 yield from. 효과적인 방법을 찾으면 알려주십시오.

나는이 같은 경우 : 나무를 방문 yield from하면 코드를보다 간단하고 깨끗하게 만듭니다.


당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.