[python] 일급함수 - 클로저(Closure)

728x90

1. 파이썬 변수 범위 - global

먼저 global에 대해 알아보겠습니다.

파이썬에서 global은 지역볌수를 전역변수로 참조하는 키워드입니다.

아래 예제를 통해 자세히 알아보겠습니다.

b = 20

def func1(a):
    print(a) #출력: 2
    print(b) #출력: 20 -> 전역변수 20 출력

func1(2)

c = 30

def func2(a):
    print(a)
    print(c) 
    c = 40 # 지역변수에 전역변수와 동일한 변수가 있을 경우 에러발생

# func2(5) #출력: UnboundLocalError: local variable 'c' referenced before assignment

def func3(a):
    global c # 전역 참조 -> 지역변수에 전역변수와 동일한 변수가 있어도 에러가 발생하지 않음
    print(a) #출력: 10
    print(c) # 출력: 30
    c=40

func3(10)
print("전역 c=",c) #출력: 전역 c= 40

 

2. 클로저

클로저란 외부에서 호출된 변수값, 상태(레퍼런스)를 복사 후 저장하여 나중에 접근(엑세스)할 수 있게 해주는 기능입니다.

쉽게 말해서 함수가 끝나도 지역변수의 값이나 상태등을 기억한다는 것입니다.

 

클로저를 사용하는 이유는 아래와 같습니다.

1) 서버 프로그래밍을 할 때 동시성을 제어하는 것이 필요합니다. 동시성을 제어하지 못한다면 메모리공간에 여러 자원이 접근했을 때 교착상태(dead lock)에 빠질 수 있습니다. 
2) 메모리를 공유하지 않고 메시지 전달로 처리하기 위함입니다.
3) 클로저는 공유하되 변경되지 않는 구조를 적극적으로 사용합니다. 이는 함수형 프로그래밍으로 이어집니다.
4) 클로저는 불변자료구조 및 원자성 등을 통해 멀티스레드 프로그래밍 강점을 가질수 있습니다. 파이썬은 멀티쓰레드가 되지 않지만 코루틴을 사용하여 비슷한 효과를 가질 수 있습니다.

 

먼저 클래스를 사용하여 평균값을 구해보겠습니다.

class Averager():
    def __init__(self):
        self._series = []

    def __call__(self, v):
        self._series.append(v)
        print(f"{self._series} / {len(self._series)}")
        return sum(self._series) / len(self._series)


averager = Averager()

print(dir(averager))
#출력: ['__call__', '__class__', '__delattr__', '__dict__', '__dir__', '__doc__',
# '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__',
# '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__',
# '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__',
# '__str__', '__subclasshook__', '__weakref__', '_series']

print(averager(10))
# 출력:
# [10] / 1
# 10.0

print(averager(30))
#출력:
# [10, 30] / 2
# 20.0

print(averager(50))
#출력:
# [10, 30, 50] / 3
# 30.0

 

위와 동일한 기능을 클로저를 사용해서 구현해보겠습니다.

def closure_ex1():
    # 자유변수
    #클로저 영역
    series = []
    def averager(v):
        series.append(v)
        print(f"{series} / {len(series)}")
        return sum(series) / len(series)
    return averager # 함수를 결과로 반환


cls1 = closure_ex1()
print(cls1) # 출력:<function closure_ex1.<locals>.averager at 0x000001A8D286F940>

print(cls1(10))
#출력:
# [10] / 1
# 10.0

print(cls1(20))
# 출력:
# [10, 20] / 2
# 15.0

print(cls1(30))
#출력:
# [10, 20, 30] / 3
# 20.0

print(dir(cls1))
#출력:['__annotations__', '__call__', '__class__', '__closure__', '__code__', '__defaults__',
# '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__get__',
# '__getattribute__', '__globals__', '__gt__', '__hash__', '__init__', '__init_subclass__',
# '__kwdefaults__', '__le__', '__lt__', '__module__', '__name__', '__ne__', '__new__',
# '__qualname__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__']

print(dir(cls1.__code__))
#출력: ['__class__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__',
# '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__',
# '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__',
# '__str__', '__subclasshook__', 'co_argcount', 'co_cellvars', 'co_code', 'co_consts', 'co_filename',
# 'co_firstlineno', 'co_flags', 'co_freevars', 'co_kwonlyargcount', 'co_lnotab', 'co_name', 'co_names',
# 'co_nlocals', 'co_posonlyargcount', 'co_stacksize', 'co_varnames', 'replace']

print(cls1.__code__.co_freevars) # 출력: ('series',) -> 자유변수
print(cls1.__closure__[0].cell_contents) # 출력: [10, 20, 30] -> 3번 호출했을때 매개변수의 값을 모두 가지고 있음

 

잘못된 클로저의 사용예제는 아래와 같습니다.

def closure_ex2():
    # 자유변수
    cnt = 0
    total = 0
    def averager(v):
        cnt += 1
        total +=v
        return total / cnt
    return averager

cls2 = closure_ex2()
# print(cls2(10)) # 에러발생 : UnboundLocalError: local variable 'cnt' referenced before assignment

 

자유변수는 averager함수에서는 전역변수에 속하게 됩니다. 따라서 전역변수와 지역변수의 변수명이 동일하여 참조 오류가 발생합니다.

이를 해결하기 위해서는 아래와 같이 처리하면 됩니다.

def closure_ex3():
    # 자유변수
    cnt = 0
    total = 0
    def averager(v):
        nonlocal cnt,total # nonlocal 키워드는 지역변수를 자유변수로 변경해 준다
        cnt += 1
        total +=v
        return total / cnt
    return averager

cls3 = closure_ex3()
print(cls3(15)) # 출력: 15.0
print(cls3(25)) # 출력: 20.0
print(cls3(35)) # 출력: 25.0