Поведение объекта.__new__ Python. Что происходит под капотом?

Я экспериментирую с метапрограммированием на Python (CPython 3.10.13) и заметил странное поведение с object.__new__ (ну, странное, по крайней мере, для меня). Взгляните на следующий эксперимент (не практический код, просто эксперимент) и комментарии. Обратите внимание, что object.__new__, похоже, меняет свое поведение в зависимости от первого аргумента:

# Empty class inherit __new__ and __init__ from object
class Empty:
    pass

# Confirmation of inheritance
assert Empty.__new__ is object.__new__, "Different __new__"
assert Empty.__init__ is object.__init__, "Different __init__"

empty_obj = Empty()
uinit_empty_obj = object.__new__(Empty)

assert type(empty_obj) is type(uinit_empty_obj), "Different types"

try:
    object.__new__(Empty, 10, 'hi', hello='bye')
except TypeError as e:
    # repr(e) mentioned the Empty class
    print(repr(e))

# Overwrite the object __new__ and __init__ methods
# __new__ and __init__ with the same signature
class Person:
    def __new__(cls, name, age):
        """Does nothing bassicaly. Just overwrite `object.__new__`."""
        print(f'Inside {cls.__name__}.__new__')
        return super().__new__(cls)
    
    def __init__(self, name, age):
        print(f'Inside {type(self).__name__}.__init__')
        self.name = name
        self.age = age

a_person = Person('John Doe', 25)
uinit_person = Person.__new__(Person, 'Michael', 40)

try:
    # Seems an obvious error since object() doesn't take any arguments
    another_uinit_person = object.__new__(Person, 'Ryan', 25)
except TypeError as e:
    # Indeed raises TypeError, but now there isn't a mention of the Person class in repr(e)
    print('`another_uinit_person` :', repr(e))

# Now, some weird things happen (well, weird for me).
# Inherit __new__ from object and overwrite __init__.
# __new__ and __init__ with unmatching signatures.
# A basic Python class. Works just fine like suppose to.
class Vehicle:
    def __init__(self, model):
        self.model = model

# Confirmation of __new__ inheritance.
assert Vehicle.__new__ is object.__new__, "Nop, it isn't"

a_vehicle = Vehicle('Honda')

# I would understand if CPython autogenerated a __new__ method matching __init__
# or a __new__ method that accepts all arguments.
# The following try-except-else suggests the last, but the assert statement above 
# indicates that Vehicle.__new__ is actually object.__new__.
try:
    # Doesn't raise any exceptions
    uinit_vehicle = Vehicle.__new__(Vehicle, 'Honda', 10, ('four-wheels',), hello='bye')
except Exception as e:
    print(repr(e))
else:
    print('`uinit_vehicle` : constructed just fine', uinit_vehicle)

# Now the following runs just fine
try:
    # Doesn't raise any exceptions
    another_unit_vehicle = object.__new__(Vehicle, 'Toyota')
    another_unit_vehicle = object.__new__(Vehicle, 'Toyota', 100, four_wheels=True)
except Exception as e:
    print(repr(e))
else:
    print('`another_unit_vehicle` : constructed just fine:', another_unit_vehicle)

Я получил следующий вывод:

TypeError('Empty() takes no arguments')
Inside Person.__new__
Inside Person.__init__
Inside Person.__new__
`another_uinit_person` : TypeError('object.__new__() takes exactly one argument (the type to instantiate)')
`uinit_vehicle` : constructed just fine <__main__.Vehicle object at 0x00000244D15A7A90>
`another_unit_vehicle` : constructed just fine: <__main__.Vehicle object at 0x00000244D15A7A30>

Мои вопросы:

  1. Почему первый TypeError упомянул класс Empty, а второй просто object.__new__?
  2. Почему object.__new__(Person, 'Ryan', 25) подняли TypeError, а object.__new__(Vehicle, 'Toyota') и object.__new__(Vehicle, 'Toyota', 100, four_wheels=True) нет?

