Можно ли заставить вывод `type` возвращать другой класс? - PullRequest
2 голосов
/ 04 июля 2019

Итак, отказ от ответственности: этот вопрос немного поднял моё любопытство, и я спрашиваю это для чисто образовательных целей. Полагаю, это еще один вызов гуру Python!

Можно ли заставить вывод type(foo) возвращать значение, отличное от фактического класса экземпляра? то есть он может выдавать себя за самозванца и пройти проверку, такую ​​как type(Foo()) is Bar?

@juanpa.arrivillaga предложил вручную переназначить __class__ для экземпляра, но это приводит к изменению способа вызова всех других методов. например,

class Foo:
    def test(self):
        return 1

class Bar:
    def test(self):
        return 2


foo = Foo()
foo.__class__ = Bar
print(type(foo) is Bar)
print(foo.test())

>>> True
>>> 2

Желаемыми выходами будут True, 1. Т.е. класс, возвращаемый в type, отличается от экземпляра, и методы экземпляра, определенные в реальном классе, все еще вызываются.

1 Ответ

3 голосов
/ 04 июля 2019

Нет - атрибут __class__ является фундаментальной информацией о макете всех объектов Python, которые «видны» на самом уровне C API.И это то, что проверяется вызовом type.

. Это означает, что каждый объект Python имеет слот в своей компоновке в памяти с пространством для одного указателя на объект Python, который является объектом этого объекта.учебный класс.

Даже если вы используете ctypes или другие средства, чтобы переопределить защиту для этого слота и изменить ее из кода Python (поскольку изменение obj.__class__ с помощью = защищено на уровне C), его изменение эффективно изменяет объектtype: значение в слоте __class__ является классом объекта, и метод test будет выбран из класса там (Bar) в вашем примере.

Однако здесь есть дополнительная информация: inвся документация type(obj) рассматривается как эквивалентная obj.__class__ - однако, если класс объектов определяет дескриптор с именем __class__, он используется, когда используется форма obj.__class__.Однако type(obj) будет непосредственно проверять слот __class__ экземпляра и возвращать истинный класс.

Таким образом, это может «лгать» коду, используя obj.__class__, но не type(obj):

class Bar:
    def test(self):
        return 2

class Foo:
    def test(self):
        return 1
    @property
    def __class__(self):
        return Bar

Свойство в метаклассе

Попытка возиться с созданием дескриптора __class__ в самом метаклассе Foo будет грязной - и type(Foo()), и repr(Foo()) сообщат instance of Bar, но "реальным" классом объекта будет Foo.В некотором смысле, да, это делает type(Foo()) ложью, но не так, как вы думали, - type (Foo ()) выведет repr из Bar(), но это - Foo, что испортилосьвверх, из-за деталей реализации внутри type.__call__:

In [73]: class M(type): 
    ...:     @property 
    ...:     def __class__(cls): 
    ...:         return Bar 
    ...:                                                                                                                                               

In [74]: class Foo(metaclass=M): 
    ...:     def test(self): 
    ...:         return 1 
    ...:                                                                                                                                               

In [75]: type(Foo())                                                                                                                                   
Out[75]: <__main__.Bar at 0x55665b000578>

In [76]: type(Foo()) is Bar                                                                                                                            
Out[76]: False

In [77]: type(Foo()) is Foo                                                                                                                            
Out[77]: True

In [78]: Foo                                                                                                                                           
Out[78]: <__main__.Bar at 0x55665b000578>

In [79]: Foo().test()                                                                                                                                  
Out[79]: 1

In [80]: Bar().test()                                                                                                                                  
Out[80]: 2

In [81]: type(Foo())().test()                                                                                                                          
Out[81]: 1

Изменение type само по себе

Поскольку никто не "импортирует" type из любого места, а просто использует встроенныйсам по себе, можно установить встроенную функцию вызова type для сообщения о ложном классе - и он будет работать для всего кода Python в одном процессе, полагаясь на вызов type:

original_type = __builtins__["type"] if isinstance("__builtins__", dict) else __builtins__.type

def type(obj_or_name, bases=None, attrs=None, **kwargs): 
    if bases is not None: 
        return original_type(obj_or_name, bases, attrs, **kwargs) 
    if hasattr(obj_or_name, "__fakeclass__"): 
        return getattr(obj_or_name, "__fakeclass__") 
    return original_type(obj_or_name) 

if isinstance(__builtins__, dict):
    __builtins__["type"] = type
else:
    __builtins__.type = type

del type

Здесь есть одна хитрость, которую я не нашел в документации: при работе с __builtins__ в программе он работает как словарь.Однако в интерактивной среде, такой как Python Repl или Ipython, это модуль - для извлечения исходного type и записи измененной версии в __builtins__ необходимо учитывать это - приведенный выше код работает в обоих направлениях.

И тестирование этого (я импортировал приведенный выше фрагмент из файла .py на диск):

>>> class Bar:
...     def test(self):
...          return 2
... 
>>> class Foo:
...    def test(self):
...         return 1
...    __fakeclass__ = Bar
... 
>>> type(Foo())
<class '__main__.Bar'>
>>> 
>>> Foo().__class__
<class '__main__.Foo'>
>>> Foo().test()
1

Хотя это работает в демонстрационных целях, замена встроенного типа вызывала «диссонанс»это оказалось фатальным в более сложной среде, такой как IPython: Ipython аварийно завершит работу и немедленно прекратит работу при запуске приведенного выше фрагмента.

...