상위 목록: 하위 목록: 작성 날짜: 읽는 데 9 분 소요

람다(lambda) 함수

람다(lambda) 함수는 사용자 정의 함수와 비슷한 역할을 합니다.

함수명을 정해 사용하는 사용자 정의 함수와 다르게 이름없이 사용하는 익명 함수입니다.

주로 간단한 기능을 정의해 다른 함수의 인수로 한 번 전달하는 용도로 사용합니다.

제 20강에서 함수도 정수나 문자열처럼 값으로 전달할 수 있는 객체라는 것을 확인했습니다. max, sorted, filter, map처럼 함수를 인수로 받는 함수를 사용하다 보면, x[1]을 꺼내는 것처럼 한 번 쓰고 버릴 아주 짧은 함수가 자주 필요합니다.

이럴 때마다 def로 이름을 붙여 별도의 함수를 정의하면 코드가 길어지고, 한 번만 쓰는 함수의 이름이 늘어납니다. 람다 함수는 이런 짧은 함수를 사용하는 자리에서 바로 작성할 수 있게 합니다.

람다 함수는 lambda 매개변수: 표현식의 형태로 작성하며, 예제처럼 함수명 = lambda 매개변수: 표현식으로 변수에 할당해 사용할 수도 있습니다.

매개변수는 x, y, z 등의 이름을 사용하며, 표현식은 매개변수의 값을 활용합니다.

람다 함수의 본문에는 하나의 표현식만 작성할 수 있으며, return을 쓰지 않아도 표현식의 결과가 반환됩니다.

lambda를 사용하여 한 줄로 되어있는 수학 함수를 생성하거나 특정 형태나 형식 등으로 변환할 수 있습니다.

간단한 수학함수가 필요한 경우 람다 함수를 호출하여 사용합니다.

이번 강좌에서는 람다 함수의 작성 방법과 한계를 살펴보고, 가장 많이 쓰이는 max, min, sort, sorted의 key 인수와 함께 사용하는 방법을 알아봅니다.



함수 생성

f = lambda x, y: x + y

print(f(1, 2))
print(f([1, 2], [3, 4]))
print(f("YUN", "DAEHEE"))
결과
3
[1, 2, 3, 4]
YUNDAEHEE

함수명 = lambda 매개변수1, 매개변수2, ...: 반환식을 사용하여 람다 함수를 선언할 수 있습니다.

함수명(인수1, 인수2 ...)를 사용하여 함수를 호출합니다.

lambda x, y: x + y에서 : 앞의 x, y가 매개변수이고, : 뒤의 x + y가 반환식입니다. 호출하면 인수가 매개변수에 대응된 뒤 반환식이 계산되어 그 값이 반환됩니다.

  • Tip : + 연산이 가능한 정수, List, 문자열 등을 모두 사용할 수 있습니다.

  • Tip : PEP 8에서는 람다 함수를 변수에 할당하는 대신 def를 사용하도록 권장합니다. def로 정의하면 함수 이름이 __name__에 남아 오류 추적이 쉽지만, 람다 함수는 이름이 <lambda>로 표시됩니다.


def add_def(x, y):
    return x + y


add_lambda = lambda x, y: x + y

print(add_def(1, 2), add_lambda(1, 2))
print(type(add_def), type(add_lambda))
print(add_def.__name__, add_lambda.__name__)
결과
3 3
<class ‘function’> <class ‘function’>
add_def <lambda>

def로 만든 함수와 lambda로 만든 함수는 같은 종류(function)의 객체이며, 호출 방법과 결과도 같습니다.

차이는 작성 방식에 있습니다. def는 이름을 붙여 함수를 정의하는 문장(statement)이고, lambda는 함수 객체를 만들어 내는 표현식(expression)입니다.

표현식이므로 sorted(L, key=lambda x: x[1])처럼 인수 자리나 리스트 원소 자리 등 값이 들어갈 수 있는 곳 어디에나 바로 작성할 수 있습니다.

