본문 바로가기
Computer Science/Python

충격!) 파이썬에서 오버로딩(overloading)이 가능하다??

by invrtd.h 2023. 2. 4.

 파이썬은 원래 오버로딩이 안 되는 언어라고 알려져 있었다. 왜냐하면 오버로딩을 하려면 타입이 필요한데 파이썬의 각 변수들은 타입이 지정되어 있지 않기 때문이다. 3.4 이후로 typing 모듈이 생기면서 파이썬에서도 타입 지정이라는 개념이 생기기는 했지만 여전히 optional 요소일 뿐이다. 그런데... 언제부터인지는 모르겠는데, 파이썬에도 타입에 따른 오버로딩이라는 개념이 생겼다.

 

 예제 코드는 다음과 같이 생겼다.

import functools as ft


@ft.singledispatch
def hello(arg) -> None:
    print("arg type not implemented")


@hello.register
def _(arg: int) -> None:
    print("Integer: {}".format(arg))


@hello.register
def _(arg: float) -> None:
    print("Floating number: {}".format(arg))


if __name__ == '__main__':
    hello(3) 		# 2번째 함수 실행
    hello(6.5) 		# 3번째 함수 실행
    hello("String") 	# 1번째 함수 실행

 

 이 개념은 파이썬 언어가 자체적으로 지원하는 기능이 아니라, 파이썬 라이브러리가 기존에 있는 문법들을 이리저리 잘 조합해서 다른 언어의 오버로딩과 비슷하게 보이게 만들 수 있도록 만들어 놓은 것이다. 그래서 @ 문법을 쓰는데, @ 문법은 오버로딩을 위해 특별하게 준비한 문법이 아니라 그냥 데코레이터 문법이다. 다음은 데코레이터 예시다.

 

import time


class Watch:
    def __init__(self, f):
        self._f = f

    def __call__(self, *args, **kwargs):
        begin = time.time()
        ret = self._f(*args, **kwargs)
        print(time.time() - begin)
        return ret
        

@Watch
def foo(x):
    s = [i ** 2 for i in range(x)]
    return 0
    
    
if __name__ == '__main__':
    foo(10000000)
    

# 출력 결과 : 0.44437408447265625

이때 foo 함수는 어떻게 아무런 지시도 없었는데 알아서 자기의 수행 시간을 출력한 것일까? 비법은 @ 기호에 있다. 이 기호가 def문 위에 적용되면 파이썬 인터프리터는 이것을 다음의 의미로 해석한다.

foo = Watch(foo)

 그렇기에 foo의 타입도, 일반 파이썬 함수 타입이 아니라 Watch 타입이라고 나온다.

# <__main__.Watch object at 0x101689b50>

 따라서 foo(10000000) 구문은 사실 foo 함수 자체를 호출하지는 않고, Watch 객체에 정의된 __call__(***) 멤버 함수를 호출한다. 이 함수는 먼저 time.time() 구문으로 timestamp를 찍은 뒤, foo 함수를 호출하고, 수행이 끝나고 나면 시작 시간과 끝 시간을 비교해 수행 시간을 계산한 뒤 출력한다.

 

 데코레이터의 개념을 알면 위의 오버로딩 구문이 정확히 무슨 일을 하는지를 파악할 수 있다. @ft.singledispatch는 인자로 어떤 함수를 받아 그 함수에 추가 정보를 붙인 뒤 그 객체를 리턴한다. 이 객체는 .register(***) 멤버 함수를 지원한다. 그래서 @hello.register이 유효한 구문이 된다. 결국

@hello.register
def _(arg: int) -> None:
    print("Integer: {}".format(arg))

 다음 코드는

def _(arg: int) -> None:
    print("Integer: {}".format(arg))
    
    
_ = hello.register(_)

 이 코드와 완벽하게 동일한 코드이기 때문이다.

 

 register는 말 그대로 등록이라는 뜻이라, hello라는 이름을 가진 객체에 어떤 함수를 하나 등록시켜 준다. 어차피 hello에 등록된 이후에는 불릴 일이 없는 함수가 굳이 이름을 가질 필요가 없기 때문에, 이름을 _로 지정해 준다. 슬픈 사실은 다음 코드가 유효하다는 것이다.

# ... 중간 생략

@hello.register
def _(arg: float) -> None:
    print("Floating number: {}".format(arg))


if __name__ == '__main__':
    hello(3)
    hello(6.5)
    hello("String")
    _("What")

 즉 _를 함수처럼 쓸 수 있다. 이는 한 번 사용하고 버려졌던 _라는 이름이 사실 버려지지 않고 name environment에 남아 있기 때문이다... 파이썬 문법의 한계상 어쩔 수 없다고 생각하고 버티자.

 

 어쨌든 hello 객체에는 많은 함수들이 등록되어 있을 것이다. hello 객체는 등록된 함수들이 인자로 어떤 타입을 받는지를 어떻게 알 수 있을까? 정답은 f.__annotations__ 어트리뷰트에 있다. Python 3.4부터 파이썬에 typing 문법이 새로 생겼는데, 이것을 쓰는 방법은 다들 잘 알 것이다.

def f(x: int, y: float) -> str:
    pass

 이렇게 선언을 하면, 인터프리터는 함수 f가 갖고 있는 __annotations__ 어트리뷰트에 접근해, x 인자가 int를 받고 y 인자가 float를 받는다는 사실, 그리고 리턴값이 str이라는 사실을 저장해준다. 인터프리터는 이것 말고는 아무것도 하지 않는다. 그러나 다른 프로그램은 __annotations__ 어트리뷰트에 접근해, 타입이 안 맞으면 예외를 던진다거나 하는 식으로 더 안전한 프로그래밍을 할 수 있다. 파이썬 프로그램을 짜는 우리도 __annotations__에 접근할 수 있다! 어쨌든, hello 객체는 저장된 함수들 중 적절한 __annotations__을 갖고 있는 함수를 뽑아 그 함수를 호출한다. 매칭할 만한 함수가 없다면 맨 위에 있는 함수가 디폴트로 작용해 그것을 실행한다. 만약 지정된 타입 이외의 타입을 받고 싶지 않다면, 디폴트 함수에서 NotImplementedError나 TypeError를 던져 주면 될 것 같다.

공식 문서 예제

 

https://docs.python.org/ko/3/library/functools.html?highlight=single 

 

functools — Higher-order functions and operations on callable objects

Source code: Lib/functools.py The functools module is for higher-order functions: functions that act on or return other functions. In general, any callable object can be treated as a function for t...

docs.python.org

공식 문서 링크

'Computer Science > Python' 카테고리의 다른 글

Python 3.12 업데이트! 유용한 기능 살펴보기  (0) 2023.10.11

댓글