22. 클로저(Closure)
1. 키워드
- 클로저(Closure)
- 전역 변수(Global Variable)와 전역 범위(Global Scope)
- 지역 변수(Local Variable)와 지역 범위(Local Scope)
- 네임스페이스(Namespace)
global
과nonlocal
2. 클로저 사용하기
- 이번에는 변수의 사용 범위와 함수를 클로저 형태로 만드는 방법을 알아보자.
3. 변수의 사용 범위 알아보기
- 파이썬 스크립트에서 변수를 만들면 다음과 같이 함수 안에서도 사용할 수 있다.
foo
함수에서 함수 바깥에 있는 변수x
의 값을 출력했다.- 물론 함수 바깥에서도
x
의 값을 출력할 수 있다. - 이처럼 함수를 포함하여 스크립트 전체에서 접근할 수 있는 변수를 전역 변수라고 부른다.
- 특히 전역 변수에 접근할 수 있는 범위를 전역 범위라고 한다.
- 그럼 변수
x
를 함수foo
안에서 만들어보자.
- 실행을 해보면
x
가 정의되지 않았다는 에러가 발생한다. - 왜냐하면 변수
x
는 함수foo
안에서 만들었기 때문에foo
의 지역 변수이다. - 따라서 지역 변수는 변수를 만든 함수 안에서만 접근할 수 있고, 함수 바깥에서 접근할 수 없다.
- 특히 지역 변수를 접근할 수 있는 범위를 지역 범위라고 한다.
1) 함수 안에서 전역 변수 변경하기
- 함수 안에서 전역 변수의 값을 변경해 보자.
- 분명 함수
foo
안에서x = 20
처럼x
의 값을20
으로 변경했다. - 하지만 함수 바깥에서
print
로x
의 값을 출력해 보면10
이 나온다. - 겉으로 보기에는
foo
안의x
는 전역 변수인 것 같지만 실제로는foo
의 지역 변수이다. - 즉, 전역 변수
x
가 있고,foo
에서 지역 변수x
를 새로 만들게 된다. - 이 둘은 이름만 같을 뿐 서로 다른 변수이다.
- 함수 안에서 전역 변수의 값을 변경하려면
global
키워드를 사용해야 한다.
- 다음과 같이 함수 안에서
global
에 전역 변수의 이름을 지정해 준다.
- 이제 함수 안에서
x
를20
으로 변경하면 함수 바깥에서x
를 출력했을 때20
이 나온다. - 이렇게 함수 안에서 변수를
global
로 지정하면 전역 변수를 사용하게 된다.
- 만약 전역 변수가 없을 때 함수 안에서
global
을 사용하면 해당 변수는 전역 변수가 된다.
네임스페이스
- 파이썬에서 변수는 네임스페이스에 저장된다.
- 다음과 같이
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
를 사용할 수도 있다.
- 네임스페이스를 보면
"x": 10
만 저장되어 있다. - 이때는 지역 범위에서 네임스페이스를 출력했으므로 지역 네임스페이스를 가져온다.
4. 함수 안에서 함수 만들기
- 다음과 같이
def
로 함수를 만들고 그 안에서 다시def
로 함수를 만들면 된다.
- 간단한게 함수 안에서 문자열을 출력하는 함수를 만들고 호출해 보자.
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
를 사용할 수 있다.
- 즉, 바깥쪽 함수의 지역 변수는 그 안에 속한 모든 함수에서 접근할 수 있다.
- 이 지역 변수의 접근 범위를 그림으로 나타내면 다음과 같은 모양이 된다.
2) 지역 변수 변경하기
- 다음과 같이 안쪽 함수
B
에서 바깥쪽 함수A
의 지역 변수x
를 변경해 보자.
- 실행을 해보면
20
이 나와야 할 것 같은데10
이 나왔다. - 왜냐하면 겉으로 보기에는 바깥쪽 함수
A
의 지역 변수x
를 변경하는 것 같지만, 실제로는 안쪽 함수B
에서 이름이 같은 지역 변수x
를 새로 만들게 된다. - 즉, 파이썬에서는 함수에서 변수를 만들면 항상 현재 함수의 지역 변수가 된다.
- 현재 함수의 바깥쪽에 있는 지역 변수의 값을 변경하려면
nonlocal
키워드를 사용해야 한다.
- 다음과 같이 함수 안에서
nonlocal
에 지역 변수의 이름을 지정해 준다.
- 이제 함수
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 + 30
은50
이 나온다. - 그리고 함수
C
에서nonlocal y
를 사용하면 바깥쪽에 있는 함수의 지역 변수y
를 사용해야 하는데 함수B
에는y
가 없다. - 이때는 한 단계 더 바깥으로 나가서 함수
A
의 지역 변수y
를 사용하게 된다. - 즉, 가까운 함수부터 지역 변수를 찾고, 지역 변수가 없으면 계속 바깥쪽으로 나가서 찾는다.
4) global
로 전역 변수 사용하기
- 특히, 함수가 몇 단계든 상관없이
global
키워드를 사용하면 무조건 전역 변수를 사용하게 된다.
- 함수
C
에서global x
를 사용하면 전역 변수x = 1
을 사용하게 된다. - 따라서
x = x + 30
은31
이 나온다. - 파이썬에서
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
에 지역 변수a
와b
를 만들고3
과5
를 저장했다. - 그다음에 함수
mul_add
에서a
와b
를 사용하여a * x + b
를 계산한 뒤 반환한다.
- 함수
mul_add
를 만든 뒤에는 이 함수를 바로 호출하지 않고return
으로 함수 자체를 반환한다. - 함수를 반환할 때는 함수 이름만 반환해야 하며
()
(괄호)를 붙이면 안 된다.
- 이제 클로저를 사용해 보자.
- 다음과 같이 함수
calc
를 호출한 뒤 반환값을c
에 저장한다. calc
에서mul_add
를 반환했으므로c
에는 함수mul_add
가 들어간다.- 그리고
c
에 숫자를 넣어서 호출해 보면a * x + b
계산식에 따라 값이 출력된다.
- 잘 보면 함수
calc
가 끝났는데도c
는calc
의 지역 변수a
,b
를 사용해서 계산을 하고 있다. - 이렇게 함수를 둘러싼 환경(지역 변수, 코드 등)을 계속 유지하다가, 함수를 호출할 때 다시 꺼내서 사용하는 함수를 클로저라고 한다.
- 여기서는
c
에 저장된 함수가 클로저이다.
- 이처럼 클로저를 사용하면 프로그램의 흐름을 변수에 저장할 수 있다.
- 즉, 클로저는 지역 변수와 코드를 묶어서 사용하고 싶을 때 활용한다.
- 또한, 클로저에 속한 지역 변수는 바깥에서 직접 접근할 수 없으므로 데이터를 숨기고 싶을 때 활용한다.
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