반대로, 본문에도 표현식 하나만 쓸 수 있으므로 if 문, for 문, 변수 할당, return 같은 문장은 사용할 수 없습니다. lambda x: return x처럼 작성하면 SyntaxError: invalid syntax가 발생합니다.



기본값 설정

f = lambda x, y=3: x + y

print(f(1))
print(f(3, 3))
결과
4
6

매개변수에 기본값을 할당할 수 있습니다.

기본값이 할당되면 함수를 호출할 때 매개변수를 채우지 않으면 기본값으로 사용합니다.

f(1)은 y가 생략되어 1 + 3이 되고, f(3, 3)은 전달한 값이 기본값보다 우선하므로 3 + 3이 됩니다.

람다 함수의 매개변수는 def의 매개변수와 같은 규칙을 따르므로, 기본값이 있는 매개변수는 뒤에 작성하고 키워드 인수(f(1, y=5))로도 호출할 수 있습니다.



다중 입력

f = lambda *x: max(x) * 2

print(f(1, 3, 7))
결과
14

매개변수에 *를 입력할 경우 개수를 지정하지 않고 인수를 전달받을 수 있습니다.

전달된 인수들은 Tuple로 묶여 x에 저장되므로, max(x)처럼 Tuple을 다루는 함수를 사용할 수 있습니다.

예제는 x가 (1, 3, 7)이 되고, max(x)인 7에 2를 곱한 14가 반환됩니다.

단, 람다 함수는 하나의 표현식만 작성할 수 있으므로, 여러 값을 반환하려면 Tuple 등으로 묶어서 반환합니다.

f = lambda x, y: (x + y, x - y)

print(f(5, 3))
결과
(8, 2)

  • Tip : Tuple을 반환할 때는 lambda x, y: x + y, x - y처럼 괄호를 생략하면 의도와 다르게 해석되므로 반드시 괄호로 감쌉니다.



조건 표현식

람다 함수에는 if 문을 쓸 수 없지만, 값을 고르는 조건 표현식(삼항 연산자)은 표현식이므로 사용할 수 있습니다.

f = lambda x: "짝수" if x % 2 == 0 else "홀수"

print(f(4))
print(f(7))
결과
짝수
홀수

참일 때 값 if 조건 else 거짓일 때 값 형태로 작성하며, 조건의 결과에 따라 두 값 중 하나가 반환됩니다.

f(4)는 4 % 2 == 0이 참이므로 "짝수", f(7)은 거짓이므로 "홀수"가 반환됩니다.

  • Tip : 조건 표현식을 여러 개 이어 붙이면 한 줄은 짧아지지만 읽기 어려워집니다. 조건이 세 가지 이상이라면 def로 함수를 정의하는 것이 좋습니다.



람다 함수 목록

f = [lambda x: x + 1, lambda x: x + 2, lambda x: x + 3]

print(f[0](1))
print(f[1](1))
print(f[2](1))
결과
2
3
4

람다 함수도 하나의 객체이므로 List에 저장할 수 있습니다.

List로 사용할 경우 대괄호([]) 를 사용하여 선택한 람다 함수를 호출합니다.

f[0]은 첫 번째 람다 함수 객체이며, 뒤에 (1)을 붙여 호출합니다. 즉, f[0](1)은 꺼내기와 호출하기를 한 번에 작성한 것입니다.

List가 아닌, Dict로 사용할 경우, 원하는 Key 값을 통해 특정 람다 함수를 사용할 수도 있습니다.

ops = {"+": lambda x, y: x + y, "-": lambda x, y: x - y}

print(ops["+"](3, 4))
print(ops["-"](3, 4))
결과
7
-1

연산 기호를 키로 사용하면 if 문을 여러 번 쓰지 않고도 입력된 기호에 맞는 계산을 선택할 수 있습니다.


funcs = [lambda x: x + i for i in range(3)]
print([f(10) for f in funcs])

