예전엔 말이야

요즘,
대화 중에 옛날에 이런 게 있었다거나 이게 10년 전에는 이러했다거나 처음 이 일을 시작한 게 20년 전이라거나 또는 내 세대 때는 이러한 일이 있었는데 라는 표현을 자주 쓴다. 그런 표현을 무심코 내뱉다가 바로 아차!하며 표현을 바꾸려 하지만 이미 나간 말이라 거둘 방법이 없다. 꼰대스러워서 쓰지 않겠다고 매번 다짐하지만 어쩐지 갈수록 더 자주 쓰고 있다.

이야기 주제에 초점을 맞추면 그만이다. 묻지도 않은 지난 경험을 말하는 건 향신료로 요리에 넣지 않은 재료의 맛과 향을 흉내내는 것과 다를 바 없다. 이야기 주제에 자신없기 때문에 나오는 방어 심리이다.

겸손 여부는 문제 원인이 아니다. 난 아직 겸손하고 말고 할 수준도 못 될만큼 공부하고 경험하는 단계이다. 능력을 발휘해 돈을 벌고 누군가를 가르치기도 하지만, 내가 뛰어나서 그렇다기 보다는 현재 내 능력이 필요한 이들이 있어 기여하고, 내 경험과 지식 수준이 필요한 이들이 있어서 가르칠 수 있기 때문이라고 여긴다. 세상엔 다양성이 존재하니까.

고민한 끝에 몸과 마음에 여유가, 그리고 생활에 잉여가 부족하기 때문이라는 결론을 내렸다. 각 대상에 대해 많이 생각하지 못하고 경험도 부족하니, 대상의 본질에 대해 생각을 표현하지 못하고 자꾸 과거 경험치를 꺼내는 것이다.

더이상 꼰대스러운 행동이나 말투, 생각이 내 안에서 일어나는 걸 용납하지 않겠다. 건강과 여유보다 중요하지 않은 일을 줄여야겠다. 잉여를 되살려 경험과 관점을 깊고 풍부하게 만드는 데 쓰겠다.


Python 3에서 함수의 키워드 인자 강제와 주석문

Python 3에 도입된 함수 선언 문법 중 키워드 인자를 강제하는 방법과 주석문(annotation)이 있다. Python의 매력 요소 중 하나가 깔끔하고 명료한 코드라 생각하는데, 이 두 문법은 기호를 남발하는 코드처럼 보여서 좀 불만스럽지만 코드 문맥(context)을 읽는 데엔 참 유익하다. 그나마 $ 기호가 사용되는 건 아니라서 다행이랄까?! :)

위치 인자 개수 지정

Python은 함수 매개 인자 방식으로 위치 인자(positional argument)와 키워드 인자(keywords argument)를 지원한다. 위치 인자는 함수로 전달하는 매개 인자를 순서대로 나열하는 것이고, 키워드 인자는 인자 이름과 인자에 할당할 값을 특정하는 것이다.

def args_func(arg1, arg2, arg3):
    print(arg1, arg2, arg3)

args_func('hello', 'world', '!')
args_func('!', arg3='hello', arg2='world')
args_func('world', arg3='!', arg2='hello')
args_func('hello', '!', arg3='world')

위 코드에서

  • args_func('hello', 'world', '!')hello world !를 출력하고,
  • args_func('!', arg3='hello', arg2='world')! world hello를 출력한다.
  • args_func('world', arg3='!', arg2='hello')world hello !를 출력하며,
  • args_func('hello', '!', arg3='world')hello ! world

출력한다. 위치 인자, 키워드 인자 순서로 전달만 하면 어떤 인자를 위치 인자로 전달하고, 어떤 인자를 키워드 인자로 전달하는지에 별다른 제한은 없다.

Python 3는 키워드 인자를 강제하는 문법을 지원한다. 바로 * 문자를 쓰는 것이다1.

def args2_func(arg1, *, arg2, arg3):
    print(arg1, arg2, arg3)

args2_func('hello', 'world', '!')
args2_func('!', arg3='hello', arg2='world')
args2_func('world', arg3='!', arg2='hello')
args2_func('hello', '!', arg3='world')

* 이후에 나열된 매개 인자는 반드시 키워드 인자로 전달돼야 한다. 위 코드에서 args2_func 함수를 실행하는 네 개 실행 줄 중 첫 번째와 네 번째는 TypeError 예외가 일어난다.

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: args2_func() takes 1 positional argument but 3 were given

즉, args2_func 함수는 위치 인자를 1개 취하는데, 이 개수보다 많은 인자가 위치 인자로 전달되었다는 뜻이다.

그렇다면 * 이전에 나열된 매개 인자를 키워드 인자로 값을 전달하면 어떻게 될까?

args2_func(arg1='hello', arg2='world', arg3='!')

