'수율'과 같은 생성기 언어 기능이 좋은 생각입니까?


9

PHP, C #, Python 및 다른 일부 언어에는 yield생성기 함수를 만드는 데 사용되는 키워드가 있습니다.

PHP에서 : http://php.net/manual/en/language.generators.syntax.php

파이썬에서 : https://www.pythoncentral.io/python-generators-and-yield-keyword/

C #에서 : https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/yield

언어 기능 / 시설로 인해 yield일부 규칙이 위반 될 수 있습니다. 그중 하나는 "확실성"입니다. 호출 할 때마다 다른 결과를 반환하는 메서드입니다. 일반 비 생성기 함수를 사용하면이를 호출 할 수 있으며 동일한 입력이 제공되면 동일한 출력을 리턴합니다. yield를 사용하면 내부 상태에 따라 다른 출력을 반환합니다. 따라서 이전 상태를 모르고 임의로 생성 함수를 호출하면 특정 결과를 리턴 할 것으로 기대할 수 없습니다.

이와 같은 기능이 언어 패러다임에 어떻게 맞습니까? 실제로 어떤 규칙을 위반합니까? 이 기능을 가지고 사용하는 것이 좋습니다? (좋은 점과 나쁜 점에 대한 예를 들자면 goto한때 많은 언어의 특징 이었지만 여전히 유해한 것으로 간주되어 Java와 같은 일부 언어에서는 제거 된 것입니다). 프로그래밍 언어 컴파일러 / 통역사는 이러한 기능을 구현하기 위해 어떤 규칙도 위반해야합니까? 예를 들어, 언어가이 기능이 작동하기 위해 멀티 스레딩을 구현해야합니까, 아니면 스레딩 기술없이 수행 할 수 있습니까?


4
yield본질적으로 상태 엔진입니다. 매번 같은 결과를 반환하는 것은 아닙니다. 그것은 무엇 것이다 절대 확실 할 것은 열거에서 호출 할 때마다 다음 항목을 반환합니다. 스레드는 필요하지 않습니다. 현재 상태를 유지하려면 클로저가 필요합니다.
Robert Harvey

1
"확실성"의 품질과 관련하여, 동일한 입력 시퀀스가 ​​주어지면 반복자에 대한 일련의 호출이 정확히 동일한 순서로 동일한 항목을 생성한다는 것을 고려하십시오.
Robert Harvey

4
C ++에는 Python과 같은 yield 키워드 가 없기 때문에 대부분의 질문이 어디에서 왔는지 잘 모르겠습니다 . 정적 메소드 std::this_thread::yield()가 있지만 키워드는 아닙니다. 따라서 this_thread거의 모든 호출을 앞에 추가하여 일반적으로 제어 흐름을 생성하는 언어 기능이 아니라 스레드를 생성하는 라이브러리 기능이라는 것이 상당히 분명합니다.
Ixrec

링크가 C #으로 업데이트되었으며 C ++ 용 링크 하나가 제거됨
Dennis

답변:


16

주의 사항-C #은 내가 가장 잘 알고있는 언어이며 yield다른 언어와 매우 유사한 것으로 보이지만 yield미지의 미묘한 차이가있을 수 있습니다.

언어 기능 / 기능으로 수율이 일부 규칙을 위반한다는 것이 우려됩니다. 그중 하나는 "확실성"입니다. 호출 할 때마다 다른 결과를 반환하는 메서드입니다.

어리석은 이야기. 당신이 할 정말 기대 Random.Next또는 Console.ReadLine 동일한 결과에게 당신이 그들을 호출 할 때마다 반환? 나머지 전화는 어떻습니까? 입증? 컬렉션에서 아이템을 가져 오시겠습니까? 불순한 모든 종류의 (좋은, 유용한) 기능이 있습니다.

이와 같은 기능이 언어 패러다임에 어떻게 맞습니까? 실제로 어떤 규칙을 위반합니까?

