Skip to content

22. 클로저(Closure)


1. 키워드

  • 클로저(Closure)
  • 전역 변수(Global Variable)와 전역 범위(Global Scope)
  • 지역 변수(Local Variable)와 지역 범위(Local Scope)
  • 네임스페이스(Namespace)
  • globalnonlocal


2. 클로저 사용하기

  • 이번에는 변수의 사용 범위와 함수를 클로저 형태로 만드는 방법을 알아보자.


3. 변수의 사용 범위 알아보기

  • 파이썬 스크립트에서 변수를 만들면 다음과 같이 함수 안에서도 사용할 수 있다.


x = 10

def foo():
    print(x)

foo() # 10
print(x) # 10


  • foo 함수에서 함수 바깥에 있는 변수 x의 값을 출력했다.
  • 물론 함수 바깥에서도 x의 값을 출력할 수 있다.
  • 이처럼 함수를 포함하여 스크립트 전체에서 접근할 수 있는 변수를 전역 변수라고 부른다.
  • 특히 전역 변수에 접근할 수 있는 범위를 전역 범위라고 한다.


001


  • 그럼 변수 x를 함수 foo 안에서 만들어보자.


def foo():
    x = 10

    print(x)

foo() # 10
print(x) # NameError: name 'x' is not defined


  • 실행을 해보면 x가 정의되지 않았다는 에러가 발생한다.
  • 왜냐하면 변수 x는 함수 foo 안에서 만들었기 때문에 foo의 지역 변수이다.
  • 따라서 지역 변수는 변수를 만든 함수 안에서만 접근할 수 있고, 함수 바깥에서 접근할 수 없다.
  • 특히 지역 변수를 접근할 수 있는 범위를 지역 범위라고 한다.


002


1) 함수 안에서 전역 변수 변경하기

  • 함수 안에서 전역 변수의 값을 변경해 보자.


x = 10

def foo():
    x = 20

    print(x)

foo() # 20
print(x) # 10


  • 분명 함수 foo 안에서 x = 20처럼 x의 값을 20으로 변경했다.
  • 하지만 함수 바깥에서 printx의 값을 출력해 보면 10이 나온다.
  • 겉으로 보기에는 foo 안의 x는 전역 변수인 것 같지만 실제로는 foo의 지역 변수이다.
  • 즉, 전역 변수 x가 있고, foo에서 지역 변수 x를 새로 만들게 된다.
  • 이 둘은 이름만 같을 뿐 서로 다른 변수이다.
  • 함수 안에서 전역 변수의 값을 변경하려면 global 키워드를 사용해야 한다.


  • 다음과 같이 함수 안에서 global에 전역 변수의 이름을 지정해 준다.


global 전역변수
x = 10

def foo():
    global x

    x = 20

    print(x)

foo() # 20
print(x) # 20


  • 이제 함수 안에서 x20으로 변경하면 함수 바깥에서 x를 출력했을 때 20이 나온다.
  • 이렇게 함수 안에서 변수를 global로 지정하면 전역 변수를 사용하게 된다.


  • 만약 전역 변수가 없을 때 함수 안에서 global을 사용하면 해당 변수는 전역 변수가 된다.


def foo():
    global x

    x = 20

    print(x)

foo() # 20
print(x) # 20


네임스페이스

  • 파이썬에서 변수는 네임스페이스에 저장된다.


  • 다음과 같이 locals 함수를 사용하면 현재 네임스페이스를 딕셔너리 형태로 출력할 수 있다.


>>> x = 10

