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

키워드 인자화(Transform Keyword Argument)

키워드 인자화(Transform Keyword Argument)는 매개변수를 키워드 인자(Keyword Argument)로만 전달받도록 강제하는 방법입니다.

이렇게 정의된 매개변수를 키워드 전용 매개변수(Keyword-Only Parameter)라 부릅니다.

**kwargs처럼 매개변수의 이름을 명확히 할당해야 함수를 사용할 수 있습니다.


제 32강에서 다룬 것처럼, 일반 매개변수는 func(1, 2)처럼 위치로도, func(a=1, b=2)처럼 키워드로도 값을 받을 수 있습니다.

어떤 방식으로 호출할지는 함수를 사용하는 쪽이 선택합니다. 그러므로, 함수를 만든 사람이 “이 값은 이름을 붙여서 전달해야 이해하기 쉽다”고 생각하더라도 위치로 전달하는 것을 막을 수 없습니다.

키워드 전용 매개변수는 함수를 만드는 쪽이 호출 방식을 정하는 기능입니다. 이름을 붙이지 않고 값을 전달하면 TypeError가 발생합니다.


이번 강좌에서는 키워드 전용 매개변수가 왜 필요한지 먼저 살펴본 다음, 정의하는 방법과 기본값, 자주 발생하는 오류, 표준 라이브러리에서 실제로 사용되는 예, 그리고 위치 전용 매개변수와 함께 사용하는 방법을 다룹니다.



키워드 전용 매개변수가 필요한 이유

def transfer(sender, receiver, amount, fee):
    print(f"{sender} → {receiver} : {amount}원 (수수료 {fee}원)")


transfer("박XX", "김XX", 10000, 500)
transfer("박XX", "김XX", 500, 10000)
결과
박XX → 김XX : 10000원 (수수료 500원)
박XX → 김XX : 500원 (수수료 10000원)

위 함수는 보내는 사람, 받는 사람, 금액, 수수료를 위치 인자로 받습니다.

두 번째 호출은 금액과 수수료의 순서를 바꿔 전달하는 실수를 한 것입니다. 두 값 모두 정수이므로 Python은 이 실수를 알아챌 수 없고, 500원을 보내면서 수수료로 10000원을 내는 잘못된 결과가 오류 없이 실행됩니다.

또한, transfer("박XX", "김XX", 10000, 500)이라는 호출만 보고 10000과 500 중 어느 쪽이 금액인지 알려면 함수의 정의를 찾아봐야 합니다.


이처럼 위치 인자는 다음과 같은 문제가 있습니다.

첫째, 호출하는 코드만으로는 값의 의미를 알기 어렵습니다. 특히 True, False나 숫자처럼 그 자체로는 의미가 드러나지 않는 값일수록 더 그렇습니다.

둘째, 자료형이 같은 인자의 순서를 바꿔도 오류가 발생하지 않습니다. 잘못된 결과가 그대로 사용되어, 나중에 엉뚱한 곳에서 문제가 드러납니다.

셋째, 함수를 수정해 매개변수의 순서를 바꾸거나 중간에 추가하면, 기존 호출 코드가 오류 없이 다른 의미로 동작하게 됩니다. 이는 아래에서 다시 다룹니다.


def transfer(sender, receiver, *, amount, fee):
    print(f"{sender} → {receiver} : {amount}원 (수수료 {fee}원)")


transfer("박XX", "김XX", amount=10000, fee=500)
transfer("박XX", "김XX", fee=500, amount=10000)

try:
    transfer("박XX", "김XX", 500, 10000)
except TypeError as e:
    print("TypeError:", e)
결과
박XX → 김XX : 10000원 (수수료 500원)
박XX → 김XX : 10000원 (수수료 500원)
TypeError: transfer() takes 2 positional arguments but 4 were given

amount와 fee를 키워드 전용 매개변수로 바꾸면, 호출하는 코드에 amount=10000, fee=500처럼 값의 의미가 함께 적힙니다.

