안녕하세요. 스쿨오브웹의 이상희입니다.

이번 강좌에서는 파이썬의 퍼스트클래스 함수 (First-class fuction)에 대해서 알아 보도록 하겠습니다.

퍼스트클래스 함수란 프로그래밍 언어가 함수 (function) 를 first-class citizen으로 취급하는 것을 뜻합니다. 쉽게 설명하자면 함수 자체를 인자 (argument) 로써 다른 함수에 전달하거나 다른 함수의 결과값으로 리턴 할수도 있고, 함수를 변수에 할당하거나 데이터 구조안에 저장할 수 있는 함수를 뜻합니다.

조금 어려운가요? 그럼 실습을 하면서 설명을 하도록 하지요.

실습을 위해서 원하는 디렉터리안에 “first_class_function.py”라는 이름의 파일을 하나 만들든 후, 다음의 코드를 입력하여 주십시오.

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도 진짜 함수처럼 호출을할 수 있는지 볼까요.

다음과 같이 코드를 수정 하고 저장한 다음에 실행하여 주십시오.

first_class_function.py

def square(x):
    return x * x

f = square

print(f(5))
$ python first_class_function.py
25

f(5) 구문으로 square 함수를 호출한 것을 볼 수 있습니다. 위에서 언급했듯이 프로그래밍 언어가 퍼스트클래스 함수를 지원하면, 금방 해본 것처럼 변수에 함수를 할당할 수 있을뿐만 아니라, 인자로써 다른 함수에 전달하거나, 함수의 리턴값으로도 사용할 수가 있습니다. 다음 예제를 보면서 설명을 하겠습니다.

다음과 같이 코드를 수정 하고 저장한 다음에 실행하여 주십시오.

first_class_function.py

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 함수 하나로 문제를 해결하면 되지 않냐고 생각하시는 분들이 있으실 겁니다. 

다음과 같이 코드를 수정 하고 저장한 다음에 실행하여 주십시오.

first_class_function.py

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와 같은 일반 함수를 사용하여 같은 결과를 낼 수도 있습니다. 하지만, 퍼스트클래스 함수를 사용하면 이미 정의된 여러 함수를 간단히 재활용할 수 있다는 장점이 있습니다. 아래의 예제를 다시 보도록 하죠.

다음과 같이 코드를 수정 하고 저장한 다음에 실행하여 주십시오.

first_class_function.py

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 함수를 하나만 정의하여 기존의 함수나 모듈을 수정할 필요없이 편리하게 쓸 수가 있는겁니다.

그렇다면 이번에는 함수의 결과값으로 또 다른 함수를 리턴하는 방법을 살펴보겠습니다. 아주 간단한 로깅 함수를 만들어 보겠습니다.

다음과 같이 코드를 수정 하고 저장한 다음에 실행하여 주십시오.

first_class_function.py

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를 호출하여 보겠습니다.

다음과 같이 코드를 수정 하고 저장한 다음에 실행하여 주십시오.

first_class_function.py

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:  Hi

logger가 지워진 뒤에도 log_hi()를 실행하여 log_message가 호출된 것을 볼 수 있습니다.

logger 함수를 완전히 삭제한 이후에도 log_message 함수는 ‘Hi’를 기억하고 있는 것을 확인했습니다. 이런식으로 closure는 여러가지로 편리하게 쓰여질 때가 많은데, closure에 대해서는 다른 강좌에서 자세히 알아 보겠습니다. 

이번에는 조금 더 실용적인 예제를 보도록 하겠습니다.

다음과 같이 코드를 수정 하고 저장한 다음에 실행하여 주십시오.

first_class_function.py

# 단순한 일반 함수
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>

#1에서 html_tag 함수를 print_h1 변수에 할당한 후, #2에서 변수의 값을 출력하니 wrap_text 함수 오브제트가 할당되어 있는 것을 볼 수 있습니다. 그리고 #3과 #4에서 간단히 문자열을 전달하여 wrap_text 함수를 호출한 것을 볼 수 있습니다. 지금 많은 분들이 복잡한 wrapper 함수를 사용하지 않고도 위의 simple_html_tag과 같은 일반 함수를 사용하면 안되는가 하는 의문을 가지는 분들이 있으실 겁니다. 하지만, html_tag와 같은 higher-order 함수등을 이해해야 뒤의 강좌에서 배울 클로저 (closure), 데코레이터 (decorator) 또는 제너레이터 (generator) 등에 대해서 쉽게 이해할 수가 있습니다. 데코레이터와 제너레이터 등의 구문을 사용하시면 이전과는 전혀 다른 새로운 차원의 코딩을 하실 수가 있습니다.