이번 시간에는 클로저(Closure)에 대한 개념과 특징, 활용 등을 통해
클로저를 자세히 들여다보는 시간을 가져보려고 합니다 :)
저도 간단하게 이해하고 있었던 개념이였는데, 이번 기회를 통해 조금 더 자세히 알게되었고, 어쩌면 실제 활용까지 할 수 있을지 모르겠네요!
어쨌든, 이 글을 통해서 많은 분들이 클로저에 대한 개념을 조금 더 잘 이해할 수 있으면 좋겠습니다.
한 번 시작해보도록 하겠습니다!
[이 글은 Python 언어 기반으로 작성되었습니다.]
먼저 클로저를 설명하기 전에, 기본적으로 이해해야 할 것들이 존재합니다.
크게 3가지에 대해서 설명할텐데요! 3가지는 다음과 같습니다.
그 개념에 대해서 이해해보고, 이어서 Closure에 대해서 이해해 보도록 하겠습니다.
중첩 함수를 설명하기 전에, 다음의 코드를 먼저 보면서 중첩의 개념에 대해서 한번 생각해 보도록 하겠습니다.
간단하게 구구단을 계산하는 코드를 구현해 볼까요?
for n in range(1, 10):
for i in range(1, 10):
print(n, " X ", i, " : ", n*i)
# 결과
1 X 1 : 1
1 X 2 : 2
1 X 3 : 3
1 X 4 : 4
...
다음과 같은 결과를 확인할 수 있네요!
위의 코드문은 for문을 이중으로 중첩하여 사용함으로서 구구단 예제를 쉽게 구현할 수 있었습니다.
대부분의 언어들은 여러 개의 for문이나 if문을 중첩하여 사용하여 다양한 로직을 구현할 수 있죠.
그렇다면 함수도 중첩해서 사용할 수 있을까요?
네! 사용 할 수 있습니다. 적어도 파이썬에서는 함수를 중첩해서 사용하는 것이 가능합니다.
함수 내부에서 중첩돼서 선언된 내부 함수를 우리는 중첩 함수(nested-function)이라고 합니다.
아래 코드처럼 선언할 수 있겠네요.
def outer_func():
def inner_func():
print("안녕하세요! 저는 재빅입니다.")
return inner_func()
outer_func()
이러한 중첩 함수는 해당 함수가 정의된 함수(outer_func 내부)에서만 사용이 가능합니다.
그렇다면, 왜 중첩 함수를 사용할까요?
첫 번째는 외부 함수 내에서 반복되는 코드를 줄여 효율적이게 작성하기 위함이 있겠네요.
이는 사실 함수를 만드는 이유와 동일합니다. 즉, 가독성을 높이기 위함입니다.
그런데 단순하게 가독성을 높이기 위함이였다면 함수 내부에 함수를 선언하지 않고
외부에 함수를 선언해서 사용할 수도 있을겁니다. 다음과 같이 말이죠.
def another_func():
print("안녕하세요 저는 재빅입니다.")
def outer_func():
return another_func()
outer_func()
그렇다면 꼭 함수 내부에 함수를 선언해야 할 이유가 있을까요?
첫 번째로는 외부 함수(outer_func)에서만 내부적으로 반복되는 로직일 때 그럴 수 있겠네요.
사실 이보다 더 중요한 이유는 우리가 앞으로 배울 Closure라는 개념을 활용하기 위함도 그 하나입니다.
즉 Closure을 구현하기 위한 필요 조건 중 하나가 바로 중첩 함수의 구현인 것입니다!
일급 객체(First-Class Object)란, 어떠한 혜택을 받는 것이 아니라,
해당 언어 내에서 다른 모든 개체(entity)와 상호작용시 어떠한 차별이 없는 것을 뜻합니다.
설명만 보면 직관적으로 이해하기가 어려운데요,
먼저 용어 자체에 대해서 생각해 보도록 하죠. 왜 일급이라는 단어를 사용했을까요?
저는 일급수와 연관지어 생각했는데요,
일급수의 사전적 정의는 다음과 같습니다.
하천의 수질(水質) 등급의 하나. 가장 맑고 깨끗한 물. 냄새가 나지 않으며 그냥 마실 수 있음. 버들치·버들개·열목어·가재 등이 살 수 있음.
즉, 일급수는 다른 급수의 물에 비해 더 많은 용도로 활용될 수 있습니다.
마실 수도 있고, 손을 씻을 수도 있고, 여러가지 용도로 사용될 수 있겠네요.
즉, 마찬가지로 일급 객체도 해당 객체가 여러가지 용도로 모두 사용 가능하다고 연관지어 생각할 수 있습니다.
여러가지 용도란, 변수에 할당, 함수의 인자, 함수의 반환값.. 등이 있겠네요!
우선, 일급 객체이기 위한 주 전제들을 살펴보도록 하죠.
다음을 만족하는 객체가 무엇이 있을까요? 먼저 파이썬에서는 모든 것을 객체로 취급합니다.
int, list, str 과 같은 자료형부터, 함수, 클래스.. 등 모든 것이 객체입니다.
이 중에 list 자료형이 일급 객체인지 살펴볼까요?
결과부터 말씀드리면, list 자료형은 일급객체입니다.
아래 코드를 통해 간단히 확인해볼까요?
#- 조건 1 -> 함수의 인자로 전달이 가능한가? --> YES!
def sum(num_list):
result = 0
for i in num_list:
result += i
return result
sum([1,2,3,4,5])
# 결과 : 15
#- 조건 2 -> 함수의 반환값이 될 수 있는가? --> YES!
def bubble_sort(num_list):
for i in range(len(num_list)-1):
for j in range(len(num_list)-1-i):
if num_list[j] > num_list[j+1]:
num_list[j], num_list[j+1] = num_list[j+1], num_list[j]
return num_list
bubble_sort([3,4,1,2,3])
# 결과 : [1, 2, 3, 3, 4]
#- 조건 3 -> 수정되고 할당할 수 있는가? --> YES!
num_list = [1,2,3,4,5] #- 할당
num_list.append(10) #- 수정
num_list
첫 번째는 간단히 list 타입을 함수의 입력을 받아 덧셈을 해주는 코드입니다.
즉, 함수의 인자로 list 타입을 입력 받을 수 있습니다.
두 번째는 간단하게 bubble-sort를 구현한 코드입니다.
정렬된 list([1, 2, 3, 3, 4]
)가 함수의 반환값으로 나오게 됩니다.
세 번째는 num_list
변수에 list값을 할당하고, append
명령어를 통해 10을 추가하여 list를 수정하였네요
이처럼 list 뿐만 아니라 int, str 등의 자료형들은 모두 일급 객체입니다.
사실 진짜~!로 하고 싶은 이야기는, 파이썬에서 함수도 일급 객체라는 것입니다.
즉, 파이썬에서 함수는
이라는 것입니다!
다음의 코드를 통해서 정말 그런지 확인해 볼까요?
def calc_square(num_list):
return [i**2 for i in num_list]
def calc_plus(num_list):
return [i+i for i in num_list]
def calc_quad(num_list):
return [i ** 4 for i in num_list]
def list_square(function, digit_list):
return function(digit_list)
num_list = [1, 2, 3, 4, 5]
for calc_func in [calc_plus, calc_square, calc_quad]:
print(list_square(calc_func, num_list))
위의 코드는 list에 저장되어 있는 수치형 값들을 원하는 형태(제곱, 더하기, 네제곱)로 계산해 주는 코드입니다.
list_square
함수의 인자로 함수 객체가 사용됐으며,list_square
함수의 반환값은 함수(function(digit_list)
) 이며calc_plus
, calc_square
, calc_quad
를 할당하였습니다.즉, 파이썬에서의 함수는 일급객체(first-class) 함수 임을 확인할 수 있습니다.
이러한 first-class function이라는 특징은 Java, C같은 언어와는 다른 사고를 가지며, 이는 함수형 프로그래밍에서부터 고안된 기법입니다.
이러한 특징이 왜 중요할까요? 향후에 여러 디자인 패턴을 응용할 시에 이러한 특징을 많이 활용할 뿐더러, 이러한 일급 객체 함수는 다양한 테크닉에서 사용됩니다. 클로저도 그 중 대표적인 하나 입니다!
세 번째 개념인 nonlocal 이라는 개념에 대해서 알아보겠습니다 :)
사실 이 개념은 저도 이번에 처음 알게 되었습니다. 왜 이제 알았지? 라고 생각할 정도로 매우 중요한 개념이며, 클로저에서의 핵심 중 하나이므로 반드시 이해하셔야 함을 강조합니다!
nonlocal은 파이썬에서 scope(namespace)의 개념의 범주 중 하나입니다.
혹시, 지역변수(local variable), 전역변수(global variable) 라는 말을 들어보셨나요?
지역변수는 특정 scope(ex) 함수, 클래스) 내에서 사용되는 변수를 말하며, 전역변수는 모든 scope에서 접근이 가능한 변수를 말합니다.
그렇다면 nonlocal은 '로컬이 아닌' 이라는 의미이니깐, global의 의미 아닐까요?
결과적으로는 아닙니다! 그러한 이유는 global과 nonlocal은 구분되어 사용되기 때문입니다.
직관적으로는 nonlocal은 global과 local의 사이쯤에 해당한다고 이해하셔도 무방합니다.
하지만 좀 더 정확한 이해를 위해 코드를 보면서 이해해 보도록 하죠.
어떤 부모님이 제발 아들내미가 공부를 하게 좀 해달라고 저한테 부탁을 하셨습니다.
그래서 제가 몰래 메세지를 조작해서 보내기로 하였다고 가정해 보겠습니다.
그러면 다음과 같이 코드를 구현해 볼 수 있겠네요
msg = "야 노래방 갈래?"
def outer_send_message(msg):
def inner_send_message():
msg = "공부 같이 할래?"
return msg
return inner_send_message()
다음 아래에 아들이 보낸 메세지는 어떻게 보내질까요?
print(outer_send_message("야 축구하자!"))
대충 결과는 예상 되시겠지만, 정확히 알기 위해서 scope에 대해서 좀 확인해보도록 하겠습니다.
함수는 자신이 가장 가깝게 제어권을 가지고 있는 scope부터 접근해 나가게 됩니다.
위의 중첩 내부 함수인 inner_send_message
는 내부 scope 부터 접근했는데,
msg 라는 변수가 있으므로, 해당 값을 반환하게 되겠네요.
즉, 아무리 outer_send_message
함수에 메세지 값을 다르게 입력하더라도, "공부 같이 할래?"가 반환되게 됩니다.
즉 inner_send_message
함수에서 scope를 바라보자면,
inner_send_message
함수 안에 있는 영역은 local scopeouter_send_message
함수 안에, inner_send_message
밖에 있는 영역은 nonlocal scopeouter_send_message
함수 밖에 있는 영역은 global scope가 됩니다. 즉, msg 값으로 매핑 시켜본다면
가 되겠네요!
그렇다면 다음의 코드 결과는 어떻게 나올까요?
msg = "술 한잔 할래?"
def outer_send_message(msg):
msg = "공부 같이 할래?"
def inner_send_message():
return msg
return inner_send_message()
print(outer_send_message("야 축구하자!"))
결과는 마찬가지로 "공부 같이 할래?" 입니다.
즉, inner_send_message
함수 입장에서는 내부 local scope에서부터
차례(local -> nonlocal -> global)로 확인해 가면서 최초로 확인되는 msg 변수의 값을 반환하게 되는 것입니다!
이번에는 다른 예시를 또 하나 보도록 하겠습니다.
부모님께서 매번 "공부 같이 할래?"라고 하는 메세지를 보내면 아들에게 들킬 것 같다면서
특정 문자만 '공부'로 바꿔서 전송해 달라고 하시네요
그래서 다음과 같이 제가 코드문을 바꿨다고 가정해 보겠습니다.
def outer_send_message(msg):
def inner_send_message():
if '축구' in msg:
msg = msg.replace('축구', '공부')
return msg
return inner_send_message()
outer_send_message("야 축구하자!")
결과는 '야 공부하자!' 로 나오...게 되는 것이 아니라 다음과 같은 에러가 발생합니다.
[UnboundLocalError: local variable 'msg' referenced before assignment]
변수를 선언하기 전에 msg local variable을 참조했다고 나오네요!
이게 무슨일일까요? 아까 분명히 local -> nonlocal -> global 순으로 찾아간다고 했을텐데요!
여기 중요한 핵심은, 읽기는 가능하지만 쓰기는 불가능하다는 것입니다.
즉, 제가 inner_send_message
함수 내에서 msg 변수를 쓰려고 하고 있는데, local scope안에 msg 변수가 없어 찾을 수 없다고 나온 것입니다.
만약에 쓰고싶다면, inner_send_message
안에 nonlocal 이라고 명시를 해주어야 합니다.
def outer_send_message(msg):
def inner_send_message():
nonlocal msg # <- nonlocal 명시
if '축구' in msg:
msg = msg.replace('축구', '공부')
return msg
return inner_send_message()
outer_send_message("야 축구하자!")
# 결과
'야 공부하자!'
이는 굉장히 중요한 포인트를 시사하는데요.
즉 변수 값에 대한 읽기는 자유롭지만, 쓰기는 코드문에 명시를 해주어야 접근이 가능하게 된다는 것입니다.
만약에 outer_send_message 바깥에 있는 global 변수에 접근하고 싶다면, global로 선언해 주면 됩니다.
그렇다면 왜 이렇게 만들어 놓았을까요?
실제 시스템이나 프로그램은 무수히 많은 중첩 함수, 변수들로 만들어져 있습니다.
만약 중첩 함수에서 저도 모르는 사이에 global 변수 x를 수정하게 되면 어떻게 될까요?
원인도 찾기 힘들뿐더러, 굉장히 난감한 상황이 생기게 됩니다.
즉, 쓰기 만큼은 코드 영역의 책임과 권한을 정확하게 나누어 독립적으로 구성하겠다는 내부적인 생각이 들어가 있습니다.
이러한 nonlocal의 개념은, 바로 배울 클로저 개념의 핵심 중 하나가 되게 됩니다.
생각보다 글이 길어졌기 때문에, 준비 운동을 마쳤으니 물한잔들 드시면서
다음 블로그에서 본격적으로 클로저에 대해서 한번 알아보도록 하겠습니다.
이 글을 작성하기에 참조한 블로그는 다음과 같습니다.
Python의 Closure에 대해 알아보자
1급 객체(first-class object)란?