PythonProgramlamaKıdemli Python Geliştirici

Python'ın sıfır argümanlı `super()`'unun bir yöntemin birden fazla alt sınıfa miras alındığında tanımlayıcı sınıfı nasıl doğru bir şekilde çözümlediği nedir?

Hintsage yapay zeka asistanı ile mülakatları geçin

Sorunun cevabı

Sıfır argümanlı super(), herhangi bir Python sınıfı tanımında sözel olarak tanımlanan bir yöntem için otomatik olarak oluşturulan bir derleyici kaynaklı kapatma hücresi olan __class__'a dayanır. Derleyici bir sınıf tanımını işlediğinde, o sınıf nesnesini gösteren __class__ adında bir hücre değişkeni oluşturur. super() argüman olmadan çağrıldığında, C implementasyonu çağırma çerçevesini inceler, bu __class__ hücresini bulur ve bunu ilk argüman (tip) olarak kullanır. Daha sonra yöntemin ilk konumsal argümanını (genellikle self) örnek olarak kullanır. Bu mekanizma sınıf referansını tanım zamanında bağlar, çağrı zamanında değil; bu, her miras alma zincirindeki yöntemin kendi belirli konumunu MRO'da (Yöntem Çözümleme Sırası) referans gösterir.

class Base: def method(self): return "Base" class Middle(Base): def method(self): # __class__ burada Middle'e bağlanır return f"Middle -> {super().method()}" class Derived(Middle): def method(self): # __class__ burada Derived'e bağlanır return f"Derived -> {super().method()}"

Gerçek hayattan bir durum

Derin bir fiyatlandırma modeli hiyerarşisine sahip niceliksel bir ticaret kütüphanesi sürdürdük. BaseModel, calculate_risk() yöntemini sağlar, EquityModel bunu hisse senedi özel mantığı ekleyecek şekilde geçersiz kılarken, AmericanOptionModel daha da özelleştirdi. EquityModel'i VanillaEquityModel olarak yeniden adlandırmak için büyük bir yeniden düzenleme sırasında, kopyalanmış olan mixin sınıflarında eski super(EquityModel, self) çağrılarının onlarcasını keşfettik. Bu eski referanslar, TypeError veya yanlış üst sınıf yönteminin çağrıldığı, üretim risk hesaplamalarını kıran sessiz mantıksal hatalara neden oldu.

Çözüm 1: Küresel arama ve değiştirme yeniden yapılandırması. 200,000 satırlık kod tabanında tüm hardcoded sınıf isimlerini bulmak ve değiştirmek için otomatik araçlar kullanmayı düşündük. Artıları: Mimari değişiklik gerektirmez ve eski Python 2 sözdizimi ile çalışır. Eksileri: Kırılgandır ve tamamlanmamıştır; dinamik olarak üretilen sınıfları, string tabanlı dinamik yöntem atamalarını ve üçüncü parti eklentilerdeki referansları atlar. Ayrıca, sınıf adı her yöntemde tekrarlandığı için DRY ilkesini ihlal eder.

Çözüm 2: Sıfır argümanlı super()'ün evrensel benimsenmesi. Tüm kod tabanını argüman olmadan super() kullanacak şekilde geçirdik. Artıları: Bu, sınıf yeniden adlandırmayı tamamen güvenli hale getirir, yeniden yapılandırma sırasında insan hatalarının büyük bir kaynağını ortadan kaldırır ve gereksiz gürültüyü gidererek okunabilirliği önemli ölçüde artırır. Karmaşık işbirlikçi çoklu miras desenlerini doğru bir şekilde işler. Eksileri: Python 3.6+ gerektirir (ki bizde vardı) ve kapatma mekanizmasını aşina olmayan geliştiriciler başlangıçta kafa karıştırıcı buldu. Tanım sonrası sınıflara dinamik olarak eklenen fonksiyonlarda kullanılamaz.

Çözüm 3: Sınıf referansları için metaclass enjekte etme. Her yönteme _defining_class niteliği eklemek için bir metaclass kullanmayı kısaca düşündük. Artıları: Bu mekanizmayı açık ve denetlenebilir hale getirir. Eksileri: Önemli bir karmaşıklık ve yük getirir, standart CPython optimizasyonlarıyla çelişir ve dil derleyicisi tarafından zaten sağlanan bir özelliği yeniden icat eder.

Çözüm 2'yi seçtik. Geçiş bir sprint süresince tamamlandı. Sonuç olarak, sınıf yeniden adlandırmasıyla ilgili sonraki yeniden yapılandırma görevlerinde harcanan sürede %40'lık bir azalma sağlandı ve CI pipeline'ımızda bozuk eski super() referanslarına bağlı bir hata sınıfı ortadan kaldırıldı.

Adayların sıklıkla kaçırdığı noktalar

Sıfır argümanlı super() çağrıldığında __class__ hücresini fiziksel olarak nasıl bulur?

CPython'da super() implementasyonu ( Objects/typeobject.c içindeki) PyEval_GetLocals() kullanarak çağırma çerçevesinin yerel değişkenlerini ve kapanmasını inceler. Özellikle __class__ adında bir serbest değişken (hücre) arar. Bu hücre, yalnızca bir fonksiyon bir sınıf tanımının sözel olarak iç kısmında tanımlandığında derleyici tarafından oluşturulur (bu, CO_OPTIMIZED bayrağı ve sınıf kapsamı ile gösterilir). Eğer hücre bulunursa, super() sınıf nesnesini çıkarır; eğer bulunmazsa, RuntimeError: super(): __class__ cell not found hatası verir. Sıfır argümanlı biçim aslında derleyici tarafından super(__class__, self) olarak dönüştürülür; burada __class__ kapanmış değişkendir.

Sınıf oluşturulmadan sonra bir sınıf niteliğine atanmış bir fonksiyon içinde sıfır argümanlı super() kullanmaya çalışırsanız ne olur?

Bir fonksiyonu sınıf gövdesinin dışında tanımlayıp daha sonra onu bir yöntem olarak atarsanız (örneğin, MyClass.method = some_function), o fonksiyonun içinde super() çağrıldığında bir RuntimeError hatası verir. Bu durum, derleyicinin yalnızca bir sınıf kümesi kapsamında derlenmiş olan kod nesneleri için __class__ hücresini oluşturmasından kaynaklanır. Hücre olmadan, super() hiyerarşideki hangi sınıfın "mevcut" sınıf olduğunu belirleyemez; çünkü fonksiyonun tanım kapsamı ile daha sonra eklendiği sınıfı ayırt edemez.

Neden sıfır argümanlı super() bir alt sınıf yöntemi super() çağırdığında ve üst sınıf yöntemi de super() çağırdığında sonsuz rekursyona neden olmaz?

Bu çalışır çünkü __class__, yöntemin tanımlandığı sınıfa atıfta bulunur, örneğin, nesnenin çalışma zamanı sınıfı değil (type(self)). Derived.method() çağrıldığında super()'u bulur, __class__'ın Derived olduğunu görür ve Derived.__mro__'da sonraki sınıfa yönlendirir (örneğin, Middle). Middle.method()'a ulaşıldığında ve super() çağrıldığında, kendi farklı __class__ hücresi Middle içerdiğinden Middle'dan sonraki sınıfa bakar (örneğin, Base). Hiyerarşinin her seviyesi, kendi tanım zamanındaki sınıf referansını kullanır, bu da MRO'nun yukarı doğru tam olarak bir kez doğru bir şekilde geçilmesini sağlar ve alt sınıfa geri dönmesini engeller.