상세 컨텐츠

본문 제목

클로저 (Closure) (2) - 클로저 개념 및 구현

개발/python-병렬처리

by Matthew0633 2022. 5. 9. 23:17

본문

클로저 (Closure)

(Closure in Wiki) In programming languages, a closure, also lexical closure or function closure, is a technique for implementing lexically scoped name binding in a language with first-class functions. Operationally, a closure is a record storing a function[a] together with an environment.[1] The environment is a mapping associating each free variable of the function (variables that are used locally, but defined in an enclosing scope) with the value or reference to which the name was bound when the closure was created.[b] Unlike a plain function, a closure allows the function to access those captured variables through the closure's copies of their values or references, even when the function is invoked outside their scope.
(https://en.wikipedia.org/wiki/Closure_(computer_programming))

클로저 개념을 위한 기초 개념으로 변수 범위 (variable scope) 를 먼저 짚고 넘어가자

변수 범위 (variable scope)

전역변수를 함수 내에서 사용

# 전역변수 b를 함수 내에서 사용

b = 20

def func_v2(a):
    print(a)
    print(b)

func_v2(10)

# 10
# 20

전역변수 값을 함수 내에서 수정 : global keyword 사용

c = 30

def func_v3(a):
    global c
    print(a)
    print(c)
    c = 40

func_v3(10)
print(c)

# 10
# 30
# 40

Closure 정의

외부에서 호출된 함수 내 enclosed 된 scope의 값과 코드가 저장되어 이후에도 해당 상태에 다시 접근하여 계속 사용 가능한 개념이다. 일반적으로 우리가 아는 함수는 호출 및 실행을 하고 결과를 반환하면, 메모리에 함수 내의 내용이 남아있지 않지만, Closure는 결과값이 반환된 (코드와 값을 둘러싼 함수가 반환되기에) 후에도 계속 호출 및 새로운 인자를 넣어 재사용이 가능하다

Closure 개념을 배우기 전까지는, Class를 활용한 프로그래밍을 통해서만, 값을 누적 및 저장하고, 이를 재접근하거나 수정하는 기능이 가능하다고 생각했었다. 그런데 Closure을 활용하면 Class로만 가능하다고 생각했던 기능에 대해 함수만으로 프로그래밍이 가능하다

Closure 사용 이유

  • Closure는 서버 프로그래밍과 같이 동시성(Concurrency) 제어가 필요한 영역에서 활용되는 개념이다
  • 일반적으로 메모리 공간에 여러 자원이 접근 -> 교착상태(Dead Lock)가 일어난다
  • 파이썬에서는 이를 피하기 위해 메모리를 공유하지 않고 메시지 전달로 처리하기 위해 활용되는 개념이 Closure 이다 (Erlang과 같이 이를 위해 따로 언어가 등장하기도)
  • 클로저는 공유하되 저장된 값이 변경되지 않는(Immutable, Read Only) 특성을 가진다 (불변성과 원자성을 지닌다)
  • 이러한 클로저는 함수형 프로그래밍에서 많이 활용되기도 한다 (함수형 프로그래밍을 추후에 공부해보자..!)
  • 파이썬은 싱글스레드 환경이지만, 클로저(좀 더 정확히는 Coroutine)를 통해 멀티스레드와 같은 프로그래밍이 가능하도록 한다..!

Closure 개념 이해를 위한 클래스 구현

아래 코드에서 Averager의 객체인 averager_clscall함수가 실행된 후 종료되지만 해당 함수를 통해 self._series에 값이 계속 누적 및 저장되고 있는 것을 알 수 있다. Closure 의 경우 해당 함수가 호출되어 객체에 할당된 뒤에도, 내부 변수와 코드가 유지되어 해당 객체의 호출을 통한 재사용 및 결과값 활용이 가능하다

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

    def __call__(self, v):
        self._series.append(v)
        print('inner >>> {} / {}'.format(self._series, len(self._series)))
        return sum(self._series) / len(self._series)


# 인스턴스 생성
averager_cls = Averager()

# 누적
print(averager_cls(15))
print(averager_cls(35))
print(averager_cls(40))

print()
print()

Closure 구현

위에서 Class로 구현했던 평균계산 기능을 Closure를 활용하여 구현해보자

# 클로저(Closure) : 함수가 실행된 후 종료된 뒤에도 자유 변수 값을 기억한다
def closure_ex1():
    # Free variable - 자유 변수
    series = []

    # 클로저 영역
    def averager(v):
        # series = [] # 주석 해제 후 확인
        series.append(v)
        print('inner >>> {} / {}'.format(series, len(series)))
        return sum(series) / len(series)
    
    return averager # 함수자체를 return


avg_closure1 = closure_ex1()

print(avg_closure1(15)) # 15.0
print(avg_closure1(35)) # 25.0
print(avg_closure1(40)) # 30.0

print()
print()

Closure의 내부를 살펴보자

# function inspection
print(dir(avg_closure1))                         # Closure 객체의 namespace의 모든 이름을 리스트로 출력 : __code__ 확인
print(dir(avg_closure1.__code__))                # compile된 함수의 body 객체 : co_freevars 확인
print(avg_closure1.__code__.co_freevars)         # co_freevars 접근 : series 자유변수 확인
print(dir(avg_closure1.__closure__[0]))          # Clousre 객체의 속성들 확인 : cell_contents 확인
print(avg_closure1.__closure__[0].cell_contents) # series 변수 값 확인
Outputs:

['__annotations__', '__call__', '__class__', '__closure__', '__code__', ....]

['__class__', '__delattr__', '__dir__' ,..., 'co_firstlineno', 'co_flags', 'co_freevars', ....]

('series',)

['__class__', '__delattr__', '__dir__' ,...,  'cell_contents']

[15, 35, 40]

Closure 사용 주의 : 자유변수에 값을 초기화하는 코드가 Closure 내부 함수에 존재할 때는 해당 자유변수를 곧바로 참조할 수 없어 에러가 발생한다.

# 잘못된 클로저 사용
def closure_ex2():
    # Free variable
    cnt = 0
    total = 0

    def averager(v):
        cnt += 1 # cnt = cnt + 1
        total += v
        return total / cnt
    
    return averager

avg_closure2 = closure_ex2()

# print(avg_closure2(15)) # 예외

이 때는 자유변수를 내부 함수 내에서도 참조하도록 nonlocal keyword 사용하면 해결된다

# non local keyword 사용
def closure_ex3():
    # Free variable
    cnt = 0
    total = 0
    
    def averager(v):
        nonlocal cnt, total
        cnt += 1
        total += v
        return total / cnt
    
    return averager

avg_closure3 = closure_ex3()

print(avg_closure3(15))
print(avg_closure3(35))
print(avg_closure3(40))

관련글 더보기

댓글 영역