PEP 3141 – 숫자를 위한 타입 계층
- Author:
- Jeffrey Yasskin <jyasskin at google.com>
- Status:
- Final
- Type:
- Standards Track
- Created:
- 23-Apr-2007
- Python-Version:
- 3.0
- Post-History:
- 25-Apr-2007, 16-May-2007, 02-Aug-2007
Table of Contents
번역·라이선스 안내
이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판
초록
이 제안은 숫자와 유사한 클래스를 표현하기 위해 추상 베이스 클래스(ABC)(PEP 3119)의 계층을 정의합니다. 이 제안은 Number :> Complex :> Real :> Rational :> Integral 계층을 제안하며, 여기서 A :> B는 “A가 B의 상위 타입”임을 의미합니다. 이 계층은 Scheme의 수치 탑(numeric tower) [2]에서 영감을 받았습니다.
근거
숫자를 인자로 받는 함수는 그 숫자의 속성을 판별할 수 있어야 하며, 타입 기반 오버로딩이 언어에 추가된다면 인자의 타입에 따라 오버로딩할 수 있어야 합니다. 예를 들어, 슬라이싱은 인자가 Integrals여야 하고, math 모듈의 함수들은 인자가 Real이어야 합니다.
명세
이 PEP는 추상 베이스 클래스의 집합을 명시하고, 일부 메서드를 구현하기 위한 일반적인 전략을 제안합니다. 이 PEP는 PEP 3119의 용어를 사용하지만, 이 계층은 클래스 집합을 정의하는 어떤 체계적인 방법에서도 의미가 있도록 의도되었습니다.
표준 라이브러리의 타입 검사는 구체적인 내장 타입 대신 이 클래스들을 사용해야 합니다.
수치 클래스
사람들이 기대하는 숫자의 종류를 쉽게 모호하게 둘 수 있도록 Number 클래스부터 시작합니다. 이 클래스는 오버로딩에만 도움이 되며, 어떤 연산도 제공하지 않습니다.
class Number(metaclass=ABCMeta): pass
복소수 구현 대부분은 해시 가능하겠지만, 그 점에 의존해야 한다면 명시적으로 확인해야 합니다. 이 계층은 가변 숫자도 지원하기 때문입니다.
class Complex(Number):
"""Complex defines the operations that work on the builtin complex type.
In short, those are: conversion to complex, bool(), .real, .imag,
+, -, *, /, **, abs(), .conjugate(), ==, and !=.
If it is given heterogeneous arguments, and doesn't have special
knowledge about them, it should fall back to the builtin complex
type as described below.
"""
@abstractmethod
def __complex__(self):
"""Return a builtin complex instance."""
def __bool__(self):
"""True if self != 0."""
return self != 0
@abstractproperty
def real(self):
"""Retrieve the real component of this number.
This should subclass Real.
"""
raise NotImplementedError
@abstractproperty
def imag(self):
"""Retrieve the imaginary component of this number.
This should subclass Real.
"""
raise NotImplementedError
@abstractmethod
def __add__(self, other):
raise NotImplementedError
@abstractmethod
def __radd__(self, other):
raise NotImplementedError
@abstractmethod
def __neg__(self):
raise NotImplementedError
def __pos__(self):
"""Coerces self to whatever class defines the method."""
raise NotImplementedError
def __sub__(self, other):
return self + -other
def __rsub__(self, other):
return -self + other
@abstractmethod
def __mul__(self, other):
raise NotImplementedError
@abstractmethod
def __rmul__(self, other):
raise NotImplementedError
@abstractmethod
def __div__(self, other):
"""a/b; should promote to float or complex when necessary."""
raise NotImplementedError
@abstractmethod
def __rdiv__(self, other):
raise NotImplementedError
@abstractmethod
def __pow__(self, exponent):
"""a**b; should promote to float or complex when necessary."""
raise NotImplementedError
@abstractmethod
def __rpow__(self, base):
raise NotImplementedError
@abstractmethod
def __abs__(self):
"""Returns the Real distance from 0."""
raise NotImplementedError
@abstractmethod
def conjugate(self):
"""(x+y*i).conjugate() returns (x-y*i)."""
raise NotImplementedError
@abstractmethod
def __eq__(self, other):
raise NotImplementedError
# __ne__ is inherited from object and negates whatever __eq__ does.
Real ABC는 값이 실수 직선 위에 있음을 나타내며, float 내장 타입의 연산을 지원합니다. 실수는 NaN(이 PEP에서는 기본적으로 무시합니다)을 제외하면 전순서를 이룹니다.
class Real(Complex):
"""To Complex, Real adds the operations that work on real numbers.
In short, those are: conversion to float, trunc(), math.floor(),
math.ceil(), round(), divmod(), //, %, <, <=, >, and >=.
Real also provides defaults for some of the derived operations.
"""
# XXX What to do about the __int__ implementation that's
# currently present on float? Get rid of it?
@abstractmethod
def __float__(self):
"""Any Real can be converted to a native float object."""
raise NotImplementedError
@abstractmethod
def __trunc__(self):
"""Truncates self to an Integral.
Returns an Integral i such that:
* i>=0 iff self>0;
* abs(i) <= abs(self);
* for any Integral j satisfying the first two conditions,
abs(i) >= abs(j) [i.e. i has "maximal" abs among those].
i.e. "truncate towards 0".
"""
raise NotImplementedError
@abstractmethod
def __floor__(self):
"""Finds the greatest Integral <= self."""
raise NotImplementedError
@abstractmethod
def __ceil__(self):
"""Finds the least Integral >= self."""
raise NotImplementedError
@abstractmethod
def __round__(self, ndigits:Integral=None):
"""Rounds self to ndigits decimal places, defaulting to 0.
If ndigits is omitted or None, returns an Integral,
otherwise returns a Real, preferably of the same type as
self. Types may choose which direction to round half. For
example, float rounds half toward even.
"""
raise NotImplementedError
def __divmod__(self, other):
"""The pair (self // other, self % other).
Sometimes this can be computed faster than the pair of
operations.
"""
return (self // other, self % other)
def __rdivmod__(self, other):
"""The pair (self // other, self % other).
Sometimes this can be computed faster than the pair of
operations.
"""
return (other // self, other % self)
@abstractmethod
def __floordiv__(self, other):
"""The floor() of self/other. Integral."""
raise NotImplementedError
@abstractmethod
def __rfloordiv__(self, other):
"""The floor() of other/self."""
raise NotImplementedError
@abstractmethod
def __mod__(self, other):
"""self % other
See
https://mail.python.org/pipermail/python-3000/2006-May/001735.html
and consider using "self/other - trunc(self/other)"
instead if you're worried about round-off errors.
"""
raise NotImplementedError
@abstractmethod
def __rmod__(self, other):
"""other % self"""
raise NotImplementedError
@abstractmethod
def __lt__(self, other):
"""< on Reals defines a total ordering, except perhaps for NaN."""
raise NotImplementedError
@abstractmethod
def __le__(self, other):
raise NotImplementedError
# __gt__ and __ge__ are automatically done by reversing the arguments.
# (But __le__ is not computed as the opposite of __gt__!)
# Concrete implementations of Complex abstract methods.
# Subclasses may override these, but don't have to.
def __complex__(self):
return complex(float(self))
@property
def real(self):
return +self
@property
def imag(self):
return 0
def conjugate(self):
"""Conjugate is a no-op for Reals."""
return +self
Demo/classes/Rat.py를 정리하여 표준 라이브러리의 rational.py로 승격해야 합니다. 그러면 그것이 Rational ABC를 구현하게 됩니다.
class Rational(Real, Exact):
""".numerator and .denominator should be in lowest terms."""
@abstractproperty
def numerator(self):
raise NotImplementedError
@abstractproperty
def denominator(self):
raise NotImplementedError
# Concrete implementation of Real's conversion to float.
# (This invokes Integer.__div__().)
def __float__(self):
return self.numerator / self.denominator
마지막으로 정수:
class Integral(Rational):
"""Integral adds a conversion to int and the bit-string operations."""
@abstractmethod
def __int__(self):
raise NotImplementedError
def __index__(self):
"""__index__() exists because float has __int__()."""
return int(self)
def __lshift__(self, other):
return int(self) << int(other)
def __rlshift__(self, other):
return int(other) << int(self)
def __rshift__(self, other):
return int(self) >> int(other)
def __rrshift__(self, other):
return int(other) >> int(self)
def __and__(self, other):
return int(self) & int(other)
def __rand__(self, other):
return int(other) & int(self)
def __xor__(self, other):
return int(self) ^ int(other)
def __rxor__(self, other):
return int(other) ^ int(self)
def __or__(self, other):
return int(self) | int(other)
def __ror__(self, other):
return int(other) | int(self)
def __invert__(self):
return ~int(self)
# Concrete implementations of Rational and Real abstract methods.
def __float__(self):
"""float(self) == float(int(self))"""
return float(int(self))
@property
def numerator(self):
"""Integers are their own numerators."""
return +self
@property
def denominator(self):
"""Integers have a denominator of 1."""
return 1
연산과 __magic__ 메서드의 변경 사항
float에서 int로(더 일반적으로는 Real에서 Integral로) 더 정밀하게 좁히는 변환을 지원하기 위해, 해당 라이브러리 함수에서 호출될 다음과 같은 새로운 __magic__ 메서드를 제안합니다. 이 메서드들은 모두 Real이 아닌 Integral을 반환합니다.
__trunc__(self)는 새 내장 함수trunc(x)에서 호출되며, 0과x사이에서x에 가장 가까운 Integral을 반환합니다.__floor__(self)는math.floor(x)에서 호출되며,<= x인 가장 큰 Integral을 반환합니다.__ceil__(self)는math.ceil(x)에서 호출되며,>= x인 가장 작은 Integral을 반환합니다.__round__(self)는round(x)에서 호출되며,x에 가장 가까운 Integral을 반환하고, 정확히 절반인 경우는 타입이 정하는 방식으로 반올림합니다.float는 3.0에서 절반인 경우 짝수 쪽으로 반올림하도록 변경됩니다. 인자 2개를 받는 버전인__round__(self, ndigits)도 있으며, 이는round(x, ndigits)에서 호출되고 Real을 반환해야 합니다.
2.6에서 math.floor, math.ceil, round는 계속 float를 반환합니다.
float가 구현하는 int() 변환은 trunc()와 동등합니다. 일반적으로 int() 변환은 먼저 __int__()를 시도하고, 그것이 없으면 __trunc__()를 시도해야 합니다.
complex.__{divmod,mod,floordiv,int,float}__도 사라집니다. 혼란스러워하는 포팅 작업자를 돕기 위해 친절한 오류 메시지를 제공하면 좋겠지만, help(complex)에 나타나지 않는 것이 더 중요합니다.
타입 구현자를 위한 참고 사항
구현자는 같은 수가 같다고 비교되고 같은 값으로 해시되도록 주의해야 합니다. 실수를 확장한 서로 다른 두 가지 방식이 있다면 이 문제는 까다로울 수 있습니다. 예를 들어, 복소수 타입은 해시 함수 hash()를 다음과 같이 구현하는 것이 합리적일 수 있습니다:
def __hash__(self):
return hash(complex(self))
다만 내장 complex의 범위나 정밀도를 벗어나는 값에는 주의해야 합니다.
수치 ABC 더 추가하기
물론 수를 위한 ABC는 이 밖에도 더 있을 수 있으며, 그런 ABC를 추가할 가능성을 막는다면 좋지 않은 계층 구조일 것입니다. 다음과 같이 Complex와 Real 사이에 MyFoo를 추가할 수 있습니다:
class MyFoo(Complex): ...
MyFoo.register(Real)
산술 연산 구현하기
혼합 모드 연산이 두 인자의 타입을 모두 알고 작성된 구현을 호출하거나, 두 인자를 가장 가까운 내장 타입으로 변환한 뒤 그 타입에서 연산을 수행하도록 산술 연산을 구현하고자 합니다. Integral의 서브타입이라면 __add__와 __radd__를 다음과 같이 정의해야 한다는 뜻입니다:
class MyIntegral(Integral):
def __add__(self, other):
if isinstance(other, MyIntegral):
return do_my_adding_stuff(self, other)
elif isinstance(other, OtherTypeIKnowAbout):
return do_my_other_adding_stuff(self, other)
else:
return NotImplemented
def __radd__(self, other):
if isinstance(other, MyIntegral):
return do_my_adding_stuff(other, self)
elif isinstance(other, OtherTypeIKnowAbout):
return do_my_other_adding_stuff(other, self)
elif isinstance(other, Integral):
return int(other) + int(self)
elif isinstance(other, Real):
return float(other) + float(self)
elif isinstance(other, Complex):
return complex(other) + complex(self)
else:
return NotImplemented
Complex의 서브클래스 사이의 혼합 타입 연산에는 5가지 경우가 있습니다. 위 코드 중 MyIntegral과 OtherTypeIKnowAbout을 참조하지 않는 부분은 모두 “보일러플레이트”라고 부르겠습니다. a는 Complex의 서브타입인 A의 인스턴스이고(a : A <: Complex), b : B <: Complex입니다. a + b를 살펴보겠습니다:
- A가 b를 받아들이는 __add__를 정의한다면 아무 문제가 없습니다.
- A가 보일러플레이트 코드로 넘어간 상황에서 __add__가 값을 반환한다면, B가 더 똑똑한 __radd__를 정의했을 가능성을 놓치게 됩니다. 따라서 보일러플레이트는 __add__에서 NotImplemented를 반환해야 합니다. (또는 A가 __add__를 아예 구현하지 않을 수도 있습니다.)
- 그러면 B의 __radd__에 기회가 돌아갑니다. 이 메서드가 a를 받아들인다면 아무 문제가 없습니다.
- 이 메서드도 보일러플레이트로 넘어간다면 더 이상 시도할 메서드가 없으므로, 기본 구현은 바로 여기에 있어야 합니다.
- B <: A라면 Python은 A.__add__보다 B.__radd__를 먼저 시도합니다. B.__radd__는 A를 알고 구현되었으므로 Complex에 위임하기 전에 A의 인스턴스를 처리할 수 있습니다. 따라서 이 동작은 문제가 없습니다.
A<:Complex이고 B<:Real이며 그 밖에 공유하는 정보가 없다면, 적절한 공통 연산은 내장 complex를 사용하는 연산입니다. 두 __radd__ 모두 그 연산으로 귀결되므로 a+b == b+a가 성립합니다.
기각된 대안
이 PEP의 초기 버전은 Haskell Numeric Prelude [1]에서 영감을 받아 MonoidUnderPlus, AdditiveGroup, Ring, Field(필드)를 포함하는 대수적 계층 구조를 정의했으며, 수에 이르기 전에 가능한 다른 여러 대수적 타입도 언급했습니다. 이 구조가 벡터와 행렬을 다루는 사람들에게 유용할 것으로 기대했지만 NumPy 커뮤니티는 별다른 관심을 보이지 않았습니다. 또한 x가 X <: MonoidUnderPlus의 인스턴스이고 y가 Y <: MonoidUnderPlus의 인스턴스이더라도 x + y가 여전히 말이 되지 않을 수 있다는 문제에 부딪혔습니다.
그다음에는 가우스 정수나 Z/nZ처럼 Complex일 수는 있지만 나눗셈 같은 연산을 반드시 지원하지는 않는 것들까지 포함하도록 수에 훨씬 더 많이 분기하는 구조를 부여했습니다. 커뮤니티는 이것이 Python에는 지나치게 복잡하다고 판단했고, 그래서 지금은 Scheme의 수치 타워와 훨씬 더 비슷해지도록 제안의 범위를 줄였습니다.
Decimal 타입
Decimal 타입의 작성자들과 협의한 결과, 현재로서는 이 타입을 수치 타워에 포함하지 않기로 결정했습니다.
참고 문헌
- Possible Python 3K Class Tree?, Bill Janssen의 위키 페이지
- (https://wiki.python.org/moin/AbstractBaseClasses)
감사의 글
애초에 이 PEP를 쓰도록 격려해 준 Neal Norwitz, numpy 사람들은 대수적 개념에 별로 관심이 없다는 점을 지적해 준 Travis Oliphant, Scheme이 이미 이 작업을 했다는 사실을 일깨워 준 Alan Isaac, 그리고 개념을 다듬어 준 Guido van Rossum과 메일링 리스트의 많은 분께 감사드립니다.
Copyright
This document has been placed in the public domain.