스택큐힙리스트

"yield" 키워드는 Python에서 무엇을 하는 역할인가요? 본문

카테고리 없음

"yield" 키워드는 Python에서 무엇을 하는 역할인가요?

스택큐힙리스트 2023. 10. 23. 23:02
반응형



Python에서 yield 키워드의 용도는 무엇인가요? 이것은 무엇을 하는 건가요?

예를 들어, 이 코드를 이해하려고 노력하고 있습니다1:

def _get_child_candidates(self, 거리, 최소_거리, 최대_거리):
if self._leftchild and 거리 - 최대_거리 < self._median:
yield self._leftchild
if self._rightchild and 거리 + 최대_거리 >= self._median:
yield self._rightchild

그리고 이것이 호출자입니다:

결과, 후보들 = [], [자신]
while 후보들:
노드 = 후보들.pop()
거리 = 노드._get_dist(대상)
만약 거리 <= 최대_거리 이고 거리 >= 최소_거리라면:
결과.extend(노드._값들)
후보들.extend(노드._get_child_candidates(거리, 최소_거리, 최대_거리))
리턴 결과

메소드 _get_child_candidates이 호출되면 무엇이 발생하나요?
리스트가 반환되나요? 하나의 요소인가요? 다시 호출되나요? 연속 호출이 언제 멈추나요?

<주석>
1. 이 코드 조각은 Jochen Schulz (jrschulz)가 작성했으며, 메트릭 공간을 위한 훌륭한 Python 라이브러리를 만들었습니다. 이것은 전체 소스에 대한 링크입니다: 모듈 mspace.

답변 1

yield가 무엇을 하는지 이해하려면, 먼저 제너레이터가 무엇인지 이해해야 합니다. 그리고 제너레이터를 이해하려면 먼저 반복 가능한 객체를 이해해야 합니다.

반복가능한 객체

리스트를 만들 때, 항목을 하나씩 읽을 수 있습니다. 항목을 하나씩 읽는 것을 반복이라고 합니다:

>>> mylist = [1, 2, 3]
>>> for i in mylist:
... print(i)
1
2
3
을 한국어로 번역하면 다음과 같습니다:
>>> 내리스트 = [1, 2, 3]
>>> 대상 요소 i 에 대해:
... 출력(i)
1
2
3

mylist반복 가능한 객체입니다. 리스트 내장을 사용하면 리스트가 생성되어 반복 가능한 객체가 됩니다.

>>> mylist = [x*x for x in range(3)]
>>> for i in mylist:
... print(i)
0
1
4

>>> mylist = [x*x for x in range(3)]
>>> for i in mylist:
... print(i)
0
1
4

당신이 for... in...을 사용할 수 있는 모든 것은 반복 가능한 객체(iterable)입니다. 리스트, 문자열, 파일...

이 반복 가능한 객체들은 원하는 만큼 여러 번 읽을 수 있어서 유용하지만, 모든 값을 메모리에 저장하고 있기 때문에 많은 값들이 있는 경우 항상 원하는 결과는 아닙니다.

발전기

제너레이터는 반복자로, 한 번만 반복할 수 있는 종류의 이터러블입니다. 제너레이터는 모든 값을 메모리에 저장하지 않으며, 값을 동적으로 생성합니다:

>>> mygenerator = (x*x for x in range(3))
>>> for i in mygenerator:
... print(i)
0
1
4

>>> mygenerator = (x*x for x in range(3))
>>> for i in mygenerator:
... print(i)
0
1
4

[] 대신에 ()를 사용하는 것만 빼면 똑같습니다. 하지만, 제너레이터는 한 번만 사용할 수 있기 때문에 for i in mygenerator를 두 번 실행할 수 없습니다: 제너레이터는 0을 계산하고 잊어버리고 1을 계산하며, 4를 계산한 후에는 한 번에 하나씩 종료됩니다.

수확률

yieldreturn과 비슷하게 사용되는 키워드로, 함수는 제너레이터를 반환합니다.