그렇습니다. yieldtry/catch/finally(과) 아주 나쁘게 연주 하고 허용되지 않습니다 ( https://blogs.msdn.microsoft.com/ericlippert/2009/07/16/iterator-blocks-part-three-why-no-yield-in-finally/ for 더 많은 정보).

이 기능을 가지고 사용하는 것이 좋습니다?

이 기능을 사용하는 것이 좋습니다. C #의 LINQ와 같은 것들이 정말 좋습니다. 게으르게 컬렉션을 평가하면 성능이 크게 향상 yield되며, 일부 코드에서 수동 롤링 반복자가 수행하는 버그의 일부만으로도 이런 종류의 작업을 수행 할 수 있습니다.

즉, yieldLINQ 스타일 수집 처리 외부 에서는 많은 용도가 없습니다 . 유효성 검사 처리, 일정 생성, 임의 추출 및 기타 몇 가지 사항에 사용했지만 대부분의 개발자가이 기능을 사용한 적이 없거나 잘못 사용했을 것으로 예상합니다.

프로그래밍 언어 컴파일러 / 통역사는 이러한 기능을 구현하기 위해 어떤 규칙도 위반해야합니까? 예를 들어, 언어가이 기능이 작동하기 위해 멀티 스레딩을 구현해야합니까, 아니면 스레딩 기술없이 수행 할 수 있습니까?

정확히. 컴파일러는 다음에 호출 될 때 다시 시작할 수 있도록 중지 된 위치를 추적하는 상태 머신 반복기를 생성합니다. 코드 생성 프로세스는 Continuation Passing Style과 유사한 작업을 수행합니다. 여기서 Continuation Passing Style은 코드 뒤의 코드를 yield자체 블록으로 가져옵니다 (및 yields, 다른 하위 블록 등이있는 경우). 그것은 함수형 프로그래밍에서 더 자주 사용되는 잘 알려진 접근법이며 C #의 비동기 / 대기 컴파일에도 나타납니다.

스레딩은 필요하지 않지만 대부분의 컴파일러에서 코드 생성에 다른 접근 방식이 필요하며 다른 언어 기능과 약간의 충돌이 있습니다.

그럼에도 불구 yield하고 특정 하위 집합 문제에 실제로 도움이되는 상대적으로 영향이 적은 기능입니다.


나는 C #을 심각하게 사용하지 않았지만이 yield키워드는 코 루틴과 비슷하거나 다른 것입니까? 그렇다면 내가 C에 하나 있었으면 좋겠다! 그런 언어 기능으로 작성하기가 훨씬 쉬운 코드 섹션을 생각할 수 있습니다.

2
@ DrunkCoder-비슷하지만 약간의 제한이 있지만 이해합니다.
Telastyn

1
또한 생산량 오용을보고 싶지 않을 것입니다. 언어의 기능이 많을수록 해당 언어로 잘못 작성된 프로그램을 찾을 가능성이 높습니다. 접근 가능한 언어를 작성하는 올바른 접근 방식이 모든 것을 당신에게 던지고 어떤 것이 붙어 있는지 보는 것이 확실하지 않습니다.
Neil

1
@ DrunkCoder : 세미 코 루틴의 제한된 버전입니다. 실제로, 그것은 일련의 메소드 호출, 클래스 및 객체로 확장되는 컴파일러에 의해 구문 패턴으로 취급됩니다. 기본적으로 컴파일러는 필드의 현재 컨텍스트를 캡처하는 연속 객체를 생성합니다. 컬렉션 의 기본 구현은 세 미코 루틴이지만 컴파일러가 사용하는 "매직"메서드를 오버로드하여 실제로 동작을 사용자 지정할 수 있습니다. 예를 들어, 언어에 async/ await를 추가 하기 전에 누군가가를 사용하여 언어를 구현했습니다 yield.
Jörg W Mittag

1
@Neil 일반적으로 거의 모든 프로그래밍 언어 기능을 잘못 사용할 수 있습니다. 당신이 말하는 것이 사실이라면, 파이썬이나 C #보다 C를 사용하여 잘못 프로그래밍하는 것이 훨씬 어렵지만, 그러한 언어에는 프로그래머가 많은 실수로부터 프로그래머를 보호하는 많은 도구가 있기 때문에 그렇지 않습니다. 실제로, 잘못된 프로그램의 원인은 나쁜 프로그래머입니다. 이는 언어에 구애받지 않는 문제입니다.
Ben Cottrell

12

yield좋은 아이디어 와 같은 생성기 언어 기능이 있습니까?

나는 파이썬 관점에서 이것에 대답하고 싶습니다. 그렇습니다 . 좋은 생각 입니다.

먼저 귀하의 질문에 대한 몇 가지 질문과 가정을 해결 한 다음 생성기의 보급 성과 나중에 파이썬에서 불합리한 유용성을 보여 드리겠습니다.

일반 비 생성기 함수를 사용하면이를 호출 할 수 있으며 동일한 입력이 제공되면 동일한 출력을 리턴합니다. yield는 내부 상태에 따라 다른 출력을 반환합니다.

이것은 거짓입니다. 객체의 메소드는 자체 내부 상태를 가진 함수 자체로 생각할 수 있습니다. 파이썬에서는 모든 것이 객체이기 때문에 실제로 객체에서 메소드를 가져 와서 그 메소드를 전달할 수 있습니다 (이 메소드는 객체에서 온 객체에 바인딩되어 있으므로 상태를 기억합니다).

다른 예로는 의도적으로 임의의 기능뿐만 아니라 네트워크, 파일 시스템 및 터미널과 같은 입력 방법이 있습니다.

이와 같은 기능이 언어 패러다임에 어떻게 맞습니까?

언어 패러다임이 퍼스트 클래스 함수와 같은 것을 지원하고 생성기가 Iterable 프로토콜과 같은 다른 언어 기능을 지원하면 완벽하게 맞습니다.

실제로 어떤 규칙을 위반합니까?

아니요. 언어로 구워 졌기 때문에이 규칙은 기본적으로 생성되며 생성기 사용을 포함하거나 필요로합니다!

프로그래밍 언어 컴파일러 / 통역사는 이러한 기능을 구현하기 위해 모든 규칙을 위반해야합니다.

다른 기능과 마찬가지로 컴파일러는 기능을 지원하도록 설계되어야합니다. Python의 경우 함수는 이미 상태가있는 객체입니다 (예 : 기본 인수 및 함수 주석).

언어가이 기능이 작동하기 위해 멀티 스레딩을 구현해야합니까, 아니면 스레딩 기술없이 수행 할 수 있습니까?

재미있는 사실 : 기본 파이썬 구현은 스레딩을 전혀 지원하지 않습니다. 여기에는 GIL (Global Interpreter Lock) 기능이 있으므로 다른 Python 인스턴스를 실행하기 위해 두 번째 프로세스를 실행하지 않으면 실제로는 동시에 실행되는 것이 없습니다.


참고 : 예제는 Python 3에 있습니다.

수확량을 넘어서

yield키워드는 어떤 함수에서도 키워드를 생성기로 바꿀 수 있지만 키워드를 만드는 유일한 방법은 아닙니다. 파이썬은 다른 반복자 (다른 생성기 포함)로 생성기를 명확하게 표현할 수있는 강력한 방법 인 생성기 표현식을 제공합니다.

>>> pairs = ((x,y) for x in range(10) for y in range(10) if y >= x)
>>> pairs
<generator object <genexpr> at 0x0311DC90>
>>> sum(x*y for x,y in pairs)
1155

보다시피, 구문은 깨끗하고 읽을 수있을뿐만 아니라 sum수락 생성기 와 같은 내장 함수 입니다.

With 문에 대한 Python Enhancement Proposal을 확인하십시오 . 다른 언어의 With 문에서 기대하는 것과는 매우 다릅니다. 표준 라이브러리의 도움을 받아 Python의 생성기는 컨텍스트 관리자로 아름답게 작동합니다.

>>> from contextlib import contextmanager
>>> @contextmanager
def debugWith(arg):
        print("preprocessing", arg)
        yield arg
        print("postprocessing", arg)


>>> with debugWith("foobar") as s:
        print(s[::-1])


preprocessing foobar
raboof
postprocessing foobar

물론 인쇄 작업은 여기서 할 수있는 가장 지루한 작업이지만 가시적 인 결과를 보여줍니다. 더 흥미로운 옵션으로는 리소스 자동 관리 (파일 / 스트림 / 네트워크 연결 열기 및 닫기), 동시성 잠금, 일시적으로 함수 래핑 또는 교체, 데이터 압축 해제 및 재 압축이 있습니다. 함수를 호출하는 것이 코드에 코드를 삽입하는 것과 같으면 with 문은 코드의 일부를 다른 코드로 래핑하는 것과 같습니다. 그러나 그것을 사용하면 언어 구조에 쉽게 연결할 수있는 확실한 예입니다. 수율 기반 생성기는 컨텍스트 관리자를 만드는 유일한 방법은 아니지만 확실히 편리한 방법입니다.

및 일부 소진

파이썬에서 for 루프는 흥미로운 방식으로 작동합니다. 다음과 같은 형식으로되어 있습니다.

for <name> in <iterable>:
    ...

먼저, 호출 한 표현식 <iterable>이 반복 가능한 객체를 얻기 위해 평가됩니다. 둘째, iterable이 __iter__호출하고 결과 iterator가 장면 뒤에 저장됩니다. 이후 __next__에 반복자에서 호출되어 입력 한 이름에 바인딩 할 값을 가져옵니다 <name>. 이 단계는를 호출 할 때까지 반복 __next__합니다 StopIteration. for 루프가 예외를 삼키고 거기서부터 실행이 계속됩니다.

제너레이터로 돌아 오기 : 제너레이터를 호출 __iter__하면 제너레이터 만 반환됩니다.

>>> x = (a for a in "boring generator")
>>> id(x)
51502272
>>> id(x.__iter__())
51502272

이것이 의미하는 것은 무언가에 대해 반복하는 것을 당신이하고 싶은 것과 분리하고 그 행동을 도중에 바꿀 수 있다는 것입니다. 아래에서 두 개의 루프에서 동일한 생성기가 어떻게 사용되는지 확인하고 두 번째 루프에서는 첫 번째에서 중단 된 위치에서 실행을 시작합니다.

>>> generator = (x for x in 'more boring stuff')
>>> for letter in generator:
        print(ord(letter))
        if letter > 'p':
                break


109
111
114
>>> for letter in generator:
        print(letter)


e

b
o
r
i
n
g

s
t
u
f
f

게으른 평가

목록과 비교할 때 생성기의 단점 중 하나는 생성기에서 액세스 할 수있는 유일한 항목은 생성기에서 나오는 것입니다. 이전 결과로 돌아가거나 중간 결과를 거치지 않고 나중 결과로 넘어갈 수 없습니다. 이것의 장점은 발전기가 동등한 목록에 비해 거의 메모리를 차지하지 않는다는 것입니다.

>>> import sys
>>> sys.getsizeof([x for x in range(10000)])
43816
>>> sys.getsizeof(range(10000000000))
24
>>> sys.getsizeof([x for x in range(10000000000)])
Traceback (most recent call last):
  File "<pyshell#10>", line 1, in <module>
    sys.getsizeof([x for x in range(10000000000)])
  File "<pyshell#10>", line 1, in <listcomp>
    sys.getsizeof([x for x in range(10000000000)])
MemoryError

발전기는 느슨하게 연결될 수도 있습니다.

logfile = open("logs.txt")
lastcolumn = (line.split()[-1] for line in logfile)
numericcolumn = (float(x) for x in lastcolumn)
print(sum(numericcolumn))

첫 번째, 두 번째 및 세 번째 줄은 각각 발전기를 정의하지만 실제 작업은 수행하지 않습니다. 마지막 행이 호출되면 sum은 numericcolumn에 값을 요청하고 numericcolumn은 lastcolumn에 값을 필요로하고 lastcolumn은 로그 파일에서 값을 요청한 다음 실제로 파일에서 행을 읽습니다. 이 스택은 sum이 첫 번째 정수를 얻을 때까지 풀립니다. 그런 다음 두 번째 줄에 대해 프로세스가 다시 발생합니다. 이 시점에서 sum에는 두 개의 정수가 있으며 함께 더합니다. 세 번째 줄은 파일에서 아직 읽지 않았습니다. 그런 다음 Sum은 numericcolumn에서 값을 요청하고 (전체적으로 체인의 나머지 부분에 대해 전혀 알 수 없음) numericcolumn이 소진 될 때까지 추가합니다.

여기서 가장 흥미로운 부분은 줄을 개별적으로 읽고 소비하고 버린다는 것입니다. 전체 파일이 한 번에 메모리에있는 것은 아닙니다. 이 로그 파일이 테라 바이트 인 경우 어떻게됩니까? 한 번에 한 줄만 읽기 때문에 작동합니다.

결론

이것은 파이썬에서 생성기의 모든 사용에 대한 완전한 검토는 아닙니다. 특히 무한 생성기, 상태 시스템, 값을 다시 전달하고 코 루틴과의 관계를 건너 뛰었습니다.

생성기를 깔끔하게 통합되고 유용한 언어 기능으로 사용할 수 있음을 입증하는 것으로 충분하다고 생각합니다.


6

클래식 OOP 언어에 익숙한 경우 yield객체 레벨이 아닌 함수 레벨에서 변경 가능한 상태가 캡처되므로 생성기에서 문제 가 발생할 수 있습니다.

"확실성"의 문제는 붉은 청어입니다. 일반적으로 참조 투명성 이라고 하며 기본적으로 함수는 항상 동일한 인수에 대해 동일한 결과를 반환합니다. 변경 가능한 상태가되면 참조 투명성을 잃게됩니다. OOP에서 객체는 종종 변경 가능한 상태를 갖습니다. 즉, 메소드 호출의 결과는 인수뿐만 아니라 객체의 내부 상태에도 의존합니다.

문제는 변경 가능한 상태를 캡처 할 위치 입니다. 클래식 OOP에서는 변경 가능한 상태가 객체 수준에 있습니다. 그러나 언어가 클로저를 지원하면 함수 수준에서 변경 가능한 상태가 될 수 있습니다. 예를 들어 JavaScript에서 :

function getCounter() {
   var cnt = 1;
   return function(){ return cnt++; }
}
var counter = getCounter();
counter() --> 1
counter() --> 2

간단히 말해서, yield클로저를 지원하는 언어는 자연 스럽지만 변경 가능한 상태 가 객체 레벨 에만 존재 하는 이전 버전의 Java와 같은 언어 에서는 적합 하지 않습니다.


언어 기능에 스펙트럼이 있다면 수율은 기능과는 거리가 멀다고 생각합니다. 반드시 나쁜 것은 아닙니다. OOP는 한때 매우 유행이었고 나중에 다시 함수형 프로그래밍이었습니다. 나는 그것의 위험이 프로그램과 예상치 못한 방식으로 동작하게하는 기능적 디자인과 수율과 같은 기능을 혼합하고 일치시키는 데 실제로 있다고 생각합니다.
Neil

0

제 생각에는 좋은 기능이 아닙니다. 그것은 매우 신중하게 가르쳐야하고 모든 사람들이 그것을 잘못 가르치기 때문에 주로 나쁜 기능입니다. 사람들은 "제너레이터"라는 단어를 사용하는데, 이는 제너레이터 기능과 제너레이터 객체 사이를 연상시킵니다. 문제는 실제 생산량을 누가 또는 무엇으로 하는가?

이것은 단지 내 의견이 아닙니다. 그가 지배하는 PEP 게시판에서 귀도조차도 제너레이터 기능은 제너레이터가 아니라 "제너레이터 팩토리"라고 인정합니다.

그건 중요 해요, 그렇지 않나요? 그러나 문서의 99 %를 읽으면 생성기 함수가 실제 생성기라는 느낌이 들며 생성기 객체가 필요하다는 사실을 무시하는 경향이 있습니다.

귀도는이 함수들에 대해 "gen"을 "def"로 대체하는 것을 고려하고 아니오라고 말했지만 어쨌든 충분하지 않다고 주장합니다. 정말해야합니다 :

def make_gen(args)
    def_gen foo
        # Put in "yield" and other beahvior
    return_gen foo
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.