__prepare__ yöntemi, sınıf oluşturma protokolündeki temel sınırlamaları ele almak için Python 3.0 ile birlikte PEP 3115 aracılığıyla tanıtıldı. Bu değişiklikten önce, sınıf gövdesinin yürütülmesi sırasında kullanılan isim alanı daima standart bir sözlük olup, özellik bildirim sırasını koruma veya atamaları gerçekleştiği anda engelleme olanağı sunmuyordu. Bu, özellikle özellik bildirimlerinin sıradaki dizisini takip etmeleri gereken ORM ve serileştirme kütüphaneleri geliştiren geliştiriciler için sorun haline geldi.
Python bir sınıf gövdesini yürüttüğünde, nihayetinde sınıf __dict__'sine dönüşecek bir isim alanı eşleme ile doldurur. Varsayılan dict türü, eski Python sürümlerinde ekleme sırasını garanti etmez ve adları tanımlandıkları anda doğrulamak veya dönüştürmek için kancalara sahip değildir. Bildirim zamanı kısıtlamalarına gereksinim duyan geliştiriciler—örneğin belirli adlandırma desenlerini yasaklamak veya ikili protokoller için alan sırasını takip etmek—sınıf nesnesi nihayete ermeden bu belirli sınıf oluşturma aşamasına müdahale etmek için temiz bir mekanizmaya sahip değildiler.
__prepare__'i bir metaklas içinde statik bir yöntem olarak uygulayarak, isim alanı olarak hizmet vermek üzere özel bir değiştirilebilir eşleme (örneğin collections.OrderedDict veya özel bir doğrulayıcı sözlük) döndürebilirsiniz. Bu eşleme, gövde yürütülmesi sırasında tüm sınıf düzeyi atamalarını yakalayarak, metaklasın __new__ yöntemi sınıfı nihayete erdirmeden önce ön işlemeye olanak tanır. Özel isim alanı daha sonra __new__'e iletilir, burada standart bir dict'e dönüştürülebilir veya sıralı erişim için korunabilir.
from collections import OrderedDict class OrderPreservingMeta(type): @staticmethod def __prepare__(name, bases, **kwargs): return OrderedDict() def __new__(mcs, name, bases, namespace, **kwargs): ordered_attrs = list(namespace.keys()) cls = super().__new__(mcs, name, bases, dict(namespace)) cls._declaration_order = ordered_attrs return cls class Schema(metaclass=OrderPreservingMeta): id = 1 name = "test" value = 3.14 print(Schema._declaration_order) # ['id', 'name', 'value']
Bir finansal ticaret platformu, protokol başlığındaki alan sırasının Python mesaj sınıfı tanımındaki bildirim sırasıyla kesin olarak eşleştiği ikili mesaj formatları oluşturmak zorundaydı. Alanları yeniden sıralamak, borsa tarafındaki eski C++ ayrıştırıcılarla uyumluluğu bozacak ve ticaret reddi veya sistem çökmesine yol açacaktı.
Çözüm A: Manuel dizinleme. Geliştiriciler, her alanı field_order = 1 gibi bir sıra numarası ile anotlayacaktı. Bu yaklaşım açık ve yeni başlayanlar için kolay anlaşılır. Ancak, DRY ilkesini ihlal eder ve yeniden yapılandırma sırasında bir bakım yükü haline gelir, çünkü ortada bir alan eklemek, tüm sonraki alanların numaralandırılmasını gerektirir.
Çözüm B: Kaynak kodu ayrıştırma. Çerçeve, sınıf tanımının kaynağını ayrıştırmak için AST modülünü kullanabilir ve atama sırasını çıkarabilir. Bu, metaklas karmaşıklığı olmadan çalışır. Ne yazık ki, çalıştığında kaynak dosyaların mevcut olmadığı durumlarda tamamen başarısız olur; örneğin, dondurulmuş ikili dağıtımlarda veya kaynak kodunu temizleyen optimize edilmiş CPython dağıtımlarında.
Çözüm C: __prepare__ ile Metaklas. __prepare__'den bir OrderedDict döndürerek, metaklas doğal bildirim sırasını otomatik olarak yakalar. Bu, tüm dağıtım senaryolarında sağlamdır ve son kullanıcılara şeffaftır. Tek olumsuz nokta, Python'un metaklas protokolünü anlamanın ek karmaşıklığıdır; bu da üst düzey bilgi gerektirir.
Seçilen Çözüm: Ekip, çözüm C'yi seçti çünkü bu, her mesaj örneği için çalışma zamanında yük olmadan tanım zamanı garantileri sağlar. Kaynak kodu olmayan ortamlarda bile güvenilir bir şekilde çalışır ve geliştiricilerin beklediği doğal sınıf sözdizimini korurken, mümkün olan en erken aşamada kısıtlamaları uygulayarak sorunu çözer.
Sonuç: Mesaj kütüphanesi otomatik olarak tel-şema uyumluluğunu korudu. Geliştiriciler doğal sınıf tanımları yazdılar ve sistem doğru ikili düzenleri oluşturdu. Kalıtım hiyerarşileri, çocuk alanlardan önce ebeveyn alan sırasını doğru bir şekilde korudu ve manuel müdahale olmadan ticaret protokolü spesifikasyonundaki karmaşık bir sorunu çözdü.
Soru 1: __prepare__'in neden bir @staticmethod (veya @classmethod) olarak tanımlanması gerektiğini ve bu dekoratörü atladığınızda hangi hatanın oluştuğunu açıklayın.
Cevap: __prepare__ metaklas nesnesi oluşturulmadan önce çağrıldığından, o anda bağlanacak herhangi bir cls veya self mevcut değildir. Python, __new__'e iletilecek olan isim alanını oluşturmak için __prepare__'i çağırır. Eğer self bekleyen bir düzenli örnek yöntemi olarak tanımlanırsa, Python bir TypeError hatası fırlatır ve bunun nedeni bu işlevin konumsal argümanlar aldığını ancak hiç verilmediğini belirtir, çünkü makina sadece isim, temeller ve anahtar kelime argümanları ile onu çağırmaya çalışır. İlk-argüman bağlanması olmaksızın çağrılmak için bir statik yöntem olmalıdır, ancak metaklasın kendisine erişim gereksiniminiz varsa classmethod iş görecektir.
Soru 2: __prepare__, dict alt sınıfı olmayan bir eşleme döndürebilir mi ve sınıf gövdesi yürütülürken düzgün çalışmak için hangi belirli protokolü sağlaması gerekir?
Cevap: Evet, MutableMapping soyut temel sınıf protokolünü uygulayan herhangi bir değiştirilebilir eşleme döndürebilir, özellikle __setitem__, __getitem__, __contains__ ve ideal olarak __iter__ veya keys() dönüşümü gerektirir. Ancak, bu eşleme dict'ten miras almak zorunda değildir. Kritik gereklilik, dize anahtarlarını ve rastgele değerleri kabul etmelidir; sınıf gövdesindeki özellik atamaları sırasında bir sözlük gibi davranmalıdır. Sınıf yürütülmesinden sonra, metaklas __new__ bu eşlemeyi alır; eğer dict alt sınıfı değilse, super().__new__ çağrılmadan önce açıkça dönüştürmelisiniz (örneğin dict(namespace)) çünkü oluşan sınıf nesnesinin __dict__'si bir sözlük olmalıdır.
Soru 3: __prepare__, sınıf tanım başlığında iletilen anahtar kelime argümanlarını (örneğin, class MyClass(metaclass=Meta, strict=True)) nasıl ele alır ve bunlar doğru bir şekilde iletilmezse ne olur?
Cevap: Sınıf başlığındaki anahtar kelime argümanları (metaklasın dışındaki) __prepare__'e **kwds olarak iletilir. Eğer __prepare__, **kwargs (veya belirli adlandırılmış argümanlar) kabul etmezse, Python beklenmedik bir anahtar kelime argümanı aldığına dair bir TypeError hatası fırlatır. Bu, metaklaslara yapılandırma seçenekleri eklerken yaygın bir tuzaktır. Yöntem imzası, __prepare__(name, bases, **kwargs) olmalıdır ki ileriye dönük uyumlu olabilsin. Bu anahtar kelimeler ayrıca __new__ ve __init__'e de aktarılır ve metaklas, isim alanı davranışını özelleştirmek için hazırlık zamanında yapılandırma almak üzere bu anahtar kelimeleri alır (örneğin, katı ve toleranslı doğrulama modları arasında seçim yapma).