hasattr лжет?

Следующий код

class Foo:
    def bar(self) -> None:
        pass

foo = Foo()
if hasattr(foo.bar, '__annotations__'):
    foo.bar.__annotations__ = 'hi'

сбой

AttributeError: 'method' object has no attribute '__annotations__'

Как это может произойти?

Всего 2 ответа


Ошибка атрибута здесь возникает потому, что вы не можете установить атрибут объекта метода:

>>> foo.bar.baz = 42
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'method' object has no attribute 'baz'

Исключение здесь, возможно, запутывает, потому что объекты method обертывают объект функции и атрибут прокси-сервера для доступа к этому базовому функциональному объекту. Поэтому, когда атрибуты функции существуют, то hasattr() в методе вернет True :

>>> hasattr(foo.bar, 'baz')
False
>>> foo.bar.__func__.baz = 42
>>> hasattr(foo.bar, 'baz')
True
>>> foo.bar.baz
42

Однако вы все равно не можете установить эти атрибуты с помощью метода, независимо от того,

>>> hasattr(foo.bar, 'baz')
True
>>> foo.bar.baz = 42
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'method' object has no attribute 'baz'

Таким образом, только потому, что атрибут может быть прочитан, это не значит, что вы можете его установить . hasattr() говорит правду, вы просто интерпретируете ее как нечто иное.

Теперь, если вы попытались установить атрибут __annotations__ непосредственно на базовый объект функции, вы получите другое сообщение об ошибке:

>>> foo.bar.__func__.__annotations__ = 'hi'
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: __annotations__ must be set to a dict object

Вы бы хотели использовать объект словаря здесь:

>>> foo.bar.__func__.__annotations__ = {'return': 'hi'}
>>> foo.bar.__annotations__
{'return': 'hi'}

Однако, поскольку __annotations__ является изменчивым словарем, проще просто манипулировать ключами и значениями для этого объекта, что вполне возможно сделать через оболочку метода:

>>> foo.bar.__annotations__['return'] = 'int'
>>> foo.bar.__annotations__
{'return': 'int'}

Теперь, если вы надеялись установить аннотации для каждого экземпляра , вы не можете уйти с настройкой атрибутов объектов метода, потому что объекты метода являются эфемерными , они создаются только для вызова, а затем обычно отбрасываются сразу после.

Вам нужно будет использовать объекты дескриптора метода через метакласс и повторно создавать атрибут __annotations__ для каждого раза, или вместо этого вы можете предварительно связать методы с новым функциональным объектом, которому будут предоставлены свои собственные атрибуты. Затем вам придется заплатить большую цену памяти:

import functools

foo.bar = lambda *args, **kwargs: Foo.bar(foo, *args, **kwargs)
functools.update_wrapper(foo.bar, Foo.bar)  # copy everything over to the new wrapper
foo.bar.__annotations__['return'] = 'hi'

В любом случае вы полностью уничтожаете важные оптимизации скорости, сделанные в Python 3.7 таким образом.

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


Вы получаете сообщение об ошибке. потому что __annotations__ является словарем. Если вы хотите изменить значения, вам нужно будет сделать это следующим образом:

if hasattr(foo.bar, '__annotations__'):
    foo.bar.__annotations__['return'] = 'hi'

Это сделает возвращаемое значение вашего foo.bar be hi вместо None . Единственное, что я не уверен в том, как __annotations__ , не позволяя вам изменять их из dict в строку, но я полагаю, что это некоторая внутренняя проверка в источнике.

ОБНОВИТЬ

Для большего контроля над сигнатурой вы можете использовать модуль inspect и получить объект Signature своего класса (или метода) и отредактировать его оттуда. Например

import inspect
sig = inspect.signature(foo.bar)
sig.return_annotation  # prints None (before modifying)
sig.replace(return_annotation="anything you want")

Подробнее об этом здесь


Есть идеи?

10000