Sorunun geçmişi. Python 3.7'den önce, genel türleri uygulamak için getitem'i ele alan karmaşık bir metaklas TypingMeta gerekiyordu, bu da List[int] gibi alt çizim işlemlerini yönetiyordu. Bu yaklaşım yavaştı, typing modülünün içinde döngüsel bağımlılıklar yaratıyordu ve her genel işlem, ağır metaklas mantığına geçtiği için hata ayıklamayı zor hale getiriyordu. PEP 560 bu performans ve mimari sorunları çözmek için özel bir protokol tanıttı.
Sorun. Genel sınıfların (örneğin, List[int]'de int gibi) sınıf seviyesinde tür argümanları kabul etmesi gerekir, bu da gerçek örnekler oluşturmadan statik tür denetimi ve çalışma zamanı incelemesine olanak tanır. Mücadele, bu argümanları, genel kaynağı ile parametreleri arasındaki ilişkiyi koruyan hafif bir nesne içinde saklamaktı, aynı zamanda sınıfların init'i çağırmadan tekrar tekrar alt çizilmesine izin vermekti.
Çözüm. Python 3.7+ üzerinde, Generic temel sınıfında class_getitem dunder metodu uygulanmıştır; bu, bir sınıf alt çizildiğinde otomatik olarak çağrılır (örneğin, Container[int]). Bu metod, orijinal sınıfı origin içinde ve tür argümanlarını args içinde saklayan bir GenericAlias nesnesi (içsel tür _GenericAlias CPython'da) döndürür. Mekanizma, tamamen örnekleme yapmadan, bu takma nesneleri etkinlik için önbelleğe alır.
from typing import Generic, TypeVar T = TypeVar('T') class Container(Generic[T]): def __init__(self, value: T) -> None: self.value = value # Çalışma zamanı alt çizilmesi bir GenericAlias oluşturur, değil bir örnek SpecializedType = Container[int] print(SpecializedType) # <class '__main__.Container[int]'> print(SpecializedType.__origin__) # <class '__main__.Container'> print(SpecializedType.__args__) # (<class 'int'>,) # Örnekleme ayrı olarak gerçekleşir instance = SpecializedType(42)
Problem tanımı. Bir veri doğrulama kütüphanesi, kullanıcı sağlayıcı tür ipuçlarına dayalı olarak iç içe JSON yapılarının Python nesnelerine ayrıştırılması gerekiyordu, örneğin Dict[str, List[User]] veya Optional[Tuple[int, str]] gibi. Temel zorluk, çalışma zamanında, genel konteynerlar içinde hangi türlerin bulunduğunu belirlemek ve her olasılık kombinasyonunu sabit kodlama gerektirmeden doğru alt nesneleri tekrar oluşturmaktı.
Çözüm 1: Tür temsillerinin dize ayrıştırması. Artılar: str(type_hint) ve regex kullanarak hızlı bir şekilde uygulanabilir. Eksiler: Son derece kırılgan, ileri referanslar, tür birleştirmeleri veya iç içe genel türlerde kırılır ve farklı modüllerde benzer isimlere sahip türleri ayırt edemez.
Çözüm 2: Her genel sınıfı süslemek için kullanıcıların manuel metaklas kaydı yapmasını gerektirir. Artılar: Tür parametrelerinin depolanması ve geri alınması üzerinde tam kontrol sağlar. Eksiler: Kütüphane kullanıcıları üzerinde ağır bir yük oluşturur, kullanıcıların sınıfları zaten özel metaklaslar kullandığında metaklas çatışmaları oluşturur ve standart kütüphanede zaten bulunan işlevselliği yineler.
Çözüm 3: get_origin() ve get_args() aracılığıyla class_getitem iç gözlemi kullanmak. Artılar: Standart GenericAlias protokolünü kullanır, keyfi olarak iç içe geçmiş yapıları sağlam bir şekilde yönetir ve ek kullanıcı kodu olmadan karmaşık kalıtım hiyerarşileri için MRO'yu dikkate alır. Eksiler: Teknik olarak uygulama detayları olan origin gibi iç özelliklerinin anlaşılmasını gerektirir, ancak modern Python sürümlerinde stabil hale gelmiştir.
Seçilen çözüm. Çözüm 3, PEP 560 ile uyumlu olduğu ve modern Python tür sistemi mimarisi ile uyduğu için seçilmiştir. get_origin(type_hint)'i kontrol ederek temel konteyneri bulmak (örneğin, dict) ve get_args(type_hint) ile parametreli türleri çıkarmak (örneğin, str, User) sayesinde kütüphane doğrulayıcıları özyinelemeli olarak oluşturur. Bu yaklaşım, kullanıcı tanımlı genel türlerin Generic[T]'den miras almasıyla sorunsuz bir şekilde çalışır ve sınıf tanımları üzerinde herhangi bir değişiklik gerektirmez.
Sonuç. Kütüphane, karmaşık iç içe yükleri tür güvenli Python nesnelerine başarıyla serileştirir. Kullanıcılar class PaginatedResponse(Generic[T]): ... tanımlayabilir ve sistem, PaginatedResponse[OrderDetail] ile karşılaştığında T'yi otomatik olarak çıkararak doğru genel alt ağacı oluşturur ve IDE desteği ve çalışma zamanı doğrulaması için tam tür bilgisini korur.
Neden isinstance([1, 2, 3], List[int]) TypeError raise ediyor ve bu kısıtlama genel tür takma adları ile somut çalışma zamanı türleri arasındaki ayrımı nasıl yansıtıyor?
Python'un isinstance ikinci argümanının bir tür, türler tuple'ı veya instancecheck metoduna sahip bir nesne olmasını gerektirir. List[int] bir GenericAlias nesnesidir ve class_getitem ile oluşturulmuştur, bu bir sınıf değildir. Python ağaca göre yazma kullanır, bu nedenle genel parametreler çalışma zamanında silinir; [1, 2, 3] listesi (List[int] ile List[str] arasında parametreli olduğunu hatırlamaz. GenericAlias üzerindeki isinstance denemesi, TypeError: isinstance() arg 2 must be a type, tuple of types, or a union hatası verir. Uyum kontrolü yapmak için yapıyı manuel olarak doğrulamak veya yalnızca yöntem varlığını kontrol eden @runtime_checkable Protocol'leri kullanmak gerekir; bu, genel parametreleri kontrol etmez.
class_getitem çoklu özel genel ebeveynlerden miras alan bir sınıf için Yöntem Çözümleme Sırası ile nasıl etkileşimde bulunur, örneğin class MyMapping(Dict[str, int], Mapping[str, Any])?
Python, MyMapping oluşturduğunda, her bir temel sınıfı işler. Dict[str, int] ve Mapping[str, Any] her biri özel kökenleri üzerinde class_getitem çağrıları sonucu oluşan GenericAlias nesneleridir. MRO hesaplaması bunları ayrı tabanlar olarak ele alır, ancak Generic mekanizması, temel tür argüman bilgilerinin korunmasını sağlamak için bu orijinal alt çizilmiş tabanları orig_bases içinde saklar. Bu, get_type_hints(MyMapping)'in MyMapping'in Dict dalından str ve int üzerine parametreli olduğunu çözmesine olanak tanırken, Mapping dalı yapısal uygunluk sağlar. Ana detay, miras sırasında class_getitem'in tekrar çağrılmadığıdır; bunun yerine, mevcut takma adlar yeni sınıfa eklenir ve mro_entries (belirli soyut temel sınıflar için) nihai MRO'yu ayarlayabilir ve genel köken sınıflarının doğru bir şekilde görünmesini sağlar.
Genel bir sınıf tanımı üzerindeki parameters ile bir özel GenericAlias üzerindeki args arasındaki fark nedir ve neden bir TypeVar ile genel bir alt çizme işlemi, args'ın kendisi yerine TypeVar nesnesini içermesini sağlar?
parameters, sınıf başlığında bildirilen formal TypeVar nesnelerinden (örneğin, T) oluşan bir sınıf niteliği demetidir ve genel türün soyut tür alanlarını temsil eder. args, class_getitem ile oluşturulan GenericAlias örneğinde görünür ve o parametreler için değiştirilen somut türleri (örneğin, int) içerir. T bir TypeVar olduğu durumda (**başka bir genel işlevin içinde yaygındır), Container[T] oluşturduğunuzda args, somut bağlama geciktirildiği için TypeVar örneğini içerir. Bu mekanizma, TypeVar'ın bound niteliği sadece nihai çözümleme typing.get_type_hints() aracılığıyla gerçekleştiğinde kullanıldığından, daha yüksek düzeyde genel kalıpları destekler.