funcs = [lambda x, i=i: x + i for i in range(3)]
print([f(10) for f in funcs])
결과
[12, 12, 12]
[10, 11, 12]

반복문으로 람다 함수 목록을 만들 때는 주의해야 합니다.

첫 번째 목록의 함수들은 10, 11, 12를 반환할 것처럼 보이지만, 실제로는 모두 12를 반환합니다.

람다 함수는 만들어질 때 i의 값을 저장하지 않고, 호출될 때 i라는 이름을 찾아 값을 읽습니다. 함수를 호출하는 시점에는 반복이 이미 끝나 i가 마지막 값 2이므로 세 함수 모두 10 + 2를 계산합니다.

두 번째 목록처럼 i=i 기본값을 사용하면, 기본값은 함수가 만들어질 때 계산되어 저장되므로 각 함수가 그 시점의 i 값을 갖게 됩니다.



최대/최소

L = [[9, 1], [8, 2], [7, 3], [6, 4]]

max1 = max(L, key=lambda x: x[0])
max2 = max(L, key=lambda x: x[1])
min1 = min(L, key=lambda x: x[0])
min2 = min(L, key=lambda x: x[1])

print(max1)
print(max2)
print(min1)
print(min2)
결과
[9, 1]
[6, 4]
[6, 4]
[9, 1]

최댓값 함수와 최솟값 함수에도 람다식을 적용하여 특정 최대/최소를 검출할 수 있습니다.

1차원 이상의 객체에서 특정 요소에 있는 값을 기준으로 찾으려 할 때, 어떤 요소의 값을 기준으로 찾을지 설정할 수 있습니다.

lambda x: x[0]라면 첫 번째 요소의 값을 기준으로 비교하며, lambda x: x[1]라면 두 번째 요소의 값을 기준으로 비교합니다.

key에 전달한 함수는 각 원소에 적용되어 비교에 사용할 값을 반환하며, 결과로는 원래 원소가 반환됩니다.

예를 들어 max2는 각 원소의 두 번째 값 1, 2, 3, 4를 비교하여 가장 큰 4를 가진 원소 [6, 4]를 반환합니다. 반환되는 것은 비교 값 4가 아니라 원래 원소라는 점이 중요합니다.


scores = {"A": 90, "B": 75, "C": 82}

print(sorted(scores.items(), key=lambda kv: kv[1]))
print(sorted(scores, key=lambda k: scores[k], reverse=True))
print(max(scores, key=scores.get))
결과
[(‘B’, 75), (‘C’, 82), (‘A’, 90)]
[‘A’, ‘C’, ‘B’]
A

사전을 값 기준으로 비교할 때도 key를 사용합니다.

scores.items()는 (키, 값) 쌍을 반환하므로 lambda kv: kv[1]은 값을 기준으로 정렬하며, 결과도 (키, 값) 쌍의 목록이 됩니다.

사전을 그대로 반복하면 키가 나오므로, 두 번째 줄처럼 lambda k: scores[k]로 키에 해당하는 값을 비교 기준으로 삼으면 점수가 높은 순서의 키 목록을 얻을 수 있습니다.

세 번째 줄의 scores.get은 키를 받아 값을 반환하는 함수이므로, 람다 함수를 새로 만들지 않고 이미 있는 메서드를 그대로 전달할 수 있습니다.



정렬

L = ["가", "각", "감", "갂", "갃", "+", "/", "!", "간", "]", "[", "\\"]

L.sort(key=lambda x: x)

print(L)

print([ord(i) for i in L])
결과
[’!’, ‘+’, ‘/’, ‘[’, ‘\’, ‘]’, ‘가’, ‘각’, ‘갂’, ‘갃’, ‘간’, ‘감’]
[33, 43, 47, 91, 92, 93, 44032, 44033, 44034, 44035, 44036, 44048]

Python 3의 모든 문자열(str)은 유니코드(Unicode) 문자열입니다.

그러므로, 문자열을 정렬할 때 각 문자의 유니코드 코드 포인트(ord()의 값)를 기준으로 비교(<, >)를 진행하게 됩니다.

