Programming/Python

[Python] PEP 8: E731 do not assign a lambda expression, use a def

Jonghyuk Baek 2022. 4. 30. 17:48

https://makeameme.org/meme/wheres-the-lambda

Introduction

파이썬에는 lambda expression (람다 표현식) 이라는 기능이 있다. 파이썬 코드를 자주 보는 사람들은 모두 한 번쯤은 람다 표현식을 써 봤거나 남이 쓰는 것을 본 적이 있을 것이다.

 

그런데 PEP 8 (참고) 규약을 기반으로 하는 정적 분석기 (ex. pycodestyle) 나 코드 오토포매터 (ex. autopep8) 를 사용하는 경우, 해당 도구들은 변수에 대입되는 람다 표현식을 목격하기만 하면 경고 메시지를 출력하거나 알아서 같은 기능을 하는 함수 정의로 대체해 버리는 것을 볼 수 있다.

 

PEP8 기반의 정적 분석기가 람다 표현식을 싫어한다.

# autopep8의 format on save를 켜 둔 상태로 Ctrl + S를 누르면..
sample_lambda_expression = lambda x: x**2

# 이렇게 바꿔 놓는다.
def sample_lambda_expression(x): return x**2

 

PEP 8가 파이썬의 일반적인 사용 방법에 대한 공인된 규약인 만큼, 람다 표현식 사용에 대한 파이썬 커뮤니티의 반응도 크게 다르지 않다. 왜 이렇게 강력히 lambda expression 의 대입 (assign)을 지양하는 것일까? 오늘은 이 주제에 대해 알아보도록 한다.


Lambda Expression

먼저 람다 표현식에 대해 소개를 하려고 한다. 람다 표현식은 Java나 C#에서도 흔히 쓰이는 기능으로, 메소드의 이름을 따로 지정하지 않으면서 (익명 함수) 함수를 단 한 줄의 식으로 간단히 표현할 수 있도록 해 주는 기능이다.

 

한번 사용 예시를 보자.

from typing import Callable


# Regular function definition.
def add_one_to_integer_regular(x: int) -> int:
	return x + 1

# Lambda expression
add_one_to_integer_lambda: Callable[int, int] = lambda x: x + 1


>>> add_one_to_integer_regular(5)
6
>>> add_one_to_integer_lambda(5)
6

 

간단한 함수를 일반적인 함수 정의와 람다 표현식 두 가지 방식으로 구현해 놓은 예시이다. Callable[int, int] 부분은 이해를 돕기 위한 타입 힌트로 실 사용 시에는 생략할 수 있다.

 

위 케이스에서는 잘 드러나지는 않지만, 특정 함수를 다른 함수의 인풋으로 전달하는 등 일급 객체로써 함수를 사용해야 하는 경우 (변수처럼 함수를 사용) 기존 방식은 함수 선언 - 다른 함수에 전달 의 과정을 거쳐야 했다면 람다 표현식을 이용할 경우엔 함수 선언과 동시에 다른 함수에 전달 과 같은 식으로 조금 더 간결하게 코드를 작성할 수 있다. 다른 예시를 보자.

def add_one_to_integer_regular(x: int) -> int:
	return x + 1


>>> list(map(add_one_to_integer_regular, [1, 2, 3]))
[2, 3, 4]

>>> list(map(lambda x: x + 1, [1, 2, 3]))
[2, 3, 4]

 

앞 실행문은 함수 선언 과정을 한번 거치고, 이 함수의 이름을 Callable을 인자로 받는 map 함수에 인자로써 넣어주고 있다. 반면 뒤 실행문은 lambda expression을 통해 익명 함수를 생성함과 동시에 map 함수의 인자로 함수 인스턴스를 전달해 주고 있다.

 

두 가지의 방식 모두 기능상의 차이는 없지만, 만약 동작이 복잡한 함수를 전달해야 하는 경우에는 전자가 더 유리하다. 람다 표현식을 통한 익명 함수 생성 시에는 내부 변수를 선언할 수 없고, 인자와 반환값의 종류가 다양해지거나 내부 구현을 한 줄로 표현하기 어려운 경우가 많기 때문이다.

 

이런 식으로 간단한 함수 하나를 변수로써 사용해야 하는 상황에서는 람다 표현식이 코드 분량의 압축에 도움이 될 수 있다.

Do not assign a lambda expression

그런데 왜 람다 표현식의 변수 대입은 사용하지 말아야 하는 걸까? 변수 대입을 스킵하고 위 예시처럼 선언과 전달을 한 번에 해 주는 경우는 괜찮은 걸까? PEP 8에 작성되어 있는 이에 대한 이유를 한번 여기로 옮겨와 보겠다.

 

람다 표현식을 바로 변수에 대입해 사용하기보다는, 항상 def 을 이용한 함수 선언을 대신 사용해야 한다.

