Подтвердить что ты не робот

Получить определяющий класс объекта несвязанного метода в 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: я только что нашел этот вопрос. Это заставляет думать, что лучшим решением является просто избежать этого, как у меня. Я все еще удивляюсь, почему это было удалено, хотя.

4b9b3361

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