>>> def create_generator():
... mylist = range(3)
... for i in mylist:
... yield i*i
...
>>> mygenerator = create_generator() # 제네레이터 생성
>>> print(mygenerator) # mygenerator는 객체입니다!
<generator object create_generator at 0xb7555c34>
>>> for i in mygenerator:
... print(i)
0
1
4

이것은 쓸모없는 예시지만, 함수가 한번만 읽어야 할 많은 값들을 반환할 경우 유용합니다.

yield을 마스터하려면 함수를 호출할 때 함수 본문에 작성한 코드는 실행되지 않는다는 것을 이해해야 합니다. 함수는 오직 제네레이터 객체를 반환하기만 합니다. 이 부분은 조금 까다로울 수 있습니다.

그런 다음, 코드는 for이 발전기를 사용할 때마다 남아 있는 위치에서 계속됩니다.

이제 어려운 부분입니다:

첫 번째로 for가 함수로부터 생성된 generator 객체를 호출할 때, 함수 안의 코드가 yield에 도달할 때까지 처음부터 실행되고, 그 다음 루프의 첫 번째 값이 반환됩니다. 그리고 각 이후 호출은 함수 안에서 작성한 루프의 다음 반복을 실행하고 다음 값을 반환합니다. 이는 함수가 yield에 도달하지 않고 실행될 때까지 계속됩니다. 이는 루프가 끝나거나 if/else 조건을 더 이상 충족하지 않을 때 함수가 비어있는 것으로 간주됩니다.

코드 설명

생성자:

# 여기에서는 노드 개체의 메소드를 생성하는 코드입니다. 이 메소드는 생성기를 반환합니다.
def _get_child_candidates(self, distance, min_dist, max_dist):
# 아래에는 생성기 객체를 사용할 때마다 호출되는 코드가 있습니다:
# 노드 개체의 왼쪽에 아직 자식이 있는 경우
# 그리고 거리가 적절한 경우 다음 자식을 반환합니다.
if self._leftchild and distance - max_dist < self._median:
yield self._leftchild
# 노드 개체의 오른쪽에 아직 자식이 있는 경우
# 그리고 거리가 적절한 경우 다음 자식을 반환합니다.
if self._rightchild and distance + max_dist >= self._median:
yield self._rightchild
# 함수가 여기에 도착하면 생성기는 비어있는 것으로 간주됩니다.
# 왼쪽과 오른쪽 자식 뿐만 아니라 두 개 이상의 값은 없습니다.

전화하는 사람:

# 빈 목록과 현재 객체 참조가 있는 목록을 만듭니다.
결과, 후보자 = list(), [self]
# 후보자들을 순환합니다 (처음에는 하나의 요소만 포함하고 있습니다)
while 후보자들:
# 마지막 후보자를 가져와 목록에서 제거합니다
노드 = 후보자들.pop()
# obj와 후보자 사이의 거리를 구합니다
거리 = 노드._get_dist(obj)
# 거리가 적절한 경우 결과를 채울 수 있습니다
if 거리 <= max_dist and 거리 >= min_dist:
결과.extend(노드._values)
# 후보자의 자식들을 후보자 목록에 추가합니다
# 그래서 후보자의 자식들, 자식들의 자식들 등을 모두 검사할 때까지 루프가 계속 실행됩니다
후보자들.extend(노드._get_child_candidates(거리, min_dist, max_dist))
return 결과

이 코드에는 몇 가지 스마트한 부분이 있습니다:


  • 루프는 목록을 반복하지만, 루프가 반복되는 동안 목록이 확장됩니다. 이는 중첩된 데이터를 모두 통과하는 간결한 방법입니다. 그러나 무한 루프에 빠질 수 있기 때문에 약간 위험할 수 있습니다. 이 경우, candidates.extend(node._get_child_candidates(distance, min_dist, max_dist))는 생성자(generator)의 모든 값을 소진합니다. 그러나 while은 이전과 같은 노드에 적용되지 않으므로 이전 값과 다른 값을 생성하는 새 생성자 객체를 계속 만듭니다.



  • extend() 메서드는 이터러블(iterable)을 기대하고 해당 값들을 목록에 추가하는 목록 객체 메서드입니다.



