티스토리 뷰

Python

[파이썬] 클로저(Closure)

안톨리니 2022. 5. 31. 18:47
def outer_func():
    score_list = []
    record = {}

    def inner_func(sub_name, score):
        score_list.append(score)
        record[sub_name] = score
        average = sum(score_list) / len(score_list)
        return f'평균점수 = {average}, 기록 = {record}'

    return inner_func

필자는 클로저 형태의 함수를 처음 마주했을 때

외부 함수가 내부 함수 객체를 그대로 반환하는 부분에 대해 이해가 가질 않았다.

 

지금 다시 생각해보면

그때 필자는

그냥 냉정하게 객체라는 개념에 대한 이해가 부족하지 않았나 싶다.

(사실 지금도 부족하다)

 

 

과거로 돌아가서 클로저를 다시 보게 된다면

필자는 클로저를 이해하기 위해

그냥 코드를 뚫어져라 쳐다보다 이해 안 돼서 포기하지 말고

차근차근

파이썬에서 함수는 일급 객체라는 사실이라는 것에 대해

먼저 알고 가야 한다고 생각한다.

 

일급 객체(First Class Object)

파이썬에서 함수는 객체라는 사실에 대해 알고 있나?
(참고로 파이썬은 순수 객체지향 언어이다.)

나무위키(파이썬)

함수뿐만이 아니라

파이썬에서는 모든 것이 다 객체로 취급된다.

 

그리고 파이썬의 모든 객체는

일급 객체이다.

 

일급 객체라는게 뭘까?
일단 일급 객체의 특성을 알아보자.

  • 런타임에 생성할 수 있다.
  • 변수에 할당 가능하다.
  • 함수 인수로 전달할 수 있다.
  • 함수 반환 값이 될 수 있다.

보통 이 4가지 기준을 충족하면

일급 객체라고 부른다.

 

add함수를 만들어서 해당 기준에 충족한 지

테스트해봤다.

def add(x, y):
    return x + y


# 함수는 객체

print(type(add))

def add(x, y):
    return x + y

print(type(add))
>>> <class 'function'>    # add함수는 function클래스의 객체이다.

-----------------------------------------------------

# 런타임에 생성할 수 있다.

콘솔에서 함수를 정의하고 실행해보자!

-----------------------------------------------------
# 변수에 할당 가능하다.

math = add
print(math)

>>> <function add at 0x100876f70>

-----------------------------------------------------

# 함수 인수로 전달할 수 있다.

def calc(func, num1, num2):
    return func(num1, num2)

print(calc(add, 10, 5))

>>> 15

-----------------------------------------------------

# 함수 반환값이 될 수 있다.

def return_func():
    return add 	        #add(함수객체)

func = return_func()
print(func)

>>> <function add at 0x100876f70>

문제없이 잘 실행되는 걸 확인할 수 있다.

(참고로 C, C++ 함수는 일급 객체가 아니다)

 

클로저 형태의 함수가 되기 위한 조건 중 하나가

내포된 함수 객체를 반환하는 것인데

 

만약 함수가 일급 객체가 아니었다면.(함수 반환 값이 될 수 없다면)

아마 오류가 발생하지 않았을까?

 

 

일급 객체(First Class Object)에 대한 참고자료

https://stackoverflow.com/questions/245192/what-are-first-class-objects

 

What are "first-class" objects?

When are objects or something else said to be "first-class" in a given programming language, and why? In what way do they differ from languages where they are not? When one says "

stackoverflow.com


다시 클로저 형태의 함수를 봐보고

이번엔 실행도 해보겠다.

def outer_func():
    score_list = []
    record = {}

    def inner_func(sub_name, score):
        score_list.append(score)
        record[sub_name] = score
        average = sum(score_list) / len(score_list)
        return f'평균점수 = {average}, 기록 = {record}'

    return inner_func

위 outer_func함수를 보면

함수 내부에 함수(inner_func)가 있는 중첩 함수 형태이고

outer_func를 호출하면 inner_func객체를

리턴한다는 걸 알 수 있다.

 

일단 outer_func을 호출해서 변수에 할당한 뒤

그 변수를 출력해보면

closure = outer_func()
print(closure)
>>> <function outer_func.<locals>.inner_func at 0x100f7b820>

함수 outer_func의 로컬(스코프)에 있는 inner_func의 메모리 주소 값이 출력된다.

