[python] 파이썬 생성기 “보내기”기능 목적?

누군가 파이썬 생성기 함수와 관련된 “보내기”함수가 존재하는 이유를 예를 들어 줄 수 있습니까? 수확량 함수를 완전히 이해합니다. 그러나 보내기 기능이 혼란 스럽습니다. 이 방법에 대한 문서는 복잡합니다.

generator.send(value)

실행을 재개하고 생성기 함수에 값을 “보냅니다”. value 인수는 현재 수익률 표현식의 결과가됩니다. send () 메소드는 생성자가 생성 한 다음 값을 리턴하거나 생성기가 다른 값을 생성하지 않고 종료하면 StopIteration을 발생시킵니다.

그게 무슨 뜻이야? 가치가 함수의 입력이라고 생각 했습니까? “send () 메소드는 제너레이터가 생성 한 다음 값을 반환합니다”라는 문구도 yield 함수의 정확한 목적인 것 같습니다. yield는 생성기에서 산출 한 다음 값을 반환합니다.

누군가 나에게 수율을 달성 할 수없는 send를 사용하는 생성기의 예를 줄 수 있습니까?



답변

방금 생성 한 생성기로 값을 보내는 데 사용됩니다. 다음은 인공적인 (유용하지 않은) 설명 적 예입니다.

>>> def double_inputs():
...     while True:
...         x = yield
...         yield x * 2
...
>>> gen = double_inputs()
>>> next(gen)       # run up to the first yield
>>> gen.send(10)    # goes into 'x' variable
20
>>> next(gen)       # run up to the next yield
>>> gen.send(6)     # goes into 'x' again
12
>>> next(gen)       # run up to the next yield
>>> gen.send(94.3)  # goes into 'x' again
188.5999999999999

으로 만 할 수는 없습니다 yield.

그것이 왜 유용한 지에 관해서, 내가 본 최고의 사용 사례 중 하나는 Twisted ‘s입니다. @defer.inlineCallbacks 입니다. 기본적으로 다음과 같은 함수를 작성할 수 있습니다.

@defer.inlineCallbacks
def doStuff():
    result = yield takesTwoSeconds()
    nextResult = yield takesTenSeconds(result * 10)
    defer.returnValue(nextResult / 10)

무슨 일이 일어나는가는 나중에 계산 될 것으로 예상되는 값인 을 takesTwoSeconds()반환합니다 Deferred. 트위스트는 다른 스레드에서 계산을 실행할 수 있습니다. 계산이 완료되면이를 연기 된 것으로 전달한 다음 값이 doStuff()함수 로 다시 전송됩니다 . 그래서doStuff() 모든 종류의 계산 및 콜백 등을 수행 할 수 있다는 점을 제외하고는 일반적인 절차 함수와 거의 비슷하게 보일 수 있습니다.이 기능 이전의 대안은 다음과 같은 작업을 수행하는 것입니다.

def doStuff():
    returnDeferred = defer.Deferred()
    def gotNextResult(nextResult):
        returnDeferred.callback(nextResult / 10)
    def gotResult(result):
        takesTenSeconds(result * 10).addCallback(gotNextResult)
    takesTwoSeconds().addCallback(gotResult)
    return returnDeferred

훨씬 더 복잡하고 다루기 힘들다.


답변

이 기능은 코 루틴을 작성하는 것입니다

def coroutine():
    for i in range(1, 10):
        print("From generator {}".format((yield i)))
c = coroutine()
c.send(None)
try:
    while True:
        print("From user {}".format(c.send(1)))
except StopIteration: pass

인쇄물

From generator 1
From user 2
From generator 1
From user 3
From generator 1
From user 4
...

컨트롤이 어떻게 전달되는지 보시겠습니까? 그것들은 코 루틴입니다. 그들은 비동기 IO와 같은 모든 종류의 멋진 것들에 사용될 수 있습니다.

생성기와 전송이없는이 방법을 생각해보십시오.

