LEGB 규칙

  • 파이썬에서는 변수의 영역(scope)을 다음과 같이 나누며, 함수에서의 우선순위는 지역변수 > 외부 함수의 지역변수 > 전역변수 > 내장형 이다.

    • Local: 함수 안의 범위

    • Enclosed function locals: 내부 함수 밖에, 외부 함수 안의 범위

    • Global: 모듈 범위로, .py 파일 내부의 모든 범위

    • Built-in: 인터프리터 전역적으로 사용가능한 내장형 함수 및 타입을 의미

 

클로저(Closure)

  • 함수 안에 함수를 선언하는 것으로, 바깥 함수를 외부 함수(outer), 안쪽 함수를 내부 함수(inner) 또는 클로저(closure)라고 한다.

  • 내부 함수에서는 외부 함수의 지역변수(매개변수 포함)에 접근할 수 있다.

    • 단, 내부 함수에서 외부 변수의 쓰기 작업을 수행하려면 nonlocal 키워드를 사용해야 한다.

    • 읽기는 자유롭게 가능하다.

>>> def outer(num):
...     num += 3
...     def inner():
...             nonlocal num
...             num *= 3
...             return num
...     return inner
...
>>> f = outer(1)
>>> f()
12
>>> def outer(num): # nonlocal 키워드를 쓰지 않을 경우 다음과 같은 예외가 발생한다.
...     num += 3
...     def inner():
...             num *= 3
...             return num
...     return inner
...
>>> f = outer(1)
>>> f()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 4, in inner
UnboundLocalError: local variable 'num' referenced before assignment
  • 메모리 관점: 외부 함수가 호출되면 외부 함수 자체는 메모리에 남아있지만(함수 주소) 변수들은 모두 제거된다. 그러나 내부 함수는 외부 함수 범위의 변수들을 사용하면 자신의 영역에 별도로 저장하기 때문에 f = outer(1) 이후에도 nums 변수는 메모리(내부 함수 영역)에 남아 있는 것이다.

    • 파이썬에서 함수는 일급 객체로 실제로 type(outer) 를 해보면 <class 'function'>이 출력된다. 따라서, 위와 같은 정보를 확인하려면 다음과 같이 객체의 멤버 변수나 함수를 이용하면 된다.

>>> type(outer)
<class 'function'>
>>> f.__closure__ # cell 이라는 객체로 튜플 타입으로 저장되어 있다.
(<cell at 0x1061e49d0: int object at 0x10607bbe0>,)
>>> type(f.__closure__[0])
<class 'cell'>
>>> f.__closure__[0].cell_contents # 변수에 저장된 값이 메모리에 남아있다!
12
  • 외부 함수에는 여러 개의 내부 함수(closure)를 선언할 수 있고 내부 함수끼리 서로 호출이 가능하다.

    • 그러나, 외부 함수의 closure 변수는 모든 내부 함수의 정보를 가지고 있지 않다.

    • 다음과 같이 내부 함수가 여러 개일 경우 호출되는 순서에 따라 내부 함수들이 저장되며, 내부 함수 A가 내부 함수 B를 호출하면 내부 함수 A의 __closure__ 에 내부 함수 B가 저장된다.

    • 사용되지 않는 내부 함수는 저장되지 않는다.

>>> def fn1():
...     def inner():
...             return 1
...     def inner2():
...             n = inner()
...             return n * 3
...     def inner3():
...             n = inner2()
...             return n + 5
...     return inner3
...

>>> x = fn1()
>>> x()
8
>>> x
<function fn1.<locals>.inner3 at 0x106303820>
>>> x.__closure__
(<cell at 0x1062dd400: function object at 0x106303940>,)
>>> x.__closure__[0].cell_contents # inner3 함수 객체 안에 inner2 함수 객체가 저장되어 있다.
<function fn1.<locals>.inner2 at 0x106303940>
>>> x.__closure__[0].cell_contents.__closure__
(<cell at 0x1062dd430: function object at 0x106303af0>,)
>>> x.__closure__[0].cell_contents.__closure__[0].cell_contents # 마찬가지.
<function fn1.<locals>.inner at 0x106303af0>

 

데코레이터(Decorator)

  • 클로저가 가장 많이 사용되는 코딩 패턴으로는 데코레이터(Decorator)가 있다. 이름에서 알 수 있듯이 코드에서 무언가를 꾸며주는 역할을 한다. 실제로 객체 지향 패턴에도 데코레이터 패턴이라는게 있는데 파이썬에서는 클래스로 선언하는 것보다 함수로 선언하는 것이 많이 사용된다.

  • 특징은 함수를 인자로 받아서 내부 함수로 한번 감싸서(wrapping) 반환을 한다는 점이다.

  • 보통 둘 이상의 함수에 대해 추가적인 작업이 선행되어야 하는 경우 자주 쓰인다.

    • 예를 들어, 데이터를 넘기기 전 공통된 전처리가 필요한 경우, 로깅, 리다이렉트, 성능 테스트

def decorator_fn(original_fn):
    def wrapped_fn(*args, **kwargs):
        if len(args) == 2:
        	args = (args[0] + args[1], args[0] * args[1])
        return original_fn(*args, **kwargs)
    return wrapped_fn

@decorator_fn
def fn1():
	print("HI")

@decorator_fn
def fn2(a, b):
	print(a, b)

fn1() # HI
fn2(2, 3) # 5, 6
  • 위에서 @을 썼는데 이는 fn = decorator_fn(fn1) 과 동일하게 동작하나 후자는 가독성이 낮아 잘 쓰이지 않는다.

  • 클래스에서는 __call__() 이라는 메소드를 이용해서 다음과 같이 선언할 수 있다. 위와 동일하게 동작한다.

class DecoratorClass:
	def __init__(self, original_fn):
    	self.fn = original_fn
        
    def __call__(self, *args, **kwargs):
    	if len(args) == 2:
        	args = (args[0] + args[1], args[0] * args[1])
        return self.fn(*args, **kwargs)
        
@DecoratorClass
def fn1():
	print("HI")

@DecoratorClass
def fn2(a, b):
	print(a, b)

fn1() # "HI"
fn2(2, 3) # 5, 6

 

+ Recent posts