정렬에 사용되는 람다식의 x는 “가”, “각”, “감”, … “\“이 되며, 이 값들의 코드 포인트를 대소 비교하여 정렬합니다.

결과의 두 번째 줄을 보면 기호 문자(33~93)가 한글(44032 이상)보다 코드 포인트가 작으므로 앞에 정렬된 것을 확인할 수 있습니다.

  • Tip : key=lambda x: x는 원소를 그대로 비교하므로 L.sort()와 결과가 같습니다. 대소문자를 무시하려면 key=str.lower, 길이로 정렬하려면 key=len처럼 기존 함수를 그대로 전달할 수도 있습니다.

만약, 정렬하려는 반복 가능한 객체(iterable)에 서로 다른 데이터 타입이 존재한다면, '<' not supported between instances of 'int' and 'str'등 의 오류가 발생합니다.

이럴 경우에는 명시적으로 요솟값을 str이나 int로 변경하여 정렬합니다.


def key_len(x):
    print("key 호출 :", x)
    return len(x)


print(sorted(["ccc", "a", "bb"], key=key_len))
결과
key 호출 : ccc
key 호출 : a
key 호출 : bb
[‘a’, ‘bb’, ‘ccc’]

key 함수는 정렬을 시작하기 전에 각 원소마다 정확히 한 번씩 호출됩니다.

정렬 함수는 원소마다 계산한 비교 값(3, 1, 2)을 기억해 두고 그 값끼리 비교하여 순서를 정한 뒤, 결과로는 원래 원소를 그 순서대로 배치합니다.

그러므로, key 함수는 원소 하나를 받아 비교할 값 하나를 반환하는 형태여야 하며, 두 원소를 받아 비교하는 함수를 전달하면 안 됩니다.


words = ["banana", "Apple", "fig", "Cherry"]

print(sorted(words))
print(sorted(words, key=str.lower))
print(sorted(words, key=len))
결과
[‘Apple’, ‘Cherry’, ‘banana’, ‘fig’]
[‘Apple’, ‘banana’, ‘Cherry’, ‘fig’]
[‘fig’, ‘Apple’, ‘banana’, ‘Cherry’]

기본 정렬은 코드 포인트 기준이므로 대문자(A=65, C=67)가 소문자(b=98)보다 앞에 놓입니다.

key=str.lower를 사용하면 모든 문자열을 소문자로 바꾼 값으로 비교하므로 사전 순서에 가깝게 정렬되지만, 결과에는 원래 문자열이 그대로 남습니다.

key=len은 길이로 비교하며, 길이가 6으로 같은 banana와 Cherry는 원래 순서를 유지합니다.


L = [3, "10", 2, "1"]

try:
    sorted(L)
except TypeError as e:
    print(e)

print(sorted(L, key=int))
print(sorted(L, key=str))
결과
‘<’ not supported between instances of ‘str’ and ‘int’
[‘1’, 2, 3, ‘10’]
[‘1’, ‘10’, 2, 3]

정수와 문자열이 섞여 있으면 둘 사이의 크기를 비교할 수 없으므로 TypeError가 발생합니다.

key=int는 비교할 때만 모든 원소를 정수로 바꾸므로 숫자 크기 순서로 정렬되며, key=str은 모든 원소를 문자열로 바꾸므로 "10"이 "2"보다 앞에 오는 문자 순서로 정렬됩니다.

두 경우 모두 결과 목록의 원소는 원래 자료형을 유지합니다. 어느 기준이 맞는지는 데이터의 의미에 따라 선택합니다.



복합 정렬

L = [[3, "d"], [5, "c"], [1, "a"], [0, "b"], [0, "a"]]
L1 = sorted(L, key=lambda x: -x[0])
L2 = sorted(L, key=lambda x: x[1])
L3 = sorted(L, key=lambda x: (x[0], x[1]))