>>> locals()
{'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <class '_frozen_importlib.BuiltinImporter'>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, 'x': 10}


  • 출력된 네임스페이스를 보면 "x": 10처럼 변수 x와 값 10이 저장되어 있다.
  • 여기서는 전역 범위에서 네임스페이스를 출력했으므로 전역 네임스페이스를 가져온다.


  • 마찬가지로 함수 안에서 locals를 사용할 수도 있다.


>>> def foo():
...     x = 10
...
...     print(locals())
...

>>> foo()
{'x': 10}


  • 네임스페이스를 보면 "x": 10만 저장되어 있다.
  • 이때는 지역 범위에서 네임스페이스를 출력했으므로 지역 네임스페이스를 가져온다.


4. 함수 안에서 함수 만들기

  • 다음과 같이 def로 함수를 만들고 그 안에서 다시 def로 함수를 만들면 된다.


def 함수이름1():
    코드
    def 함수이름2():
        코드


  • 간단한게 함수 안에서 문자열을 출력하는 함수를 만들고 호출해 보자.


def print_hello():
    hello = "Hello, world!"

    def print_message():
        print(hello)

    print_message()

print_hello() # Hello, world!


  • 함수 print_hello 안에서 다시 def로 함수 print_message를 만들었다.
  • 그리고 print_hello 안에서 print_message()처럼 함수를 호출했다.
  • 하지만 아직 함수를 정의만 한 상태이므로 아무것도 출력되지 않는다.
  • 두 함수가 실제로 동작하려면 바깥쪽에 있는 print_hello를 호출해 주어야 한다.
  • 즉, print_hello > print_message 순으로 실행된다.


1) 지역 변수의 범위

  • print_hello 함수와 print_message 함수에서 지역 변수의 범위를 살펴보자.


  • 안쪽 함수 print_message에서는 바깥쪽 함수 print_hello의 지역 변수 hello를 사용할 수 있다.


def print_hello():
    hello = "Hello, world!"

    def print_message():
        print(hello)


  • 즉, 바깥쪽 함수의 지역 변수는 그 안에 속한 모든 함수에서 접근할 수 있다.


  • 이 지역 변수의 접근 범위를 그림으로 나타내면 다음과 같은 모양이 된다.


003


2) 지역 변수 변경하기

  • 다음과 같이 안쪽 함수 B에서 바깥쪽 함수 A의 지역 변수 x를 변경해 보자.


def A():
    x = 10

    def B():
        x = 20

    B()
    print(x)

A() # 10


  • 실행을 해보면 20이 나와야 할 것 같은데 10이 나왔다.
  • 왜냐하면 겉으로 보기에는 바깥쪽 함수 A의 지역 변수 x를 변경하는 것 같지만, 실제로는 안쪽 함수 B에서 이름이 같은 지역 변수 x를 새로 만들게 된다.
  • 즉, 파이썬에서는 함수에서 변수를 만들면 항상 현재 함수의 지역 변수가 된다.


def A():
    x = 10

    def B():
        x = 20


  • 현재 함수의 바깥쪽에 있는 지역 변수의 값을 변경하려면 nonlocal 키워드를 사용해야 한다.


  • 다음과 같이 함수 안에서 nonlocal에 지역 변수의 이름을 지정해 준다.


nonlocal 지역변수
def A():
    x = 10

    def B():
        nonlocal x

        x = 20

    B()
    print(x)

A() # 20


  • 이제 함수 B에서 함수 A의 지역 변수 x를 변경할 수 있다.
  • 즉, nonlocal은 현재 함수의 지역 변수가 아니라는 뜻이며 바깥쪽 함수의 지역 변수를 사용한다.


3) nonlocal이 변수를 찾는 순서

  • nonlocal은 현재 함수의 바깥쪽에 있는 지역 변수를 찾을 때 가장 가까운 함수부터 먼저 찾는다.


  • 이번에는 함수의 단계를 A, B, C로 만들었다.


def A():
    x = 10
    y = 100

    def B():
        x = 20

        def C():
            nonlocal x
            nonlocal y

            x = x + 30
            y = y + 300

            print(x)
            print(y)

        C()
    B()

A()

# 50
# 400


  • 함수 C에서 nonlocal x를 사용하면 바깥쪽에 있는 함수 B의 지역 변수 x = 20을 사용하게 된다.
  • 따라서 x = x + 3050이 나온다.
  • 그리고 함수 C에서 nonlocal y를 사용하면 바깥쪽에 있는 함수의 지역 변수 y를 사용해야 하는데 함수 B에는 y가 없다.
  • 이때는 한 단계 더 바깥으로 나가서 함수 A의 지역 변수 y를 사용하게 된다.
  • 즉, 가까운 함수부터 지역 변수를 찾고, 지역 변수가 없으면 계속 바깥쪽으로 나가서 찾는다.


4) global로 전역 변수 사용하기

  • 특히, 함수가 몇 단계든 상관없이 global 키워드를 사용하면 무조건 전역 변수를 사용하게 된다.


x = 1

def A():
    x = 10

    def B():
        x = 20

        def C():
            global x

            x = x + 30

            print(x)

        C()
    B()