==========       yield      ========
Generator |   ------------> | User |
==========                  ========

하지만 send를 사용하면 양방향 거리가됩니다.

==========       yield       ========
Generator |   ------------>  | User |
==========    <------------  ========
                  send

그러면 사용자가 제너레이터의 동작 사용자 정의하고 제너레이터 가 사용자에게 응답 하는 사용자에게 문이 열립니다 .


답변

이것은 누군가를 도울 수 있습니다. 다음은 send 기능에 영향을받지 않는 생성기입니다. 인스턴스화시 number 매개 변수를 사용하며 send의 영향을받지 않습니다.

>>> def double_number(number):
...     while True:
...         number *=2
...         yield number
...
>>> c = double_number(4)
>>> c.send(None)
8
>>> c.next()
16
>>> c.next()
32
>>> c.send(8)
64
>>> c.send(8)
128
>>> c.send(8)
256

다음은 send를 사용하여 동일한 유형의 함수를 수행하는 방법입니다. 각 반복마다 숫자 값을 변경할 수 있습니다.

def double_number(number):
    while True:
        number *= 2
        number = yield number

숫자에 대한 새 값을 보내면 결과가 변경되는 것을 볼 수 있으므로 다음과 같습니다.

>>> def double_number(number):
...     while True:
...         number *= 2
...         number = yield number
...
>>> c = double_number(4)
>>>
>>> c.send(None)
8
>>> c.send(5) #10
10
>>> c.send(1500) #3000
3000
>>> c.send(3) #6
6

이것을 for 루프에 넣을 수도 있습니다 :

for x in range(10):
    n = c.send(n)
    print n

도움이 필요하면이 훌륭한 튜토리얼을 확인하십시오 .


답변

발전기 사용에 대한 일부 사용 사례 send()

send()허용되는 발전기 :

  • 내부 실행 상태 기억
    • 우리의 현재 단계
    • 데이터의 현재 상태는 무엇입니까
  • 일련의 값 반환
  • 입력 시퀀스 수신

사용 사례는 다음과 같습니다.

레시피를 따르려고 시도

미리 정의 된 입력 세트를 순서대로 기대하는 레시피를 보자.

우리는 :

  • watched_attempt레시피에서 인스턴스 생성
  • 입력을 받자
  • 각 입력은 현재 냄비에있는 것에 대한 정보를 반환합니다.
  • 각 입력 검사에서 입력이 예상되는 것임을 확인하고 그렇지 않으면 실패합니다.

    def recipe():
        pot = []
        action = yield pot
        assert action == ("add", "water")
        pot.append(action[1])
    
        action = yield pot
        assert action == ("add", "salt")
        pot.append(action[1])
    
        action = yield pot
        assert action == ("boil", "water")
    
        action = yield pot
        assert action == ("add", "pasta")
        pot.append(action[1])
    
        action = yield pot
        assert action == ("decant", "water")
        pot.remove("water")
    
        action = yield pot
        assert action == ("serve")
        pot = []
        yield pot

이를 사용하려면 먼저 watched_attempt인스턴스를 작성하십시오 .

>>> watched_attempt = recipe()
>>> watched_attempt.next()
[]                                                                                                     

.next()생성기의 실행을 시작 하려면 호출 이 필요합니다.

반환 된 값은 냄비가 현재 비어 있음을 보여줍니다.

이제 레시피가 기대하는대로 몇 가지 조치를 수행하십시오.

>>> watched_attempt.send(("add", "water"))
['water']
>>> watched_attempt.send(("add", "salt"))
['water', 'salt']
>>> watched_attempt.send(("boil", "water"))
['water', 'salt']
>>> watched_attempt.send(("add", "pasta"))
['water', 'salt', 'pasta']
>>> watched_attempt.send(("decant", "water"))
['salt', 'pasta']
>>> watched_attempt.send(("serve"))
[] 

보시다시피, 냄비는 마침내 비어 있습니다.