print(L)
print(L1)
print(L2)
print(L3)
결과
[[3, ‘d’], [5, ‘c’], [1, ‘a’], [0, ‘b’], [0, ‘a’]]
[[5, ‘c’], [3, ‘d’], [1, ‘a’], [0, ‘b’], [0, ‘a’]]
[[1, ‘a’], [0, ‘a’], [0, ‘b’], [5, ‘c’], [3, ‘d’]]
[[0, ‘a’], [0, ‘b’], [1, ‘a’], [3, ‘d’], [5, ‘c’]]

정렬하려는 객체가 2차원 이상이라면, 비교하는 x에는 [3, “d”], [5, “c”], … [0, “a”]가 됩니다.

만약, 특정 원소를 지정하지 않고 key=lambda x: x처럼 사용한다면, 리스트끼리 비교하므로 첫 번째 원소를 먼저 비교하고, 같으면 두 번째 원소를 비교하는 방식으로 정렬하게 됩니다.

특정한 원소를 기준으로 정렬한다면, 어떤 값을 기준으로 정렬할지 표기합니다.

여기서, 정렬 함수는 기본적으로 오름차순 형태로 정렬합니다.

그러므로, 비교하는 원소를 음수로 변경한다면 내림차순으로 변경할 수 있습니다. 숫자가 아닌 값은 음수로 바꿀 수 없으므로 reverse=True 인수를 사용합니다.

여러가지 기준으로 정렬하려고 한다면, Tuple 등으로 감싸 정렬 우선순위를 할당할 수 있습니다.

예제의 L3의 경우 첫 번째 원소를 기준으로 정렬하면서, 두 번째 원소를 차선으로 정렬하게 됩니다.

즉, [0, "b"]와 [0, "a"]를 비교할 때, 0이 동일하므로, 그 다음 조건으로 a와 b를 대상으로 다시 정렬하게 됩니다.

Tuple끼리의 비교는 첫 번째 값부터 차례로 비교하다가 처음으로 다른 값이 나오는 위치에서 결정되므로, 원하는 우선순위대로 값을 나열하면 됩니다.

또한, sorted()는 정렬된 새 목록을 반환하므로 첫 번째 출력처럼 원본 L은 바뀌지 않습니다. 원본 자체를 정렬하려면 L.sort()를 사용하며, 이 메서드는 None을 반환합니다.

  • Tip : Python의 정렬은 안정 정렬(stable sort)이므로, L2의 [1, 'a']와 [0, 'a']처럼 비교 기준 값이 같은 원소는 원래 순서를 유지합니다.

  • Tip : 숫자는 내림차순, 문자는 오름차순처럼 기준마다 방향이 다르다면 key=lambda x: (-x[0], x[1])처럼 숫자 쪽만 음수로 바꿔 하나의 Tuple로 작성합니다.



정리

이름 설명 반환값/특징
lambda 매개변수: 표현식 이름 없는 함수 객체 생성 표현식의 결과를 자동 반환
값1 if 조건 else 값2 람다 안에서 쓰는 조건 표현식 조건에 따라 두 값 중 하나
max / min(범위, key=함수) 비교 기준을 지정해 최대/최소 검색 원래 원소 반환
sorted(범위, key=함수) 비교 기준을 지정해 정렬 새 목록 반환, 원본 유지
목록.sort(key=함수) 원본 목록을 정렬 None 반환
reverse=True 내림차순 정렬 숫자가 아닌 기준에도 사용 가능


lambda와 def는 다음 기준으로 선택합니다.

  • 한 번만 쓰는 짧은 표현식을 key, filter, map 등에 넘길 때는 lambda를 사용합니다.
  • str.lower, len, scores.get처럼 이미 있는 함수로 충분하다면 람다 함수로 감싸지 않고 그대로 전달합니다.
  • 여러 줄의 처리, 반복문, 예외 처리가 필요하거나 여러 곳에서 재사용하는 함수는 def로 이름을 붙여 정의합니다.

댓글 남기기