A() # 31


  • 함수 C에서 global x를 사용하면 전역 변수 x = 1을 사용하게 된다.
  • 따라서 x = x + 3031이 나온다.
  • 파이썬에서 global을 제공하지만 함수에서 값을 주고받을 때는 매개변수와 반환값을 사용하는 것이 좋다.
  • 특히 전역 변수는 코드가 복잡해졌을 때 변수의 값을 어디서 바꾸는지 알기가 힘들다.
  • 따라서 전역 변수는 가급적이면 사용하지 않는 것을 권장한다.


5. 클로저 사용하기

  • 다음은 함수 바깥쪽에 있는 지역 변수 a, b를 사용하여 a * x + b를 계산하는 함수 mul_add를 만든 뒤에 함수 mul_add 자체를 반환한다.


def calc():
    a = 3
    b = 5

    def mul_add(x):
        return a * x + b

    return mul_add

c = calc()

print(c(1), c(2), c(3), c(4), c(5))
# 8 11 14 17 20


  • 먼저 calc에 지역 변수 ab를 만들고 35를 저장했다.
  • 그다음에 함수 mul_add에서 ab를 사용하여 a * x + b를 계산한 뒤 반환한다.


def calc():
    a = 3
    b = 5

    def mul_add(x):
        return a * x + b


  • 함수 mul_add를 만든 뒤에는 이 함수를 바로 호출하지 않고 return으로 함수 자체를 반환한다.
  • 함수를 반환할 때는 함수 이름만 반환해야 하며 ()(괄호)를 붙이면 안 된다.


    return mul_add


  • 이제 클로저를 사용해 보자.


  • 다음과 같이 함수 calc를 호출한 뒤 반환값을 c에 저장한다.
  • calc에서 mul_add를 반환했으므로 c에는 함수 mul_add가 들어간다.
  • 그리고 c에 숫자를 넣어서 호출해 보면 a * x + b 계산식에 따라 값이 출력된다.


c = calc()

print(c(1), c(2), c(3), c(4), c(5))
# 8 11 14 17 20


  • 잘 보면 함수 calc가 끝났는데도 ccalc의 지역 변수 a, b를 사용해서 계산을 하고 있다.
  • 이렇게 함수를 둘러싼 환경(지역 변수, 코드 등)을 계속 유지하다가, 함수를 호출할 때 다시 꺼내서 사용하는 함수를 클로저라고 한다.
  • 여기서는 c에 저장된 함수가 클로저이다.


004


  • 이처럼 클로저를 사용하면 프로그램의 흐름을 변수에 저장할 수 있다.
  • 즉, 클로저는 지역 변수와 코드를 묶어서 사용하고 싶을 때 활용한다.
  • 또한, 클로저에 속한 지역 변수는 바깥에서 직접 접근할 수 없으므로 데이터를 숨기고 싶을 때 활용한다.


1) lambda로 클로저 만들기

  • 클로저는 다음과 같이 lambda로도 만들 수 있다.


def calc():
    a = 3
    b = 5

    return lambda x: a * x + b

c = calc()

print(c(1), c(2), c(3), c(4), c(5))
# 8 11 14 17 20


  • return lambda x: a * x + b처럼 람다 표현식을 만든 뒤 람다 표현식 자체를 반환했다.
  • 이렇게 람다를 사용하면 클로저를 좀 더 간단하게 만들 수 있다.
  • 보통 클로저는 람다 표현식과 함께 사용하는 경우가 많아 둘을 혼동하기 쉽다.
  • 람다는 이름이 없는 익명 함수를 뜻하고, 클로저는 함수를 둘러싼 환경을 유지했다가 나중에 다시 사용하는 함수를 뜻한다.


2) 클로저의 지역 변수 변경하기

  • 지금까지 클로저의 지역 변수를 가져오기만 했는데, 클로저의 지역 변수를 변경하고 싶다면 nonlocal을 사용하면 된다.


  • 다음은 a * x + b의 결과를 함수 calc의 지역 변수 total에 누적한다.


def calc():
    a = 3
    b = 5
    total = 0

    def mul_add(x):
        nonlocal total

        total = total + a * x + b

        print(total)

    return mul_add

c = calc()

c(1) # 8
c(2) # 19
c(3) # 33

References