https://dojang.io/mod/page/view.php?id=2428

42.2 매개변수와 반환값을 처리하는 데코레이터 만들기

지금까지 매개변수와 반환값이 없는 함수의 데코레이터를 만들었습니다. 이번에는 매개변수와 반환값을 처리하는 데코레이터는 어떻게 만드는지 알아보겠습니다. 다음은 함수의 매개변수와 반환값을 출력하는 데코레이터입니다.

decorator_param_return.py

def trace(func):          # 호출할 함수를 매개변수로 받음     def wrapper(a, b):    # 호출할 함수 add(a, b)의 매개변수와 똑같이 지정         r = func(a, b)    # func에 매개변수 a, b를 넣어서 호출하고 반환값을 변수에 저장         print(f'{func.__name__}(a={a}, b={b}) -> {r}')  # 매개변수와 반환값 출력         return r          # func의 반환값을 반환     return wrapper        # wrapper 함수 반환   @trace              # @데코레이터 def add(a, b):      # 매개변수는 두 개     return a + b    # 매개변수 두 개를 더해서 반환   print(add(10, 20))

실행 결과

add(a=10, b=20) -> 30
30

add 함수를 호출했을 때 데코레이터를 통해서 매개변수와 반환값이 출력되었습니다. 매개변수와 반환값을 처리하는 데코레이터를 만들 때는 먼저 안쪽 wrapper 함수의 매개변수를 호출할 함수 add(a, b)의 매개변수와 똑같이 만들어줍니다.

def trace(func):          # 호출할 함수를 매개변수로 받음
    def wrapper(a, b):    # 호출할 함수 add(a, b)의 매개변수와 똑같이 지정

wrapper 함수 안에서는 func를 호출하고 반환값을 변수에 저장합니다. 그다음에 print로 매개변수와 반환값을 출력합니다. 이때 func에는 매개변수 a와 b를 그대로 넣어줍니다. 또한, add 함수는 두 수를 더해서 반환해야 하므로 func의 반환값을 return으로 반환해줍니다.

def trace(func):          # 호출할 함수를 매개변수로 받음
    def wrapper(a, b):    # 호출할 함수 add(a, b)의 매개변수와 똑같이 지정
        r = func(a, b)    # func에 매개변수 a, b를 넣어서 호출하고 반환값을 변수에 저장
        print(f'{func.__name__}(a={a}, b={b}) -> {r}')    # 매개변수와 반환값 출력
        return r          # func의 반환값을 반환
    return wrapper        # wrapper 함수 반환

만약 wrapper 함수에서 func의 반환값을 반환하지 않으면 add 함수를 호출해도 반환값이 나오지 않으므로 주의해야 합니다. 참고로 wrapper 함수에서 func의 반환값을 출력할 필요가 없으면 return func(a, b)처럼 func를 호출하면서 바로 반환해도 됩니다.

데코레이터를 사용할 때는 @로 함수 위에 지정해주면 됩니다. 또한, @로 데코레이터를 사용했으므로 add 함수는 그대로 호출해줍니다.

@trace              # @데코레이터
def add(a, b):      # 매개변수는 두 개
    return a + b    # 매개변수 두 개를 더해서 반환

42.2.1  가변 인수 함수 데코레이터

def add(a, b):는 매개변수의 개수가 고정된 함수입니다. 그러면 매개변수(인수)가 고정되지 않은 함수는 어떻게 처리할까요? 이때는 wrapper 함수를 가변 인수 함수로 만들면 됩니다.

decorator_variable_argument.py

def trace(func):                     # 호출할 함수를 매개변수로 받음     def wrapper(*args, **kwargs):    # 가변 인수 함수로 만듦         r = func(*args, **kwargs)    # func에 args, kwargs를 언패킹하여 넣어줌         print(f'{func.__name__}(args={args}, kwargs={kwargs}) -> {r}')                                      # 매개변수와 반환값 출력         return r                     # func의 반환값을 반환     return wrapper                   # wrapper 함수 반환   @trace                   # @데코레이터 def get_max(*args):      # 위치 인수를 사용하는 가변 인수 함수     return max(args)   @trace                   # @데코레이터 def get_min(**kwargs):   # 키워드 인수를 사용하는 가변 인수 함수     return min(kwargs.values())   print(get_max(10, 20)) print(get_min(x=10, y=20, z=30))

실행 결과

get_max(args=(10, 20), kwargs={}) -> 20
20
get_min(args=(), kwargs={'x': 10, 'y': 20, 'z': 30}) -> 10
10

get_max 함수와 get_min 함수는 가변 인수 함수입니다. 따라서 데코레이터도 가변 인수 함수로 만들어줍니다. 이때 위치 인수와 키워드 인수를 모두 받을 수 있도록 *args와 **kwargs를 지정해줍니다.

def trace(func):                     # 호출할 함수를 매개변수로 받음
    def wrapper(*args, **kwargs):    # 가변 인수 함수로 만듦

wrapper 함수 안에서는 func를 호출해주는데 args는 리스트이고, kwargs는 딕셔너리이므로 func에 넣을 때는 언패킹하여 넣어줍니다. 그리고 print로 매개변수와 반환값을 출력합니다

def trace(func):                     # 호출할 함수를 매개변수로 받음
    def wrapper(*args, **kwargs):    # 가변 인수 함수로 만듦
        r = func(*args, **kwargs)    # func에 args, kwargs를 언패킹하여 넣어줌
        print(f'{func.__name__}(args={args}, kwargs={kwargs}) -> {r}')
                                     # 매개변수와 반환값 출력
        return r                     # func의 반환값을 반환
    return wrapper                   # wrapper 함수 반환

이렇게 만든 데코레이터 trace는 위치 인수와 키워드 인수를 모두 처리할 수 있습니다. 따라서 가변 인수 함수뿐만 아니라 일반적인 함수에도 사용할 수 있습니다.

>>> @trace
... def add(a, b):
...    return a + b
...
>>> add(10, 20)
add(args=(10, 20), kwargs={}) -> 30
30
참고 | 메서드에 데코레이터 사용하기

