W wczesnych wersjach Pythona (przed-2.2), metody były typowanymi obiektami odrębnymi od funkcji, wymagającymi jawnych sprawdzeń typów w celu obsługi powiązanych i niepowiązanych stanów. Wprowadzenie klas nowego stylu i zjednoczonego modelu typów/klas w Pythonie 2.2 wyeliminowało typ metody jako osobny byt dla funkcji, przenosząc odpowiedzialność za powiązanie do protokołu deskryptora. Ta ewolucja pozwoliła funkcjom zaimplementować __get__, tworząc powiązane metody dynamicznie tylko w momencie uzyskania dostępu przez instancje, co uprościło model obiektowy języka i zmniejszyło wewnętrzną złożoność typów.
Gdy użytkownik definiuje metodę wewnątrz klasy, podstawowy obiekt przechowywany w słowniku klasy jest zwykłą funkcją oczekującą self jako pierwszy argument. Wyzwanie polega na zapewnieniu, że gdy atrybut ten jest pobierany przez instancję (np. obj.method), Python przezroczysto tworzy wywoływalny obiekt, który automatycznie dostarcza tę instancję jako pierwszy argument pozycyjny bez konieczności ręcznego stosowania częściowych aplikacji lub kodu opakowującego. Musi to odbywać się wydajnie przy każdym dostępie do atrybutu, zachowując jednocześnie możliwość uzyskania dostępu do niepowiązanej funkcji przez klasę (np. Class.method) w celu jawnego przekazywania self lub inspekcji dziedziczenia.
Funkcje implementują protokół deskryptora za pomocą swojej metody __get__. Gdy jest uzyskiwana na klasie (None instancja), __get__ zwraca sam obiekt funkcji. Gdy jest uzyskiwana na instancji, __get__(self, instance, owner) zwraca obiekt method, który kapsułkuje zarówno funkcję, jak i instancję. Po wywołaniu ta powiązana metoda dodaje instancję do krotki argumentów przed wywołaniem podległej funkcji.
class Demo: def compute(self, value): return value * 2 d = Demo() # Dostęp do klasy zwraca surową funkcję unbound = Demo.__dict__['compute'] print(type(unbound)) # <class 'function'> # Dostęp przez instancję wywołuje __get__, zwracając powiązaną metodę bound = unbound.__get__(d, Demo) print(type(bound)) # <class 'method'> print(bound(5)) # 10, co odpowiada d.compute(5)
Opracowywanie systemu handlu o wysokiej częstotliwości wymaga, aby obiekty strategii zarejestrowały obsługiwacze aktualizacji cen z kanałem danych rynkowych. Początkowo deweloperzy przekazywali strategy.on_price_update jako odwołanie do wywołania zwrotnego. Podczas testów obciążeniowych profilowanie pamięci ujawniło, że usunięte strategie nie były zbierane przez garbage collector, ponieważ kanał trzymał odniesienia do powiązanych metod, co skutkowało niezamierzonymi cyklami silnych odniesień, które trwały przez czas życia aplikacji.
Jedno z podejść polegało na przechowywaniu słabych odniesień do strategii i niepowiązanej funkcji osobno, a następnie ręcznym ich łączeniu w czasie wywołania. To zapobiega cyklom odniesień i umożliwia natychmiastowe zbieranie garbage collector porzuconych strategii. Jednakże wprowadza to złożoną logikę wywoływania zwrotnych, potencjalne warunki wyścigu, jeśli obiekt jest zbierany między sprawdzeniem żywotności a wywołaniem, oraz łamie intuicyjny idiom przekazywania metod przez Pythona.
Inną opcją było przekształcenie on_price_update w @staticmethod i przekazanie instancji strategii jawnie podczas rejestracji. To upraszcza zarządzanie odniesieniami, unikając całkowicie tworzenia powiązanych metod. Niestety to narusza zasady enkapsulacji obiektowej, zmusza do wprowadzenia zmian w API rejestracyjnym, aby akceptowało zarówno funkcję, jak i instancję osobno, a także produkuje mniej czytelny kod, który zaciemnia związek między strategią a jej obsługiwaczem.
Zastanawialiśmy się nad wdrożeniem własnego deskryptora zwracającego obiekt podobny do powiązanej metody, która przechowuje słabe odniesienie do instancji zamiast silnego. To utrzymuje składnię wywołania obj.method i zapobiega wyciekom pamięci, pozostając przy tym idiomatycznym z perspektywy wywołującego. Wadą jest wymóg głębokiej wiedzy o protokole deskryptora, aby wdrożyć go poprawnie, i niewielkie narzuty związane z sprawdzaniem żywotności odniesienia przy każdym wywołaniu.
Wybraliśmy Rozwiązanie 3, wdrażając deskryptor WeakMethod, który imituje standardowe powiązanie funkcji, ale używa weakref.ref dla instancji. To pozwoliło kanałowi danych rynkowych przechowywać wywołania zwrotne, nie uniemożliwiając zbierania garbage collector strategii. Podejście zachowało czysty kod rejestracji: feed.register(ticker, strategy.on_price_update).
Ta optymalizacja wyeliminowała wycieki pamięci w długoterminowych sesjach handlowych i zmniejszyła zużycie pamięci o 40% podczas testów wstecznych z milionami przejściowych instancji strategii. System utrzymał czysty projekt API zorientowany na obiekty bez wymagania od użytkowników rozumienia złożoności zarządzania odniesieniami. Ostatecznie zrozumienie mechanizmu tworzenia powiązanych metod okazało się niezbędne do budowy oprogramowania finansowego klasy produkcyjnej.
Dlaczego przechowywanie powiązanej metody w długożyjącym kontenerze zapobiega zbieraniu garbage collector skojarzonej instancji, nawet po zniknięciu wszystkich pierwotnych odniesień?
Obiekt powiązanej metody utrzymuje wewnętrzny atrybut __self__, który przechowuje silne odniesienie do instancji. Gdy jest przechowywany w globalnym rejestrze lub pamięci podręcznej, metoda utrzymuje instancję osiągalną na zawsze. Aby tego uniknąć, deweloperzy muszą używać weakref.WeakMethod lub przechowywać niepowiązane funkcje z osobnymi słabymi odniesieniami instancji.
W jaki sposób implementacja __get__ deskryptora @classmethod różni się od standardowych funkcji, aby umożliwić polimorficzne metody fabryczne?
classmethod jest deskryptorem nie-danych, który wiąże klasę owner z pierwszym argumentem, a nie instancją. Gdy jest uzyskiwana na podklasie, otrzymuje tę podklasę jako cls, umożliwiając alternatywne konstruktory, które instancjonują właściwy typ pochodny. Sytuacja ta kontrastuje z metodami statycznymi, które nie otrzymują automatycznego powiązania i nie mogą określić klasy wywołującej bez jawnej inspekcji.
Jakie obciążenie występuje na poziomie CPython, gdy wielokrotnie uzyskuje się dostęp do metod instancji w ciasnych pętlach, i dlaczego pamięć podręczna metod poprawia wydajność?
Każdy dostęp do obj.method uruchamia protokół deskryptora, przydzielając nowy PyMethodObject na stercie zawierający wskaźniki do funkcji i instancji. To powtarzające się alokowanie i dealokowanie generuje znaczne obciążenie w pętlach o wysokiej częstotliwości. Pamięć podręczna powiązanej metody poza pętlą ponownie używa tego samego obiektu, eliminując koszty wyszukiwania deskryptora i zmniejszając czas wykonania o 20-30% w mikrobenchmarkach.