Как назначение функции в качестве атрибута класса становится методом в Python?

>>> class A(object): pass
>>> def func(cls): pass
>>> A.func = func
>>> A.func
<unbound method A.func>

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

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

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

Но что более важно, как именно работает присвоение функции классу? Какая магия решает эти три проблемы?

Еще больше смущает вот это:

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

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


person Oliver Zheng    schedule 21.02.2010    source источник
comment
Вы забываете, что методы экземпляра — это просто переменные экземпляра, содержащие функцию, первым аргументом которой является сам экземпляр.   -  person Esteban Küber    schedule 22.02.2010
comment
Что, черт возьми, переменная экземпляра?!   -  person Oliver Zheng    schedule 22.02.2010
comment


Ответы (5)


Вы правы, что это как-то связано с протоколом дескриптора. Дескрипторы — это то, как передача объекта получателя в качестве первого параметра метода реализована в Python. Вы можете прочитать более подробную информацию о поиске атрибутов Python из ">здесь. Ниже показано на более низком уровне, что происходит, когда вы выполняете A.func = 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 — это просто немного разные дескрипторы, classmethod возвращает связанный объект метода, привязанный к объекту типа, а staticmethod просто возвращает исходную функцию.

person Ants Aasma    schedule 22.02.2010
comment
Спасибо, я думаю, что это самый правильный ответ здесь. Более подробный анализ я нашел здесь: cafepy.com/article/python_attributes_and_methods/ . Ключ (магия) здесь в том, что функция становится методом, когда к ней обращаются, а не устанавливают, и это происходит с помощью дескриптора __get__, доступного для каждого функционального объекта. classmethod и staticmethod имеют разные __get__, которые преобразуют функцию в класс/статический метод. Просто так получается, что __get__ по умолчанию для функций действуют как методы экземпляра. - person Oliver Zheng; 22.02.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 в Объекты/funcobject.c.

person Josh Lee    schedule 21.02.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 просто отбрасывает первый параметр и вызывает функцию без него.

person Esteban Küber    schedule 21.02.2010

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

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

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

person Gabriel Hurley    schedule 21.02.2010
comment
Вопрос в том, как сделать его методом этого класса? Что в этом особенного, чего A.a = 3 не делает? - person Oliver Zheng; 22.02.2010
comment
@MTsoul, разница в том, что callable(func) это True, а callable(3) это False. - person John La Rooy; 22.02.2010
comment
@gnibbler Это не так - создание объекта с помощью метода __call__ не вызывает этого. - person Josh Lee; 22.02.2010

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

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

person Rune    schedule 21.02.2010
comment
Нет, объект с методом __call__ не превращается волшебным образом в instancemethod. - person Josh Lee; 22.02.2010
comment
@jleedev: он превращает его не в метод экземпляра, а в вызываемый. Будучи вызываемым, который является атрибутом объекта, он действительно превращает его в метод экземпляра. - person Esteban Küber; 22.02.2010
comment
@voyager codepad.org/eOI0Hf88 Callable просто означает, что у него есть метод __call__, но фактическая привязка метода обрабатывается дескрипторы. Смотрите мой ответ. - person Josh Lee; 22.02.2010