클래스를 만들면서 메서드에 데코레이터를 사용할 때는 self를 주의해야 합니다. 인스턴스 메서드는 항상 self를 받으므로 데코레이터를 만들 때도 wrapper 함수의 첫 번째 매개변수는 self로 지정해야 합니다(클래스 메서드는 cls). 마찬가지로 func를 호출할 때도 self와 매개변수를 그대로 넣어야 합니다.

decorator_method.py

def trace(func):
    def wrapper(self, a, b):   # 호출할 함수가 인스턴스 메서드이므로 첫 번째 매개변수는 self로 지정
        r = func(self, a, b)   # self와 매개변수를 그대로 넣어줌
        print(f'{func.__name__}(a={a}, b={b}) -> {r}')   # 매개변수와 반환값 출력
        return r               # func의 반환값을 반환
    return wrapper
 
class Calc:
    @trace
    def add(self, a, b):    # add는 인스턴스 메서드
        return a + b
 
c = Calc()
print(c.add(10, 20))

실행 결과

add(a=10, b=20) -> 30
30

https://dojang.io/mod/page/view.php?id=2427


Unit 42. 데코레이터 사용하기

파이썬은 데코레이터(decorator)라는 기능을 제공합니다. 데코레이터는 장식하다, 꾸미다라는 뜻의 decorate에 er(or)을 붙인 말인데 장식하는 도구 정도로 설명할 수 있습니다.

지금까지 클래스에서 메서드를 만들 때 @staticmethod@classmethod@abstractmethod 등을 붙였는데, 이렇게 @로 시작하는 것들이 데코레이터입니다. 즉, 함수(메서드)를 장식한다고 해서 이런 이름이 붙었습니다.

class Calc:
    @staticmethod    # 데코레이터
    def add(a, b):
        print(a + b)

이번 유닛에서는 데코레이터를 만들고 사용하는 방법을 알아보겠습니다.

참고로 데코레이터는 장식자라고 부르기도 합니다. 이 책에서는 데코레이터를 사용하겠습니다.

42.1 데코레이터 만들기

데코레이터는 함수를 장식한다고 했는데 도대체 어디에 사용하는 것일까요? 데코레이터는 함수를 수정하지 않은 상태에서 추가 기능을 구현할 때 사용합니다. 예를 들어서 함수의 시작과 끝을 출력하고 싶다면 다음과 같이 함수 시작, 끝 부분에 print를 넣어주어야 합니다.

function_begin_end.py

def hello():
    print('hello 함수 시작')
    print('hello')
    print('hello 함수 끝')
 
def world():
    print('world 함수 시작')
    print('world')
    print('world 함수 끝')
 
hello()
world()

실행 결과

hello 함수 시작
hello
hello 함수 끝
world 함수 시작
world
world 함수 끝

만약 다른 함수도 시작과 끝을 출력하고 싶다면 함수를 만들 때마다 print를 넣어야 합니다. 따라서 함수가 많아지면 매우 번거로워집니다.

이런 경우에는 데코레이터를 활용하면 편리합니다. 다음은 함수의 시작과 끝을 출력하는 데코레이터입니다.

데코레이터는 기본적으로 클로저의 개념을 포함합니다. 함수를 둘러싼 환경(지역 변수, 코드 등)을 계속 유지하다가, 함수를 호출할 때 다시 꺼내서 사용하는 함수를 클로저(closure)라고 합니다.

아래의 코드가 이해가 잘 되지 않으신다면 클로저를 보고오세요.

decorator_closure.py

def trace(func):                             # 호출할 함수를 매개변수로 받음
    def wrapper():                           # 호출할 함수를 감싸는 함수
        print(func.__name__, '함수 시작')    # __name__으로 함수 이름 출력
        func()                               # 매개변수로 받은 함수를 호출
        print(func.__name__, '함수 끝')
    return wrapper                           # wrapper 함수 반환
 
def hello():
    print('hello')
 
def world():
    print('world')
 
trace_hello = trace(hello)    # 데코레이터에 호출할 함수를 넣음
trace_hello()                 # 반환된 함수를 호출
trace_world = trace(world)    # 데코레이터에 호출할 함수를 넣음
trace_world()                 # 반환된 함수를 호출

실행 결과

hello 함수 시작
hello
hello 함수 끝
world 함수 시작
world
world 함수 끝

hello와 world 함수의 시작과 끝이 출력되었습니다. 먼저 데코레이터 trace는 호출할 함수를 매개변수로 받습니다(trace는 추적하다라는 뜻인데 프로그래밍에서 함수의 실행 상황을 추적할 때 trace라는 말을 사용합니다).

def trace(func):                             # 호출할 함수를 매개변수로 받음

trace 함수 안에서는 호출할 함수를 감싸는 함수 wrapper를 만듭니다(wrapper는 물건을 싸는 포장지라는 뜻인데 여기서는 함수를 감싼다고 해서 이런 이름을 붙였습니다. 다른 이름을 사용해도 상관없습니다).

    def wrapper():                           # 호출할 함수를 감싸는 함수

이제 wrapper 함수에서는 함수의 시작을 알리는 문자열을 출력하고, trace에서 매개변수로 받은 func를 호출합니다. 그다음에 함수의 끝을 알리는 문자열을 출력합니다. 여기서 매개변수로 받은 함수의 원래 이름을 출력할 때는 __name__ 속성을 활용합니다. 마지막으로 wrapper 함수를 다 만들었으면 return을 사용하여 wrapper 함수 자체를 반환합니다.

def trace(func):                             # 호출할 함수를 매개변수로 받음
    def wrapper():                           # 호출할 함수를 감싸는 함수
        print(func.__name__, '함수 시작')    # __name__으로 함수 이름 출력
        func()                               # 매개변수로 받은 함수를 호출
        print(func.__name__, '함수 끝')
    return wrapper                           # wrapper 함수 반환

즉, 함수 안에서 함수를 만들고 반환하는 클로저입니다.