일반적으로 우리는 리스트를 전달합니다:

>>> a = [1, 2]
>>> b = [3, 4]
>>> a.extend(b)
>>> print(a)
[1, 2, 3, 4]

>>> a = [1, 2]
>>> b = [3, 4]
>>> a.extend(b)
>>> print(a)
[1, 2, 3, 4]

하지만 코드에서 제네레이터를 얻게됩니다. 이것은 좋은 이유입니다. :


  1. 값을 두 번 읽을 필요가 없습니다.

  2. 많은 양의 자식을 가질 수 있고, 모두 메모리에 저장하고 싶지 않을 수 있습니다.

그리고 Python이 메소드의 인자가 리스트인지 여부를 신경쓰지 않기 때문에 이것이 작동합니다. Python은 iterable을 기대하므로 문자열, 리스트, 튜플 및 생성기와 함께 사용할 수 있습니다! 이를 duck typing이라고 하며 이것이 Python이 아주 멋진 이유 중 하나입니다. 하지만 이것은 다른 이야기이며, 다른 질문에 대한 것입니다...

여기서 멈출 수도 있고, 제너레이터의 고급 사용법을 보려면 조금 더 읽어볼 수도 있습니다:

발전기 고갈 제어

>>> class Bank(): # 은행을 만들어 보자, ATM을 만들어 보죠.
... 위기 = False
... def ATM_만들기(self):
... while not self.위기:
... yield $100
>>> hsbc = Bank() # 모든 것이 정상일 때, ATM은 원하는 만큼 돈을 줍니다.
>>> corner_street_atm = hsbc.ATM_만들기()
>>> print(corner_street_atm.next())
$100
>>> print(corner_street_atm.next())
$100
>>> print([corner_street_atm.next() for cash in range(5)])
['$100', '$100', '$100', '$100', '$100']
>>> hsbc.위기 = True # 위기가 찾아왔습니다, 돈은 더 이상 없습니다!
>>> print(corner_street_atm.next())
<type 'exceptions.StopIteration'>
>>> wall_street_atm = hsbc.ATM_만들기() # 새로운 ATM에도 같은 상황이 적용됩니다.
>>> print(wall_street_atm.next())
<type 'exceptions.StopIteration'>
>>> hsbc.위기 = False # 문제는, 위기를 지나도 ATM은 여전히 공허합니다.
>>> print(corner_street_atm.next())
<type 'exceptions.StopIteration'>
>>> brand_new_atm = hsbc.ATM_만들기() # 다시 사업을 시작하기 위해 새로운 ATM을 만듭니다.
>>> for cash in brand_new_atm:
... print cash
$100
$100
$100
$100
$100
$100
$100
$100
$100
...

참고: Python 3의 경우, print(corner_street_atm.__next__()) 또는 print(next(corner_street_atm))을 사용하세요.

리소스에 대한 액세스 제어와 같은 다양한 용도로 유용할 수 있습니다.

Itertools, 당신의 최고의 친구

itertools 모듈에는 반복 가능한 개체를 조작하는 특수 함수가 포함되어 있습니다. 제너레이터를 복제하고 싶어 졌나요?
두 개의 제너레이터를 연결하고 싶나요? 중첩된 리스트에 값을 그룹화하고 싶나요? 또 다른 리스트를 만들지 않고 매핑/집합(zip)을 하고 싶나요?

그럼 그냥 import itertools를 가져오세요.

예를 들어, 4마리 말 경주에 대한 가능한 도착 순서를 살펴보겠습니다:

>>> horses = [1, 2, 3, 4]
>>> races = itertools.permutations(horses)
>>> print(races)
<itertools.permutations object at 0xb754f1dc>
>>> print(list(itertools.permutations(horses)))
[(1, 2, 3, 4),
(1, 2, 4, 3),
(1, 3, 2, 4),
(1, 3, 4, 2),
(1, 4, 2, 3),
(1, 4, 3, 2),
(2, 1, 3, 4),
(2, 1, 4, 3),
(2, 3, 1, 4),
(2, 3, 4, 1),
(2, 4, 1, 3),
(2, 4, 3, 1),
(3, 1, 2, 4),
(3, 1, 4, 2),
(3, 2, 1, 4),
(3, 2, 4, 1),
(3, 4, 1, 2),
(3, 4, 2, 1),
(4, 1, 2, 3),
(4, 1, 3, 2),
(4, 2, 1, 3),
(4, 2, 3, 1),
(4, 3, 1, 2),
(4, 3, 2, 1)]