# Correct:
def f(x): return 2*x
# Wrong:
f = lambda x: 2*x

첫 번째 형태는 결과로 만들어지는 함수가 정확히 "f" 라는 이름을 가지는 반면 두 번째 형태에서는 제네릭 타입인 "<lambda>" 이름을 가지는데, 전자 쪽이 명확한 이름을 가지는 편이 에러 트레이스백과 스트링 표현 시에 더 유용하다.

 

그리고, 변수에 대입하는 행위는 간단한 함수를 다른 더 큰 expression 내부에 포함시켜 버릴 수 있다는 람다 표현식이 가지는 오직 하나의 장점마저 없애버린다.

 

from typing import Callable


# Regular function definition.
def divide_five_by_integer(x: int) -> int:
    return 5 / x


# Lambda expression
divide_five_by_integer_lambda: Callable[int, int] = lambda x: 5 / x


>>> divide_five_by_integer_lambda(0)
Traceback (most recent call last):
  File "c:\Users\User\OneDrive\code\python\main.py", line 13, in <module>
    divide_five_by_integer(0)
  File "c:\Users\User\OneDrive\code\python\main.py", line 6, in divide_five_by_integer
    return 5 / x
ZeroDivisionError: division by zero

>>> divide_five_by_integer(0)
Traceback (most recent call last):
  File "c:\Users\User\OneDrive\code\python\main.py", line 13, in <module>
    divide_five_by_integer_lambda(0)
  File "c:\Users\User\OneDrive\code\python\main.py", line 10, in <lambda>
    divide_five_by_integer_lambda: Callable[int, int] = lambda x: 5 / x  
ZeroDivisionError: division by zero

실제로 코드를 돌려보면 에러 트레이스백 상에서 함수의 이름이나 대입된 변수의 이름 대신 <lambda> 라는 이름의 함수 내부에서의 호출 스택을 보여주고 있다. 반면 def을 이용해 선언한 함수는 정상적으로 함수의 이름을 표현하고 있다 (divice_five_by_integer). 익명 함수를 만든다는 점이 사용시의 불리한 점으로 작용하고 있는 것이다.

 

# Regular function definition.
def divide_five_by_integer(x): return 5 / x

# Lambda expression
divide_five_by_integer_lambda = lambda x: 5 / x

또한, 사실 람다 표현식으로 표현될 만한 간단한 함수는 def을 이용한 선언으로도 충분히 한 줄로 간략히 표현할 수 있다. 위의 예시를 참고하자. (물론 경우에 따라 이렇게 한 줄로 선언한 함수를 좋아하지 않는 분석기나 포매터도 있다)

 

PEP 8의 말마따나 람다 표현식이 가지고 있던, 함수를 다른 표현식에 임베드 할 수 있다 는 단 하나의 장점을 없애는 동시에 에러 트레이스백은 모호해지는 def의 완벽한 하위호환이 되어 버리는 것이다. 사실상 이런 식으로 람다 표현식을 대입해 쓸 이유는 하나도 없다는 말이다.

 

# Lambda expression
divide_five_by_integer_lambda = lambda x: 5 / x


def power_of_integer(x):
    return x**2


>>> power_of_integer(divide_five_by_integer_lambda(0))
Traceback (most recent call last):
  File "c:\Users\User\OneDrive\code\python\main.py", line 12, in <module>
    power_of_integer(divide_five_by_integer_lambda(0))
  File "c:\Users\User\OneDrive\code\python\main.py", line 5, in <lambda>
    divide_five_by_integer_lambda = lambda x: 5 / x
ZeroDivisionError: division by zero

참고로, 위 케이스처럼 람다 표현식을 더 큰 표현식에 넣어서 사용하는 경우에도, 람다 표현식 내부에서 에러가 발생할 때 트레이스백은 기존과 동일하게 <lambda> 함수 안에서 에러가 발생했다 고만 표현해 준다. 다만 코드를 간결화 시킬 수 있다는 장점 딱 하나로 이 정도의 문제는 봐 주는 분위기인 듯 하다.

 


이번 글의 내용을 요약하면 다음과 같다.

 

  • 람다 표현식은 익명 함수를 만들어내기 때문에 나중에 에러 트레이스백을 받아볼 때 출처를 파악하기 귀찮아진다.
  • 코드를 간결하게 표현하기 위해 다른 표현식에 람다 표현식을 임베딩 하는 것 정도는 괜찮다.
  • 하지만 람다 표현식을 만들어 변수 대입을 하는 건 def 에 비해 단 하나의 이점도 없다.

아래는 참고할 만한 사이트들이다.

References

PEP 8 – Style Guide for Python Code: Programming Recommendations

https://peps.python.org/pep-0008/#programming-recommendations

Python 3.10.4 Documentation: Expressions - Lambda

https://docs.python.org/3/reference/expressions.html#lambda

 

반응형