Как расширяющие классы (Monkey Patching) работают в Python?

class Foo(object):
  pass

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

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

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

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


person Amjad Masad    schedule 08.06.2011    source источник


Ответы (2)


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

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

person Thomas Wouters    schedule 08.06.2011
comment
Я думал, что это невозможно сделать, и это отличало Ruby. - person OscarRyz; 09.06.2011
comment
Значит, диспетчеризация методов в динамических языках на основе классов работает так же, как и в языках на основе прототипов? - person Amjad Masad; 09.06.2011
comment
Итак, отправка метода... работает одинаково... ? Это зависит от того, насколько вы хотите обобщить. Смотрите мой ответ, чтобы узнать, как это работает конкретно для Python. Другие динамические языки, основанные на классах, не обязательно делают то же самое. Другие языки, основанные на прототипах, не обязательно делают то же самое, что и Javascript, FWIW. - person Karl Knechtel; 09.06.2011
comment
вместо того, чтобы все это делается во время выполнения, более полезно сказать, что все разрешения имен выполняются во время доступа, а не во время определения или времени создания экземпляра. - person Lie Ryan; 27.02.2013

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

  • Can we find foo.__getattribute__ by the following process? If so, use the result of foo.__getattribute__('bar').
    • (Looking up __getattribute__ will not cause infinite recursion, but the implementation of it might.)
    • (На самом деле мы всегда будем находить __getattribute__ в объектах нового стиля, поскольку реализация по умолчанию предоставляется в object, но эта реализация относится к следующему процессу. ;)) )
    • (Если мы определим метод __getattribute__ в Foo, а доступ foo.__getattribute__, foo.__getattribute__('__getattribute__') будет вызываться! Но это не подразумевает бесконечную рекурсию — если вы будете осторожны. эм> ;))
  • Является ли bar «специальным» именем для атрибута, предоставляемого средой выполнения Python (например, __dict__, __class__, __bases__, __mro__)? Если это так, используйте это. (Насколько я могу судить, __getattribute__ попадает в эту категорию, что позволяет избежать бесконечной рекурсии.)
  • Есть ли bar в словаре foo.__dict__? Если да, используйте foo.__dict__['bar'].
  • Does foo.__mro__ exist (i.e., is foo actually a class)? If so,
    • For each base-class base in foo.__mro__[1:]:
      • (Note that the first one will be foo itself, which we already searched.)
      • Is bar in base.__dict__? If so:
        • Let x be base.__dict__['bar'].
        • Can we find (again, recursively, but it won't cause a problem) x.__get__?
          • If so, use x.__get__(foo, foo.__class__).
          • (Обратите внимание, что функция bar сама по себе является объектом, и компилятор Python автоматически присваивает функциям атрибут __get__, предназначенный для такого использования.)
          • В противном случае используйте x.
  • For each base-class base of foo.__class__.__mro__:
    • (Note that this recursion is not a problem: those attributes should always exist, and fall into the "provided by the Python runtime" case. foo.__class__.__mro__[0] will always be foo.__class__, i.e. Foo in our example.)
    • (Обратите внимание, что мы делаем это, даже если foo.__mro__ существует. Это потому, что у классов тоже есть класс: его имя type, и он, среди прочего, предоставляет метод, используемый для вычисления атрибутов __mro__ в первую очередь.)
    • Is bar in base.__dict__? If so:
      • Let x be base.__dict__['bar'].
      • Can we find (again, recursively, but it won't cause a problem) x.__get__?
        • If so, use 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__ для своих собственных объектов и заставить его выполнять особые действия, когда вы устанавливаете атрибут класса для экземпляра объекта, а затем получаете к нему доступ из экземпляра этого другого класса. . Python чрезвычайно рефлексивен. :) Но если вы хотите узнать, как это сделать, и получить действительно полное представление о ситуации, вы много чтения делать. ;)

person Karl Knechtel    schedule 08.06.2011
comment
Спасибо @karl, это очень полезно и несколько запутанно :) - person Amjad Masad; 09.06.2011