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

2 Solutions collect form web for “Получить определяющий класс объекта несвязанного метода в Python 3”

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

Для остроумия …:

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

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

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

В худшем случае угадание вообще не срабатывает, а функция возвращает None . Однако ни при каких обстоятельствах он не должен возвращать неправильный класс.

TL; DR

Окончательный вариант нашей функции успешно преодолевает самые простые случаи и несколько ловушек.

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

  • Для связанного метода он просто пересекает MRO , аналогично тому, как это сделано в принятом ответе на эквивалентный вопрос для Python 2 .
  • Для «несвязанного метода» он основан на разборе его квалифицированного имени , которое доступно только с Python 3.3 и довольно безрассудно (если эта функция не нужна, лучше всего удалить этот блок кода и просто вернуть None вместо этого).

Существует также специальная обработка для методов, определенных через дескрипторы, которые не классифицируются как обычные методы или функции (например, 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 (см. Мотивацию Гвидо здесь ):

Концепция «несвязанных методов» была удалена из языка. Когда вы ссылаетесь на метод как атрибут класса, теперь вы получаете простой объект функции.

Это делает практически невозможным надежное извлечение класса, в котором определен определенный «несвязанный метод», если он не связан с объектом этого класса (или одного из его подклассов).

Обработка связанных методов

Итак, давайте сначала рассмотрим «более простой случай», в котором у нас есть связанный метод. Обратите внимание, что связанный метод должен быть написан на 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 ... >>> Aa <function Aa at 0x7f13b58dfc80> >>> get_class_that_defined_method(Aa) this is a function <class '__main__.A'> >>> >>> A().a <bound method Aa of <__main__.A object at 0x7f13b58ca9e8>> >>> get_class_that_defined_method(A().a) this is a method <class '__main__.A'> >>> >>> Ca <function Ca at 0x7f13b58dfea0> >>> get_class_that_defined_method(Ca) this is a function <class '__main__.C'> >>> >>> C().a <bound method Ca of <__main__.C object at 0x7f13b58ca9e8>> >>> get_class_that_defined_method(C().a) this is a method <class '__main__.C'> >>> >>> Cb <function Bb at 0x7f13b58dfe18> >>> get_class_that_defined_method(Cb) this is a function <class '__main__.B'> >>> >>> C().b <bound method Cb 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 ... >>> Zy <function x at 0x7f13b58dfa60> >>> get_class_that_defined_method(Zy) this is a function <function x at 0x7f13b58dfa60> >>> >>> Z().y <bound method Zx 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 >>> >>> Zz <function Z.<lambda>.<locals>.<lambda> at 0x7f13b58d40d0> >>> get_class_that_defined_method(Zz) 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'> 

Заключительные штрихи

  • Результат, сгенерированный Zy может быть частично исправлен (чтобы вернуть None ), подтвердив, что возвращаемое значение является классом, прежде чем фактически вернуть его.
  • Результат, сгенерированный Z().z может быть исправлен путем возврата к анализу атрибута __qualname__ функции (функция может быть извлечена с помощью meth.__func__ ).
  • Результат, полученный Z.class_meth и Z().class_meth неверен, поскольку доступ к методу класса всегда возвращает метод bound, чей атрибут __self__ является самим классом, а не его объектом. Таким образом, дальнейший доступ к __class__ поверх этого атрибута __self__ не работает __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' 

РЕДАКТИРОВАТЬ:

В соответствии с проблемой, поднятой Брайсом , можно обрабатывать объекты 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__ атрибут __objclass__ .

  • ZipFile.testzip () возвращает разные результаты на Python 2 и Python 3
  • Multiplex on queue.Queue?
  • Проверка регулярных выражений в Python с использованием py.test
  • В списке списков есть способ получить элементы в строке и столбце элемента одновременно?
  • Зачем распространять и устанавливать pip на мой файл ./local/bin?
  • Захват элементов с помощью селектора в скрипте python
  • Замена метода во время выполнения без обновления Частные атрибуты
  • Могу ли я использовать Python 3 super () в Python 2.5.6?
  • python 2 vs python 3 производительность случайных, особенно `random.sample` и` random.shuffle`
  • Как сделать регрессию OLS с последней версией Pandas
  • Разъяснение по типу Decimal в Python
  • Python - лучший язык программирования в мире.