조리법을 따르지 않으면 실패 할 것입니다 (무엇을 요리하려고 시도한 결과의 바람직한 결과 일 수 있습니다-지시 사항을 받았을 때 충분히주의를 기울이지 않았다는 것을 배우는 것입니다.

>>> watched_attempt = running.recipe()
>>> watched_attempt.next()
[]
>>> watched_attempt.send(("add", "water"))
['water']
>>> watched_attempt.send(("add", "pasta"))

---------------------------------------------------------------------------
AssertionError                            Traceback (most recent call last)
<ipython-input-21-facdf014fe8e> in <module>()
----> 1 watched_attempt.send(("add", "pasta"))

/home/javl/sandbox/stack/send/running.py in recipe()
     29
     30     action = yield pot
---> 31     assert action == ("add", "salt")
     32     pot.append(action[1])
     33

AssertionError:

그것을주의해라:

  • 예상되는 단계의 선형 순서가 있습니다
  • 단계가 다를 수 있습니다 (일부는 제거 중, 일부는 냄비에 추가)
  • 복잡한 클래스 나 유사한 구조를 사용할 필요없이 함수 / 제너레이터가 모든 작업을 수행합니다.

누계

생성기를 사용하여 전송 된 총 값을 추적 할 수 있습니다.

우리가 숫자를 추가 할 때마다 입력 수와 총 합계가 반환됩니다 (이전 입력이 전송 된 순간에 유효 함).

from collections import namedtuple

RunningTotal = namedtuple("RunningTotal", ["n", "total"])


def runningtotals(n=0, total=0):
    while True:
        delta = yield RunningTotal(n, total)
        if delta:
            n += 1
            total += delta


if __name__ == "__main__":
    nums = [9, 8, None, 3, 4, 2, 1]

    bookeeper = runningtotals()
    print bookeeper.next()
    for num in nums:
        print num, bookeeper.send(num)

결과는 다음과 같습니다.

RunningTotal(n=0, total=0)
9 RunningTotal(n=1, total=9)
8 RunningTotal(n=2, total=17)
None RunningTotal(n=2, total=17)
3 RunningTotal(n=3, total=20)
4 RunningTotal(n=4, total=24)
2 RunningTotal(n=5, total=26)
1 RunningTotal(n=6, total=27)


답변

send()방법은 수확량 표현식의 왼쪽에있는 값을 제어합니다.

수확량의 차이와 보유 가치를 이해하려면 먼저 파이썬 코드가 평가되는 순서를 빠르게 새로 고칩니다.

섹션 6.15 평가 순서

파이썬은 왼쪽에서 오른쪽으로 식을 평가합니다. 과제를 평가하는 동안 오른쪽이 왼쪽보다 먼저 평가됩니다.

따라서 a = b오른쪽 표현식 이 먼저 평가됩니다.

다음과 같이 a[p('left')] = p('right')오른쪽을 먼저 평가합니다.

>>> def p(side):
...     print(side)
...     return 0
...
>>> a[p('left')] = p('right')
right
left
>>>
>>>
>>> [p('left'), p('right')]
left
right
[0, 0]

yield는 무엇입니까?, yield, 함수 실행을 일시 중단하고 호출자에게 반환하고 일시 중단하기 전에 중단했던 곳에서 실행을 다시 시작합니다.

실행이 정확히 어디서 중단됩니까? 이미 추측했을 수도 있습니다 … 수율 표현의 오른쪽과 왼쪽 사이에 실행이 일시 중단되었습니다. 따라서 부호 new_val = yield old_val에서 실행이 중지되고 =오른쪽의 값 (정지 전이고 호출자에게 반환 된 값)이 왼쪽의 값 (다시 시작한 후 할당되는 값)과 다를 수 있습니다. 실행).

yield 하나는 오른쪽에, 다른 하나는 왼쪽에 2 개의 값을 생성합니다.

수익률 표현식의 왼쪽에있는 값을 어떻게 제어합니까? .send()방법을 통해 .