데코레이터를 사용할 때는 trace에 호출할 함수 hello 또는 world를 넣습니다. 그다음에 데코레이터에서 반환된 함수를 호출합니다. 이렇게 하면 데코레이터에 인수로 넣은 함수를 호출하고 시작과 끝을 출력합니다.

trace_hello = trace(hello)    # 데코레이터에 호출할 함수를 넣음
trace_hello()                 # 반환된 함수를 호출
trace_world = trace(world)    # 데코레이터에 호출할 함수를 넣음
trace_world()                 # 반환된 함수를 호출

물론 trace에 다른 함수를 넣은 뒤 반환된 함수를 호출하면 해당 함수의 시작과 끝을 출력할 수 있습니다.

42.1.1  @로 데코레이터 사용하기

이제 @을 사용하여 좀 더 간편하게 데코레이터를 사용해보겠습니다. 다음과 같이 호출할 함수 위에 @데코레이터 형식으로 지정합니다.

@데코레이터
def 함수이름():
    코드

decorator_closure_at_sign.py

def trace(func):                             # 호출할 함수를 매개변수로 받음
    def wrapper():
        print(func.__name__, '함수 시작')    # __name__으로 함수 이름 출력
        func()                               # 매개변수로 받은 함수를 호출
        print(func.__name__, '함수 끝')
    return wrapper                           # wrapper 함수 반환
 
@trace    # @데코레이터
def hello():
    print('hello')
 
@trace    # @데코레이터
def world():
    print('world')
 
hello()    # 함수를 그대로 호출
world()    # 함수를 그대로 호출

실행 결과

hello 함수 시작
hello
hello 함수 끝
world 함수 시작
world
world 함수 끝

hello와 world 함수 위에 @trace 를 붙인 뒤에 hello와 world 함수를 그대로 호출하면 끝입니다.

@trace    # @데코레이터
def hello():
    print('hello')
 
@trace    # @데코레이터
def world():
    print('world')
 
hello()    # 함수를 그대로 호출
world()    # 함수를 그대로 호출

물론 다른 함수 위에 @trace를 붙인 뒤 함수를 호출하면 해당 함수의 시작과 끝을 출력할 수 있습니다.

이 데코레이터를 그림으로 표현하면 다음과 같은 모양이 됩니다.

▼ 그림 42-1 데코레이터

이렇게 데코레이터는 함수를 감싸는 형태로 구성되어 있습니다. 따라서 데코레이터는 기존 함수를 수정하지 않으면서 추가 기능을 구현할 때 사용합니다.

참고 | 데코레이터를 여러 개 지정하기

함수에는 데코레이터를 여러 개 지정할 수 있습니다. 다음과 같이 함수 위에 데코레이터를 여러 줄로 지정해줍니다. 이때 데코레이터가 실행되는 순서는 위에서 아래 순입니다.

@데코레이터1
@데코레이터2
def 함수이름():
    코드

multiple_decorators.py

def decorator1(func):
    def wrapper():
        print('decorator1')
        func()
    return wrapper
 
def decorator2(func):
    def wrapper():
        print('decorator2')
        func()
    return wrapper
 
# 데코레이터를 여러 개 지정
@decorator1
@decorator2
def hello():
    print('hello')
 
hello()

실행 결과

decorator1
decorator2
hello

@을 사용하지 않았을 때는 다음 코드와 동작이 같습니다.

decorated_hello = decorator1(decorator2(hello))
decorated_hello()



https://realpython.com/introduction-to-python-generators/

Understanding Generators

So far, you’ve learned about the two primary ways of creating generators: by using generator functions and generator expressions. You might even have an intuitive understanding of how generators work. Let’s take a moment to make that knowledge a little more explicit.

Generator functions look and act just like regular functions, but with one defining characteristic. Generator functions use the Python yield keyword instead of return. Recall the generator function you wrote earlier:

def infinite_sequence():
    num = 0
    while True:
        yield num
        num += 1

This looks like a typical function definition, except for the Python yield statement and the code that follows it. yield indicates where a value is sent back to the caller, but unlike return, you don’t exit the function afterward.

Instead, the state of the function is remembered. That way, when next() is called on a generator object (either explicitly or implicitly within a for loop), the previously yielded variable num is incremented, and then yielded again. Since generator functions look like other functions and act very similarly to them, you can assume that generator expressions are very similar to other comprehensions available in Python.

Building Generators With Generator Expressions

Like list comprehensions, generator expressions allow you to quickly create a generator object in just a few lines of code. They’re also useful in the same cases where list comprehensions are used, with an added benefit: you can create them without building and holding the entire object in memory before iteration. In other words, you’ll have no memory penalty when you use generator expressions. Take this example of squaring some numbers:

>>>
>>> nums_squared_lc = [num**2 for num in range(5)]
>>> nums_squared_gc = (num**2 for num in range(5))

Both nums_squared_lc and nums_squared_gc look basically the same, but there’s one key difference. Can you spot it? Take a look at what happens when you inspect each of these objects:

>>>
>>> nums_squared_lc
[0, 1, 4, 9, 16]
>>> nums_squared_gc
<generator object <genexpr> at 0x107fbbc78>

The first object used brackets to build a list, while the second created a generator expression by using parentheses. The output confirms that you’ve created a generator object and that it is distinct from a list.

Profiling Generator Performance

You learned earlier that generators are a great way to optimize memory. While an infinite sequence generator is an extreme example of this optimization, let’s amp up the number squaring examples you just saw and inspect the size of the resulting objects. You can do this with a call to sys.getsizeof():

>>>
>>> import sys
>>> nums_squared_lc = [i * 2 for i in range(10000)]
>>> sys.getsizeof(nums_squared_lc)
87624
>>> nums_squared_gc = (i ** 2 for i in range(10000))
>>> print(sys.getsizeof(nums_squared_gc))
120

In this case, the list you get from the list comprehension is 87,624 bytes, while the generator object is only 120. This means that the list is over 700 times larger than the generator object!

There is one thing to keep in mind, though. If the list is smaller than the running machine’s available memory, then list comprehensions can be faster to evaluate than the equivalent generator expression. To explore this, let’s sum across the results from the two comprehensions above. You can generate a readout with cProfile.run():

