본문 바로가기

더 나은 엔지니어가 되기 위해/파이썬을 파이썬스럽게

슬기로운 파이썬 트릭 2 - 효과적인 함수

이 글은 슬기로운 파이썬 트릭을 읽고 핵심만 빠르게 정리한 글이다.


3.1. 파이썬 함수는 일급 객체다

함수는 객체다

파이썬의 함수는 일급 "객체"다.
즉, 함수를 변수에 할당하고 데이터 구조에 저장하고, 인자로 다른 함수에 전달하고, 반환할 수 있다.
함수 객체와 이름은 별개다.

def yell(text):
  return text.upper() + "!"
>>> bark = yell
>>> bark("heumsi")
"HEUMSI!"
>>> bark.__name__
"yell"

함수는 데이터 구조에 저장할 수 있다.

>>> funcs = [bark, str.lower, str.capitalize]
>>> funcs[0]("heumsi")
"HEUMSI!"

함수는 다른 함수로 전달할 수 있다.

def greet(func):
    greeting = func("Hi, I'm python program")
    print(greeting)
>>> greet(bark)
"HI, I'M PYTHON PROGRAM"

함수는 지역 상태를 포착할 수 있다.

def get_speak_func(text, volumne):
    def whisper():
        return text.lower() + "..."
    def yell():
        return text.upper() + "!"

    if volumne >= 0.5:
        return yell
    else:
        return whisper
>>> get_speak_func("Hello, World", 0.7)()
"HELLO, WORLD!"

위 코드에서 whisperyell 내부를 보면 부모 함수에서 정의된 text 에 아무 이상없이 접근하고 있다.
이렇게 동작하는 함수를 렉시컬 클로저, 짧게 클로저라고 한다.
클로저는 프로그램 흐름이 더 이상 해당 범위에 있지 않은 경우에도 둘러싼 어휘(lexical) 범위 안의 값들을 기억한다.


3.2. 람다는 단일 표현식 함수다

람다를 사용할 수 있는 경우

람다는 함수를 정의하면 간단한 익명함수 작성이 가능하다.
또한, 편리하고 "비격식적인" 지름길을 제공한다.
가장 흔한 사례는 정렬에 사용하는 key 함수를 작성하는 것이다.

>>> tuples = [(1, 'd'), (2, 'b'), (4, 'a'), (3, 'c')]
>>> sorted(tuples, key=labmda x: x[1])
[(4, 'a'), (2, 'b'), (3, 'c'), (1, 'd')]

또한 클로저로써 함수 팩토리를 사용하는 사례도 있다.

def make_adder(n):
    return labmda x: x + n
>>> plus_3 = make_adder(3)
>>> plus_5 = make_adder(5)
>>> plus_3(4)
7
>>> plus_6(4)
9

람다 함수를 자제하는 경우

람다 함수로는 간단한 일만 해야한다. 다음 예를 보면 확 와닿을 것이다.

# 나쁜 코드
>>> list(filter(lambda x: x % 2 == 0, range(16)))
[0, 2, 4, 6, 8, 10, 12, 14]

# 더 나은 코드
>>> [x for x in range(16) if x % 2 == 0]
[0, 2, 4, 6, 8, 10, 12, 14]

3.3. 데코레이터의 힘

데코레이터는 다른 함수를 "장식" 하거나 "포장"하고, 감싼 함수가 실행되기 전후에 다른 코드를 실행할 수 있게 한다.
데코레이터가 유용한 경우는 다음과 같은 경우다.

  • 로그 남기기
  • 접근 제어와 인증 시행
  • 계측 및 시간 측정
  • 비율 제한
  • 캐싱 및 기타

데코레이터에 대해 조금 더 자세히 알아보자.

파이썬 데코레이터 기초

아래 두 코드는 같다.

def null_decorator(func):
    return func

def greet():
    return "Hello!"

>>> null_decorator(greet)
def null_decorator(func):
    return func

@null_decorator
def greet():
    return "Hello!"

>>> greet()

@ 구문을 사용하면 정의 시간에 즉시 함수가 장식된다.

데코레이터는 동작을 수정할 수 있다

아래 예제는 원래 동작하려던 함수 동작 후, 추가적인 작업을 데코레이터에서 진행하는 예제다.

def uppercase(func):
    def wrapper():
        original_result = func()
        modified_result = original_result.upper()
        return modified_result
    return wrapper

@uppercase
def greet():
    return "Hello!"
>>> greet()
"HELLO!"

데코레이터로 장식된 함수의 동작을 수정하려면 클로저를 만드는 작업을 수행해야 한다.
원본 함수와 데코레이터 함수를 분리하여 구현한 뒤, 동적으로 함수에 추가 장식(데코레이터)를 구현할 수 있다.

다중 데코레이터 적용하기

아래 두 코드는 같다. (코드에 사용된 strongemphasis 는 데코레이터 함수다.)

decorated_greet = strong(emphasis(greet))

>>> decorated_greet()
@strong
@emphasis
def greet():
    return "Hello"

>>> greet()

함수에 @ 구문으로 데코레이터가 붙으면, 아래에서 위 순서로 적용되는 것을 알 수 있다.

인자를 받는 함수 장식하기

다음처럼 *** 로 받으면, 어떤 함수든 인자를 전달할 수 있다.

def decorator(func):
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs):
    return wrapper

"디버깅 가능한" 데코레이터 작성법

실제로 데코레이터를 작성할 때는 @functools.wraps 를 사용해주어야 한다.

# functools.wraps 를 사용하지 않을 시

def uppercase(func):
    def wrapper():
        func.upper()
    return wrapper

@uppercase
def greet():
    return "Hello"

>>> a = greet
>>> a.__name__
"wrapper"  # "greet" 이 아니라 "wrapper" 가 등장한다.
# functools.wraps 를 사용할 시

import functools

def uppercase(func):
    @functools.wraps(func)  # 이 코드가 추가 되었다.
    def wrapper():
        func.upper()
    return wrapper

@uppercase
def greet():
    return "Hello"

>>> a = greet
>>> a.__name__
"greet"  # 이제 정상적으로 "greet" 이 잘 등장한다.

3.4. args 와 *kwargs 재미있게 활용하기

사실 너무 당연한 내용이라 그냥 요점만 적는다.
(지금까지 포스팅한 것도 다 당연한 내용이지만.. 리마인드 할겸 정리한 것...)

  • *args**kwargs 를 이용하면 파이썬에서 인자 개수가 가변적인 함수를 작성할 수 있다.
  • *args 는 여분의 위치 인자를 튜플로 수집한다.
  • **kwargs 는 여분위 키워드 인자를 딕셔너리로 수집한다.
  • 실제 문법은 *** 이고 argskwargs 는 이름에 불과하다. 그래도 관례므로 따르는 편이 좋다.

3.5. 함수 인자 풀기

별 내용 없어서 스킵.. 진짜로 별 내용 없다.


3.6. 반환할 것이 없는 경우

파이썬은 함수 내부에 return 문이 없는 경우 None 을 암묵적으로 반환한다.
이를 명시적으로 return None 으로 둘 것이냐, 아니면 굳이 필요없는데 넣을 것이냐의 문제다.
예를 들면 다음의 세 코드는 모두 같다.

def func(value):
    if value:
        return value
    else:
        return None
def func(value):
    if value:
        return value
    else:
        return
def func(value):
    if value:
        return value

어떤 것이 더 옳고 클린한 코드인가? 는 취향 차이라고 한다.
나는... 개인적으로 첫 번째 처럼 명시해주는 게 좀 더 클린하다고 생각한다. (개인적인 의견 ;;;)