В ранних версиях Python (до 2.2) методы представляли собой типизированные объекты, отличные от функций, что требовало явных проверок типов для обработки связанного и несвязанного состояний. Введение новых стилей классов и единой модели типов/классов в Python 2.2 устранило тип метода как отдельную сущность для функций, перенесли ответственность за связывание на протокол дескрипторов. Эта эволюция позволила функциям реализовывать __get__, создавая связанные методы динамически только при доступе через экземпляры, тем самым упрощая объектную модель языка и снижая внутреннюю сложность типов.
Когда пользователь определяет метод внутри класса, основной объект, хранящийся в словаре класса, представляет собой обычную функцию, ожидающую self в качестве первого аргумента. Проблема заключается в том, чтобы гарантировать, что когда этот атрибут извлекается через экземпляр (например, obj.method), Python прозрачно создает вызываемый объект, который автоматически предоставляет этот экземпляр в качестве первого позиционного аргумента, не требуя ручного частичного применения или обертки. Это должно происходить эффективно при каждом доступе к атрибуту, сохраняя возможность доступа к несвязанной функции через класс (например, Class.method) для явной передачи self или инспекции наследования.
Функции реализуют протокол дескрипторов через метод __get__. Когда к ним обращаются в классе (None экземпляр), __get__ возвращает сам объект функции. Когда к ним обращаются через экземпляр, __get__(self, instance, owner) возвращает объект method, который инкапсулирует как функцию, так и экземпляр. При вызове этот связанный метод добавляет экземпляр в кортеж аргументов перед вызовом основной функции.
class Demo: def compute(self, value): return value * 2 d = Demo() # Доступ к классу возвращает сырую функцию unbound = Demo.__dict__['compute'] print(type(unbound)) # <class 'function'> # Доступ к экземпляру вызывает __get__, возвращая связанный метод bound = unbound.__get__(d, Demo) print(type(bound)) # <class 'method'> print(bound(5)) # 10, эквивалентно d.compute(5)
Разработка системы высокочастотной торговли требует, чтобы объекты стратегий регистрировали обработчики обновления цен с источником рыночных данных. Изначально разработчики передавали strategy.on_price_update как ссылку на обратный вызов. Во время нагрузочного тестирования профилирование памяти показало, что удаленные стратегии не подлежат сбору мусора, поскольку источник удерживал ссылки на связанные методы, создавая случайные циклы сильных ссылок, которые сохранялись на протяжении всего времени работы приложения.
Один из подходов заключался в том, чтобы хранить слабые ссылки на стратегию и несвязанную функцию отдельно, а затем вручную комбинировать их во время вызова. Это предотвращает круговые ссылки и позволяет немедленно собирать мусор для abandoned стратегий. Однако это вводит сложную логику вызова обратных вызовов, потенциальные гонки, если объект будет собран между проверкой на живучесть и вызовом, и нарушает интуитивно понятную идиому передачи методов в Python.
Еще один вариант заключался в преобразовании on_price_update в @staticmethod и явной передачи экземпляра стратегии во время регистрации. Это упрощает управление ссылками, полностью избегая создания связанных методов. К сожалению, это нарушает принципы объектно-ориентированной инкапсуляции, принуждает к изменениям в API регистрации, чтобы принимать как функцию, так и экземпляр отдельно, и производит менее читаемый код, который затемняет связь между стратегией и ее обработчиком.
Мы рассматривали возможность реализации пользовательского дескриптора, возвращающего объект, похожий на связанный метод, удерживающий слабую ссылку на экземпляр, а не сильную. Это сохраняет синтаксис вызова obj.method и предотвращает утечки памяти, оставаясь при этом идиоматичным с точки зрения вызывающего. Недостатком является необходимость глубоких знаний протокола дескрипторов для правильной реализации и небольшие накладные расходы на проверку времени жизни ссылок при каждом вызове.
Мы выбрали Решение 3, реализовав дескриптор WeakMethod, который имитирует стандартное связывание функции, но использует weakref.ref для экземпляра. Это позволило источнику рыночных данных удерживать обратные вызовы, не препятствуя сбору мусора стратегии. Подход сохранил чистый код регистрации: feed.register(ticker, strategy.on_price_update).
Эта оптимизация устранила утечки памяти в долгосрочных торговых сессиях и сократила использование памяти на 40% во время обратного тестирования с миллионами временных экземпляров стратегии. Система сохранила чистый дизайн объектно-ориентированного API, не требуя от пользователей понимания сложностей управления ссылками. В конечном итоге понимание механизма создания связанных методов оказалось жизненно важным для разработки программного обеспечения финансового класса.