>>> 말 = [1, 2, 3, 4]
>>> 경주 = itertools.permutations(말)
>>> print(경주)
<itertools.permutations object at 0xb754f1dc>
>>> print(list(itertools.permutations(말)))
[(1, 2, 3, 4),
(1, 2, 4, 3),
(1, 3, 2, 4),
(1, 3, 4, 2),
(1, 4, 2, 3),
(1, 4, 3, 2),
(2, 1, 3, 4),
(2, 1, 4, 3),
(2, 3, 1, 4),
(2, 3, 4, 1),
(2, 4, 1, 3),
(2, 4, 3, 1),
(3, 1, 2, 4),
(3, 1, 4, 2),
(3, 2, 1, 4),
(3, 2, 4, 1),
(3, 4, 1, 2),
(3, 4, 2, 1),
(4, 1, 2, 3),
(4, 1, 3, 2),
(4, 2, 1, 3),
(4, 2, 3, 1),
(4, 3, 1, 2),
(4, 3, 2, 1)]

반복의 내부 메커니즘 이해하기

반복은 이터러블을 의미하는 프로세스입니다 ( __iter__() 메서드 구현) 및 이터레이터 ( __next__() 메서드 구현).
이터러블은 이터레이터를 얻을 수 있는 모든 객체입니다. 이터레이터는 이터러블을 반복할 수 있게 해주는 객체입니다.

이 문서에는 for 루프의 작동 방식에 대해 더 자세히 설명되어 있습니다.

답변 2

yield 키워드는 파이썬에서 중요한 역할을 하는데, 이 키워드는 제너레이터(generator) 함수를 정의하고 사용할 때 사용됩니다. 제너레이터는 반복 가능한 객체를 생성하는 함수로, 하나의 값을 생성하고 그 값을 메모리에 유지하며, 다음에 호출될 때에 이전 상태를 유지한 채로 다음 값을 반환합니다. 이를 통해 매번 전체 시퀀스를 모두 메모리에 저장할 필요 없이 필요한 만큼의 값을 생성할 수 있습니다.
yield 키워드가 사용된 함수는 제너레이터 함수로 변환됩니다. 일반적인 함수와 달리 제너레이터 함수는 함수 실행 중 값들을 기억하고 있다가 호출자에게 반환하고, 다음 호출 시에는 이전 상태를 기억한 채로 실행을 진행합니다. 이를 통해 메모리 사용량을 줄이고 작업 속도를 향상시킬 수 있습니다.
yield 키워드를 사용한 제너레이터 함수는 간편하게 작성할 수 있습니다. 함수 안에서 yield 키워드를 사용하여 결과 값을 반환하면, 해당 함수는 제너레이터가 됩니다. yield 표현식은 함수의 실행을 일시 중지하고, 값을 호출자에게 반환합니다. 이후 호출 시에는 해당 함수는 yield 문 이후부터 실행을 이어나갑니다.
이러한 제너레이터 함수는 대량의 데이터를 처리할 때 특히 유용합니다. 예를 들어, 파일의 줄 단위로 데이터를 읽거나 임의의 크기로 데이터를 생성해야 할 때, 제너레이터 함수를 사용하면 필요한 만큼의 데이터만을 처리하고 나머지는 메모리에 저장하지 않고도 작업을 수행할 수 있습니다.
파이썬에서 yield 키워드는 제너레이터 함수를 구현하고 이를 사용하여 반복 가능한 객체를 생성하는 강력한 도구입니다. 이를 통해 더 효율적이고 메모리를 절약하며, 높은 성능을 갖는 코드를 작성할 수 있습니다.

반응형
Comments