본문 바로가기

공부/컴퓨터 언어

[python] decorator, closure, iterator, and generator

1. 클로저 함수

: 함수 안에 함수가 구현되어, 외부함수가 내부 함수를 리턴하도록 설계된 구조의 함수 (정확히 말하면 내부함수가 클로저함수이다. 함수 밖의 변수를 기억하는 함수). 이때 외부 함수는 자신이 가진 변숫값 등을 내부 함수에 전달할 수 있다.

# wrapper.py
def mul(m): # 인자 m 필요
    def wrapper(n):
        return m * n
    return wrapper

if __name__ == "__main__":
    mul3 = mul(3) # mul3 = wapper(m=3): 함수 호출 시 내부함수에 m 전달  
    mul5 = mul(5) 

    print(mul3(10))  # 30 출력 
    print(mul5(10))  # 50 출력

 

  1. 외부 함수 mul 안에 내부 함수 wrapper를 구현
  2. 외부 함수는 내부 함수 wrapper를 리턴
    • m은 wrapper 밖 변수이나 mul 함수의 "return wrapper" 라인에서 내부 wrapper의 m에 전달해 저장한다.다시 말하자면 mul 함수에서 wrapper 함수를 리턴할 때, (mul 함수 호출 시 인수로 받은) m을 wrapper 함수에 저장하여 리턴한다.
    • 클래스에서 instance를 생성해 객체를 만드는 과정과 매우 비슷하다.

 

  함수를 만들어주는 함수는 팩토리라고 한다. 위 코드에서 mul은 wrapper를 만들어주는 factory이다. 

 

  위처럼 클로저를 이용하면 기존 함수에 기능을 덧붙이기가 매우 편리하다. 덧붙여, 기존 함수를 바꾸지 않고 기능을 추가할 수 있는 아래 elapsed 함수와 같은 클로저를 데코레이터(decorator)라고 한다.

 

2. 데코레이터 함수

  @함수명은 데코레이터 함수로 인식된다. 이때, 데코레이터에 의해 myfunc 함수는 ‘elapsed 데코레이터를 통해 수행될 것이다. 클로저 함수를 사용할 때 함수의 호출과 실행을 2줄에 거쳐 해야했다. 하지만 데코레이터 사용하는 경우, 이를 1줄에 끝낼 수 있어 가독성이 향상된다.

# 기존
import time

def myfunc():
    start = time.time()
    print("함수가 실행됩니다.")
    end = time.time()
    print("함수 수행시간: %f 초" % (end-start))

myfunc()



# 클로저 사용
import time

def elapsed(original_func): # 기존 함수를 인수로 받는다. 함수도 객체이므로 함수 자체를 인수로 전달 가능
    def wrapper():
        start = time.time()
        result = original_func() # 2) 이 때 실행하는 것은 전달받은 함수(myfunc)
        end = time.time()
        print("함수 수행시간: %f 초" % (end - start))
        return result 
    return wrapper # 1) 이 때 실행하는 것은 wrapper이고

def myfunc():
    print("함수가 실행됩니다.")

decorated_myfunc = elapsed(myfunc) 
decorated_myfunc()
# elapsed 함수 내부의 wrapper 함수가 실행되고, 
# 이 함수는 전달받은 myfunc 함수를 실행하면서 실행 시간을 함께 출력한다.



# 데코레이터 사용
import time

def elapsed(original_func): 
    def wrapper():
        start = time.time()
        result = original_func()  
        end = time.time()
        print("함수 수행시간: %f 초" % (end - start))  
        return result  # 기존 함수의 수행 결과를 리턴한다.
    return wrapper

#elapsed(myfunc)처럼 안긴 형태
@elapsed 
def myfunc():
    print("함수가 실행됩니다.")

myfunc()

# decorated_myfunc = elapsed(myfunc)  # @elapsed 데코레이터로 인해 더이상 필요하지 않다.
# decorated_myfunc()

 

데코레이터는 함수 선언와 유사하다.

  •  "(g * f)(x)" == "g(f(x))"
  • "@g@f def foo() // foo()" ==  "foo=g(f(foo))"

 함수를 정의한다는 것은 "실행가능한 상태"를 만든다는 것이며, 함수 정의 자체로는 함수를 실행하지 않는다. 함수는 호출될 때 실행된다.

  • 정의된 함수는 하나 이상의 데코레이터 표현식(@)으로 감싸질 수 있다. 데코레이터로 호출되는 함수는 함수 객체를 유일한 인수로 한다.
  • 반환값은 함수 객체가 아닌 함수의 이름에 종속된다 (예. return wrapper). 

