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 출력
- 외부 함수 mul 안에 내부 함수 wrapper를 구현
- 외부 함수는 내부 함수 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()
그리하여 아래 데코레이터 함수의 실행 순서는
- @is_multiple()는 먼저 실행되어 decorator라는 함수를 반환합니다.
- 파이썬은 그 다음 줄의 함수(add)를 자동으로 decorator(add) 형태로 감쌉니다. (= is_multiple()(add)
- 이때 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) 정리
- 함수 받음
- 매개변수 저장
- 함수 호출
- 안쪽 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는 함수를 즉시 실행하고 그 결과로 대체 됨
코드가 파이썬 인터프리터에 의해 처리될 때 다음과 같은 일이 발생한다.
- Python이 test 함수를 정의합니다.
- 즉시 test = cached(test)를 실행합니다.
- cached(test) 내부에서:
- result = func() → test() 함수를 실행합니다.
- 이때 counter가 1이 되고 "# test function called 1 times." 출력
- test() 함수의 반환값인 1을 result에 저장
- return lambda: result → result(값 1)를 반환하는 새 함수를 생성
- 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 |