>>>
>>> import cProfile
>>> cProfile.run('sum([i * 2 for i in range(10000)])')
         5 function calls in 0.001 seconds

   Ordered by: standard name

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    0.001    0.001    0.001    0.001 <string>:1(<listcomp>)
        1    0.000    0.000    0.001    0.001 <string>:1(<module>)
        1    0.000    0.000    0.001    0.001 {built-in method builtins.exec}
        1    0.000    0.000    0.000    0.000 {built-in method builtins.sum}
        1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}


>>> cProfile.run('sum((i * 2 for i in range(10000)))')
         10005 function calls in 0.003 seconds

   Ordered by: standard name

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
    10001    0.002    0.000    0.002    0.000 <string>:1(<genexpr>)
        1    0.000    0.000    0.003    0.003 <string>:1(<module>)
        1    0.000    0.000    0.003    0.003 {built-in method builtins.exec}
        1    0.001    0.001    0.003    0.003 {built-in method builtins.sum}
        1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}

Here, you can see that summing across all values in the list comprehension took about a third of the time as summing across the generator. If speed is an issue and memory isn’t, then a list comprehension is likely a better tool for the job.

Remember, list comprehensions return full lists, while generator expressions return generators. Generators work the same whether they’re built from a function or an expression. Using an expression just allows you to define simple generators in a single line, with an assumed yield at the end of each inner iteration.

The Python yield statement is certainly the linchpin on which all of the functionality of generators rests, so let’s dive into how yield works in Python.



http://pythonstudy.xyz/python/article/23-Iterator%EC%99%80-Generator

for 문과 반복자

파이썬에서는 “반복자”라는 특별한 성질을 가진 타입들이 있다. 반복자는 영어로 iterator라고 하는데, 파이썬에서 반복자는 기술적으로는 단순히 __next__() 라는 메소드를 가지고 있는 객체를 말한다. (사실 반복자 그 자체는 제너레이터의 일종으로 볼 수도 있고, 기술적으로 제너레이터이다라고 해도 틀린표현은 아니다.) 보통 반복자들은 일련의 연속적인 값 혹은 특정한 규칙을 가진 수열을 계산하는 값을 내부에 가지고 있고, 이를 __next__() 가 호출될 때마다 내부적으로 관리하는 수열의 다음항을 리턴할 수 있는 객체가 된다.

참고로 내장함수의 도움말을 살펴보면 다음과 같다. 여기서는 iterator 라는 표현을 직접적으로 쓰고 있으며, 반복자로부터 다음번 아이템 얻어 리턴한다고 쓰여있다.

In [1]: next? Docstring: next(iterator[, default])  
Return the next item from the iterator. 
If default is given and the iterator is exhausted, 
it is returned instead of raising StopIteration. 
Type: builtin_function_or_method

참고로 반복자가 더 이상 만들어 낼 다음번 항이 없는 경우에는 StopIteration 예외를 일으키고, 이는 곧 순회(iteration)의 끝을 의미한다.

반복가능 (iterable) 프로토콜

파이썬에서 반복가능하다는 말은 결국 for ... in 문에 적용가능하다는 말과 동치이고, 기술적으로는 이터레이터(iterator, 반복자)를 가지고 있는 객체라는 의미이다. 반복가능한 타입의 객체로부터 반복자를 얻어내는 내장함수는 iter() 함수이다. (뒤에서 살펴보겠지만, next(x)x.__next__()를 호출하듯이, iter(x)x.__iter__()를 호출한다. 사실 이것이 반복가능 프로토콜의 핵심이다. )역시 반복 가능한 객체에 대한 힌트를 얻기 위해서 이 함수의 도움말을 살펴보도록 하자.

In [4]: iter? Docstring: iter(iterable) 
-> iterator iter(callable, sentinel) 
-> iterator  Get an iterator from an object. 
In the first form, the argument must supply its own iterator, 
or be a sequence. In the second form, the callable is called until it returns the sentinel. 
Type: builtin_function_or_method

우리는 여기서 많은 것을 볼 수 있다.

  1. iter 함수는 어떤 객체로부터 반복자를 얻어서 리턴한다. (list를 만든 후 dir(list)를 통해보면, 디셔너리 자료형이 반환되는데, 그안에 __iter__함수가 존재하는 것을 볼 수 있다. iter()함수는 바로 이 __iter__()함수의 리턴값을 반환한다)
  2. 반복가능한 객체는 반복자를 이미 가지고 있거나, 아니면 그 스스로가 연속열이다.
  3. 반복자 뿐만 아니라 특정한 종결값을 리턴할 때까지 계속 어떠한 값을 리턴할 수 있는 함수도 반복가능으로 취급한다.

지금까지의 힌트를 취합하면 다음과 같은 사실들을 알아내었다고 정리할 수 있다.

  1. iter() 함수를 이용하면 iterable한 객체의 반복자를 얻을 수 있다.
  2. next() 함수를 이용하면 반복자의 매 항을 얻을 수 있다.
  3. next() 함수를 이용했을 때 반복자가 더 이상 내 줄 값이 없으면 StopIteration 예외를 일으킨다.
  4. 그리고 range() 함수가 리턴하는 객체는 for ... in 문에 사용할 수 있으니, iterable 하다.

for … in 루프의 구조

그러면 이러한 사실로부터 우리는 파이썬의 for ... in 문을 while 문으로 재구성해볼 수 있다.

for i in range(3):   print(3)

위 반복문은 우리가 알아낸 사실에 근거하여 다음과 같이 쓸 수 있다.

## range(3)은 iterable 한 객체를 리턴하고 
## iter() 함수를 이용하면 그로부터 반복자를 얻을 수 있다.  
x = range(3)

## range는 다음과 같은 네임스페이스를 갖는다. range()함수는 __iter__ 함수를 가지고 있는 것을 확인할 수 있다. 
dir(x)
>>> ['__bool__', '__class__', '__contains__', '__delattr__', '__dir__', 
'__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__gt__', 
'__hash__', '__init__', '__init_subclass__', '__iter__', '__le__', '__len__', '__lt__', 
'__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__reversed__', '__setattr__', 
'__sizeof__', '__str__', '__subclasshook__', 'count', 'index', 'start', 'step', 'stop']
def customFor(x):
iteratorX = x.__iter__()   