6.2.9. 수율 표현

재개 후 수익률 표현식의 값은 실행을 재개 한 방법에 따라 다릅니다. __next__()를 사용하는 경우 (일반적으로 for 또는 next()내장을 통해) 결과는 None입니다. 그렇지 않으면를 send()사용하면 결과가 해당 메소드에 전달 된 값이됩니다.


답변

send방법은 코 루틴을 구현 합니다.

코 루틴이 발생하지 않으면 프로그램 흐름 방식이 바뀌기 때문에 머리를 감쌀 수 없습니다. 자세한 내용 은 좋은 자습서 를 읽을 수 있습니다 .


답변

“수율”이라는 단어에는 두 가지 의미가 있습니다 : 무언가를 생산하는 것 (예 : 옥수수를 생산하는 것)과 누군가 / 다른 것을 계속하게하는 것 (예 : 보행자에게 양보하는 자동차). 두 정의 모두 파이썬에 적용됩니다.yield 키워드에 . 생성기 함수를 특별하게 만드는 것은 일반 함수와 달리 생성자 함수를 종료하지 않고 일시 중지하는 동안 호출자에게 값을 “반환”할 수 있다는 것입니다.

“왼쪽”끝과 “오른쪽”끝이있는 양방향 파이프의 한쪽 끝으로 생성기를 상상하는 것이 가장 쉽습니다. 이 파이프는 생성기 자체와 생성기 함수의 본문 사이에 값이 전송되는 매체입니다. 파이프의 각 끝에는 두 개의 작업이 있습니다.. 파이프 의 다른 끝이 값을 누를 때까지 차단하고 푸시 된 값을 반환합니다. 런타임에 실행은 파이프의 한쪽에있는 컨텍스트 사이에서 앞뒤로 튀어 오릅니다. 각 쪽은 다른쪽에 값을 보낼 때까지 실행됩니다.이 시점에서 중지되고 다른 쪽이 실행되도록하고 반대쪽이 멈추고 다시 시작합니다. 즉, 파이프의 각 끝은 값을받는 순간부터 값을 보내는 순간까지 이어집니다.push 이 값은 파이프의 다른 쪽 끝이 값을 가져올 때까지 값을 보내고 차단하며 아무것도 반환하지 않습니다. 과pull

파이프는 기능적으로 대칭이지만 일반적 으로이 답변에서 정의하고 있습니다. 왼쪽 끝은 생성기 함수의 본문 내에서만 사용할 수 있으며 yield키워드 를 통해 액세스 할 수 있으며 오른쪽 끝 생성기이며 발전기의 send기능. 단일 파이프의 각 단부에 대한 인터페이스로서, yield그리고 send이중 임무를 수행 및 그들 각각의 푸시 풀 값 모두에 / 파이프의 단부에서 yield우측으로 가압하면서 좌측 당기기 send반대한다. 이 이중 의무는와 같은 문장의 의미를 둘러싼 혼란의 핵심입니다 x = yield y. 실연 yieldsend이 명시 적 푸시 / 풀 단계로 그들의 의미는 훨씬 더 명확하게합니다 :

  1. g생성기가 있다고 가정 하십시오. g.send파이프의 오른쪽 끝을 통해 왼쪽으로 값을 푸시합니다.
  2. g일시 정지 컨텍스트 내에서 실행 하여 생성기 함수의 본문을 실행할 수 있습니다.
  3. 밀린 값 은 파이프의 왼쪽 끝에서 왼쪽 g.send으로 당겨 yield집니다. 에서 x = yield y, x끌어온 값이 지정됩니다.
  4. 다음 행을 포함 할 때까지 생성기 함수의 본문 내에서 실행이 계속됩니다 yield.
  5. yield파이프의 왼쪽 끝을 통해 오른쪽으로 값을 밀어 올립니다 g.send. 이어 x = yield y, y파이프를 통해 우측으로 가압된다.
  6. 제너레이터 기능의 바디 내에서 실행이 일시 중지되어 외부 스코프가 중단 된 위치에서 계속 될 수 있습니다.
  7. g.send 다시 시작하여 값을 가져 와서 사용자에게 반환합니다.
  8. g.send다음라고하며, 1 단계로 돌아갑니다.