По сути: что object.__new__ делает под капотом?

Мне кажется, что он выполняет несколько странную проверку методов переопределения __new__ и/или __init__ первого аргумента, если таковые имеются.

Есть отличный ответ относительно метакласса, который объясняет __new__() - хотя это и не дает прямого ответа на ваш вопрос, это может быть действительно полезно, чтобы понять концепцию этого!

jupiterbjy 28.06.2024 01:10

@Грисмар, извини. Удаленный.

Cornélio Sousa 28.06.2024 01:49
Почему в Python есть оператор "pass"?
Почему в Python есть оператор "pass"?
Оператор pass в Python - это простая концепция, которую могут быстро освоить даже новички без опыта программирования.
Некоторые методы, о которых вы не знали, что они существуют в Python
Некоторые методы, о которых вы не знали, что они существуют в Python
Python - самый известный и самый простой в изучении язык в наши дни. Имея широкий спектр применения в области машинного обучения, Data Science,...
Основы Python Часть I
Основы Python Часть I
Вы когда-нибудь задумывались, почему в программах на Python вы видите приведенный ниже код?
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
Алиса и Боб имеют неориентированный граф из n узлов и трех типов ребер:
Оптимизация кода с помощью тернарного оператора Python
Оптимизация кода с помощью тернарного оператора Python
И последнее, что мы хотели бы показать вам, прежде чем двигаться дальше, это
Советы по эффективной веб-разработке с помощью Python
Советы по эффективной веб-разработке с помощью Python
Как веб-разработчик, Python может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
4
2
58
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Базовые методы Python object.__init__ и object.__new__ подавляют ошибки о избыточных аргументах в обычной ситуации, когда ровно один из них был переопределен, а другой — нет. Непереопределенный метод будет игнорировать дополнительные аргументы, поскольку они обычно передаются автоматически (а не путем явного вызова __new__ или __init__, о чем программисту следует знать лучше).

То есть ни один из этих классов не вызовет проблем в методах, которые они наследуют:

class OnlyNew:
    def __new__(self, *args):
        pass

    # __init__ is inherited from object

class OnlyInit:
    def __init__(self, *args):
        pass

    # __new__ is inherited from object

# tests:
object.__new__(OnlyInit, 1, 2, 3, 4)                  # no error
object.__init__(object.__new__(OnlyNew), 1, 2,3, 4)   # also no error

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

# bad tests:
try:
    object.__new__(OnlyNew, 1, 2, 3, 4)
except Exception as e:
    print(e) # object.__new__() takes exactly one argument (the type to instantiate)
try:
    object.__init__(object.__new__(OnlyInit), 1, 2, 3, 4)
except Exception as e:
    print(e) # object.__init__() takes exactly one argument (the type to instantiate)

Более того, если вы переопределяете оба метода: __new__ и __init__, вам нужно будет вызывать оба метода базового класса без дополнительных аргументов, поскольку вы должны знать, что делаете, если реализуете оба метода.

class OverrideBoth:
    def __new__(self, *args):
        pass

    def __init__(self, *args):
        pass

# more bad tests, object has zero tolerance for extra arguments in this situation
try:
    object.__new__(OverrideBoth, 1, 2, 3, 4)
except Exception as e:
    print(e) # object.__new__() takes exactly one argument (the type to instantiate)

try:
    object.__init__(object.__new__(OverrideBoth), 1, 2,3, 4)
except Exception as e:
    print(e) # object.__init__() takes exactly one argument (the instance to initialize)

Реализацию этих проверок вы можете увидеть в исходном коде CPython. Даже если вы не очень хорошо знаете C, вам совершенно ясно, что он делает. Существует другой путь кода, который обрабатывает такие классы, как ваш Empty, которые не переопределяют ни один из методов (именно поэтому это сообщение об исключении немного отличается).

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