Python

*args, **kwargs

둔진 2020. 6. 23. 05:08

*args, **kwargs

Python 코드를 보다보면 함수에 *args, **kwargs와 같은 표현을 볼 수 있습니다. 가변 인자들인데요. 가변 인자라는 말부터가 어렵습니다. 함수에 전달한 인자들이 딱 고정되어 있지 않고, 상황에 따라 바뀔 때를 지원하기 위해서 사용하는데요. 예를 들어서 한번 살펴보겠습니다.

positional arguments, keyword arguemnts

본격적으로 *args, **kwargs에 대해서 이야기하기 전에 positional arguments와 keyword arguments를 먼저 다루어보겠습니다. 예를 들어 아래와 같은 함수가 있다고 가정해보겠습니다.

def f(a, b, c):
    print(a, b, c)

아래와 같이 일반적으로 호출을 할 수 있습니다.

f(1, 2, 3)

순서대로 a=1, b=2, c=3으로 함수에 전달이 됩니다. 이렇게 인자의 위치에 따라 값을 정하기 때문에 positional arguments라고 부릅니다.

위의 함수를 아래와 같이 호출할 수도 있습니다.

f(a=1, b=2, c=3)

앞의 예시와 같지만 명시적으로 a=1, b=2, c=3이라고 지정했습니다. 인자의 이름(keyword)을 명시했기 때문에 keyword arguments라고 부릅니다.

물론 이 두가지를 섞어서 쓸 수도 있습니다.

f(1, 2, c=3)

keyword arguments의 장점은 위치를 따지지 않기 때문에 아래와 같이 순서를 바꿀 수도 있다는 점입니다.

f(1, c=3, b=2)

하지만 항상 keyword arguments는 positional arguments 다음에 나와야합니다. 아래와 같이 하면 SyntaxError: positional argument follows keyword argument라는 에러가 발생합니다.

f(a=1, 2, 3)

개인적으로는 keyword arguments를 positional arguments보다 선호합니다. positional arguments는 표현이 간략하기는 하지만 코드를 읽거나 디버깅할 때 각 인자의 의미를 알기가 어렵습니다. 아주 흔한 소프트웨어 버그 중 하나가 바로 positional arguments의 순서를 헷갈려서 잘못 호출한 경우입니다. 요즘은 IDE가 워낙 좋아서 코드를 쓸 때 힌트를 많이 주기는 하지만 여전히 위험한 것은 사실입니다.

parameters, arguments

함수의 인자를 이야기할 때 parameters 또는 arguments라고 영어로 이야기하는데요. 둘의 차이는 무엇일까요? parameters는 함수를 선언할 때 사용한 것들이고, arguments는 실제로 이 함수를 호출할 때 사용하는 것들입니다.

def f(a, b, c):
    print(a, b, c)

이 경우에 a, b, c는 함수 f의 parameters입니다.

f(1, 2, 3)

이 경우에 1, 2, 3을 f에 arguments로 전달하고 있습니다. parameters와 arguments를 구분해야할 때도 있지만 대부분은 혼용해서 사용합니다.

*args

다시 *args, **kwargs 이야기로 돌아오겠습니다. *args는 미리 정해지지 않은 positional arguments를 받을 때 사용합니다.

def f_args(name, title, *args):
    print('Hi {} {}'.format(title, name))
    for arg in args:
        print(arg)

f_args('Ceongjeein', 'Mr.', 'Guitar', 180)

위 함수는 "nametitle은 꼭 필요하지만 나머지 인자는 선택적으로 받을게"라는 의미입니다. f_args('Ceongjeein', 'Mr.', 'Guitar', 180)라고 호출하면 name'Ceongjeein'이 되고, title'Mr.'가 됩니다. 하지만 'Guitar', 180은 갈 자리가 없기 때문에 list 형태로 args에 전달됩니다. args는 일반적인 parameter이기 때문에 아무 이름이나 사용할 수 있습니다.

만약에 *args가 없다면 어떻게 될까요?

def f_args(name, title):
    print('Hi {} {}'.format(title, name))

f_args('Ceongjeein', 'Mr.', 'Guitar', 180)

TypeError: f_args() takes 2 positional arguments but 4 were given 라는 에러가 발생합니다. "난 positional arguments 2개만 받을 건데 왜 4개 줬니?" 이런 의미입니다.

호출 시에 사용하는 *

헷갈려지기 시작하는 부분은 이 *를 호출시에도 사용할 수 있다는 점입니다. 함수 정의시에 사용한 *가 넘치는 인자를 하나의 list로 모으는 역할을 했다면, 함수 호출시에 *는 list를 풀어서 positional arguments로 전달하는 역할을 합니다. 설명보다는 예시를 보겠습니다.

def f(a, b, c):
    print(a, b, c)

value = [1, 2, 3]
f(*value)

위의 예시는 f(1, 2, 3)과 같습니다. 호출 시에 사용한 *value라는 표현이 list의 값을 순서대로 함수의 인자로 전달합니다. 물론 f(value[0], value[1], value[2])라고 해도 같은 표현입니다. 하지만 *value가 더 간략하기도 하고, 무엇보다 value의 길이를 모를 때는 더 유용합니다.

함수 정의시에 사용하는 *와 호출 시에 사용하는 *는 다른 의미이기 때문에 두가지를 같이 사용할 수도 있습니다.

def f_args(name, title, *args):
    print('Hi {} {}'.format(title, name))
    for arg in args:
        print(arg)

info = ['Ceongjeein', 'Mr.', 'Guitar', 180]
f_args(*info)

