파이썬 – 퍼스트클래스 함수 (First Class Function)
이번 글에서는 파이썬의 퍼스트클래스 함수(First-class function)를 정리합니다.
퍼스트클래스 함수란 프로그래밍 언어가 함수(function)를 first-class citizen으로 취급한다는 의미입니다. 즉, 함수 자체를 다른 함수의 인자(argument)로 전달하거나, 다른 함수의 리턴값으로 반환받거나, 변수에 할당하거나, 데이터 구조에 저장할 수 있는 함수를 의미합니다.
개념만으로는 와닿지 않으므로 실습으로 살펴봅니다.
작업 디렉터리에 first_class_function.py 파일을 만들고 다음 코드를 입력합니다.
def square(x):
return x * x
print(square(5))
f = square
print(square)
print(f)파일을 저장한 후, 파일이 저장된 디렉터리에서 터미널이나, 커맨드창을 열고 다음의 명령어로 파이썬 파일을 실행해 보겠습니다.
$ python first_class_function.py
25
<function square at 0x000001FE1433D1F0>
<function square at 0x000001FE1433D1F0>위의 코드를 보면 아주 간단한 함수 “square”를 정의하고 호출하였습니다. 그 다음에 square 함수를 f라는 변수에 할당한 후에 square와 f의 값을 출력해 보았습니다. 둘다 메모리 주소값인 0x1018dfe60에 저장된 square 함수 오브젝트가 할당되어 있는 것을 볼 수 있습니다. 그럼 f도 진짜 함수처럼 호출을 할 수 있는지 볼까요.
다음과 같이 코드를 수정 하고 저장한 다음에 실행해 주십시오.
def square(x):
return x * x
f = square
print(f(5))$ python first_class_function.py
25f(5) 구문으로 square 함수를 호출한 것을 볼 수 있습니다. 위에서 언급했듯이 프로그래밍 언어가 퍼스트클래스 함수를 지원하면, 금방 해본 것처럼 변수에 함수를 할당할 수 있을뿐만 아니라, 인자로써 다른 함수에 전달하거나, 함수의 리턴값으로도 사용할 수가 있습니다. 다음 예제를 보면서 설명을 하겠습니다.
다음과 같이 코드를 수정 하고 저장한 다음에 실행해 주십시오.
def square(x):
return x * x
def my_map(func, arg_list):
result = []
for i in arg_list:
result.append(func(i)) # square 함수 호출, func == square
return result
num_list = [1, 2, 3, 4, 5]
squares = my_map(square, num_list)
print(squares)$ python first_class_function.py
[1, 4, 9, 16, 25]my_map 함수에 square 함수를 인자로 전달한 후 for 루프안에서 square 함수를 호출한 것을 볼 수 있습니다. 그런데 밑에와 같이 simple_sqaure 함수 하나로 문제를 해결하면 되지 않냐고 생각하시는 분들이 있으실 겁니다.
다음과 같이 코드를 수정 하고 저장한 다음에 실행해 주십시오.
def square(x):
return x * x
num_list = [1, 2, 3, 4, 5]
def simple_square(arg_list):
result = []
for i in arg_list:
result.append(i * i)
return result
simple_squares = simple_square(num_list)
print(simple_squares)$ python first_class_function.py
[1, 4, 9, 16, 25]옹?!?, 더 간단한 코드로 같은 결과가 나왔습니다. 그렇습니다. 간단히 함수 하나만을 실행하고 싶을때는 simple_square와 같은 일반 함수를 사용하여 같은 결과를 낼 수도 있습니다. 하지만, 퍼스트클래스 함수를 사용하면 이미 정의된 여러 함수를 간단히 재활용할 수 있다는 장점이 있습니다. 아래의 예제를 다시 보도록 합시다.
다음과 같이 코드를 수정 하고 저장한 다음에 실행해 주십시오.
def square(x):
return x * x
def cube(x):
return x * x * x
def quad(x):
return x * x * x * x
def my_map(func, arg_list):
result = []
for i in arg_list:
result.append(func(i)) # square 함수 호출, func == square
return result
num_list = [1, 2, 3, 4, 5]
squares = my_map(square, num_list)
cubes = my_map(cube, num_list)
quads = my_map(quad, num_list)
print(squares)
print(cubes)
print(quads)$ python first_class_function.py
[1, 4, 9, 16, 25]
[1, 8, 27, 64, 125]
[1, 16, 81, 256, 625]위의 예제와 같이 이미 정의되어 있는 함수 square, cube, quad와 같은 여러개의 함수나 모듈이 있다고 가정했을때 my_map과 같은 wrapper 함수를 하나만 정의하여 기존의 함수나 모듈을 수정할 필요없이 편리하게 쓸 수가 있는겁니다.
그렇다면 이번에는 함수의 결과값으로 또 다른 함수를 리턴하는 방법을 살펴보겠습니다. 아주 간단한 로깅 함수를 만들어 보겠습니다.
다음과 같이 코드를 수정 하고 저장한 다음에 실행해 주십시오.
def logger(msg):
def log_message(): # 1
print('Log: ', msg)
return log_message
log_hi = logger('Hi')
print(log_hi) # log_message 오브젝트가 출력됩니다.
log_hi() # "Log: Hi"가 출력됩니다.$ python first_class_function.py
<function logger.<locals>.log_message at 0x0000022AB43EAA60>
Log: Hi위의 #1에서 정의된 log_message라는 함수를 logger 함수의 리턴값으로 리턴하여 log_hi라는 변수에 할당한 후 호출한 것을 볼 수 있습니다.그런데 여기서 특이한 점을 볼 수 있습니다. msg와 같은 함수의 지역변수값은 함수가 호출된 이후에 메모리상에서 사라지므로 다시 참조할 수가 없는데, msg 변수에 할당됐던 ‘Hi’값이 logger 함수가 종료된 이후에도 참조 됐다는 것입니다. 이런 log_message와 같은 함수를 “클로저 (closure)”라고 부르며 클로저는 다른 함수의 지역변수를 그 함수가 종료된 이후에도 기억을 할 수가 있습니다. log_message가 정말 기억을 하고 있는지 msg 변수를 지역변수로 가지고 있는 logger 함수를 글로벌 네임스페이스에서 완전히 지운 후, log_message를 호출하여 보겠습니다.
다음과 같이 코드를 수정 하고 저장한 다음에 실행해 주십시오.
def logger(msg):
def log_message(): # 1
print('Log: ', msg)
return log_message
log_hi = logger('Hi')
print(log_hi) # log_message 오브젝트가 출력됩니다.
log_hi() # "Log: Hi"가 출력됩니다.
del logger # 글로벌 네임스페이스에서 logger 오브젝트를 지웁니다.
# logger 오브젝트가 지워진 것을 확인합니다.
try:
print(logger)
except NameError:
print('NameError: logger는 존재하지 않습니다.')
log_hi() # logger가 지워진 뒤에도 Log: Hi"가 출력됩니다.$ python first_class_function.py
<function logger.<locals>.log_message at 0x0000022EC0BBAAF0>
Log: Hi
NameError: logger는 존재하지 않습니다.
Log: Hilogger가 지워진 뒤에도 log_hi()를 실행하여 log_message가 호출된 것을 볼 수 있습니다.
logger 함수를 완전히 삭제한 이후에도 log_message 함수는 ‘Hi’를 기억하고 있는 것을 확인했습니다. 이런식으로 클로저는 여러가지로 편리하게 쓰여질 때가 많은데, 클로저에 대해서는 다른 강좌에서 자세히 알아 보겠습니다.
이번에는 조금 더 실용적인 예제를 보도록 하겠습니다.
다음과 같이 코드를 수정 하고 저장한 다음에 실행해 주십시오.
# 단순한 일반 함수
def simple_html_tag(tag, msg):
print('<{0}>{1}<{0}>'.format(tag, msg))
simple_html_tag('h1', '심플 헤딩 타이틀')
print('-' * 30)
# 함수를 리턴하는 함수
def html_tag(tag):
def wrap_text(msg):
print('<{0}>{1}<{0}>'.format(tag, msg))
return wrap_text
print_h1 = html_tag('h1') # 1
print(print_h1) # 2
print_h1('첫 번째 헤딩 타이틀') # 3
print_h1('두 번째 헤딩 타이틀') # 4
print_p = html_tag('p')
print_p('이것은 패러그래프 입니다.')$ python first_class_function.py
<h1>심플 헤딩 타이틀<h1>
------------------------------
<function html_tag.<locals>.wrap_text at 0x00000272C3CFAAF0>
<h1>첫 번째 헤딩 타이틀<h1>
<h1>두 번째 헤딩 타이틀<h1>
<p>이것은 패러그래프 입니다.<p>19번 줄 #1에서 html_tag 함수를 print_h1 변수에 할당하고, 20번 줄 #2에서 변수 값을 출력하면 wrap_text 함수 오브젝트가 할당되어 있음을 확인할 수 있습니다. 21번 줄 #3과 22번 줄 #4에서는 문자열을 전달해 wrap_text 함수를 호출하고 있습니다. 이 시점에서 “굳이 wrapper 함수를 만들지 않고 simple_html_tag 같은 일반 함수를 그대로 사용하면 되지 않을까” 하는 의문이 들 수 있습니다. 그러나 html_tag와 같은 higher-order 함수의 동작을 이해해야 이후 다룰 클로저(closure), 데코레이터(decorator), 제너레이터(generator) 같은 개념을 자연스럽게 이해할 수 있습니다.
다음 글에서는 클로저를 다루겠습니다.