이로써 함수 객체를 성공적으로 반환한 걸 확인할 수 있었다.

 

이제 이 closure변수를 통해

inner_func을 호출하기 전에

한 가지 드는 의문점이 있다.

def outer_func():
    score_list = []
    record = {}

    def inner_func(sub_name, score):
--------------------------------------------------------------
        score_list.append(score)  # score_list(비전역 변수)
        record[sub_name] = score  # record(비전역 변수)
--------------------------------------------------------------        
        average = sum(score_list) / len(score_list)
        return f'평균점수 = {average}, 기록 = {record}'

    return inner_func

inner_func함수를 보면

outer_func의 변수(score_list, record)를 사용하고 있다.

 

그러나 우린 이미 outer_func를 호출해서 inner_func객체를 성공적으로 만들어 줬고

outer_func는 스택 메모리에서 사라지게 된다.

그러면 outer_func의 로컬 범위도 접근할 수 없는 상태가 아닌가?

closure = outer_func()
print(closure('수학', 70))
>>> 평균점수 = 70.0, 기록 = {'수학': 70}

생각한 것과는 달리

outer_func의 로컬 범위에 있는 변수에 접근을 할 수 있었다.

이 뜻은 누군가가 outer_func의 스코프를 참조하고 있다는 뜻이다.

 

어떻게 함수 본체 외부에 정의된 비전역 변수에 접근할 수 있는 걸까?

def outer_func():
    score_list = []
    record = {}

    def inner_func(sub_name, score):
        score_list.append(score)
        record[sub_name] = score
        average = sum(score_list) / len(score_list)
        return f'평균점수 = {average}, 기록 = {record}'

    return inner_func


closure = outer_func()
print(closure.__code__.co_varnames)
print(closure.__code__.co_freevars)

그 의문을 해결하기 위해

변수(closure)의 코드 객체에 접근한 뒤 읽기 전용 어트리뷰트(co_varnames, co_freevars)를

통해 함수 내부 속성들을 파악할 수 있었다.

>>> ('sub_name', 'score', 'average')
>>> ('record', 'score_list')

먼저 co_varnames는 함수가 가진 매개변수와 로컬 변수들을 튜플형으로 반환하는데

inner_func에서 사용하는 매개변수('sub_name', 'score')와 로컬 변수('average')가

있는 걸 확인할 수 있다.

 

그리고 co_freevars를 사용하면

그 함수가 가진 자유 변수들에 대해 알 수 있는데

자유 변수로 outer_func의 로컬 변수들인 'record'와 'score_list'가 있는 걸 확인할 수 있다.

 

 

 

자유 변수(Free Variable)

여기서 잠깐

자유 변수에 대해 설명하면

자유 변수는 해당 객체의 스코프 안에 정의되지 않은 변수인데 사용되고 있는 변수라고 생각하면 된다.

def outer_func():
    score_list = []
    record = {}

    def inner_func(sub_name, score):
        score_list.append(score)
        record[sub_name] = score
        average = sum(score_list) / len(score_list)
        return f'평균점수 = {average}, 기록 = {record}'

    return inner_func

코드를 보면 outer_func몸체 안에 변수 score_list와 record가 선언되어있다.

그러므로 outer_func에게 score_list와 record는 지역변수이다

 

하지만

inner_func에 score_list와 record는 선언되어 있지도 않고

글로벌 변수도 아니다.

그런데 inner_func에서 score_list와 record는 사용되고 있다.

이런 변수들을 자유 변수라고 한다.

 

실제로 record변수를 inner_func에서 사용하지 않게 된다면

def outer_func():
    score_list = []
    record = {}

    def inner_func(sub_name, score):
        score_list.append(score)
        average = sum(score_list) / len(score_list)
        return f'평균점수 = {average}'

    return inner_func

closure = outer_func()
closure('수학', 80)
closure('영어', 50)
closure('과학', 90)
print(closure.__code__.co_freevars)
('score_list',)

자유 변수 목록 중에 record가 없어진 걸 확인할 수 있다.

 

만약 자유 변수 값들을 알고 싶다면

 __closure__매직 메서드에 대해 알아야 한다.

 

참고로 이건 파이썬 공식문서에 써져있는 __closure__매직 메서드에 대한 내용이다.

__closure__ None or a tuple of cells that contain bindings for the   function’s free variables. See below for information on the cell_contents attribute. Read-only