## 이터레이터 객체를 갖는다면 이터레이터화 한다.

while True:      
	try:        
		i = next(iteratorX) 
		## 반복자로부터 다음 항을 얻는다.         
		return i      
	except StopIteration: 
		## 반복자가 고갈되면 StopIteration이 뜬다.        
		print("iteratrion finished.")        
		break  

# 0 # 1 # 2 # iteration finished.

그리고 파이썬의 for ... in 문은 실제로 이렇게 돌아간다. 임의의 리스트나 문자열, 튜플 등에 대해서 아래와 같이 반복자를 직접 얻어서 next() 함수를 계속 호출해볼 수 있다.

In [8]: x = iter([1,2,3,4])    
In [9]: x  Out[9]: <list_iterator at 0x222958f2b38>    
In [10]: next(x)  Out[10]: 1    
In [11]: next(x)  Out[11]: 2    
In [12]: next(x)  Out[12]: 3    
In [13]: next(x)  Out[13]: 4    
In [14]: next(x)  ---------------------------------------------------------------------------  
StopIteration Traceback (most recent call last)  <ipython-input-14-5e4e57af3a97> in <module>()  
----> 1 next(x)    StopIteration:

반복자

그렇다면 이번에는 반복자에 대해서 좀 더 알아보도록 하자. 반복자는 앞서 말했듯이 __next__() 메소드를 가지고 있는 객체라고 했다. 내장함수 next()는 모종의 약속에 의해서 인자로 받는 객체의 __next__() 메소드를 호출하고 그 결과를 리턴해주는 역할을 할 뿐이다.

커스텀 반복자 클래스

그렇다면 예를 들어서 피보나치 수열의 항을 순차적으로 리턴할 수 있는 반복자를 만들 수 있지 않을까?

class FibonacciSeq:   
    def __init__(self):     
    	self.a, self.b = 0, 1    

    def __next__(self):     
        self.a, self.b = self.b, self.a + self.b     
        ## 무한 수열이 될 수 있으니, 200보다 큰 값이 만들어지면 끝낸다.      
            if self.a > 200:       
                raise StopIteration     
                return self.a  
        ## 테스트 
        f = FibonacciSeq() 
        next(f)

# 1 next(f) # 1 next(f) # 3 next(f) # 5 next(f) # 8

대략 성공적이다. 하지만 이렇게 반복자를 만들더라도 iterable한 객체는 따로 존재한다. 즉 지금까지는 itrable한 객체가 있고, 여기에 iter() 함수를 통해서 반복자를 만들어서 각 항을 순회했다는 것이다. 즉 for ... in 문에서 필요한 것은 반복자 그 자체가 아닌 반복자를 생성할 수 있는 객체, 즉 iterable 한 객체이다.

하지만 방금 작성한 FibonacciSeq 클래스는 그 자체가 반복자이면서 반복가능한 객체가 될 수 있다. 왜냐하면 next()함수와 마찬가지로 iter() 함수는 인자로 받은 객체에 대해서 __iter__() 메소드를 호출하여 반복자를 얻기 때문이다. 이 역시 모종의 약속이 미리 정해져 있는 셈이다. 어쨌든 그 스스로가 반복자의 모든 요건을 갖추고 있으므로 __iter__()는 그 자신을 리턴하는 것으로만 간단히 정의하면 된다.

따라서 다음과 같이 피보나치 생성 클래스를 수정해보자. 수정하는 김에 한계값 자체는 생성시에 인자로 받을 수 있게끔 함께 변경한다.

class FibonacciSeq:   
    def __init__(self, upto=200):     
        self.limit = upto     
        self.a, self.b = 0, 1    

    def __iter__(self):     
        return self    

    def __next__(self):     
        self.a, self.b = self.b, self.a + self.b 
        
        if self.a > self.limit:       
            raise StopIteration     
            return self.a  

## 테스트 : 이제 for ... in 문에서도 잘 작동한다.
for f in FibonacciSeq(200):   
	print(f)

반복가능한 객체

내부에 __iter__(), __next__() 메소드를 가지는 객체를 만들기만 하면 이 객체는 기술적으로는 반복가능한 객체가 된다고 했다. 이를 통해서 특정한 하나의 값을 반복하는 리피터라든지, 여러 개의 연속열을 한꺼번에 순회할 수 있는 체인시퀀스 같은 도구를 만들 수도 있을 것이며, 그외에 내부 속성들을 for … in 문을 통해서 순회할 수 있는 객체도 디자인할 수 있을 것이다. 예를 들면 어떤 학생들의 시험 성적을 관리하는 코드에서 Student라는 클래스를 만들고 이 클래스 내부에 eng, math, sci 등 과목들의 점수를 저장했을 때, __next__() 메소드에서 미리 정한 순서에 따라 해당 속성값을 리턴하도록 한다면 Student 클래스의 인스턴스는 for ... in 문을 통해서 개별 과목의 점수를 순회할 수 있을 것이다.

https://dojang.io/mod/page/view.php?id=2414



40.3 yield from으로 값을 여러 번 바깥으로 전달하기

지금까지 yield로 값을 한 번씩 바깥으로 전달했습니다. 그래서 값을 여러 번 바깥으로 전달할 때는 for 또는 while 반복문으로 반복하면서 yield를 사용했습니다. 다음은 리스트의 1, 2, 3을 바깥으로 전달합니다.

generate_for_yield.py

def number_generator():
    x = [1, 2, 3]
    for i in x:
        yield i
 
for i in number_generator():
    print(i)

실행 결과

1
2
3

이런 경우에는 매번 반복문을 사용하지 않고, yield from을 사용하면 됩니다. yield from에는 반복 가능한 객체, 이터레이터, 제너레이터 객체를 지정합니다(yield from은 파이썬 3.3 이상부터 사용 가능).

  • yield from 반복가능한객체
  • yield from 이터레이터
  • yield from 제너레이터객체

