python 퍼스트 클래스 함수

유동헌·2022년 2월 18일
0

파이썬 퍼스트클래스 함수 (First Class Function)

이 글은 파이썬 – 퍼스트클래스 함수 (First Class Function) 글을 보고 정리한 글입니다. 내용을 그대로 참고하였습니다.

퍼스트 함수란?

  • 프로그래밍 언어가 함수를 first-class-citizen으로 취급하는 것을 말합니다.
  • 함수 자체를 인자로써 다른 함수에 전달하거나 다른 함수의 결과값으로 리턴 할 수도 있고, 함수를 변수에 할당하거나 데이터 구조 안에 저장할 수 있는 함수를 뜻합니다.
def square(x):
    return x * x

print(square(5))

f = square

print(square)
print(f)

#
25
<function square at 0x7fdac81244c0>
<function square at 0x7fdac81244c0>
  • 간단한 함수 square를 정의하고 호출하였습니다. 그리고 그 후에는 f라는 변수에 square 함수를 할당하고 호출을 해보았습니다. 둘 다 메모리 주소값이 출력이 되고, 같은 것이 할당되어 있는 걸 보니 함수 객체가 할당이 되어 있는 것 같습니다.
def square(x):
    return x * x

f = square

print(f(5))

#
25
  • 위의 함수는 print(f(5)) 구문으로 함수를 호출하였습니다. 그리고 위에 언급했듯이 프로그래밍 언어가 퍼스트클래스 함수를 지원하면, 지금 보는 것처럼 변수에 함수를 할당할 수 있을 뿐만 안이라, 인자로써 다른 함수에 전달하거나, 함수의 리턴 값으로도 사용할 수 있습니다.

함수의 비교

num_list = [1, 2, 3, 4, 5]

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

squares = my_map(square, num_list) # 두 가지 인자를 전달. 하나는 square 함수 / 하나는 num_list

print(squares)

#
[1, 4, 9, 16, 25]
  • square라는 함수를 인자로 하여 num_list와 함께 인자로 보내서 함수의 결과값을 출력합니다.
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)

#
[1, 4, 9, 16, 25]
  • 위와 마찬가지의 결과를 얻을 수 있습니다. 위에서 사용된 퍼스트클래스 함수처럼 사용하는 방법과 이렇게 간단하게 하나의 함수만을 이용해 결과값을 출력하는 방법이 있습니다.
  • 하지만 def 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)

#
[1, 4, 9, 16, 25]
[1, 8, 27, 64, 125]
[1, 16, 81, 256, 625]
  • 위의 예제와 같이 이미 정의되어 있는 함수 square, cube, quad와 같은 여러 개의 함수나 모듈이 있다고 가정했을 때, my_map과 같은 wrapper 함수를 하나만 정의하여 기존의 함수나 모듈을 수정할 필요가 없이 편리하게 사용할 수 있습니다.

클로저 사용하기

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
  • 함수 바깥쪽에 있는 지역 변수 a, b를 사용하여 a * x + b를 계산하는 함수를 만들어 봅니다.
  • def mul_add(x) 함수의 바깥에 위치한 지역변수 a, b를 가지고 연산이 진행됩니다.
def calc():
    a = 3
    b = 5
    def mul_add(x):
        return a * x + b
		return mul_add
  • 함수 mul_add를 만든 뒤에는 이 함수를 바로 호출하지 않고 return으로 함수 자체를 반환합니다. (함수를 반환할 때는 함수 이름만 반환해야 하며 () 괄호를 붙이면 안됩니다)
c = calc()
print(c(1), c(2), c(3), c(4), c(5))
  • 지금부터는 클로저에 대한 이야기인데요, 다음과 같이 함수 calc를 호출한 뒤 반환값을 c에 저장합니다. calc에서 mul_add를 반환을 했음으로, c에는 함수 mul_add가 들어갑니다. 그리고 c에 숫자를 넣어서 호출해보면 a x b 계산이 이루어집니다.
  • 전체 함수를 보게 되면 함수 calc가 끝났는데도 c는 calc의 지역 변수 a, b를 사용하여 계산하고 있습니다. 이렇게 함수를 둘러싼 환경(지역 변수, 코드 등)을 계속 유지하다가 함수를 호출할 때 다시 꺼내서 사용하는 함수를 클로저라고 합니다. 여기서는 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처럼 람다 표현식을 만든 뒤 람다 표현식 자체를 반환했습니다. 이렇게 람다를 사용하면 클로저를 좀 더 간단하게 만들 수 있습니다.
  • 보통 클로저는 람다 표현식과 함께 사용하는 경우가 많아 둘을 혼동하기 쉽습니다. 람다는 이름이 없는 익명 함수를 뜻하고, 클로저는 함수를 둘러싼 환경을 유지했다가 나중에 다시 사용하는 함수를 뜻합니다.

함수의 결과값 리턴

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"가 출력됩니다.

#
<function logger.<locals>.log_message at 0x7f83f0184550>
Log:  Hi
  • 위의 # 1에서 정의된 log_message라는 함수를 logger 함수의 리턴 값으로 리턴하여 log_hi라는 변수에 할당한 후 호출했습니다.
  • 여기서 한 가지 특이한 점을 살펴볼 수 있는데, msg와 같은 함수의 지역 변수 값은 함수가 호출된 이후에 메모리상에서 사라지므로 다시 참조할 수가 없는데, msg 변수에 할당했던 ‘HI’ 값이 logger 함수가 종료된 이후에도 참조가 됐다는 것입니다.
  • 이런 log_message와 같은 함수를 클로저라고 부르며, 클로저는 다른 함수의 지역 변수를 그 함수가 종료된 이후에도 기억할 수가 있습니다. 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"가 출력됩니다.

#
<function logger.<locals>.log_message at 0x0000022EC0BBAAF0>
Log:  Hi
NameError: logger는 존재하지 않습니다.
Log:  Hi
  • logger가 지워진 뒤에도 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('이것은 패러그래프 입니다.')

#
<h1>심플 헤딩 타이틀<h1>
------------------------------
<function html_tag.<locals>.wrap_text at 0x7fac481245e0>
<h1>첫 번째 헤딩 타이틀<h1>
<h1>두 번째 헤딩 타이틀<h1>
<p>이것은 패러그래프 입니다.<p>
  • html_tag 함수를 point_h1 변수에 할당한 후에 출력을 해보니 wrap_text 함수 오브젝트가 할당되어 있는 것을 볼 수 있습니다. 그리고 문자열을 전달해 wrap_text 함수를 호출합니다.
  • 단순한 일반 함수로 분류된 함수의 사용과 함수를 리턴하는 함수의 사용이 다르다는 것을 알 수 있습니다.

Reference

코딩 도장 클로저 : 클로저에 대한 부분을 참고하였습니다.

profile
지뢰찾기 개발자

0개의 댓글