두 번째 호출처럼 순서를 바꿔 작성해도 이름으로 할당되므로 결과가 같습니다. 순서를 외울 필요가 없으므로 순서 실수가 생기지 않습니다.

이름 없이 위치로 전달하면 TypeError가 발생하므로, 앞의 예제처럼 잘못된 값이 조용히 사용되는 일을 막을 수 있습니다.



Keyword Argument (1)

def func(name, *, value1, value2):
    total = value1 + value2
    print(name + "는", total, "입니다.")


func("Plus", value1=2, value2=3)
결과
Plus는 5 입니다.

매개변수를 선언할 때 이름 없는 애스터리스크(Asterisk, *)를 중간에 작성하면, 그 뒤의 매개변수들은 키워드 전용 매개변수가 됩니다.

함수의 value1과 value2는 함수를 사용할 때, 명시적으로 할당되어야 합니다.

인자의 사용 유/무를 강제하는 것이 아닌, 인자를 전달할 때 키워드로 전달하도록 강제합니다.

만약, 기본값이 할당되어 있다면 필수로 할당하지 않아도 됩니다.

또한, 인자를 전달할 때 위치 인자(Positional Argument)를 사용하듯이 순서를 지키지 않아도 됩니다.


이름 없는 *는 *args와 같은 위치에 작성하지만, 값을 받지 않는 구분자입니다.

*args는 남은 위치 인자를 튜플로 모으지만, 이름 없는 *는 위치 인자를 더 이상 받지 않겠다는 표시입니다. 그러므로, name 이후에 위치 인자를 더 전달하면 오류가 발생합니다.

* 앞의 name은 일반 매개변수이므로 위치로도 키워드로도 전달할 수 있습니다. 예제에서는 "Plus"를 위치 인자로 전달했습니다.

위의 사항을 다음과 같이 사용할 수 있습니다.



Keyword Argument (2)

def func(name, *, value1, value2=3, value3):
    total = value1 + value2 + value3
    print(name + "는", total, "입니다.")


func("Plus", value3=2, value1=3)
결과
Plus는 8 입니다.

위와 같이 인자의 순서를 무시해도 됩니다.

기본값이 할당되어 있다면 필수적으로 값을 입력하지 않아도됩니다.

예제는 value1에 3, 생략한 value2에 기본값 3, value3에 2가 할당되어 3 + 3 + 2 = 8이 출력됩니다.


일반 매개변수는 기본값이 있는 매개변수 뒤에 기본값이 없는 매개변수를 둘 수 없습니다. def func(a=1, b):는 SyntaxError입니다.

하지만 키워드 전용 매개변수는 위치로 할당되지 않으므로 이 제약이 없습니다. 예제처럼 기본값이 있는 value2 뒤에 기본값이 없는 value3을 둘 수 있습니다.

그러므로, 매개변수를 의미에 맞는 순서로 자유롭게 배치할 수 있습니다.


def save(path, data, *, overwrite=False, encoding="utf-8"):
    print(path, data, overwrite, encoding)


save("a.txt", "Hello")
save("a.txt", "Hello", overwrite=True)
결과
a.txt Hello False utf-8
a.txt Hello True utf-8

실제로 가장 많이 사용하는 형태는 기본값이 있는 키워드 전용 매개변수입니다.

반드시 필요한 값(path, data)은 위치 인자로 받고, 동작을 바꾸는 옵션(overwrite, encoding)은 기본값을 둔 키워드 전용 매개변수로 받습니다.

대부분의 호출은 첫 번째 줄처럼 필수 값만 전달하고, 옵션이 필요할 때만 overwrite=True처럼 이름을 붙여 전달합니다.

save("a.txt", "Hello", True)처럼 작성할 수 없으므로, 호출 코드에서 True가 무엇을 의미하는지 항상 알 수 있습니다.



키워드로 전달하지 않은 경우

def func(name, *, value1, value2):
    total = value1 + value2
    print(name + "는", total, "입니다.")


