Получить определяющий класс объекта несвязанного метода в Python 3 - PullRequest
32 голосов
/ 28 августа 2010

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

В 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, и если нет, то каким было обоснование для ее полного удаления.

РЕДАКТИРОВАТЬ : Я только что нашел этот вопрос . Это создает впечатление, что лучшее решение - просто избегать его, как я. Мне все еще интересно, почему он был удален.

Ответы [ 4 ]

58 голосов
/ 21 сентября 2014

Я подумал, что стоит написать что-то, что лучше всего подходит для угадывания определяющего класса. Для полноты этого ответа также рассматриваются связанные методы.

В худшем случае, угадывание должно завершиться неудачей с функцией, возвращающей 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.
36 голосов
/ 28 августа 2010

Точка, которую вы, похоже, упускаете, состоит в том, что в Python 3 тип «несвязанный метод» полностью исчез - метод, до тех пор, пока он не ограничен, является просто функцией, без странных «несвязанных методов» проверки типаиспользуется для выполнения.Это делает язык проще!

То есть ...:

>>> class X:
...   def Y(self): pass
... 
>>> type(X.Y)
<class 'function'>

и вуаля - еще одно менее тонкое понятие и различие, о котором нужно беспокоиться.Такие упрощения являются основным преимуществом Python 3 по сравнению с Python 2, который (на протяжении многих лет) накапливал столько тонкостей, что ему угрожала опасность (если функции добавлялись к нему), когда он действительно терял свой статус простого . язык.В Python 3 простота составляет назад ! -)

5 голосов
/ 23 января 2019

Начиная с 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 метода экземпляра получить доступ к классу? и, следовательно, мой ответ на ответ я там предоставил .

1 голос
/ 08 февраля 2019

Небольшое расширение для Python 3,6 (Python 2,7 работал нормально) на большой ответ https://stackoverflow.com/a/25959545/4013571

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):
        class_name = meth.__qualname__.split('.<locals>', 1)[0].rsplit('.', 1)[0]
        try:
            cls = getattr(inspect.getmodule(meth), class_name)
        except AttributeError:
            cls = meth.__globals__.get(class_name)
        if isinstance(cls, type):
            return cls
    return None  # not required since None would have been implicitly returned anyway

Я обнаружил, что для doctest* требуется следующая корректировка1008 *

        except AttributeError:
            cls = meth.__globals__.get(class_name)

По какой-то причине при использовании nose inspect.getmodule(meth) не содержал определяющий класс

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...