그럼 yield from에 리스트를 지정해서 숫자 1, 2, 3을 바깥으로 전달해보겠습니다.

generator_yield_from_iterable.py

def number_generator():
    x = [1, 2, 3]
    yield from x    # 리스트에 들어있는 요소를 한 개씩 바깥으로 전달
 
for i in number_generator():
    print(i)

실행 결과

1
2
3

yield from x와 같이 yield from에 리스트(반복 가능한 객체)를 지정했습니다. 이렇게 하면 리스트에 들어있는 요소를 한 개씩 바깥으로 전달합니다. 즉, yield from을 한 번 사용하여 값을 세 번 바깥으로 전달합니다. 따라서 next 함수(__next__ 메서드)를 세 번 호출할 수 있습니다.

>>> g = number_generator()
>>> next(g)
1
>>> next(g)
2
>>> next(g)
3
>>> next(g)
Traceback (most recent call last):
  File "<pyshell#105>", line 1, in <module>
    next(g)
StopIteration

40.3.1  yield from에 제너레이터 객체 지정하기

이번에는 yield from에 제너레이터 객체를 지정해보겠습니다(이터레이터는 제너레이터와 동작이 같으므로 생략하겠습니다).

generator_yield_from_generator.py

def number_generator(stop):
    n = 0
    while n < stop:
        yield n
        n += 1
 
def three_generator():
    yield from number_generator(3)    # 숫자를 세 번 바깥으로 전달
 
for i in three_generator():
    print(i)

실행 결과

0
1
2

먼저 제너레이터 number_generator는 매개변수로 받은 숫자 직전까지 숫자를 만들어냅니다. 그리고 three_generator에서는 yield from number_generator(3)과 같이 yield from에 제너레이터 객체를 지정했습니다.

number_generator(3)은 숫자를 세 개를 만들어내므로 yield from number_generator(3)은 숫자를 세 번 바깥으로 전달합니다. 따라서 for 반복문에 three_generator()를 사용하면 숫자를 세 번 출력합니다(next 함수 또는 __next__ 메서드도 세 번 호출 가능).


이번 유닛에서는 제너레이터와 yield 사용 방법을 알아보았습니다. 이 부분은 파이썬에서도 고급 기능에 해당하는 부분이라 초보자들은 이해하기가 쉽지 않습니다. 그래서 지금 당장 yield가 이해되지 않는다고 해서 걱정할 필요가 없습니다. 나중에 프로그래밍에 익숙해지면 자연스럽게 익히게 됩니다.

참고 | 제너레이터 표현식

리스트 표현식을 사용할 때 [ ](대괄호)를 사용했습니다. 같은 리스트 표현식을 ( )(괄호)로 묶으면 제너레이터 표현식이 됩니다. 리스트 표현식은 처음부터 리스트의 요소를 만들어내지만 제너레이터 표현식은 필요할 때 요소를 만들어내므로 메모리를 절약할 수 있습니다.

(식 for 변수 in 반복가능한객체)

>>> [i for i in range(50) if i % 2 == 0]
[0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48]
>>> (i for i in range(50) if i % 2 == 0)
<generator object <genexpr> at 0x024F02A0>



https://dojang.io/mod/page/view.php?id=2413

40.2 제너레이터 만들기

제너레이터와 yield에 대해 알아보았으니 이번에는 range(횟수)처럼 동작을 하는 제너레이터를 만들어보겠습니다.

generator.py

def number_generator(stop):
    n = 0              # 숫자는 0부터 시작
    while n < stop:    # 현재 숫자가 반복을 끝낼 숫자보다 작을 때 반복
        yield n        # 현재 숫자를 바깥으로 전달
        n += 1         # 현재 숫자를 증가시킴
 
for i in number_generator(3):
    print(i)

실행 결과

0
1
2

코드는 간단합니다. 제너레이터 안에서 변수 n을 만들고 0을 저장합니다. 그리고 while n < stop:과 같이 반복을 끝낼 숫자보다 작을 때 반복하도록 만듭니다. 반복문 안에서는 yield n으로 숫자를 바깥으로 전달한 뒤 n을 1 증가시키면 됩니다. 여기서는 yield가 3번 나오므로 for 반복문도 3번 반복합니다.

물론 next 함수(__next__ 메서드)도 3번 사용할 수 있습니다.

>>> g = number_generator(3)
>>> next(g)
0
>>> next(g)
1
>>> next(g)
2
>>> next(g)
Traceback (most recent call last):
  File "<pyshell#100>", line 1, in <module>
    next(g)
StopIteration

40.2.1  yield에서 함수 호출하기

그럼 yield에서 함수(메서드)를 호출하면 어떻게 될까요? 다음은 리스트에 들어있는 문자열을 대문자로 변환하여 함수 바깥으로 전달합니다.

generator_yield_function.py

def upper_generator(x):
    for i in x:
        yield i.upper()    # 함수의 반환값을 바깥으로 전달
 
fruits = ['apple', 'pear', 'grape', 'pineapple', 'orange']
for i in upper_generator(fruits):
    print(i)

실행 결과

APPLE
PEAR
GRAPE
PINEAPPLE
ORANGE

리스트 fruits에 들어있는 문자열이 모두 대문자로 출력되었습니다. yield i.upper()와 같이 yield에서 함수(메서드)를 호출하면 해당 함수의 반환값을 바깥으로 전달합니다. upper는 호출했을 때 대문자로 된 문자열을 반환하므로 yield는 이 문자열을 바깥으로 전달합니다. 즉, yield에 무엇을 지정하든 결과만 바깥으로 전달합니다(함수의 반환값, 식의 결과).

이처럼 yield의 동작 방식만 이해하면 이터레이터보다 훨씬 간단하게 만들 수 있습니다.


https://dojang.io/mod/page/view.php?id=2412



Unit 40. 제너레이터 사용하기

