Получить определяющий класс объекта несвязанного метода в Python 3
Скажем, я хочу сделать декоратор для методов, определенных в классе. Я хочу, чтобы этот декоратор при вызове мог установить атрибут класса, определяющего метод (чтобы зарегистрировать его в списке методов, которые служат для определенной цели).
В Python 2 метод im_class
отлично выполняет это:
def decorator(method):
cls = method.im_class
cls.foo = 'bar'
return method
Однако в Python 3 такой атрибут (или его замена) не существует. Полагаю, идея заключалась в том, что вы можете вызвать type(method.__self__)
, чтобы получить класс, но это не работает для несвязанных методов, так как __self__ == None
в этом случае.
ПРИМЕЧАНИЕ.. Этот вопрос на самом деле немного неактуальен для моего случая, так как я выбрал вместо этого атрибут самого метода, а затем проверил экземпляр всех его методов, ищущих этот атрибут в соответствующее время. Я также (в настоящее время) использую Python 2.6. Тем не менее, мне любопытно, есть ли какая-либо замена для функциональности версии 2, а если нет, то зачем было ее полностью удалять.
EDIT: я только что нашел этот вопрос. Это заставляет думать, что лучшим решением является просто избежать этого, как у меня. Я все еще удивляюсь, почему это было удалено, хотя.
Ответы
Ответ 1
Точка, в которой вы, кажется, отсутствуете, в Python 3 полностью исключен тип "unbound method" - метод до и без привязки - это просто функция, без странных "несвязанных" типов проверки типов используемый для выполнения. Это упрощает язык!
Для справки...:
>>> class X:
... def Y(self): pass
...
>>> type(X.Y)
<class 'function'>
и вуаля - одно менее тонкое понятие и различие, о котором нужно беспокоиться. Такие упрощения являются основным преимуществом Python 3 wrt Python 2, который (на протяжении многих лет) накапливал так много тонкостей, что он был в опасности (если функции продолжали добавляться к нему) действительно терял свой статус как простой. С Python 3, простота возвращается! -)
Ответ 2
Я думал, что было бы полезно написать что-то, что лучше всего подходит для угадывания определяющего класса. Для полноты этого ответа также рассматриваются связанные методы.
В худшем случае угадание вообще не срабатывает, функция возвращает None
. Однако ни при каких обстоятельствах он не должен возвращать неправильный класс.
TL; DR
Окончательная версия нашей функции успешно преодолевает самые простые случаи и несколько ошибок.
Вкратце, его реализация отличает связанные методы и "несвязанные методы" (функции), поскольку в Python 3
нет надежного способа для извлечения закрывающего класса из "несвязанного метода".
Существует также специальная обработка для методов, определенных через дескрипторы, которые не классифицируются как обычные методы или функции (например, set.union
, int.__add__
и int().__add__
).
Результирующая функция:
def get_class_that_defined_method(meth):
if inspect.ismethod(meth):
for cls in inspect.getmro(meth.__self__.__class__):
if cls.__dict__.get(meth.__name__) is meth:
return cls
meth = meth.__func__ # fallback to __qualname__ parsing
if inspect.isfunction(meth):
cls = getattr(inspect.getmodule(meth),
meth.__qualname__.split('.<locals>', 1)[0].rsplit('.', 1)[0])
if isinstance(cls, type):
return cls
return getattr(meth, '__objclass__', None) # handle special descriptor objects
Небольшой запрос
Если вы решите использовать эту реализацию и столкнетесь с любыми оговорками, прокомментируйте и опишите, что произошло.
"Unbound methods" являются регулярными функциями
Прежде всего, стоит отметить следующее изменение, сделанное в Python 3
(см. мотивацию Guido здесь):
Концепция "несвязанных методов" была удалена с языка. Когда вы ссылаетесь на метод как атрибут класса, теперь вы получаете простой объект функции.
Это делает практически невозможным надежное извлечение класса, в котором определен определенный "несвязанный метод", если он не связан с объектом этого класса (или одного из его подклассов).
Обработка связанных методов
Итак, давайте сначала обработаем "более простой случай", в котором у нас есть связанный метод. Обратите внимание, что связанный метод должен быть записан в Python
, как описано в inspect.ismethod
документации.
def get_class_that_defined_method(meth):
# meth must be a bound method
if not inspect.ismethod(meth):
return None
for cls in inspect.getmro(meth.__self__.__class__):
if cls.__dict__.get(meth.__name__) is meth:
return cls
return None # not required since None would have been implicitly returned anyway
Однако это решение не является совершенным и имеет свои опасности, поскольку методы могут быть назначены во время выполнения, что делает их имя возможно иным, чем их атрибут (см. пример ниже). Эта проблема существует и в Python 2
. Возможным обходным путем было бы перебрать все атрибуты класса, ища идентификатор, который идентичен указанному методу.
Обработка "несвязанных методов"
Теперь, когда мы получили это в сторону, мы можем предложить взломать, который пытается обрабатывать "несвязанные методы". Взлом, его обоснование и некоторые слова разочарования можно найти в этом ответе. Он основан на ручном анализе атрибута __qualname__
, доступен только от Python 3.3
, настоятельно не рекомендуется, но он должен работать для простых случаев:
def get_class_that_defined_method(meth):
if inspect.isfunction(meth):
return getattr(inspect.getmodule(meth),
meth.__qualname__.split('.<locals>', 1)[0].rsplit('.', 1)[0])
return None # not required since None would have been implicitly returned anyway
Объединение обоих подходов
Так как inspect.isfunction
и inspect.ismethod
являются взаимоисключающими, объединение обоих подходов в одно решение дает нам следующее (с добавленными средствами ведения журнала для следующих примеров):
def get_class_that_defined_method(meth):
if inspect.ismethod(meth):
print('this is a method')
for cls in inspect.getmro(meth.__self__.__class__):
if cls.__dict__.get(meth.__name__) is meth:
return cls
if inspect.isfunction(meth):
print('this is a function')
return getattr(inspect.getmodule(meth),
meth.__qualname__.split('.<locals>', 1)[0].rsplit('.', 1)[0])
print('this is neither a function nor a method')
return None # not required since None would have been implicitly returned anyway
Пример выполнения
>>> class A:
... def a(self): pass
...
>>> class B:
... def b(self): pass
...
>>> class C(A, B):
... def a(self): pass
...
>>> A.a
<function A.a at 0x7f13b58dfc80>
>>> get_class_that_defined_method(A.a)
this is a function
<class '__main__.A'>
>>>
>>> A().a
<bound method A.a of <__main__.A object at 0x7f13b58ca9e8>>
>>> get_class_that_defined_method(A().a)
this is a method
<class '__main__.A'>
>>>
>>> C.a
<function C.a at 0x7f13b58dfea0>
>>> get_class_that_defined_method(C.a)
this is a function
<class '__main__.C'>
>>>
>>> C().a
<bound method C.a of <__main__.C object at 0x7f13b58ca9e8>>
>>> get_class_that_defined_method(C().a)
this is a method
<class '__main__.C'>
>>>
>>> C.b
<function B.b at 0x7f13b58dfe18>
>>> get_class_that_defined_method(C.b)
this is a function
<class '__main__.B'>
>>>
>>> C().b
<bound method C.b of <__main__.C object at 0x7f13b58ca9e8>>
>>> get_class_that_defined_method(C().b)
this is a method
<class '__main__.B'>
До сих пор так хорошо, но...
>>> def x(self): pass
...
>>> class Z:
... y = x
... z = (lambda: lambda: 1)() # this returns the inner function
... @classmethod
... def class_meth(cls): pass
... @staticmethod
... def static_meth(): pass
...
>>> Z.y
<function x at 0x7f13b58dfa60>
>>> get_class_that_defined_method(Z.y)
this is a function
<function x at 0x7f13b58dfa60>
>>>
>>> Z().y
<bound method Z.x of <__main__.Z object at 0x7f13b58ca9e8>>
>>> get_class_that_defined_method(Z().y)
this is a method
this is neither a function nor a method
>>>
>>> Z.z
<function Z.<lambda>.<locals>.<lambda> at 0x7f13b58d40d0>
>>> get_class_that_defined_method(Z.z)
this is a function
<class '__main__.Z'>
>>>
>>> Z().z
<bound method Z.<lambda> of <__main__.Z object at 0x7f13b58ca9e8>>
>>> get_class_that_defined_method(Z().z)
this is a method
this is neither a function nor a method
>>>
>>> Z.class_meth
<bound method type.class_meth of <class '__main__.Z'>>
>>> get_class_that_defined_method(Z.class_meth)
this is a method
this is neither a function nor a method
>>>
>>> Z().class_meth
<bound method type.class_meth of <class '__main__.Z'>>
>>> get_class_that_defined_method(Z().class_meth)
this is a method
this is neither a function nor a method
>>>
>>> Z.static_meth
<function Z.static_meth at 0x7f13b58d4158>
>>> get_class_that_defined_method(Z.static_meth)
this is a function
<class '__main__.Z'>
>>>
>>> Z().static_meth
<function Z.static_meth at 0x7f13b58d4158>
>>> get_class_that_defined_method(Z().static_meth)
this is a function
<class '__main__.Z'>
Заключительные штрихи
- Результат, созданный
Z.y
, может быть частично исправлен (возвращать None
), проверяя, что возвращаемое значение является классом, прежде чем фактически вернуть его.
- Результат, созданный
Z().z
, может быть исправлен путем возврата к анализу функции __qualname__
(функция может быть извлечена через meth.__func__
).
-
Результат, созданный Z.class_meth
и Z().class_meth
, неверен, поскольку доступ к методу класса всегда возвращает связанный метод, атрибут __self__
- это сам класс, а не его объект. Таким образом, дальнейший доступ к атрибуту __class__
поверх этого атрибута __self__
не работает должным образом:
>>> Z().class_meth
<bound method type.class_meth of <class '__main__.Z'>>
>>> Z().class_meth.__self__
<class '__main__.Z'>
>>> Z().class_meth.__self__.__class__
<class 'type'>
Это можно устранить, проверяя, возвращает ли атрибут __self__
экземпляр type
. Однако это может сбивать с толку, когда наша функция вызывается против методов метакласса, поэтому мы оставим ее как есть на данный момент.
Вот окончательная версия:
def get_class_that_defined_method(meth):
if inspect.ismethod(meth):
for cls in inspect.getmro(meth.__self__.__class__):
if cls.__dict__.get(meth.__name__) is meth:
return cls
meth = meth.__func__ # fallback to __qualname__ parsing
if inspect.isfunction(meth):
cls = getattr(inspect.getmodule(meth),
meth.__qualname__.split('.<locals>', 1)[0].rsplit('.', 1)[0])
if isinstance(cls, type):
return cls
return None # not required since None would have been implicitly returned anyway
Удивительно, но это также фиксирует результат Z.class_meth
и Z().class_meth
, которые теперь корректно возвращают Z
. Это связано с тем, что атрибут __func__
метода класса возвращает регулярную функцию, чей атрибут __qualname__
может быть проанализирован:
>>> Z().class_meth.__func__
<function Z.class_meth at 0x7f13b58d4048>
>>> Z().class_meth.__func__.__qualname__
'Z.class_meth'
EDIT:
В соответствии с вопросом, поднятым Bryce, можно обрабатывать объекты method_descriptor
, такие как set.union
и wrapper_descriptor
объекты, например int.__add__
, просто вернув атрибут __objclass__
(введенный PEP-252), если таковой существует:
if inspect.ismethoddescriptor(meth):
return getattr(meth, '__objclass__', None)
Однако inspect.ismethoddescriptor
возвращает False
для объектов соответствующего экземпляра объекта, т.е. для set().union
и для int().__add__
:
-
Так как int().__add__.__objclass__
возвращает int
, предложение выше if может быть отказано для решения проблемы для int().__add__
.
-
К сожалению, это не относится к вопросу о set().union
, для которого не указан атрибут __objclass__
.
Ответ 3
Начиная с Python 3.6 вы можете выполнить то, что вы описываете, используя декоратор, который определяет метод __set_name__
. В документации говорится, что object.__set_name__
вызывается при создании класса.
Вот пример, который украшает метод "для того, чтобы зарегистрировать его в списке методов, которые служат определенной цели":
>>> class particular_purpose:
... def __init__(self, fn):
... self.fn = fn
...
... def __set_name__(self, owner, name):
... owner._particular_purpose.add(self.fn)
...
... # then replace ourself with the original method
... setattr(owner, name, self.fn)
...
... class A:
... _particular_purpose = set()
...
... @particular_purpose
... def hello(self):
... return "hello"
...
... @particular_purpose
... def world(self):
... return "world"
...
>>> A._particular_purpose
{<function __main__.A.hello(self)>, <function __main__.A.world(self)>}
>>> a = A()
>>> for fn in A._particular_purpose:
... print(fn(a))
...
world
hello
Обратите внимание, что этот вопрос очень похож на Может ли декоратор Python метода экземпляра получить доступ к классу? и, следовательно, мой ответ на ответ, который я дал там.