순환하는 동안,이 절차가 시작을 가지고 : 때 g.send(None)– 무엇 인 next(g)에 대한 짧은 – 처음이라고합니다 (이 아닌 다른 뭔가를 통과하는 것은 불법입니다 None처음에 send호출). 그리고 yield생성기 함수의 본문에 더 이상 설명 이 없을 때 끝날 수 있습니다 .

yield진술 (또는 더 정확하게는 생성기)을 그렇게 특별 하게 만드는 이유를 알고 있습니까? measly return키워드 와 달리 yield호출자에게 값을 전달하고 존재하는 기능을 종료하지 않고도 호출자로부터 값을받을 수 있습니다! (물론 함수 또는 제너레이터를 종료하려는 경우 return키워드를 사용하는 것이 편리합니다 .) yield명령문이 발생하면 제너레이터 함수는 일시 정지 한 다음 왼쪽으로 돌아 오는 즉시 다시 선택합니다. 다른 값을 보내면 꺼집니다. 그리고 send외부에서 발전기 기능 내부와 통신하기위한 인터페이스 일뿐입니다.

이 푸시 / 풀 / 파이프 유추를 가능한 한 세분화하려면 1 ~ 5 단계를 제외 yield하고 send는 동일한 코인 파이프의 양면 인 홈을 구동하는 다음 의사 코드로 끝납니다 .

  1. right_end.push(None) # the first half of g.send; sending None is what starts a generator
  2. right_end.pause()
  3. left_end.start()
  4. initial_value = left_end.pull()
  5. if initial_value is not None: raise TypeError("can't send non-None value to a just-started generator")
  6. left_end.do_stuff()
  7. left_end.push(y) # the first half of yield
  8. left_end.pause()
  9. right_end.resume()
  10. value1 = right_end.pull() # the second half of g.send
  11. right_end.do_stuff()
  12. right_end.push(value2) # the first half of g.send (again, but with a different value)
  13. right_end.pause()
  14. left_end.resume()
  15. x = left_end.pull() # the second half of yield
  16. goto 6

중요한 변화는 우리가 분할해야한다는 것입니다 x = yield yvalue1 = g.send(value2)두 문장에 각각 : left_end.push(y)x = left_end.pull(); 및 value1 = right_end.pull()right_end.push(value2). yield키워드 에는 두 가지 특별한 경우가 있습니다 : x = yieldyield y. 이들에 대한 각각 문법적 있습니다 x = yield None_ = yield y # discarding value.

파이프를 통해 값이 전송되는 정확한 순서에 대한 자세한 내용은 아래를 참조하십시오.


다음은 위의 다소 긴 구체적인 모델입니다. 우선, 먼저 임의 생성기 주목해야한다 g, next(g)정확히 동일하다 g.send(None). 이를 염두에두고 우리는 send작동 방식 에만 초점을 맞추고 발전기를 발전시키는 것에 대해서만 이야기 할 수 있습니다 send.

우리가 가지고 있다고 가정

def f(y):  # This is the "generator function" referenced above
    while True:
        x = yield y
        y = x
g = f(1)
g.send(None)  # yields 1
g.send(2)     # yields 2

이제 f다음과 같은 일반적인 (비 생성기) 기능에 대한 대략적인 설탕 제거 정의 :

def f(y):
    bidirectional_pipe = BidirectionalPipe()
    left_end = bidirectional_pipe.left_end
    right_end = bidirectional_pipe.right_end

    def impl():
        initial_value = left_end.pull()
        if initial_value is not None:
            raise TypeError(
                "can't send non-None value to a just-started generator"
            )

        while True:
            left_end.push(y)
            x = left_end.pull()
            y = x

    def send(value):
        right_end.push(value)
        return right_end.pull()

    right_end.send = send

    # This isn't real Python; normally, returning exits the function. But
    # pretend that it's possible to return a value from a function and then
    # continue execution -- this is exactly the problem that generators were
    # designed to solve!
    return right_end
    impl()