제너레이터는 이터레이터를 생성해주는 함수입니다. 이터레이터는 클래스에 __iter____next__ 또는 __getitem__ 메서드를 구현해야 하지만 제너레이터는 함수 안에서 yield라는 키워드만 사용하면 끝입니다. 그래서 제너레이터는 이터레이터보다 훨씬 간단하게 작성할 수 있습니다.

참고로 제너레이터는 발생자라고 부르기도 합니다. 이 책에서는 제너레이터를 사용하겠습니다.

40.1 제너레이터와 yield 알아보기

함수 안에서 yield를 사용하면 함수는 제너레이터가 되며 yield에는 값(변수)을 지정합니다.

  • yield 

이제 yield를 사용해서 제너레이터를 만들고 for 반복문에서 0, 1, 2 숫자 세 개를 출력해보겠습니다.

yield.py

def number_generator():
    yield 0
    yield 1
    yield 2
 
for i in number_generator():
    print(i)

실행 결과

0
1
2

for 반복문에 number_generator()를 지정해서 값을 출력해보면 yield에 지정했던 0, 1, 2가 나옵니다. 이터레이터와 사용 방법이 똑같죠?

* 커스터마이징 for를 제작해보기https://valuefactory.tistory.com/667?category=765138

40.1.1  제너레이터 객체가 이터레이터인지 확인하기

그럼 number_generator 함수로 만든 객체가 정말 이터레이터인지 살펴보겠습니다. 다음과 같이 dir 함수로 메서드 목록을 확인해봅니다.