데코레이터는 호출할 함수를 매개변수로 받는다 (데코레이터 외부 함수의 인자 데코되는 함수로 대체된다).

  • 데코레이터 함수는 안에 래퍼 함수가 있다. 데코레이터 사용 시 @함수의 인자로 데코된 함수를 넣어주기 때문에, 래퍼(클로저) 함수데코된 함수를 인자로 받게 된다.
  • 래퍼 함수 내부는 외부 함수를 끌어와 기능을 추가(혼합)하기 위해 설계된다(원본 함수는 원본 함수를 쓸 때 단 한번만 호출된다). 래퍼함수가 끝나면 해당 함수의 return값을 반환하고, 반환되는 값이 원래 함수의 호출을 대체한다.

함수를 감싸는 형태가 되기 때문에, 데코레이터는 기존 함수를 수정하지 않으면서 추가 기능을 구현할 때 사용

 

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

 

 

1) 데코레이터 팩토리

# 데코레이터 문법 작동 방식
@decorator
def func():
    pass

#는 다음과 같다:

def func():
    pass
func = decorator(func)

#############################
# 데코레이터 팩토리를 사용할 때
@decorator_factory(args)
def func():
    pass

# 는 다음과 같다:

def func():
    pass
func = decorator_factory(args)(func)

 

  아래의 예시를 보자. 먼저 is_multiple 함수를 만들고 데코레이터가 사용할 매개변수 x를 지정합니다. 그리고 is_multiple 함수 안에서 실제 데코레이터 역할을 하는 real_decorator를 만듭니다. 즉, 이 함수에서 호출할 함수를 매개변수로 받습니다. 그다음에 real_decorator 함수 안에서 wrapper 함수를 만들어주면 됩니다.

  • dis_multiple(3)는 데코레이터 팩토리. 즉, 데코레이터를 생성하는 함수이다.
    • @is_multiple(3)에서 3은 데코레이터에 전달할 조건 값.
  • add는 단순히 데코레이터가 적용될 대상 함수.
    • add 함수는 @is_multiple(3)의 조건과 무관하게 본래 기능 (x + 10)만 수행한다
#1
decorator = is_multiple(3)  # 여기서 n=3인 데코레이터 함수가 생성됨
add = decorator(add)        # 데코레이터에 func 함수 적용

#2
def is_multiple(n):
    def decorator(func):
        def wrapper(x):
            if x % n == 0:
                return func(x)
            else:
                return f"{x} is not a multiple of {n}"
        return wrapper
    return decorator

# "add = is_multiple(3)(add)" ==
@is_multiple(3) 
def add(x):
    return x + 10
#1) decorator = is_multiple(3) #n=3인 상태에서 내부의 decorator 함수를 반환
#2) add = decorator(add)  

add()

 

그리하여 아래 데코레이터 함수의 실행 순서는

  1. @is_multiple()는 먼저 실행되어 decorator라는 함수를 반환합니다.
  2. 파이썬은 그 다음 줄의 함수(add)를 자동으로 decorator(add) 형태로 감쌉니다. (= is_multiple()(add)
  3. 이때 add는 아무 말 없이도 자동으로 decorator의 인자로 들어갑니다.
@is_multiple()
def add(x):
    return x + 10

add(2) # 4. "return x + 10" 출력

# 1. 먼저 데코레이터 팩토리 실행
decorator = is_multiple()  # 이 시점에 decorator 함수가 반환됨

# 2. add 정의
def add(x):
    return x + 10

# 3. 데코레이터 적용
add = decorator(add)  # 파이썬이 이걸 자동으로 처리함

 

  내가 직접 넘긴 적은 없는데 어떻게 add가 decorator의 인자로 전달되는지 궁금해졌다. 코드 자체가 n은 계산을 위한 상수로, func는 인자를 받는 함수로 설계되었고, n = 3이라는 숫자는 의도에 맞게 넣어준 숫자라는 것이다.

 

 

2) 데코레이터 중첩

함수에는 데코레이터를 여러 개 지정할 수 있다. 이때 데코레이터가 실행되는 순서는 위에서 아래 순이다. 위의 데코레이터가 아래 데코레이터/함수를 인자로 받는다.

#decorator1
#decorator2
#hello

#1)
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()

#2)
decorated_hello = decorator1(decorator2(hello))
decorated_hello()

 

 

  여러 개 데코레이터를 사용하면 데코레이터에서 반환된 wrapper 함수가 다른 데코레이터의 func로 들어간다. 때문에 두번째 @의 결과로 __name__이 wrapper가 출력된다. 원래 이름인 add를 출력하고 싶다면 functools 모듈의 wraps 데코레이터를 사용해야 합니다. 다음과 같이 @functools.wraps func를 넣은 뒤 wrapper 함수 위에 지정해줍니다(from functools import wraps로 데코레이터를 가져왔다면 @wraps(func)를 지정).

  @functools.wraps는 원래 함수의 정보를 유지시켜준다. 따라서 디버깅을 할 때 유용하므로 데코레이터를 만들 때는 @functools.wraps를 사용하는 것이 좋다.

 

