Following system colour scheme Selected dark colour scheme Selected light colour scheme

Python 개선 제안 한국어 번역

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을 반환합니다.

  1. __trunc__(self)는 새 내장 함수 trunc(x)에서 호출되며, 0과 x 사이에서 x에 가장 가까운 Integral을 반환합니다.
  2. __floor__(self)는 math.floor(x)에서 호출되며, <= x인 가장 큰 Integral을 반환합니다.
  3. __ceil__(self)는 math.ceil(x)에서 호출되며, >= x인 가장 작은 Integral을 반환합니다.
  4. __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를 살펴보겠습니다:

  1. A가 b를 받아들이는 __add__를 정의한다면 아무 문제가 없습니다.
  2. A가 보일러플레이트 코드로 넘어간 상황에서 __add__가 값을 반환한다면, B가 더 똑똑한 __radd__를 정의했을 가능성을 놓치게 됩니다. 따라서 보일러플레이트는 __add__에서 NotImplemented를 반환해야 합니다. (또는 A가 __add__를 아예 구현하지 않을 수도 있습니다.)
  3. 그러면 B의 __radd__에 기회가 돌아갑니다. 이 메서드가 a를 받아들인다면 아무 문제가 없습니다.
  4. 이 메서드도 보일러플레이트로 넘어간다면 더 이상 시도할 메서드가 없으므로, 기본 구현은 바로 여기에 있어야 합니다.
  5. 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 타입의 작성자들과 협의한 결과, 현재로서는 이 타입을 수치 타워에 포함하지 않기로 결정했습니다.

참고 문헌

감사의 글

애초에 이 PEP를 쓰도록 격려해 준 Neal Norwitz, numpy 사람들은 대수적 개념에 별로 관심이 없다는 점을 지적해 준 Travis Oliphant, Scheme이 이미 이 작업을 했다는 사실을 일깨워 준 Alan Isaac, 그리고 개념을 다듬어 준 Guido van Rossum과 메일링 리스트의 많은 분께 감사드립니다.