>>> g = number_generator()
>>> g
<generator object number_generator at 0x03A190F0>
>>> dir(g)
['__class__', '__del__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__iter__', '__le__', '__lt__', '__name__', '__ne__', '__new__', '__next__', '__qualname__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'close', 'gi_code', 'gi_frame', 'gi_running', 'gi_yieldfrom', 'send', 'throw']

number_generator 함수를 호출하면 제너레이터 객체(generator object)가 반환됩니다. 이 객체를 dir 함수로 살펴보면 이터레이터에서 볼 수 있는 __iter____next__ 메서드가 들어있습니다.

실제로 제너레이터 객체의 __next__를 호출해보면 숫자 0, 1, 2가 나오다가 StopIteration 예외가 발생합니다.

>>> g.__next__()
0
>>> g.__next__()
1
>>> g.__next__()
2
>>> g.__next__()
Traceback (most recent call last):
  File "<pyshell#29>", line 1, in <module>
    g.__next__()
StopIteration

이터레이터와 동작이 똑같습니다.

이처럼 함수에 yield만 사용해서 간단하게 이터레이터를 구현할 수 있습니다. 단, 이터레이터는 __next__ 메서드 안에서 직접 return으로 값을 반환했지만 제너레이터는 yield에 지정한 값이 __next__ 메서드(next 함수)의 반환값으로 나옵니다. 또한, 이터레이터는 raise로 StopIteration 예외를 직접 발생시켰지만 제너레이터는 함수의 끝까지 도달하면 StopIteration 예외가 자동으로 발생합니다.

제너레이터는 제너레이터 객체에서 __next__ 메서드를 호출할 때마다 함수 안의 yield까지 코드를 실행하며 yield에서 값을 발생시킵니다(generate). 그래서 이름이 제너레이터(generator)입니다.

40.1.2  for와 제너레이터

그럼 for 반복문과 제너레이터를 살펴보겠습니다. 다음과 같이 for 반복문은 반복할 때마다 __next__를 호출하므로 yield에서 발생시킨 값을 가져옵니다. 그리고 StopIteration 예외가 발생하면 반복을 끝냅니다.

▼ 그림 40-1 for 반복문과 제너레이터

참고로 제너레이터 객체에서 __iter__를 호출하면 self를 반환하므로 같은 객체가 나옵니다(제너레이터 함수 호출 > 제너레이터 객체 > __iter__는 self 반환 > 제너레이터 객체).

그런데 generate라는 키워드를 사용하면 되지 왜 yield라고 이름을 지었을까요? yield는 생산하다라는 뜻과 함께 양보하다라는 뜻도 가지고 있습니다. 즉, yield를 사용하면 값을 함수 바깥으로 전달하면서 코드 실행을 함수 바깥에 양보합니다. 따라서 yield는 현재 함수를 잠시 중단하고 함수 바깥의 코드가 실행되도록 만듭니다.

40.1.3  yield의 동작 과정 알아보기

그럼 yield의 동작 과정을 알아보기 위해 for 반복문 대신 next 함수로 __next__ 메서드를 직접 호출해보겠습니다.

  • 변수 = next(제너레이터객체)

yield_next.py

def number_generator():
    yield 0    # 0을 함수 바깥으로 전달하면서 코드 실행을 함수 바깥에 양보
    yield 1    # 1을 함수 바깥으로 전달하면서 코드 실행을 함수 바깥에 양보
    yield 2    # 2를 함수 바깥으로 전달하면서 코드 실행을 함수 바깥에 양보
 
g = number_generator()
 
a = next(g)    # yield를 사용하여 함수 바깥으로 전달한 값은 next의 반환값으로 나옴
print(a)       # 0
 
b = next(g)
print(b)       # 1
 
c = next(g)
print(c)       # 2

실행 결과

0
1
2

yield를 사용하여 바깥으로 전달한 값은 next 함수(__next__ 메서드)의 반환값으로 나온다고 했습니다. 따라서 next(g)의 반환값을 출력해보면 yield에 지정한 값 0, 1, 2가 차례대로 나옵니다. 즉, 제너레이터 함수가 실행되는 중간에 next로 값을 가져옵니다.

next와 yield의 동작 과정을 그림으로 살펴보겠습니다.

먼저 g = number_generator()와 같이 제너레이터 객체를 만듭니다. 그다음에 next(g)를 호출하면 제너레이터 안의 yield 0이 실행되어 숫자 0을 전달한 뒤 바깥의 코드가 실행되도록 양보합니다. 함수 바깥에서는 print(a)로 next(g)에서 반환된 값을 출력합니다.

▼ 그림 40-2 yield 0의 실행 양보

값을 출력했으면 next(g)로 다시 제너레이터 안의 코드를 실행합니다. 이때는 yield 1이 실행되고 숫자 1을 발생시켜서 바깥으로 전달합니다. 그리고 함수 바깥에서는 print(b)로 next(g)에서 반환된 값을 출력합니다.

▼ 그림 40-3 yield 1의  실행 양보

마찬가지로 과정으로 yield 2도 숫자를 발생시키고 print(c)로 제너레이터에서 나온 값을 출력합니다.

▼ 그림 40-4 yield 2의 실행 양보

이렇게 제너레이터는 함수를 끝내지 않은 상태에서 yield를 사용하여 값을 바깥으로 전달할 수 있습니다. 즉, return은 반환 즉시 함수가 끝나지만 yield는 잠시 함수 바깥의 코드가 실행되도록 양보하여 값을 가져가게 한 뒤 다시 제너레이터 안의 코드를 계속 실행하는 방식입니다. 

참고 | 제너레이터와 return

제너레이터는 함수 끝까지 도달하면 StopIteration 예외가 발생합니다. 마찬가지로 return도 함수를 끝내므로 return을 사용해서 함수 중간에 빠져나오면 StopIteration 예외가 발생합니다.

특히 제너레이터 안에서 return에 반환값을 지정하면 StopIteration 예외의 에러 메시지로 들어갑니다.

generator_return.py

def one_generator():
    yield 1
    return 'return에 지정한 값'
 
try:
    g = one_generator()
    next(g)
    next(g)
except StopIteration as e:
    print(e)    # return에 지정한 값

실행 결과

return에 지


https://dojang.io/mod/page/view.php?id=2366


33.3 클로저 사용하기

이제 함수를 클로저 형태로 만드는 방법을 알아보겠습니다. 다음은 함수 바깥쪽에 있는 지역 변수 ab를 사용하여 a * x + b를 계산하는 함수 mul_add를 만든 뒤에 함수 mul_add 자체를 반환합니다.

closure.py

def calc():
    a = 3
    b = 5
    def mul_add(x):
        return a * x + b    # 함수 바깥쪽에 있는 지역 변수 a, b를 사용하여 계산
    return mul_add          # mul_add 함수를 반환
 
c = calc()
print(c(1), c(2), c(3), c(4), c(5))

실행 결과

8 11 14 17 20

먼저 calc에 지역 변수 a와 b를 만들고 3과 5를 저장했습니다. 그다음에 함수 mul_add에서 a와 b를 사용하여 a * x + b를 계산한 뒤 반환합니다.

def calc():
    a = 3
    b = 5
    def mul_add(x):
        return a * x + b    # 함수 바깥쪽에 있는 지역 변수 a, b를 사용하여 계산

함수 mul_add를 만든 뒤에는 이 함수를 바로 호출하지 않고 return으로 함수 자체를 반환합니다(함수를 반환할 때는 함수 이름만 반환해야 하며 ( )(괄호)를 붙이면 안 됩니다).

    return mul_add          # mul_add 함수를 반환

이제 클로저를 사용해보겠습니다. 다음과 같이 함수 calc를 호출한 뒤 반환값을 c에 저장합니다. calc에서 mul_add를 반환했으므로 c에는 함수 mul_add가 들어갑니다. 그리고 c에 숫자를 넣어서 호출해보면 a * x + b 계산식에 따라 값이 출력됩니다.

c = calc()
print(c(1), c(2), c(3), c(4), c(5))    # 8 11 14 17 20

잘 보면 함수 calc가 끝났는데도 c는 calc의 지역 변수 ab를 사용해서 계산을 하고 있습니다. 이렇게 함수를 둘러싼 환경(지역 변수, 코드 등)을 계속 유지하다가, 함수를 호출할 때 다시 꺼내서 사용하는 함수를 클로저(closure)라고 합니다. 여기서는 c에 저장된 함수가 클로저입니다.

▼ 그림 33-4 클로저의 개념

이처럼 클로저를 사용하면 프로그램의 흐름을 변수에 저장할 수 있습니다. 즉, 클로저는 지역 변수와 코드를 묶어서 사용하고 싶을 때 활용합니다. 또한, 클로저에 속한 지역 변수는 바깥에서 직접 접근할 수 없으므로 데이터를 숨기고 싶을 때 활용합니다.

33.3.1  lambda로 클로저 만들기

클로저는 다음과 같이 lambda로도 만들 수 있습니다.

closure_lambda.py

def calc():
    a = 3
    b = 5
    return lambda x: a * x + b    # 람다 표현식을 반환
 
c = calc()
print(c(1), c(2), c(3), c(4), c(5))

실행 결과

8 11 14 17 20

return lambda x: a * x + b처럼 람다 표현식을 만든 뒤 람다 표현식 자체를 반환했습니다. 이렇게 람다를 사용하면 클로저를 좀 더 간단하게 만들 수 있습니다.

보통 클로저는 람다 표현식과 함께 사용하는 경우가 많아 둘을 혼동하기 쉽습니다. 람다는 이름이 없는 익명 함수를 뜻하고, 클로저는 함수를 둘러싼 환경을 유지했다가 나중에 다시 사용하는 함수를 뜻합니다.

33.3.2  클로저의 지역 변수 변경하기

지금까지 클로저의 지역 변수를 가져오기만 했는데, 클로저의 지역 변수를 변경하고 싶다면 nonlocal을 사용하면 됩니다. 다음은 a * x + b의 결과를 함수 calc의 지역 변수 total에 누적합니다.

closure_nonlocal.py

def calc():
    a = 3
    b = 5
    total = 0
    def mul_add(x):
        nonlocal total
        total = total + a * x + b
        print(total)
    return mul_add
 
c = calc()
c(1)
c(2)
c(3)

실행 결과

8
19
33

지금까지 전역 변수, 지역 변수, 변수의 범위, 클로저에 대해 알아보았습니다. 클로저는 다소 어려운 개념이므로 지금 당장 완벽하게 이해하지 않아도 상관없습니다. 나중에 파이썬에 익숙해지면 자연스럽게 익히게 됩니다.


+ Recent posts