def is_multiple(x):              
    def real_decorator(func):    
    	# @functools.wraps에 func를 넣은 뒤 wrapper 함수 위에 지정
    	@functools.wraps(func)   
        def wrapper(a, b):      
            r = func(a, b)      
            if r % x == 0:      
                print('{0}의 반환값은 {1}의 배수입니다.'.format(func.__name__, x))
            else:
                print('{0}의 반환값은 {1}의 배수가 아닙니다.'.format(func.__name__, x))
            return r             
        return wrapper          
    return real_decorator       
 
# @데코레이터(인수)
@is_multiple(3)
@is_multiple(7)
def add(a, b):
    return a + b
 
add(10, 20)
#add의 반환값은 7의 배수가 아닙니다.
#wrapper의 반환값은 3의 배수입니다.


# @functools.wraps(func)를 사용하지 않은 경우
decorated_add = is_multiple(3)(is_multiple(7)(add))
decorated_add(10, 20)

 

 

3) 인수

  데코레이터(@)는 기존 함수가 어떤 입력 인수를 취할지 파악하지 못하기 때문에, 기존 함수의 입력 인수에 상관없이 동작하도록 만들어야 한다. 만약 기존 함수의 입력 인수를 알 수 없는 경우에는 클로저 함수에 *args와 **kwargs 매개변수를 적용하면 된다.

def func(*args, **kwargs):
    print(args)
    print(kwargs)
 
# func 함수가 입력 인수의 개수와 형태에 상관없이 모든 입력을 처리
func(1, 2, 3, name='foo', age=3)
# (1, 2, 3)
# {'age': 3, 'name': 'foo'}



def home_work(func):
    def wrapper(*arg):
        print(*arg, func(*arg))
        return 
    return wrapper

@home_work
def add(a,b):
    return a + b

add(2, 3)

 

 

4) 정리

  1. 함수 받음
  2. 매개변수 저장
  3. 함수 호출
  4. 안쪽 return 값 반환 (데코된 함수 대체)
# 1-1
def f(arg):
    pass  
f = staticmethod(f)

# 1-2
@staticmethod
def f(arg):
    pass


# 2-1
@dec2
@dec1
def func(arg1, arg2, ...):
    pass
    
# 2-2    
def func(arg1, arg2, ...):
    pass
func = dec2(dec1(func))


# 3-1
@decomaker(argA, argB, ...)
def func(arg1, arg2, ...):
    pass
 
 #3-2
func = decomaker(argA, argB, ...)(func)

 

 
 

5) 연습문제

counter = 0

def cached(func):
  result = func() # count = 1 
# def lambda():
#     return result
# return lambda
  return lambda: result #lambda 함수가 클로저 역할을 하여, result값을 참조하게 함


@cached
def test():
  global counter # global 키워드가 없으면 함수 내에서는 전역 변수를 읽을 수는 있지만, 수정할 수는 없다.
  counter += 1
  print('# test function called', counter, 'times.')
  return counter

print('=== 스따또! ===')
print(test()) # test = cached(test)
print(test()) # def test(): return 1
print(test())

# === 스따또! ===
## test function called 1 times.
#1
#1
#1

## decorator를 사용하지 않는 경우
def test():
    global counter
    counter += 1
    print('# test function called', counter, 'times.')
    return counter

# 함수 직접 대체
result = test()  # 원본 함수 실행
test = lambda: result  # 함수 대체

 

데코레이터는 코드가 정의될 때 가능한 시점까지 즉시 적용된다. 때문에 @cached는 함수를 즉시 실행하고 그 결과로 대체 됨

 

코드가 파이썬 인터프리터에 의해 처리될 때 다음과 같은 일이 발생한다.

  1. Python이 test 함수를 정의합니다.
  2. 즉시 test = cached(test)를 실행합니다.
  3. cached(test) 내부에서:
    • result = func() → test() 함수를 실행합니다.
    • 이때 counter가 1이 되고 "# test function called 1 times." 출력
    • test() 함수의 반환값인 1을 result에 저장
    • return lambda: result → result(값 1)를 반환하는 새 함수를 생성
  4. test 함수는 이제 lambda: result(항상 1을 반환하는 함수)로 대체됩니다.

정리하자면,

  • 데코레이터는 함수 정의 시점에 실행/적용됩니다 (프로그램 실행 시작 시)
  • test는 한 번만 실행됩니다 (데코레이터가 적용되는 시점에)
  • 그 후로는 원래 test 함수가 아닌, 캐시된 결과 1을 반환하는 새 함수가 됩니다

 

 

