Комментарии предоставили информацию, которая помогла ответить Почему разница? и Какой лучший способ?
Первый: Почему разница ?
В исходных определениях 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'>
Итак, это может быть нормально , если в приложении никогда не будет необходимости иметь фактическое различие типов .