try:
    func("Plus", 2, 3)
except TypeError as e:
    print("TypeError:", e)

try:
    func("Plus", value1=2)
except TypeError as e:
    print("TypeError:", e)
결과
TypeError: func() takes 1 positional argument but 3 were given
TypeError: func() missing 1 required keyword-only argument: ‘value2’

키워드 전용 매개변수에 값을 위치로 전달하면, 받을 수 있는 위치 인자의 개수를 넘었다는 TypeError가 발생합니다.

func는 위치 인자를 name 하나만 받을 수 있는데 3개를 전달했으므로 takes 1 positional argument but 3 were given이 출력됩니다.

기본값이 없는 키워드 전용 매개변수를 생략하면, missing 1 required keyword-only argument 오류가 발생합니다.

오류 메시지에 keyword-only가 표시되므로, 이 매개변수는 이름을 붙여 전달해야 한다는 것을 알 수 있습니다.

키워드 인자화는 True, False나 숫자처럼 의미를 알기 어려운 값을 전달하는 매개변수에 유용합니다.


def func(*, value):
    print(value)


func(value=1)

try:
    func(1)
except TypeError as e:
    print("TypeError:", e)
결과
1
TypeError: func() takes 0 positional arguments but 1 was given

*를 매개변수 목록의 맨 앞에 작성하면, 모든 매개변수가 키워드 전용이 됩니다.

위치 인자를 하나도 받지 않으므로, func(1)은 takes 0 positional arguments 오류가 발생합니다.


def func(name, *):
    pass
결과
SyntaxError: named arguments must follow bare *

이름 없는 * 뒤에는 반드시 하나 이상의 매개변수가 있어야 합니다.

* 뒤에 아무 매개변수도 없으면 *를 작성할 이유가 없으므로 SyntaxError가 발생합니다.


def func(name, *, value1, value2):
    print(name, value1, value2)


options = {"value1": 2, "value2": 3}
func("Plus", **options)
결과
Plus 2 3

키워드 전용 매개변수에는 사전을 **로 풀어서 전달할 수도 있습니다.

**options는 value1=2, value2=3으로 풀리므로, 키워드로 전달한 것과 같습니다. 설정값을 사전으로 관리하다가 함수에 한 번에 전달할 때 유용합니다.



표준 라이브러리의 키워드 전용 매개변수

Python의 내장 함수와 표준 라이브러리에도 키워드 전용 매개변수가 널리 사용됩니다.

특히 동작을 바꾸는 옵션에 해당하는 매개변수는 대부분 키워드 전용으로 정의되어 있습니다.


data = ["banana", "kiwi", "apple"]

print(sorted(data, key=len, reverse=True))

try:
    sorted(data, len, True)
except TypeError as e:
    print("TypeError:", e)

try:
    data.sort(len)
except TypeError as e:
    print("TypeError:", e)
결과
[‘banana’, ‘apple’, ‘kiwi’]
TypeError: sorted expected 1 argument, got 3
TypeError: sort() takes no positional arguments

sorted(data, key=len, reverse=True)의 key와 reverse는 키워드 전용 매개변수이므로, 호출하는 코드만 보고도 “길이를 기준으로, 역순으로 정렬한다”는 의미를 알 수 있습니다.

sorted(data, len, True)처럼 위치로 전달하면 sorted는 위치 인자를 하나만 받으므로 TypeError가 발생합니다.

목록의 sort 메서드는 모든 매개변수가 키워드 전용이므로, 위치 인자를 하나도 받지 않습니다.

만약 위치로 전달할 수 있었다면, sorted(data, True)처럼 reverse를 의도하고 작성한 코드가 key에 True를 전달하는 실수가 생길 수 있습니다.


import inspect
import json

print(inspect.signature(sorted))
print(inspect.signature(json.dumps))
print(inspect.signature(print))
결과
(iterable, /, *, key=None, reverse=False)
(obj, *, skipkeys=False, ensure_ascii=True, check_circular=True, allow_nan=True, cls=None, indent=None, separators=None, default=None, sort_keys=False, **kw)
(*args, sep=’ ‘, end=’\n’, file=None, flush=False)

