Я подумал, что стоит написать что-то, что лучше всего подходит для угадывания определяющего класса. Для полноты этого ответа также рассматриваются связанные методы.
В худшем случае, угадывание должно завершиться неудачей с функцией, возвращающей None
. Однако ни при каких условиях он не должен вызывать исключение или возвращать неправильный класс.
TL; DR
Финальная версия нашей функции успешно преодолевает самые простые случаи, а также несколько подводных камней.
В двух словах, его реализация различает связанные методы и «несвязанные методы» (функции) , поскольку в Python 3
нет надежного способа извлечения класса включения из «несвязанного метода».
Существует также частичная обработка для методов, определенных через дескрипторы, которые не классифицируются как обычные методы или функции (например, set.union
, int.__add__
и int().__add__
, но не set().union
).
Результирующая функция:
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
Небольшая просьба
Если вы решите использовать эту реализацию и столкнетесь с какими-либо оговорками, пожалуйста, прокомментируйте и опишите, что произошло.
Полная версия
«Несвязанные методы» являются обычными функциями
Прежде всего, стоит отметить следующее изменение , сделанное в Python 3
(см. Мотивацию Гвидо здесь ):
Понятие «несвязанные методы» удалено из языка. При ссылке на метод в качестве атрибута класса вы теперь получаете простой объект функции.
Это делает практически невозможным надежное извлечение класса, в котором был определен определенный «несвязанный метод», если только он не привязан к объекту этого класса (или к одному из его подклассов).
Обработка связанных методов
Итак, давайте сначала рассмотрим «более простой случай», в котором у нас есть связанный метод. Обратите внимание, что связанный метод должен быть записан в Python
, как описано в документации inspect.ismethod
.
def get_class_that_defined_method(meth):
# meth must be a bound method
if inspect.ismethod(meth):
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:
В соответствии с проблемой, поднятой Брайсом , возможно обрабатывать объекты method_descriptor
, такие как set.union
, и wrapper_descriptor
, такие как int.__add__
, просто возвращая их * Атрибут 1156 * (введен 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__
. Во избежание исключения AttributeError
в таком случае к атрибуту __objclass__
обращаются не напрямую, а через функцию getattr
.