아무 문제도 발생하지 않는다. 즉, *는 위치 인자 개수를 특정하거나(exact) * 앞에 나열된 인자를 위치 인자로 강제하는 것이 아니라 * 이후에 나열되는 인자는 반드시 키워드 인자로 전달 받도록 강제하는 것이다. 만약 위치 인자를 단 한 개도 허용하지 않고자 한다면 다음과 같이 함수 매개 인자를 선언하면 된다.

def kwargs_func(*, arg1, arg2, arg3):
    print(arg1, arg2, arg3)

이 함수는 모든 매개 인자를 키워드 인자로 전달해야 한다.

주석문 (annotation)

annotation 문법은 함수 매개 인자와 반환 값에 대한 주석(annotation)을 지정하는 것이다2.

def anno_func(arg1: str, arg2: 'also str', arg3: 1 is True) -> bool:
    print(arg1, arg2, arg3)

표현된 코드를 보면 마치 인자의 형(type)을 지정하는 것 같지만, 실제로는 주석이기 때문에 인자의 형이 무엇이 되든 영향을 받지 않아서 다음과 같이 함수를 호출해도 아무 문제가 발생하지 않는다.

anno_func(1, True, 'world')

반환하는(return) 값의 형도 주석으로 설명한 것과 달라도 무방하다. anno_func은 주석으로 반환 값을 bool이라 명기했지만, 실제로는 return문이 따로 없기 때문에 None 값을 반환한다. 물론, 아무 문제도 없다.

이렇게 지정한 주석은 함수 객체에 __annotations__ 속성에 담겨 있으며, 사전형(dict) 객체이다.

print(anno_func.__annotations__)
print(anno_func.__annotations__['arg1'])

재밌는 점은 __annotations__에는 주석으로 지정한 값(value)이 그대로 할당되어 있다는 점이다. 이 점을 이용하면 함수 매개 인자를 특정할 수 있다.

def static_args_func(arg1: str, arg2: str, arg3: int) -> bool:
    args = locals()
    for _k, _v in args.items():
        arg_type = static_args_func.__annotations__[_k]

        if isinstance(_v, arg_type):
            continue

        raise TypeError(
            "The type of '{}' does not match '{}' type".format(
                _k, arg_type.__name__
            )
        )
    print(arg1, arg2, arg3)

static_args_func(1, 2, 3)

static_args_func 함수의 arg1, arg2 인자는 주석으로 str형을 명기했다. 그래서 사전형 속성인 __annotations__arg1키에는 명기한 값인 str이 할당되어 있다. __annotations__에 할당되어 있는 주석 값을 이용해 arg1과 같은 함수 매개 인자의 형을(type) 검사한 것이 if not isinstance(_v, arg_type): 부분이다.

이 함수를 static_args_func(1, 2, 3)와 같이 호출하면 arg1arg2에 대해 코드에서 지정한 TypeError 예외가 일어난다.

다음 코드는 가변 매개 인자도 형 검사를 한다. 더이상 형 검사를 하지 않는 위치부터 나머지 인자까지는 Ellipsis 형(...)을 썼다. 즉, 두 번째 인자까지는 형 검사를 하고, 이후 인자는 형 검사를 생략한다.

def type_checking_func(*args: (int, int, ...)):
    annotations = type_checking_func.__annotations__

    if (
        not isinstance(annotations, dict) or
        len(annotations) == 0
    ):
        return type_checking_func(*args)

    try:
        _check_index = annotations['args'].index(Ellipsis)
    except ValueError:
        _check_index = len(annotations) - 1

    for i, _v in enumerate(args[:_check_index]):
        arg_type = annotations['args'][i]

        if isinstance(_v, arg_type):
            continue

        raise TypeError(
            "The type of '{}' does not match '{}' type".format(
                _v, arg_type.__name__
            )
        )
    print(*args)

type_checking_func(1, 2, '3', 'a')

인자의 형을 검사하는 기능을 장식자(decorator)로 만들어서 여러 함수에 간편하게 사용하면 더 낫다.

def check_argument_type(func):
    def wrapper(*args):
        annotations = func.__annotations__
        if (
            not isinstance(annotations, dict) or
            len(annotations) == 0
        ):
            return func(*args)

        try:
            check_index = annotations['args'].index(Ellipsis)
        except ValueError:
            check_index = len(annotations['args']) - 1

        for _i, _v in enumerate(args[:check_index]):
            _arg_type = annotations['args'][_i]

            if isinstance(_v, _arg_type):
                continue

            raise TypeError(
                "The type of '{}' does not match '{}' type".format(
                    _v, _arg_type.__name__
                )
            )
        return func(*args)
    return wrapper

@check_argument_type
def hello_func(*args: (int, int, ...)):
    print(*args)

hello_func(1, 2, '3', 'a')

Python스러운 구현인 지 아닌 지 모르겠지만, 함수 매개 인자가 어떤 자료형으로 넘어올 지 몰라서 받는 스트레스는 줄어들 것 같다. :)