inspect.signature(함수)는 함수의 매개변수 구성(시그니처)을 반환합니다. help(함수)로도 같은 내용을 확인할 수 있습니다.

sorted는 iterable 하나만 위치로 받고(/), 나머지 key와 reverse는 * 뒤에 있으므로 키워드 전용입니다.

json.dumps는 변환할 객체 obj 하나만 위치로 받고, indent, sort_keys, ensure_ascii 등 9개의 옵션이 모두 키워드 전용입니다. 옵션이 이렇게 많으면 위치로 전달할 때 순서를 기억하기 어렵기 때문입니다.

print는 *args 뒤에 sep, end, file, flush가 있으므로, 이 매개변수들은 * 없이도 키워드 전용이 됩니다. print("A", "B", "-")의 "-"는 구분자가 아니라 출력할 값으로 처리됩니다.


import json

print(json.dumps({"b": 1, "a": [1, 2]}, sort_keys=True, ensure_ascii=False))
결과
{“a”: [1, 2], “b”: 1}

json.dumps(객체, sort_keys=True, ensure_ascii=False)처럼 옵션의 이름이 호출 코드에 적혀 있으므로, json 모듈의 문서를 보지 않아도 “키를 정렬하고, ASCII가 아닌 문자를 그대로 출력한다”는 의도를 알 수 있습니다.

이 밖에도 max(iterable, *, key, default)의 key와 default, zip(*iterables, strict=False)의 strict 등이 키워드 전용 매개변수입니다.


from dataclasses import dataclass


@dataclass(kw_only=True)
class Point:
    x: int
    y: int


print(Point(x=1, y=2))

try:
    Point(1, 2)
except TypeError as e:
    print("TypeError:", e)
결과
Point(x=1, y=2)
TypeError: Point.__init__() takes 1 positional argument but 3 were given

dataclasses 모듈의 @dataclass(kw_only=True)를 사용하면, 자동으로 생성되는 __init__의 모든 매개변수가 키워드 전용이 됩니다. (Python 3.10 이상)

속성이 많은 클래스에서 Point(1, 2)처럼 위치로 생성하면 어느 값이 어느 속성인지 알기 어려우므로, 키워드로만 생성하도록 강제할 때 사용합니다.

오류 메시지의 takes 1 positional argument는 self 하나만 위치 인자로 받는다는 의미입니다.

  • Tip : 데이터 클래스는 제 50강에서 자세히 다룹니다.



함수를 수정할 때의 안전성

키워드 전용 매개변수는 함수를 나중에 수정할 때도 도움이 됩니다.

여러 곳에서 사용하는 함수에 매개변수를 추가하거나 순서를 바꾸면, 위치 인자로 호출하던 기존 코드의 의미가 바뀔 수 있습니다.


# 1.0 버전
# def save(path, data, overwrite=False):

# 2.0 버전 : encoding 매개변수를 중간에 추가
def save(path, data, encoding="utf-8", overwrite=False):
    print(path, data, encoding, overwrite)


save("a.txt", "Hello", True)
결과
a.txt Hello True False

1.0 버전에서 save("a.txt", "Hello", True)는 “덮어쓰기”를 의미하는 호출이었습니다.

2.0 버전에서 encoding 매개변수를 overwrite 앞에 추가하자, 같은 호출의 True가 encoding에 할당되었습니다.

결과처럼 overwrite는 False가 되고 encoding은 True가 되지만, 오류는 발생하지 않습니다. 기존 코드를 하나도 고치지 않았는데 동작이 바뀐 것입니다.


옵션을 처음부터 def save(path, data, *, overwrite=False)처럼 키워드 전용으로 정의했다면, 호출 코드는 반드시 overwrite=True로 작성되어 있습니다.

