Как работает расширение классов (Monkey Patching) в Python? - PullRequest
12 голосов
/ 09 июня 2011
class Foo(object):
  pass

foo = Foo()
def bar(self):
  print 'bar'

Foo.bar = bar
foo.bar() #bar

Исходя из JavaScript, если прототип "класса" был дополнен определенным атрибутом.Известно, что все экземпляры этого «класса» будут иметь этот атрибут в своей цепочке прототипов, поэтому не нужно вносить никаких изменений ни в один из его экземпляров или «подклассов».

В этом смысле какможет ли язык на основе классов, такой как Python, добиться исправления Monkey?

Ответы [ 2 ]

11 голосов
/ 09 июня 2011

Реальный вопрос, как это не может? В Python классы являются первоклассными объектами сами по себе. Доступ к атрибутам в экземплярах класса разрешается путем поиска атрибутов в экземпляре, а затем в классе, а затем в родительских классах (в порядке разрешения методов). Все эти поиски выполняются во время выполнения (как и все в Python.) Если вы добавите атрибут в класс после создания экземпляра, экземпляр все равно будет «видеть» новый атрибут просто потому, что ничто не мешает ему.

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

10 голосов
/ 09 июня 2011

Я только что прочитал кучу документации, и, насколько я могу судить, вся история о том, как foo.bar разрешена, выглядит следующим образом:

  • Можем ли мы найти foo.__getattribute__ с помощью следующего процесса?Если это так, используйте результат foo.__getattribute__('bar').
    • (Поиск __getattribute__ не вызовет бесконечной рекурсии, но реализация этого может.)
    • (В действительности, мы всегда найдем __getattribute__ в объектах нового стиля, так какреализация по умолчанию предоставляется в object - но эта реализация имеет следующий процесс.;))
    • (Если мы определяем метод __getattribute__ в Foo и получаем доступ foo.__getattribute__, foo.__getattribute__('__getattribute__') будет вызываться! Но это не подразумевает бесконечную рекурсию - , если вы осторожны ;))
  • Является ли bar «специальным» именем для атрибута, предоставленного средой исполнения Python (например, __dict__, __class__, __bases__, __mro__)?Если так, используйте это.(Насколько я могу судить, __getattribute__ попадает в эту категорию, которая избегает бесконечной рекурсии.)
  • Является ли bar в диктате foo.__dict__?Если это так, используйте foo.__dict__['bar'].
  • Существует ли foo.__mro__ (т. Е. Действительно ли foo является классом)?Если это так,
    • Для каждого базового класса base в foo.__mro__ [1:]:
      • (Обратите внимание, что первым будет сам foo, который мы уже искали.)
      • Является ли bar в base.__dict__?Если так:
        • Пусть x будет base.__dict__['bar'].
        • Можем ли мы найти (опять же, рекурсивно, но это не вызовет проблемы) x.__get__?
          • Если это так, используйте x.__get__(foo, foo.__class__).
          • (Обратите внимание, что функция bar сама является объектом, и компилятор Python автоматически присваивает функциям атрибут __get__, который предназначендля использования таким образом.)
          • В противном случае используйте x.
  • Для каждого базового класса base из foo.__class__.__mro__:
    • (Обратите внимание, что эта рекурсия не является проблемой: эти атрибуты всегда должны существовать и попадать в случай «предоставленный Python runtime». foo.__class__.__mro__[0] всегда будет foo.__class__, то есть Foo в нашем примере.)
    • (Обратите внимание, что мы делаем это, даже если существует foo.__mro__. Это потому, что у классов тоже есть класс: его имя равно type, и, кроме всего прочего, предоставляет метод, используемый для вычисления __mro__ атрибутов.)
    • Является ли bar в base.__dict__?Если так:
      • Пусть x будет base.__dict__['bar'].
      • Можем ли мы найти (опять же, рекурсивно, но это не вызовет проблемы) x.__get__?
        • Если это так, используйте x.__get__(foo, foo.__class__).
        • (Обратите внимание, что функция bar сама по себе является объектом, и компилятор Python автоматически дает функциям атрибут __get__, который предназначениспользовать таким образом.)
        • В противном случае используйте x.
  • Если мы все еще не нашел что-то для использования: можем ли мы найти foo.__getattr__ с помощью предыдущего процесса?Если это так, используйте результат foo.__getattr__('bar').
  • Если все не удалось, raise AttributeError.

bar.__get__ на самом деле не функция - это«метод-обертка» - но вы можете представить, что он реализован примерно так:

# Somewhere in the Python internals
class __method_wrapper(object):
    def __init__(self, func):
        self.func = func
    def __call__(self, obj, cls):
        return lambda *args, **kwargs: func(obj, *args, **kwargs)
        # Except it actually returns a "bound method" object
        # that uses cls for its __repr__
    # and there is a __repr__ for the method_wrapper that I *think*
    # uses the hashcode of the underlying function, rather than of itself,
    # but I'm not sure.

# Automatically done after compiling bar
bar.__get__ = __method_wrapper(bar)

«Связывание», которое происходит внутри __get__, автоматически присоединяемого к bar (называемого дескриптор ), кстати, является более или менее причиной, по которой вы должны явно указывать self параметры для методов Python.В Javascript this само по себе волшебно;в Python это просто процесс привязки вещей к self, который является магическим.;)

И да, вы можете явно установить метод __get__ для ваших собственных объектов и заставить его делать особые вещи, когда вы устанавливаете атрибут класса для экземпляра объекта, а затемполучить доступ к нему из экземпляра этого другого класса.Питон чрезвычайно отражающий.:) Но если вы хотите научиться делать это и получить действительно полное понимание ситуации, у вас есть много чтения , чтобы сделать .;)

...