Добавление числового типа между Real и Rational и поддержка функциональности типа для чисел Rational

Python предлагает набор абстрактных базовых классов для типов чисел. Это начинается с Number, из которых Complex это подкласс, и так далее через Real, Rational а также Integral, Так как каждый является подклассом последнего, каждый поддерживает специальные функциональные возможности классов, которые предшествовали ему в последовательности. Например, вы можете написать (1).numerator получить числитель числа Python 1, созданный с использованием целочисленного литерала 1 считается рациональным числом.

Примечания на связанной странице: Конечно, есть более возможные азбуки для чисел, и это было бы плохой иерархией, если бы исключало возможность их добавления. Вы можете добавить MyFoo между Complex и Real с помощью:

class MyFoo(Complex): ...
MyFoo.register(Real)

Это приводит к добавлению нового подкласса комплексных чисел, так что объекты типа Real будет проверяться как экземпляры нового класса - таким образом добавляя новый класс "между" Complex а также Real в некотором смысле. Это, однако, не учитывает возможность того, что новый класс может вводить функциональность (например, numerator собственность), не предлагаемая его подклассом.

Например, предположим, что вы хотите добавить класс, экземпляры которого представляют числа вида a + b√2 где a а также b рациональные числа. Вы, вероятно, представляли бы эти числа внутренне как пару Fraction s (случаи fraction.Fraction из стандартной библиотеки Python). Очевидно, этот класс чисел является подклассом Real и мы бы хотели лечить Rational как его подкласс (потому что каждое рациональное число является числом нашего нового типа, в котором b == 0). Итак, мы бы сделали это:

class FractionWithRoot2Part (Real): ...
FractionWithRoot2Part.register(Rational)

Мы могли бы хотеть добавить свойства к новому классу, которые (скажем) возвращают числа a а также b, Эти свойства можно назвать что-то вроде RationalPart а также CoefficientOfRoot2, Это неудобно, потому что существующие номера типа Rational не будет иметь этих свойств. Если мы напишем (1).RationalPart тогда мы получим AttributeError, Демонстрация:

Python 3.3.1 (v3.3.1:d9893d13c628, Apr  6 2013, 20:25:12) [MSC v.1600 32 bit (In
tel)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> from abc import *
>>> class c1 (metaclass = ABCMeta):
...     def x (self): return 5
...
>>> class c2: pass
...
>>> c1.register(c2)
<class '__main__.c2'>
>>> a1 = c1()
>>> a2 = c2()
>>> a1.x()
5
>>> a2.x()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'c2' object has no attribute 'x'

Таким образом, мы действительно не ввели новый тип, который находится "между" существующими двумя типами, потому что тип "внизу" отношения подкласса не поддерживает поведение класса "в середине".

Каков общепринятый способ обойти это? Одной из возможностей является предоставление функции (а не метода какого-либо класса), которая может обрабатывать любые виды ввода и действовать разумно; что-то вроде этого:

def RationalPart (number):
    if isinstance(number, FractionWithRoot2Part):
        try:
            return number.RationalPart
        except AttributeError:
            # number is presumably of type Rational
            return number
    else:
        raise TypeError('This is not supported you dummy!')

Есть ли лучший способ, чем этот?

1 ответ

Решение

Вы не можете использовать ABC для изменения существующих классов таким образом (и на самом деле, вы не можете безопасно изменять существующие классы таким образом вообще). ABC являются лишь механизмом для настройки того, тестирует ли класс как подкласс другого (и экземпляры этого класса как экземпляры другого), а не для фактического изменения реализаций подкласса. Когда в документации говорится об определении нового класса "между", это означает, что это означает; это просто означает промежуточное в терминах проверок подкласса / экземпляра, а не фактического наследования. Это описано здесь:

ABC вводят виртуальные подклассы, которые являются классами, которые не наследуются от класса, но все еще распознаются isinstance() а также issubclass()

Обратите внимание, что он говорит: виртуальные подклассы на самом деле не наследуются от вашего ABC, они просто тестируют, как будто они делают. Вот как азбука предназначена для работы. Способ их использования предлагается здесь:

Азбука может быть разделена на подклассы напрямую, а затем действует как смешанный класс.

Таким образом, вы не можете изменить существующий класс Rational с помощью ABC. Способ сделать то, что вы хотите, - создать новый класс, который наследуется от Rational и использует ваш ABC в качестве миксина. Затем используйте этот класс вместо обычного Rational.

На самом деле, вам может даже не понадобиться здесь использовать азбуку. Единственное преимущество использования ABC - это то, что ваши новые рациональные числа выглядят как Rational, если кто-то явно тестирует; но пока вы наследуете от Rational и от вашего нового класса, который добавляет желаемое поведение, новые классы будут действовать как Rationals в любом случае.

Когда ты сказал

Однако это неудобно, поскольку существующие числа типа Rational не будут иметь этих свойств.

вы нацелились на суть ситуации. Это может показаться неловким, но было бы очень неловко, если бы кто-то мог зайти и с конечным запуском ABC начать изменять поведение ваших существующих классов, вставив над ними новый суперкласс в иерархии наследования. Это не так, как это работает. Не существует безопасного способа добавить новое поведение к существующим экземплярам любого класса; единственная безопасная вещь - добавить новое поведение в ваш новый класс и попросить людей использовать этот новый класс вместо старого.

Другие вопросы по тегам