개인적으로는 함수 정의 시에 parameter에 사용하는 *와 호출 시에 argument에 사용하는 *가 (비슷하지만) 다른 역할을 하는데도 같은 기호라는 것이 혼동스럽다고 생각합니다 (이 문장도 혼동스럽습니다). 간결한 표현이라는 의견도 있지만, 전 간결함보다는 명료함이 더 중요하다고 생각합니다.

**kwargs

*args가 가변적인 positional arguments를 담기 위해서 쓰인다면, **kwargs는 가변적인 keyword arguments를 담기 위해서 쓰입니다. kwargskw는 keyword를 의미합니다. 물론 kwargs도 일반적인 변수이기 때문에 아무 이름이나 사용할 수 있습니다.

def f_kwargs(name, title, **kwargs):
    print('Hi {} {}'.format(title, name))

    for key, value in kwargs.items():
        print('{} -> {}'.format(key, value))        

    if 'blood_type' in kwargs:
        print(kwargs['blood_type'])

f_kwargs('Ceongjeein', 'Mr.', blood_type='A', city='Seoul')

*args 설명에서 사용한 예시와 비슷합니다. *args에서는 넘치는 인자가 list로 전달되었는데, **kwargs에서는 넘치는 인자가 dict로 전달됩니다. keyword arguments이기 때문에 이름과 값이 모두 필요하기 때문입니다.

*args에서 처럼 **kwargs가 없다면 에러가 발생합니다.

def f_kwargs(name, title):
    print('Hi {} {}'.format(title, name))

f_kwargs('Ceongjeein', 'Mr.', blood_type='A', city='Seoul')

위와 같은 코드에서는 f_kwargs() got an unexpected keyword argument 'blood_type'라고하면서 blood_type은 함수가 모르는 존재라고 불평합니다.

호출 시에 사용하는 **

함수 호출 시에 *를 사용하면 list를 positional arguments로 풀어서 전달할 수 있었던 것처럼, **를 사용하면 dict를 keyword arguments로 풀어서 전달할 수 있습니다.

more_info = {
    'blood_type': 'A',
    'city': 'Seoutl'
}

f_kwargs('Ceongjeein', 'Mr.', **more_info)

more_info dict의 key와 value를 각각 함수 인자의 이름과 값으로 치환해서 f_kwargs 함수를 호출합니다.

*args와 **kwargs 같이 사용하기

당연히 *args와 **kwargs를 같이 사용할 수 있습니다. 일반적임 경우처럼 positional arguments가 keyword arguments보다 먼저 나와야한다는 점만 유의하면 됩니다.

def f_args_kwargs(name, title, *args, **kwargs):
    for arg in args:
        print(arg)

    for key, value in kwargs.items():
        print('{} -> {}'.format(key, value))

f_args_kwargs('Ceongjeein', 'Mr.', 'Guitar', '180', blood_type='A', city='Seoul')

info = ['Ceongjeein', 'Mr.', 'Guitar', 180]
more_info = {
    'blood_type': 'A',
    'city': 'Seoutl'
}

f_args_kwargs('Ceongjeein', 'Mr.', *info, **more_info)

f_args_kwargs('Ceongjeein', 'Mr.', *info, blood_type='A', city='Seoul')

아래처럼 keyword arguments를 먼저 정의하면 SyntaxError가 발생합니다.

def f_args_kwargs(name, title, **kargs, *args):
    for arg in args:
        print(arg)

    for key, value in kwargs.items():
        print('{} -> {}'.format(key, value))

positional arguments를 제한하기 (keyword arguments를 강제하기)

앞에서도 잠깐 언급했지만 positional arguments 때문에 생기는 버그들이 꽤 많습니다. 특히 같은 타입의 인자를 여러 개 받거나, 인자 목록이 긴 경우에 이런 버그가 자주 생깁니다. 이를 해결하기 위해서는 가급적이면 keyword arguments를 사용하는 것이 좋습니다.

한편으로는 positional arguments를 잘 사용하면 간략하고 명확한 표현을 할 수 있습니다. 예를 들어 add(first=1, second=3) 보다는 add(1, 3)이 더 나은 표현입니다.

이 두가지의 절충안이 꼭 필요한 만큼만 positional arguments로 받아들이고 나머지는 강제로 keyword arguments로 쓰도록하는 방식입니다.

def f_keyword_only(name, title, *, blood_type=None, city=None):
    print('Hi {} {}'.format(title, name))

    if blood_type:
        print('Blood Type: ', blood_type)   

    if city:
        print('City: ', city)

f_keyword_only('Ceongjeein', 'Mr.', blood_type='A', city='Seoul')

name, title 뒤에 *을 볼 수 있습니다. nametitle까지만 positional arguments로 받겠다는 의미입니다. 아래와 같이 호출하면 f_keyword_only() takes 2 positional arguments but 3 positional arguments 에러가 발생합니다.

f_keyword_only('Ceongjeein', 'Mr.', 'A', city='Seoul')

name, title 외에는 강제로 keyword arguments를 사용해야 합니다. 이 방식을 적절히 잘 사용하면 positional arguments와 keyword arguments의 장점을 모두 취할 수 있습니다.

'Python' 카테고리의 다른 글

List Comprehension  (0) 2021.05.04
Jupyter Lab  (0) 2021.05.01
Python Reflection 맛보기  (0) 2020.06.09
pyc 파일에 대해서  (5) 2020.06.03
-m 실행 옵션과 __name__  (6) 2020.05.28