파이썬에서 제네릭 함수 정의하기 – singledispatch

어떤 하나의 함수 (혹은 겉으로 보기에 이름이 다른 여러 다른 함수)가 여러 타입의 인자를 받고, 인자의 타입에 따라 적절한 동작을 하는 함수를 제네릭 함수라고 한다. C++이나 Swift에 이런 제네릭 관련 기능이 언어 레벨에서 지원되는데, 사실 파이썬은 동적 타입 언어이기 때문에 언어수준의 명시적인 제네릭 지원 기능은 없다.1

파이썬의 제네릭

비록 파이썬이 동적 타입 언어이기는 하지만, 파이썬 내에는 제네릭과 비슷한 함수들이 있다. 기본 함수 중 len() 함수는 리스트, 집합, 사전, 문자열 등 타입에 대해 객체의 길이값을 구할 수 있다. 이 때 길이는 구하는 방법은 타입마다 다를 수 있기 때문에 len()이 실제로 동작하는 방식은 인자로 전달된 객체의 타입에 따라 달라져야 할 것이다.

사실 이처럼 파이썬은 아주 오래 전부터 내장함수들에 대해서 제네릭처럼 동작하는 방식을 사용해왔으나, 사용자가 순서 파이썬 문법만으로 제네릭 함수를 작성하여 사용하는 것은 매우 까다로웠다.

PEP443 – singledispatch

이 불편함에서 출발한 PEP443은 제네릭을 생성하는 아주 쉬운 표준 라이브러리 모듈을 제공하기 위한 기능을 정의한 제안이다. 이 제안의 최종안은 2013년에 표준 파이썬 구현에 포함되는 것이 확정되었고, 파이썬 3.4.3에서 functools 모듈2singledispatch가 도입되었다.

singledispatch는 단일 디스패치 제네릭을 구현하는 방법을 제공한다. singledispatch 자체는 사실 함수를 래핑해주는 함수이며, 래핑된 결과는 다시 인자의 타입이 특정 타입이 될 때 호출될 별도의 함수를 래핑하는 함수가 된다. 즉 데코레이터를 생성하는 데코레이터를 생성하는 데코레이터이다. 말은 복잡해 보이지만, 사용방법은 상당히 단순하다.

사용법

singledispatch를 통해서 제네릭 함수를 정의하는 방법은 다음과 같다.

  1. @singledispatch 데코레이터를 통해서 임의의 함수 a를 만든다. 이렇게 만들어진 a 자체는 타입이 정의되지 않은 함수로, 기본적으로 일반 파이썬 함수와 다르지 않다.
  2. 다시 @a.register(type)을 통해서 특정 파라미터 타입에 대한 함수를 정의할 수 있다 . 이때, 장식을 받은 함수의 이름을 어떤 것이어도 상관없고, 함수 a는 인자의 타입이 type과 동일한 경우에는 이곳에서 정의한 함수를 호출하게 된다.

예제를 보면 좀 더 명확하게 이해가 될 것이다.

from functools import singledispatch

@singledispatch
def fun(x):
    print("This is default behavior:", x)


@fun.register(int)
## x가 정수타입인 경우에는 fun은 아래 코드를 실행하게된다.
def _(x):
    print("I like Integer:", x)

@fun.register(str)
def _(x):
    print("string is strong:", x.upper())

@fun.register(float)
def _(x):
    print("floating number is beautifule:", x/2)

위 예에서 @singledispatch 데코레이터를 통해서 정의한 fun() 함수는 디폴트 동작으로 정의된다. 그리고 다시 @fun.register()를 통해서 특정한 타입의 변수가 들어오는 경우, 해당 타입에 대한 동작을 따로 등록하여 특정 타입인 경우의 동작을 별도로 정의한다. 이는 일종의 오버로드로도 볼 수 있으며, 동일 함수가 호출되었으나 타입에 따라서 다른 코드를 실행한다는 점에서 말 그대로 single dispatch generic이라고 표현한다.

>>> fun('apple')
string is strong: APPLE

>>> fun(12)
I like Integer: 12

>>> fun(12.0)
floating number is beautiful: 6.0

물론 새로 추가된지 4년 가까이 되었음에도 이런 기능을 소개하는 자료가 별로 없었던 걸 보면, 제네릭은 아주 특별한 경우에 필요한 일부 사람들에게 절실한 기능이었는지도 모른다. 언뜻 생각할 때 동적 언어에서 제네릭이 무슨 소용이 있나 싶은 생각도 들었지만, 제네릭을 이용하면 특정 타입 외부에 있는 함수가, 상이한 타입들에 대해서 같은 이름을 사용하는 방식을 통해서 인터페이스를 통일하고 깔끔한 구조를 가질 수 있게 해준다는 점에서 매우 의미있는 접근이라고 생각된다.


  1. 제네릭(Generic)은 함수나 다른 타입에 의존하는 타입을 정의할 때, 인자나 중첩된 타입의 인자에 구애받지 않고, 같은 이름의 함수를 인자 타입에 따라 반복적으로 정의하지 않아도 되도록 하는 정적 언어의 편의 장치라고 보면 된다. 그런데 동적 언어는 선언이나 정의의 시점에 타입은 거의 결정되지 않는다. 따라서 파이썬은 제네릭이라는 문법 장치를 도입할 필요가 없다.  
  2. reduce가 유배를 간 그 모듈이다.