Как метод python автоматически получает "я" в качестве первого аргумента?

Рассмотрим этот пример шаблона стратегии в Python (адаптированный из примера здесь). В этом случае альтернативная стратегия является функцией.

class StrategyExample(object):
    def __init__(self, strategy=None) :
        if strategy:
             self.execute = strategy

    def execute(*args):
        # I know that the first argument for a method
        # must be 'self'. This is just for the sake of
        # demonstration 
        print locals()

#alternate strategy is a function
def alt_strategy(*args):
    print locals()

Ниже приведены результаты для стратегии по умолчанию.

>>> s0 = StrategyExample()
>>> print s0
<__main__.StrategyExample object at 0x100460d90>
>>> s0.execute()
{'args': (<__main__.StrategyExample object at 0x100460d90>,)}

В приведенном выше примере s0.execute - это метод (не простая ванильная функция), и, следовательно, первый аргумент в args, как и ожидалось, равен self.

Ниже приведены результаты для альтернативной стратегии.

>>> s1 = StrategyExample(alt_strategy)
>>> s1.execute()
{'args': ()}

В этом случае s1.execute является простой ванильной функцией и, как и ожидалось, не получает self. Следовательно, args пусто. Подожди минуту! Как это произошло?

И метод, и функция вызывались таким же образом. Как метод автоматически получает self в качестве первого аргумента? И когда метод заменяется простой ванильной функцией, как он не получает self в качестве первого аргумента?

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

>>> print dir(s0.execute)
['__cmp__', '__func__', '__self__', ...]
>>> print dir(s1.execute)
# does not have __self__ attribute

Есть ли атрибут __self__ в s0.execute (метод), но его отсутствие на s1.execute (функция) каким-то образом объясняет эту разницу в поведении? Как все это работает внутри?

Ответ 1

Вы можете прочитать полное описание здесь в ссылке на python в разделе "Определенные пользователем методы". Более короткое и простое объяснение можно найти в описании учебника python объектов метода:

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

В основном, что происходит в вашем примере:

  • функция, назначенная классу (как это происходит при объявлении метода внутри тела класса), является... методом.
    • При доступе к этому методу через класс, например. StrategyExample.execute вы получаете "несвязанный метод": он не "знает", к какому экземпляру он "принадлежит", поэтому, если вы хотите использовать его в экземпляре, вам нужно предоставить экземпляр как первый аргумент самостоятельно, например. StrategyExample.execute(s0)
    • При доступе к методу через экземпляр, например. self.execute или s0.execute, вы получаете "связанный метод": он "знает", к какому объекту он "принадлежит", и будет вызван с экземпляром в качестве первого аргумента.
  • функция, которую вы назначаете непосредственно атрибуту экземпляра, но, как и в self.execute = strategy или даже s0.execute = strategy, является... просто простой функцией (вопреки методу, она не проходит через класс)

Чтобы ваш пример работал одинаково в обоих случаях:

  • либо вы превратите функцию в "реальный" метод: вы можете сделать это с помощью types.MethodType:

    self.execute = types.MethodType(strategy, self, StrategyExample)
    

    (вы более или менее говорите классу, что, когда execute запрашивается для этого конкретного экземпляра, он должен превратить strategy в связанный метод)

  • или - если вашей стратегии действительно не нужен доступ к экземпляру, вы идете наоборот и превращаете исходный метод execute в статический метод (что делает его нормальной функцией еще раз: t вызывается с экземпляром в качестве первого аргумента, поэтому s0.execute() будет делать то же самое, что и StrategyExample.execute()):

    @staticmethod
    def execute(*args):
        print locals()
    

Ответ 2

Вам необходимо назначить несвязанный метод (т.е. с параметром self) классу или связанному методу с объектом.

Через механизм дескриптора вы можете создавать свои собственные методы привязки, а также почему он работает, когда вы назначаете (несвязанные) функции на класс:

my_instance = MyClass()    
MyClass.my_method = my_method

При вызове my_instance.my_method() поиск не найдет запись на my_instance, поэтому в конце концов это сделает: MyClass.my_method.__get__(my_instance, MyClass) - это протокол дескриптора. Это вернет новый метод, привязанный к my_instance, который затем выполняется с помощью оператора () после свойства.

Это будет распространять метод среди всех экземпляров MyClass, независимо от того, когда они были созданы. Однако они могли бы "скрывать" метод до того, как вы назначили это свойство.

Если вы хотите, чтобы определенные объекты имели этот метод, просто создайте связанный метод вручную:

my_instance.my_method = my_method.__get__(my_instance, MyClass)

Подробнее о дескрипторах (руководстве) см. здесь.

Ответ 3

Этот метод является оболочкой для функции и вызывает функцию с экземпляром в качестве первого аргумента. Да, он содержит атрибут __self__ (также im_self в Python до 3.x), который отслеживает, к какому экземпляру он привязан. Однако добавление этого атрибута к простой функции не сделает его методом; вам нужно добавить обертку. Вот как (хотя вы можете использовать MethodType из модуля types, чтобы получить конструктор, а не использовать type(some_obj.some_method).

Функция, обернутая, кстати, доступна через атрибут __func__ (или im_func) метода.

Ответ 4

Когда вы выполняете self.execute = strategy, вы устанавливаете атрибут на простой метод:

>>> s = StrategyExample()
>>> s.execute
<bound method StrategyExample.execute of <__main__.StrategyExample object at 0x1dbbb50>>
>>> s2 = StrategyExample(alt_strategy)
>>> s2.execute
<function alt_strategy at 0x1dc1848>

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

Смотрите: Python: привязать несвязанный метод?