이 경우 encoding을 어느 위치에 추가하든, 키워드 전용 매개변수의 순서를 바꾸든 기존 호출 코드는 그대로 같은 의미로 동작합니다.

그러므로, 다른 사람이 사용하는 함수나 라이브러리를 만들 때는 옵션 매개변수를 키워드 전용으로 정의해 두는 것이 좋습니다.



위치 전용 매개변수와 함께 사용

def func(name, /, value1, *, value2):
    total = value1 + value2
    print(name + "는", total, "입니다.")


func("Plus", 2, value2=3)
func("Plus", value1=2, value2=3)
결과
Plus는 5 입니다.
Plus는 5 입니다.

Python 3.8 이상에서는 슬래시(/)로 위치 전용 매개변수(Positional-Only Parameter)도 정의할 수 있습니다.

/ 앞의 name은 위치로만, /와 * 사이의 value1은 위치나 키워드로, * 뒤의 value2는 키워드로만 전달할 수 있습니다.

결과처럼 value1은 위치(2)로 전달해도, 키워드(value1=2)로 전달해도 같은 결과가 됩니다.


def func(name, *args, value):
    print(name, args, value)


func("Plus", 1, 2, value=3)
결과
Plus (1, 2) 3

*args를 사용하는 경우, *args 자체가 구분자 역할을 하므로 *args 뒤의 매개변수는 별도의 * 없이도 키워드 전용 매개변수가 됩니다.

*args가 남은 위치 인자 1, 2를 모두 가져가므로, value에는 키워드로만 값을 전달할 수 있습니다.


import inspect


def func(a, b=2, /, c=3, *args, d, e=5, **kwargs):
    pass


for name, param in inspect.signature(func).parameters.items():
    print(name, param.kind.name)
결과
a POSITIONAL_ONLY
b POSITIONAL_ONLY
c POSITIONAL_OR_KEYWORD
args VAR_POSITIONAL
d KEYWORD_ONLY
e KEYWORD_ONLY
kwargs VAR_KEYWORD

inspect.signature(함수).parameters로 각 매개변수가 어떤 방식으로 값을 받는지 확인할 수 있습니다.

/ 앞의 a, b는 POSITIONAL_ONLY(위치 전용), c는 POSITIONAL_OR_KEYWORD(위치 또는 키워드), *args 뒤의 d, e는 KEYWORD_ONLY(키워드 전용)입니다.

*args와 **kwargs는 각각 VAR_POSITIONAL, VAR_KEYWORD로 표시됩니다.

  • Tip : 위치 전용 매개변수에 대한 자세한 설명은 제 32강에서 확인할 수 있습니다.



정리

형태 설명 특징
def func(a, *, b) * 뒤의 b는 키워드 전용 func(1, b=2)로만 호출
def func(*, a, b) 모든 매개변수가 키워드 전용 위치 인자를 받지 않음
def func(*args, b) *args 뒤의 b는 키워드 전용 * 없이도 키워드 전용
def func(a, /, b, *, c) 위치 전용, 일반, 키워드 전용 혼합 위치 전용은 3.8 이상
기본값 *, b=1, c 키워드 전용은 기본값 순서 제약 없음
위치로 전달 시 takes N positional arguments TypeError
생략 시 missing required keyword-only argument TypeError
@dataclass(kw_only=True) 생성자를 키워드 전용으로 3.10 이상


키워드 전용 매개변수는 호출하는 코드에 값의 의미가 드러나게 하고, 자료형이 같은 인자의 순서 실수를 오류로 바꿔 줍니다.

또한, 함수에 매개변수를 추가하거나 순서를 바꿔도 기존 호출 코드의 의미가 바뀌지 않으므로, 오래 유지하는 함수일수록 안전합니다.

sorted의 key, reverse나 json.dumps의 옵션처럼, 필수 값은 위치로 받고 동작을 바꾸는 옵션은 키워드 전용으로 정의하는 것이 일반적인 설계 방식입니다.

댓글 남기기