ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [lambda x: i * x for i in range(4)]
    Python 2022. 11. 7. 15:24
    def create_multipliers(x):
        return [lambda x : i * x for i in range(4)]
    
    for multiplier in create_multipliers():
        print(multiplier(2))

    출력 값이 어떻게 나올까?

    0,2,4,6이라 생각했지만 6,6,6,6이 나왔다.

    한참을 생각했고 도저히 이해가 되지 않았다.

    찾아보다 python closure 때문이라는 것을 찾고 개념을 보았다. 배경지식부터 [lambda x: i * x for i in range(4)] 의 출력 값의 이유를 알아보자

     

    중첩 함수

    def first():
        def hello_world():
            print("hello world!")
        hello_world()
    
    first()

    hello_world라는 함수를 first 함수로 감싸서 hello_world를 호출하고 있다. 이것이 중첩 함수이다.

    나는 중첩 함수이면 다 클로저라고 알았지만 아니었다.

     

    1급 객체 (first class object)

    처음 들어보는 개념이다.

    일반적으로 다른 개체에 통용 가능한 동작이 지원되는 개체라고 한다.

    통용 가능한 동작이란 크게 3가지이다.

    1. 함수의 인자로 전달
    2. 함수의 반환 값이 됨
    3. 수정되고 할당되는 것들을 전제로 한다.

    무슨 소린지 감이 안 온다.

    우리가 흔히 사용하던 자료형들 int, str, list 등 위의 3가지 동작이 가능하다. 1급 객체이다

    파이썬에서는 함수도 1급 객체이다.

    def add(a, b):
        return a + b
    
    def execute(func, *args):
        return func(*args)
    
    func = add
    
    print(execute(func, 3, 5))
    1. func라는 함수를 함수의 인자로 전달했다.
    2. 함수 내부에서 전달받은 func함수를 문제없이 사용한다.
    3. add 함수를 func라는 새 이름에 할당했다.

    이 특성이 있어야 closure가 성립될 수 있다.

     

    nonlocal

    파이썬에서 중첩 함수가 가능한 것을 확인했다.

    z = 3
    
    def outer(x):
        y = 2
        def inner():
            x = 1
            return x
        return inner()
    
    print(outer(10))

    위의 예시대로 한다면 인자를 10으로 줬을 때 반환되는 값은 몇일까?

    결국 scope에 관한 이야기이다. inner함수의 입장에서 보자.

    • inner 함수 안에 있는 영역은 local scope라고 불린다. 안의 모든 개체들은 inner의 제어 아래에 있다
    • outer안에 있고, inner 밖에 있는 영역은 nonlocal scope라고 불린다.
    • outer 함수 밖의 영역은 global scope이다.

    이런 scope의 구분은 자신의 영역에 자유롭고 다른 영역의 변수나 객체에 대해서는 제한적인 제어를 가지게 된다.

    def first(x):
        def hello_world():
            print(x)
        hello_world()
    
    first("헬로 월드")

    문제없이 헬로 월드가 출력된다.

    다른 영역 즉 외부 스코프에 대해 "읽기"가 문제없이 가능한 것을 확인했다.

    def first(x):
        def hello_world():
            x = x + "!!"
            print(x)
        hello_world()
    
    first("헬로 월드")
    
    # UnboundLocalError: local variable 'x' referenced before assignment

    뭐가 문제일까? local 영역에서 다른 영역에 대한 값을 할당하거나 수정 즉 "쓰는 것"은 불가능하다.

    왜 이렇게 설계가 되어있을까? 코드 영역의 책임과 권한을 명확히 나누는 것이 좋기 때문이다.

     

    Closure

    자신을 둘러싼 scope의 상태 값을 기억하는 함수이다.

    다음의 세 가지 조건을 만족해야 한다.

    1. 해당 함수는 어떤 함수 내의 중첩된 함수 이어야 한다.
    2. 해당 함수는 자신을 둘러싼(enclose) 함수 내의 상태 값을 반드시 참조해야 한다.
    3. 해당 함수를 둘러싼 함수는 이 함수를 반환해야 한다.

    클로저의 특징은 무엇일까?

    • 클로저는 자신을 둘러싼 함수 스코프의 상태 값을 참조하는데 이 값은 메모리에서 사라져도 값이 유지가 된다.
    • 클로저의 내부 변수가 아닌 감싸고 있는 함수의 변수에 접근하는 것을 지원한다. 
    someting.__closure__[0].cell_contents

    클로저 사용 시 장점은 무엇일까?

    • 관리와 책임을 명확히 할 수 있다.
    • 변수가 섞여 발생하는 충돌을 방지할 수 있다.
    • 임의대로 내부구조를 조정할 수 있다.

     

    자 그럼 마지막으로 해결해야 하는 [lambda x: i * x for i in range(4)]의 이유를 알아보자

    결과적으로 이유는 파이썬의 late binding closure 때문이다.

    실제로 위의 함수를 풀어보면

    def create_multipliers(x):
        multipliers = []
    
        for i in range(4):
            def multiplier(x):
                return i * x
            multipliers.append(multiplier)
    
        return multipliers

    위의 함수와 같다. 즉 클로저 안에서 사용되는 변수는 안에서 호출될 때 정해진다. 그러므로 for loop가 끝나고 마지막인 3이 i값이 된다.

    'Python' 카테고리의 다른 글

    Python AES256 암호화, 복호화  (0) 2022.12.22
    파이썬 오버로딩과 오버라이딩  (2) 2022.11.10
    Python 패키징의 역사  (0) 2022.10.12
    Python Celery  (0) 2022.10.06
    Python getter setter property  (0) 2022.09.28

    댓글

Designed by Tistory.