파이썬 | 클로저 사용하기

CHOI·2021년 12월 31일
0

Python

목록 보기
26/33
post-thumbnail

이번에는 클로저에 대해서 알아보자. 클로저는 개념이 다소 어려울 수 있으니 변수의 사용 범위부터 알아본 뒤에 설명해보겠다.

1. 전역 변수 / 지역 변수

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

x = 10          # 전역 변수
def foo():
    print(x)    # 전역 변수 출력
 
foo()
print(x)        # 전역 변수 출력

실행 결과

10
10

foo 함수에서 함수 바깥에 있는 변수 x 를 사용했다. 물론 함수 바깥에서도 변수 x 를 사용할 수 있다. 이처럼 함수를 포함하여 스크립트 전체에서 사용할 수 있는 변수를 전역 변수(global variable)이라고 하고 특히 전역 변수가 접근할 수 있는 범위를 전역 범위(global scope)라고 한다.

그렇다면 변수 x 를 함수 foo 안에서 만들면 어떨까?

def foo():
    x = 10      # foo의 지역 변수
    print(x)    # foo의 지역 변수 출력
 
foo()
print(x)        # 에러. foo의 지역 변수는 출력할 수 없음

실행 결과

10
Traceback (most recent call last):
  File "C:\project\local_variable.py", line 6, in <module>
    print(x)        # 에러. foo의 지역 변수는 출력할 수 없음
NameError: name 'x' is not defined

함수안에서는 사용할 수 있지만 함수 바깥에서는 사용할 수 없다. 이는 변수 xfoo지역 변수(local variable)이기 때문이다. 지역 변수는 변수를 만든 함수 안에서만 접근할 수 있고 함수 바깥에서는 접근할 수 없다. 특히 지역 변수를 접근할 수 있는 범위를 지역 범위(local scope) 라고 한다.

함수 안에서 전역 변수 변경

만약에 함수 안에서 전역 변수를 변경하면 어떻게 될까?

x = 10          # 전역 변수
def foo():
    x = 20      # x는 foo의 지역 변수
    print(x)    # foo의 지역 변수 출력
 
foo()
print(x)        # 전역 변수 출력

실행 결과

20
10

분명 foo 함수 안에서 전역 변수 x 의 값을 20으로 변경하였는데 함수 바깥에서 x 를 출력해보면 10 이 나온다. 그 이유는 사실 겉으로 보기에는 foo 안에 x 는 전역 변수 같지만 실제로는 foo 의 지역 변수이다. 즉, 전역 변수 x 가 있고 foo 지역 변수 x 를 새롭게 만들게 된다. 이 둘은 이름만 같을 뿐 서로 다른 변수이다.

함수 안에서 전역 변수의 값을 변경하려면 global 이라는 키워드를 사용하여 전역 변수의 이름을 지정해줘여한다.

x = 10          # 전역 변수
def foo():
    global x    # 전역 변수 x를 사용하겠다고 설정
    x = 20      # x는 전역 변수
    print(x)    # 전역 변수 출력
 
foo()
print(x)        # 전역 변수 출력

실행 결과

20
20

% 네임 스페이스

파이썬에서 변수는 네임 스페이스(name space)에 저장이 된다. 다음과 같이 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}

물론 함수 안에서도 네임 스페이스를 확인할 수 있다.

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

2. 함수 안에서 함수

이번에는 함수 안에서 함수를 만드는 법에 대해서 알아보자. 다음과 같이 간단하게 만들면 된다.

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

간단하게 함수를 만들어보고 출력해보자.

def print_hello():
    hello = 'Hello, world!'
    def print_message():
        print(hello)
    print_message()
 
print_hello()

실행 결과

Hello, world!

위와 같이 print_hello 안에서 print_message 함수를 정의하고 호출하였다 하지만 아무것도 출력되지 않는다. print_hello() 로 함수를 호출해야만 print_message 함수가 호출이 된다.

즉, 두 함수가 실제로 실행되려면 함수 바깥에서 print_hello 함수를 호출해야 한다. print_helloprint_message 순서로 호출이 되는 것이다.

지역 변수의 범위

print_hello 함수와 print_message 함수 에서의 지역 변수의 범위를 살펴보자. 안쪽 함수 print_message 에서는 바깥 함수 print_hello 의 지역 변수 hello 를 사용할 수 있었다.

def print_hello():
    hello = 'Hello, world!'
    def print_message():
        print(hello)    # 바깥쪽 함수의 지역 변수를 사용 

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

지역 변수 변경 ( nonlocal )

그러면 이번에는 지역 변수를 변경해보자.

def A():
    x = 10        # A의 지역 변수 x
    def B():
        x = 20    # x에 20 할당
 
    B()
    print(x)      # A의 지역 변수 x 출력
 
A()

실행 결과

10

