python: подкласс подкласса int - PullRequest
1 голос
/ 17 июня 2020

Я пытаюсь понять, как правильно создать подкласс int. Одна из целей - определить типы, используемые в структурах в пределах определенного формата двоичного файла. Например, 16-разрядное целое число без знака. Я определил класс следующим образом, который, кажется, делает то, что я ожидаю:

class uint16(int):

    def __new__(cls, val):
        if (val < 0 or val > 0xffff):
            raise ValueError("uint16 must be in the range %d to %d" % (0, 0xffff))
        return super(cls, cls).__new__(cls, val)

Теперь я не очень понимаю, как использовать super без аргументов по сравнению с (тип, объект) vs. (тип, тип). Я использовал super(cls, cls), как я видел, который используется для аналогичного сценария.

Теперь C упрощает создание типов, которые фактически являются псевдонимами существующих типов. Например,

typedef unsigned int        UINT;

Псевдонимы могут считаться полезными, чтобы помочь прояснить предполагаемое использование типа. Согласны вы с этим или нет, но описание двоичного формата иногда может сделать это, и если да, то для ясности было бы полезно воспроизвести это в Python.

Итак, я попробовал следующее:

class Offset16(uint16):

    def __new__(cls, val):
        return super(cls, cls).__new__(cls, val)

Я мог бы сделать Offset16 подклассом int, но тогда я бы хотел повторить проверку (более дублированный код). Подклассифицируя uint16, я избегаю дублирования кода.

Но когда я пытаюсь создать объект Offset16, я получаю ошибку рекурсии:

>>> x = Offset16(42)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 3, in __new__
  File "<stdin>", line 5, in __new__
  File "<stdin>", line 5, in __new__
  File "<stdin>", line 5, in __new__
  [Previous line repeated 987 more times]
  File "<stdin>", line 3, in __new__
RecursionError: maximum recursion depth exceeded in comparison
>>> 

Поскольку стек вызовов просто строка 5 повторяется (а не строки 3/5 поочередно), строка в uint16.__new__ снова вводится.

Затем я попытался изменить Offset16.__new__ разными способами, изменив аргументы на super , большинство из которых не работали. Но последняя попытка была следующей:

class Offset16(uint16):

    def __new__(cls, val):
        return super(uint16, cls).__new__(cls, val)

Кажется, это работает:

>>> x = Offset16(42)
>>> x
42
>>> type(x)
<class '__main__.Offset16'>

Почему разница?

Этот последний подход, кажется, частично побеждает цель super: избегать ссылки на базовый класс, чтобы упростить его поддержку. Есть ли способ сделать эту работу, которая не требует ссылки на uint16 в реализации __new__?

И как лучше всего это сделать?

1 Ответ

0 голосов
/ 17 июня 2020

Комментарии предоставили информацию, которая помогла ответить Почему разница? и Какой лучший способ?


Первый: Почему разница ?

В исходных определениях uint16 и Offset16 метод __new__ использует super(cls,cls). Как отметил @ juanpa.arrivillaga, когда вызывается Offset16.__new__, это приводит к тому, что uint16.__new__ вызывает себя рекурсивно. Если Offset16.__new__ использовать super(uint16,cls), это изменяет поведение внутри uint16.__new__.

Некоторые дополнительные пояснения могут помочь понять:

Аргумент cls, переданный в Offset16.__new__, является сам класс Offset16. Итак, когда реализация метода ссылается на cls, это ссылка на Offset16. Итак,

    return super(cls, cls).__new__(cls, val)

в этом случае эквивалентно

    return super(Offset16, Offset16).__new__(Offset16, val)

. Теперь мы можем думать о super как о возвращении базового класса, но его семантика, когда указаны аргументы, более тонкая. : super разрешает ссылку на метод, и аргументы влияют на то, как это разрешение происходит. Если аргументы не указаны, super().__new__ - это метод в непосредственном суперклассе. Когда предоставлены аргументы, это влияет на поиск. В частности, для super(type1, type2) MRO (порядок разрешения методов) type2 будет выполняться поиск вхождения type1, и будет использоваться класс , следующий за type1 в этой последовательности.

(Это объясняется в документации к super, хотя формулировка могла бы быть более ясной.)

MRO для Offset16: (Offset16, uint16, int, object ). Следовательно,

    return super(Offset16, Offset16).__new__(Offset16, val)

преобразуется в

    return uint16.__new__(Offset16, val)

Когда uint16.__new__ вызывается таким образом, ему передается аргумент класса Ofset16, а не uint16. В результате, когда его реализация имеет

    return super(cls, cls).__new__(cls, val)

, это снова преобразуется в

    return uint16.__new__(Offset16, val)

Вот почему мы получаем бесконечное l oop.

Но в измененном определении Offset16,

class Offset16(uint16):

    def __new__(cls, val):
        return super(uint16, cls).__new__(cls, val)

последняя строка эквивалентна

        return super(uint16, Offset16).__new__(Offset16, val)

и согласно MRO для Offset16 и семантики для super упомянутый выше, который разрешается в

        return int.__new__(Offset16, val)

Это объясняет, почему измененное определение приводит к другому поведению.


Второй: Как лучше всего это сделать?

В комментариях были представлены разные альтернативы, которые могут соответствовать разным ситуациям.

@juanpa.arrivillaga предлагается (при условии Python3) просто использовать super() без аргументы. Для подхода, использованного в вопросе, это имеет смысл. Причиной передачи аргументов в super может быть манипулирование поиском MRO. В этой простой иерархии классов в этом нет необходимости.

@ Jason Yang предложил ссылаться непосредственно на указанный суперкласс c вместо использования super. Например:

class Offset16(uint16):

    def __new__(cls, val):
        return uint16.__new__(cls, val)

Это отлично подходит для этой простой ситуации. Но это может быть не лучшим вариантом для другого сценария ios с более сложными отношениями классов. Обратите внимание, например, что uint16 дублируется выше. Если бы у подкласса было несколько методов, которые обертывали (а не заменяли) метод суперкласса, было бы много повторяющихся ссылок, и внесение изменений в иерархию классов привело бы к трудным для анализа ошибкам. Избежание таких проблем - одно из предполагаемых преимуществ использования super.

Наконец, @Adam.Er8 предложил просто использовать

Offset16 = uint16

Это действительно очень просто. Единственное предостережение: Offset16 на самом деле не более чем псевдоним для uint16; это не отдельный класс. Например:

>>> Offset16 = uint16
>>> x = Offset16(24)
>>> type(x)
<class 'uint16'>

Итак, это может быть нормально , если в приложении никогда не будет необходимости иметь фактическое различие типов .

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...