None 또는 함수의 자유 변수(free variable)들에 대한 연결을 가진 셀(cell)들의 튜플.

이라고 적혀있다.

실제로 출력해보면

print(closure.__closure__)
(<cell at 0x103522fd0: dict object at 0x1033a06c0>, <cell at 0x103522cd0: list object at 0x1033ab9c0>)

두 개의 셀을 확인할 수 있는데

잘 보면 (dict object), (list object)라고 적혀있다.

inner_func가 가진 자유 변수 score_list는 리스트형이고

record는 딕셔너리형이다.

 

이제 이 매직 메서드(__closure)에 cell_content 어트리뷰트에 접근하면

셀(자유 변수) 값을 확인할 수 있다.

closure('수학', 80)
closure('영어', 50)
closure('과학', 90)
for i in closure.__closure__:
    print(i.cell_contents)
>>> {'수학': 80, '영어': 50, '과학': 90}
>>> [80, 50, 90]

어떻게 함수 본체 외부에 정의된 비전역 변수에 접근할 수 있는 걸까? 

라는 의문에 대한 답을 우린 알 수 있었다.

 

inner_func객체는 자유 변수로

outer_func스코프에 등록된 변수들을 참조하고 있는 상태였다.

그렇기 때문에

outer_func이 종료되었음에도 불구하고
누군가(inner_func)가 참조하고 있기 때문에

outer_func스코프에 있는 변수들이 생존한 것이다.

그래서 inner_func이 접근할 수 있었던 것이다.

 

그런데 생각해보면.

그냥 중첩 함수 없이 자유 변수들을 글로벌 변수로 선언하면 되는 거 아닌가?

score_list = []
record = {}

def inner_func(sub_name, score):
    score_list.append(score)
    record[sub_name] = score
    average = sum(score_list) / len(score_list)
    return f'평균점수 = {average}, 기록 = {record}'

print(inner_func('수학', 100))

물론 이렇게 해도 실행은 된다.

 

하지만 우린 score_list와 record라는 변수에

언제든지 접근할 수 있다는 단점이 있다.

 

물론 이게 왜 단점인지 싶지만

코드가 엄청나게 길어진다면

변수에 접근 가능성이 쉬울수록

오류를 범할 확률이 크다.

def outer_func():
    score_list = []
    record = {}

    def inner_func(sub_name, score):
        score_list.append(score)
        record[sub_name] = score
        average = sum(score_list) / len(score_list)
        return f'평균점수 = {average}, 기록 = {record}'

    return inner_func

하지만 이런 클로저 형태의 함수는

오직 내부 함수만이 특정 변수에 접근하면서

메모 라이징까지 가능하게 된다.

 

이제 Closure(폐쇄)라는 단어가 무슨 의미를 뜻하는지

이해가 되지 않는가?

 

이제 클로저 형태의 함수가 어떤 건지 알 수 있었다.

 

내부 함수의 몸체가 아닌 외부 함수의 몸체에 있는

자유 변수에 대한 바인딩을 유지하는 함수다.

 

그렇기 때문에 클로저 형태의 함수는

데이터를 숨겨야할 때, 전역 변수의 사용을 줄일 때

사용된다고 한다.

 

또 클래스로도 똑같이 구현할 수 있는데

만약 클래스에 __init__메서드외에 메서드가 한개라면

클래스를 남용하지말고 클로저형태의 함수를 쓰는게 좋은 대안이라고 한다.

https://www.programiz.com/python-programming/closure

 

Python Closures: How to use it and Why?

Python Closures In this tutorial, you'll learn about Python closure, how to define a closure, and the reasons you should use it. Nonlocal variable in a nested function Before getting into what a closure is, we have to first understand what a nested functio

www.programiz.com

 

후기

처음 파이썬을 배우기 시작했을 때

클로저를 인강을 통해 배웠었다.

 

하지만

당시 도통 무슨 소리인지 이해를 하지 못해서

그 인강을 2달째 안 듣고 있다가

이 글을 쓰기 위해 어제 다시 한번 들었는데

"참 다행이다"라는 생각이 들었다.

 

이런 바보 같은 나에게도

좋은 선생님이 있다.

 

Closure부분은

Fluent Python이라는 책이 나에게 많은 도움을 준거 같다.

덕분에 드디어 인강이 조금씩 이해가 됐고

이제 진도를 나갈 수 있게 되었다.

 

 

 

댓글
공지사항