이번에도 역시 20이 나와야 할 것 같지만 10이 나왔다. 즉 바깥 함수의 지역 변수가 변경되지 않는 것이다. 이유는 보기에는 바깥 함수의 지역 변수 x 를 변경하는 것 같지만 사실은 안쪽 함수 B 에서 이름이 같은 지역 변수 x 를 새롭게 만드는 것이기 때문이다. 즉, 파이썬에서는 함수 안에 변수를 만들면 항상 그 함수의 지역 변수로 만들어진다.

def A():
    x = 10        # A의 지역 변수 x
    def B():
        x = 20    # B의 지역 변수 x를 새로 만듦

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

def A():
    x = 10        # A의 지역 변수 x
    def B():
        nonlocal x    # 현재 함수의 바깥쪽에 있는 지역 변수 사용
        x = 20        # A의 지역 변수 x에 20 할당
 
    B()
    print(x)      # A의 지역 변수 x 출력
 
A()

실행 결과

20

nonlocal 이 변수를 찾는 순서

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

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 함수에서 nonlocalx 를 찾을 때 B 에서 찾아보고 그 다음에 A 에서 찾아보게 된다. 따라서 변수 xB 에서 찾아서 B 에 지역 변수의 값을 변경하고 변수 yB 함수에서도 없기 때문에 A 까지 가서 y 변수를 찾아 값을 변경하게 되는 것이다.

실무에서는 이렇게 여러 단계로 함수를 만들 일은 거의 없다. 그리고 함수마다 이름이 같은 변수를 사용하기 보다는 변수 이름을 다르게 짓는 것이 좋다.

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

파이썬에서는 global 을 제공하지만 함수에서 값을 주고 받을 때는 매개변수와 반환값을 사용하는 것이 좋다. 특히 전역 변수는 코드가 복잡해지면 어디에서 값이 변경되는지 알아보기 힘들다 따라서 가급적이면 전역변수는 사용하지 않는 것이 좋다.

3. 클로저

이제 클로저 형태로 만드는 방법에 대해서 알아보자. 다음은 함수 바깥쪽에 있는 지역 변수 a , b 를 사용하여 a * x + b 를 계산하는 함수 mul_add 를 만든 뒤에 함수 mul_add 자체를 반환한다.

def calc():
    a = 3
    b = 5
    def mul_add(x):
        return a * x + b    # 함수 바깥쪽에 있는 지역 변수 a, b를 사용하여 계산
    return mul_add          # mul_add 함수를 반환
 
c = calc()
print(c(1), c(2), c(3), c(4), c(5))

실행 결과

8 11 14 17 20

먼저 calc 함수는 지역 변수 a, b 를 만들고 그 다음에 mul_add 함수에서 a, b 를 활용하여 a * x + b 를 반환한다.

def calc():
    a = 3
    b = 5
    def mul_add(x):
        return a * x + b    # 함수 바깥쪽에 있는 지역 변수 a, b를 사용하여 계산

함수 mul_add 을 만든 뒤에 함수를 호출하지 않고 return 으로 함수 자체를 반환한다. 여기서 함수를 호출하는 것이 아니기 때문에 () 는 붙이면 안된다.

return mul_add          # mul_add 함수를 반환

이제 클로저를 사용해보자. 다음과 같이 calc 함수를 호출한 뒤에 반환값을 c 에 저장하였다. calc 함수의 반환값은 mul_add 함수이기 때문에 c = calc() 와 같이 하면 변수 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 를 사용하여 계산하고 있다. 이렇게 함수를 둘러싸고 있는 환경 (지역 변수, 코드 등)을 계속 유지하다가 함수를 호출할 때 다시 꺼내서 사용하는 함수를 클로저(closer) 라고 한다. 여기에서는 c 에 저장된 함수가 클로저이다.

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

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 처럼 람다 표현식 자체를 리턴했다. 이렇게 람다 표현식을 사용하면 클로저를 좀 더 간단하게 만들 수 있다.

보통 클로저는 람다 표현식과 같이 사용하는 경우가 많아서 둘이 혼동하기 쉽다. 람다는 이름이 없는 익명 함수를 뜻하고 클로저는 함수를 둘러싼 환경을 유지했다가 나중에 다시 사용하는 함수를 말한다.

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

지금까지 클로저 지역 변수를 가져오기만 했는데, 클로저의 지역 변수를 변경하고 싶다면 앞서 배운 nonlocal 을 사용하면 된다. 다음은 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)
c(2)
c(3)

실행 결과

8
19
33

이번에는 조금 생소한 클로저에 대해서 알아보았다. 아마도 처음 보는 것이여서 지금 당장 완벽하게 이해되지 않는 것이 당연하다. 나중에 파이썬을 보다보면 자연스럽게 익히게 될 것이다.

profile
벨로그보단 티스토리를 사용합니다! https://flight-developer-stroy.tistory.com/

0개의 댓글