3. 이터레이터 함수

a = [1, 2, 3]
ia = iter(a)
type(ia)
# <class 'list_iterator'>
next(ia)
# 1

 


1) iterable: 반복문을 사용할 수 있는 객체/값을 차례대로 꺼낼 수 있는 객체/__iter__ 함수를 호출할 수 있는 객체

  • __iter__ 메소드를 포함하고 있는 객체를 iterable 객체라고 한다 (예를 들어, 리스트가 반복 가능(iterable)한 객체이다)
  • iterable은 성격이고, 이 성격을 지닌 객체는 내부적으로 __iter__가 내장되어 있다. 
  • __iter__를 실행하기 전까지는 iterable한 객체이며, 이 함수를 실행하면 무엇이 되느냐?

일단,  Iterable한 것은 __next__ 메소드가 존재하지 않는다. 내부에 __Iter__라는 메소드만 가지고 있다.

 

2) iterator 객체: next를 호출할 수 있는 객체/ 값을 차례대로 꺼낼 수 있는 객체 (동작 요소)

  • __Iter__ 를 거쳐 생성된 객체로, __next__ 메서드가 내장되어 이를 호출할 수 있는 객체 (다음 값을 가져올 수 있음)
  • 이터레이터는 for 문을 이용하여 반복하고 난 후에는 다시 반복하더라도 더는 그 값을 가져오지 못한다. 즉, for문이나 next로 그 값을 한 번 읽으면 그 값을 다시는 읽을 수 없다.

  iter()라는 내장 함수를 호출하면 내부적으로 해당 객체의 __iter__ 메소드를 호출하게 된다. __iter__ 메소드는 이터레이터 객체를 반환해주는데, 이 때 이터레이터 객체는 __next__ 메소드를 반드시 구현하고 있어야 한다. 이터러블 객체로부터 이터레이터 객체를 얻었다면 우리는 next 내장 함수를 호출하여 계속해서 값을 얻어올 수 있다. 

 

  정리하자면, 이터러블 객체는 내장된  iter()가 호출되면 이터레이터 객체로 변환되고, 이터레이터는 next()를 호출할 수 있다. 이터레이터는  for/while과 같은 반복문의 규칙/문법에 따라 next를 호출한다. 따라서, 이터레이터 객체를 만들 때 __iter__와 __next__ 메소드를 구현해야 한다.

 

# iter 함수는 iter가 적용된 self를 반환하여 스스로(self) iterator가 되게 만든다.
iterator = iter(obj)  # obj.__iter__() == iter(obj)
next(iterator)        # iterator.__next__()

 

** iterable(obj)은 iter()를, iterator가 next()를 제공

** iterator는 next 포인터 뒤로 미루는...어디까지 순회했는지 기억해주는...(흐름을 개발자가 조절 가능)

 

4. 제네레이터 함수

: 이터레이터를 생성해 주는 함수. 제너레이터로 생성한 객체는 이터레이터와 마찬가지로 next 함수 호출 시 그 값을 차례대로 얻을 수 있다. 이때 제너레이터에서는 차례대로 결과를 반환하고자 return 대신 yield 키워드를 사용한다.

>>> def mygen():
...     yield 'a'
...     yield 'b'
...     yield 'c'
... 
>>> g = mygen()
>>> type(g)
<class 'generator'>
>>> next(g)
'a'
>>> next(g)
'b



# generator.py
def mygen():
    for i in range(1, 1000):
        result = i * i
        yield result

gen = mygen()

print(next(gen))
print(next(gen))
print(next(gen))
#1
#4
#9



(i * i for i in range(1, 1000))

class MyIterator:
    def __init__(self):
        self.data = 1

    def __iter__(self):
        return self

    def __next__(self):
        result = self.data * self.data
        self.data += 1
        if self.data >= 1000:
            raise StopIteration
        return result

 

 

# generator2.py
import time

def longtime_job():
    print("job start")
    time.sleep(1)  # 1초 지연
    return "done"

list_job = [longtime_job() for i in range(5)]
print(list_job[0])

#job start
#job start
#job start
#job start
#job start
#done

# generator2.py
import time

def longtime_job():
    print("job start")
    time.sleep(1)
    return "done"

list_job = (longtime_job() for i in range(5))
print(next(list_job))
# job start
# done

 

 

 

 

 

 

 

 

 

 

 

 

 

'공부 > 컴퓨터 언어' 카테고리의 다른 글

[C언어] 포인터 이해 정리  (0) 2025.07.02
[python] 파이썬 이론 공부  (0) 2025.05.10
[python] 이론 정리  (0) 2025.03.09
[Front-end] Javascript  (0) 2025.03.08
[Front-end] CSS  (0) 2025.03.08