Как назначение функции в качестве атрибута класса становится методом в Python? - PullRequest
10 голосов
/ 22 февраля 2010
>>> class A(object): pass
>>> def func(cls): pass
>>> A.func = func
>>> A.func
<unbound method A.func>

Как это назначение создает метод? Кажется неочевидным, что назначение делает следующее для классов:

  • Превратить функции в методы несвязанных экземпляров
  • Превратить функции, заключенные в classmethod(), в методы класса (на самом деле это довольно интуитивно понятно)
  • Превратить функции, заключенные в staticmethod(), в функции

Кажется, что для первого должен быть instancemethod(), а для последнего вообще не должно быть функции-обертки. Я понимаю, что они предназначены для использования внутри блока class, но почему они должны применяться вне его?

Но, что более важно, как именно назначение функции в классе работает? Какое волшебство происходит, что разрешает эти 3 вещи?

Еще более запутанно это:

>>> A.func
<unbound method A.func>
>>> A.__dict__['func']
<function func at 0x...>

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

Ответы [ 5 ]

4 голосов
/ 22 февраля 2010

Вы правы, что это как-то связано с протоколом дескриптора. Дескрипторы - это способ передачи объекта-получателя в качестве первого параметра метода в Python. Подробнее о поиске атрибутов Python можно прочитать по ссылке здесь . Ниже показано немного ниже, что происходит, когда вы делаете A.func = func; A.func:

# A.func = func
A.__dict__['func'] = func # This just sets the attribute
# A.func
#   The __getattribute__ method of a type object calls the __get__ method with
#   None as the first parameter and the type as the second.
A.__dict__['func'].__get__(None, A) # The __get__ method of a function object
                                    # returns an unbound method object if the
                                    # first parameter is None.
a = A()
# a.func()
#   The __getattribute__ method of object finds an attribute on the type object
#   and calls the __get__ method of it with the instance as its first parameter.
a.__class__.__dict__['func'].__get__(a, a.__class__)
#   This returns a bound method object that is actually just a proxy for
#   inserting the object as the first parameter to the function call.

Таким образом, поиск функции в классе или экземпляре превращает ее в метод, а не в присвоение атрибута класса.

classmethod и staticmethod - это просто немного разные дескрипторы, метод класса возвращает объект связанного метода, привязанный к объекту типа, а метод static просто возвращает исходную функцию.

4 голосов
/ 22 февраля 2010

Дескрипторы - это магия 1 , которая превращает обычную функцию в связанный или несвязанный метод при извлечении ее из экземпляра или класса, поскольку все они - просто функции, которые нуждаются разные стратегии связывания. Декораторы classmethod и staticmethod реализуют другие стратегии связывания, а staticmethod на самом деле просто возвращает необработанную функцию, которая является тем же поведением, которое вы получаете от нефункционального callable объекта.

См. «Пользовательские методы» для некоторых кровавых подробностей, но обратите внимание:

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

Так что, если вы хотите это преобразование для вашего собственного вызываемого объекта, вы можете просто обернуть его в функцию, но вы также можете написать дескриптор для реализации своей собственной стратегии связывания.

Вот действующий декоратор staticmethod, возвращающий базовую функцию при обращении к ней.

>>> @staticmethod
... def f(): pass
>>> class A(object): pass
>>> A.f = f
>>> A.f
<function f at 0x100479398>
>>> f
<staticmethod object at 0x100492750>

В то время как обычный объект с __call__ методом не преобразуется:

>>> class C(object):
...         def __call__(self): pass
>>> c = C() 
>>> A.c = c 
>>> A.c
<__main__.C object at 0x10048b890>
>>> c
<__main__.C object at 0x10048b890>

1 Специальная функция func_descr_get in Objects / funcobject.c .

3 голосов
/ 22 февраля 2010

Следует учитывать, что в Python все является объектом . Установив, что легче понять, что происходит. Если у вас есть функция def foo(bar): print bar, вы можете сделать spam = foo и позвонить spam(1), получив, конечно, 1.

Объекты в Python хранят свои атрибуты экземпляра в словаре с именем __dict__ с «указателем» на другие объекты. Поскольку функции в Python также являются объектами , они могут назначаться и управляться как простые переменные, передаваться другим функциям и т. Д. Реализация объектной ориентации в Python использует это преимущество и рассматривает методы как атрибуты, так как функции, которые находятся в __dict__ объекта.

Методы экземпляра ' первым параметром всегда является сам объект экземпляра , обычно называемый self (но это можно назвать this или banana). Когда метод вызывается непосредственно в class, он не привязан ни к какому экземпляру, поэтому вы должны указать ему объект экземпляра в качестве первого параметра (A.func(A())). Когда вы вызываете связанную функцию (A().func()), первый параметр метода, self, является неявным, но за кулисами Python делает то же самое, что непосредственно вызывает несвязанную функцию и передает экземпляр объекта в качестве первого параметра.

Если это понятно, тот факт, что присвоение A.func = func (которое за занавесом делает A.__dict__["func"] = func) оставляет вас несвязанным методом, неудивительно.

В вашем примере cls в def func(cls): pass на самом деле будет передаваться экземпляр (self) типа A. Когда вы применяете classmethod или staticmethod декораторы ничего не делаете, кроме как берете первый аргумент, полученный во время вызова функции / метода, и преобразуете это во что-то еще, прежде чем вызывать функцию.

classmethod принимает первый аргумент, получает объект class экземпляра и передает его в качестве первого аргумента в вызов функции, тогда как staticmethod просто отбрасывает первый параметр и вызывает функцию без него.

0 голосов
/ 22 февраля 2010

Относительно комментария MTsoul к ответу Габриэля Херли:

Отличие состоит в том, что func имеет метод __call__(), что делает его "вызываемым", то есть к нему можно применить оператор (). Проверьте документы по Python (найдите __call__ на этой странице).

0 голосов
/ 22 февраля 2010

Точка 1: определенная вами функция func существует как Объект первого класса в Python.

Пункт 2: Классы в Python хранят свои атрибуты в своих __dict__.

Так что же происходит, когда вы передаете функцию в качестве значения атрибута класса в Python? Эта функция хранится в классе '__dict__, что делает ее методом этого класса, доступным путем вызова имени атрибута, которому вы ее присвоили.

...