이 변환에서 다음이 발생했습니다 f.

  1. 구현을 중첩 함수로 옮겼습니다.
  2. left_end중첩 함수에 right_end의해 액세스되고 외부 스코프에 의해 반환 및 액세스되는 양방향 파이프를 생성했습니다 right_end. 생성기 객체로 알고 있습니다.
  3. 중첩 된 함수 내, 우리가하는 첫 번째 일은 검사입니다 left_end.pull()입니다 None과정에서 밀어 값을 소모.
  4. 중첩 함수 내에서 명령문 x = yield y은 두 줄로 대체되었습니다 : left_end.push(y)x = left_end.pull().
  5. 에 대한 send함수를 정의했습니다.이 함수 는 이전 단계에서 명령문을 right_end바꾼 두 줄 x = yield y에 해당합니다.

이 환상의 세계에서 함수가 돌아온 후에도 계속 될 수 있고 g할당 된 right_end다음 impl()호출됩니다. 위의 예에서, 우리는 실행을 한 줄씩 따라야했지만, 대략 다음과 같은 일이 일어날 것입니다 :

left_end = bidirectional_pipe.left_end
right_end = bidirectional_pipe.right_end

y = 1  # from g = f(1)

# None pushed by first half of g.send(None)
right_end.push(None)
# The above push blocks, so the outer scope halts and lets `f` run until
# *it* blocks

# Receive the pushed value, None
initial_value = left_end.pull()

if initial_value is not None:  # ok, `g` sent None
    raise TypeError(
        "can't send non-None value to a just-started generator"
    )

left_end.push(y)
# The above line blocks, so `f` pauses and g.send picks up where it left off

# y, aka 1, is pulled by right_end and returned by `g.send(None)`
right_end.pull()

# Rinse and repeat
# 2 pushed by first half of g.send(2)
right_end.push(2)
# Once again the above blocks, so g.send (the outer scope) halts and `f` resumes

# Receive the pushed value, 2
x = left_end.pull()
y = x  # y == x == 2

left_end.push(y)
# The above line blocks, so `f` pauses and g.send(2) picks up where it left off

# y, aka 2, is pulled by right_end and returned to the outer scope
right_end.pull()

x = left_end.pull()
# blocks until the next call to g.send

위의 16 단계 의사 코드에 정확하게 매핑됩니다.

오류가 전파되는 방법 및 발생기 끝에 도달 할 때 발생하는 상황 (파이프가 닫힘)과 같은 다른 세부 사항이 있지만이를 사용하면 기본 제어 흐름이 작동하는 방식이 명확해야합니다 send.

이 동일한 설탕 제거 규칙을 사용하여 두 가지 특별한 경우를 살펴 보겠습니다.

def f1(x):
    while True:
        x = yield x

def f2():  # No parameter
    while True:
        x = yield x

대부분의 경우와 같은 방식으로 설탕을 제거합니다 f. 단, 차이점은 yield문장이 어떻게 변환 되는지입니다 .

def f1(x):
    # ... set up pipe

    def impl():
        # ... check that initial sent value is None

        while True:
            left_end.push(x)
            x = left_end.pull()

    # ... set up right_end


def f2():
    # ... set up pipe

    def impl():
        # ... check that initial sent value is None

        while True:
            left_end.push(x)
            x = left_end.pull()

    # ... set up right_end

첫 번째로 전달 된 값 f1은 처음에 푸시 (수율) 된 다음 당겨진 (송신 된) 모든 값이 다시 푸시 (수율)됩니다. 제에서, x첫 회 온 (아직) 값이없는 push이되도록 UnboundLocalError상승된다.