Python Sınıfı Nitelikleri: Aşırı Kapsamlı Bir Kılavuz
Yayınlanan: 2022-03-11Geçenlerde, ortak bir metin düzenleyici kullandığımız bir telefon ekranı olan bir programlama röportajım vardı.
Benden belirli bir API uygulamam istendi ve bunu Python'da yapmayı seçtim. Problem ifadesini soyutlayarak, diyelim ki örnekleri bazı data
ve bazı other_data
depolayan bir sınıfa ihtiyacım vardı.
Derin bir nefes alıp yazmaya başladım. Birkaç satır sonra şöyle bir şey yaşadım:
class Service(object): data = [] def __init__(self, other_data): self.other_data = other_data ...
Görüşmecim beni durdurdu:
- Görüşmeci: “Bu satır:
data = []
. Bunun geçerli Python olduğunu sanmıyorum?” - Ben: "Eminim öyledir. Sadece örnek özelliği için varsayılan bir değer ayarlıyor.”
- Görüşmeci: "Bu kod ne zaman yürütülür?"
- Ben: “Gerçekten emin değilim. Karışıklığı önlemek için düzelteceğim.”
Başvuru için ve size ne için gittiğim hakkında bir fikir vermek için, kodu şu şekilde değiştirdim:
class Service(object): def __init__(self, other_data): self.data = [] self.other_data = other_data ...
Görünüşe göre ikimiz de yanılmışız. Asıl cevap, Python sınıfı nitelikleri ile Python örneği nitelikleri arasındaki farkı anlamakta yatıyor.
Not: Sınıf nitelikleri konusunda uzman bir bilginiz varsa, vakaları kullanmak için ileri atlayabilirsiniz.
Python Sınıfı Nitelikleri
Görüşmecim, yukarıdaki kodun sözdizimsel olarak geçerli olduğu konusunda yanılıyordu.
Örnek özniteliği için bir "varsayılan değer" ayarlamadığından ben de yanılmışım. Bunun yerine, data
[]
değerine sahip bir sınıf niteliği olarak tanımlar.
Tecrübelerime göre, Python sınıfı nitelikleri, birçok insanın hakkında bir şeyler bildiği, ancak çok azının tamamen anladığı bir konudur.
Python Sınıf Değişkeni ve Örnek Değişkeni: Fark Nedir?
Python sınıfı özniteliği, bir sınıf örneğinin özniteliği yerine, sınıfın bir özniteliğidir (dairesel, biliyorum).
Farkı göstermek için bir Python sınıfı örneği kullanalım. Burada class_var
bir sınıf niteliğidir ve i_var
bir örnek niteliğidir:
class MyClass(object): class_var = 1 def __init__(self, i_var): self.i_var = i_var
Sınıfın tüm örneklerinin class_var
erişimi olduğunu ve buna sınıfın kendisinin bir özelliği olarak da erişilebileceğini unutmayın:
foo = MyClass(2) bar = MyClass(3) foo.class_var, foo.i_var ## 1, 2 bar.class_var, bar.i_var ## 1, 3 MyClass.class_var ## <— This is key ## 1
Java veya C++ programcıları için class niteliği, statik üyeye benzer ancak aynı değildir. Nasıl farklı olduklarını daha sonra göreceğiz.
Sınıf ve Örnek Ad Alanları
Burada neler olduğunu anlamak için kısaca Python ad alanlarından bahsedelim.
Ad alanı, farklı ad alanlarındaki adlar arasında sıfır ilişki olması özelliğiyle, adlardan nesnelere bir eşlemedir. Bu soyutlanmış olmasına rağmen, genellikle Python sözlükleri olarak uygulanırlar.
Bağlama bağlı olarak, nokta sözdizimini (örneğin, object.name_from_objects_namespace
) veya yerel bir değişken (örneğin, object_from_namespace
) kullanarak bir ad alanına erişmeniz gerekebilir. somut bir örnek olarak:
class MyClass(object): ## No need for dot syntax class_var = 1 def __init__(self, i_var): self.i_var = i_var ## Need dot syntax as we've left scope of class namespace MyClass.class_var ## 1
Python sınıfları ve sınıf örneklerinin her biri, sırasıyla MyClass.__dict__
ve instance_of_MyClass.__dict__
ile temsil edilen kendi farklı ad alanlarına sahiptir.
Bir sınıfın örneğinden bir özniteliğe erişmeye çalıştığınızda, ilk önce örnek ad alanına bakar. Özniteliği bulursa, ilişkili değeri döndürür. Değilse , sınıf ad alanına bakar ve özniteliği döndürür (varsa, aksi takdirde bir hata atar). Örneğin:
foo = MyClass(2) ## Finds i_var in foo's instance namespace foo.i_var ## 2 ## Doesn't find class_var in instance namespace… ## So look's in class namespace (MyClass.__dict__) foo.class_var ## 1
Örnek ad alanı, sınıf ad alanı üzerinde üstünlüğü alır: her ikisinde de aynı ada sahip bir öznitelik varsa, önce örnek ad alanı kontrol edilir ve değeri döndürülür. Öznitelik araması için kodun (kaynak) basitleştirilmiş bir sürümü:
def instlookup(inst, name): ## simplified algorithm... if inst.__dict__.has_key(name): return inst.__dict__[name] else: return inst.__class__.__dict__[name]
Ve görsel biçimde:
Sınıf Nitelikleri Atamayı Nasıl İşler?
Bunu akılda tutarak, Python sınıfı niteliklerinin atamayı nasıl ele aldığını anlayabiliriz:
Bir sınıf niteliği, sınıfa erişilerek ayarlanırsa, tüm örnekler için değeri geçersiz kılar. Örneğin:
foo = MyClass(2) foo.class_var ## 1 MyClass.class_var = 2 foo.class_var ## 2
Ad alanı düzeyinde…
MyClass.__dict__['class_var'] = 2
ayarını yapıyoruz. (Not: __dict__ bir dictproxy, doğrudan atamayı önleyen değişmez bir sarmalayıcı döndürdüğü için bu tam kod değildir (__dict__
setattr(MyClass, 'class_var', 2)
), ancak tanıtım için yardımcı olur). Ardından,class_var
eriştiğimizdefoo.class_var
, sınıf ad alanında yeni bir değere sahip olur ve bu nedenle 2 döndürülür.Bir örneğe erişerek bir Paython sınıfı değişkeni ayarlanırsa, değeri yalnızca o örnek için geçersiz kılar. Bu, esasen sınıf değişkenini geçersiz kılar ve onu yalnızca o örnek için sezgisel olarak kullanılabilen bir örnek değişkenine dönüştürür. Örneğin:
foo = MyClass(2) foo.class_var ## 1 foo.class_var = 2 foo.class_var ## 2 MyClass.class_var ## 1
Ad alanı düzeyinde…
class_var
özniteliğinifoo.__dict__
, bu nedenlefoo.class_var
2 değerini döndürürüz. Bu arada,class_var
MyClass
, bu nedenleclass_var
bulmaya devam ederler.MyClass.__dict__
içinde ve böylece 1 döndürür.
değişebilirlik
Test sorusu: Sınıf özelliğinizin değişken bir türü varsa ne olur? Sınıf özniteliğini belirli bir örnek aracılığıyla erişerek değiştirebilir ( sakatlayabilir misiniz?)
Bu en iyi örnekle gösterilir. Daha önce tanımladığım Service
geri dönelim ve bir sınıf değişkeni kullanmamın yolun aşağısında nasıl sorunlara yol açabileceğini görelim.
class Service(object): data = [] def __init__(self, other_data): self.other_data = other_data ...
Amacım, data
için varsayılan değer olarak boş listeye ( []
) sahip olmak ve her Service
örneğinin zaman içinde örnek bazında değiştirilecek kendi verilerine sahip olmasıydı. Ancak bu durumda, aşağıdaki davranışı elde ederiz ( Service
bu örnekte isteğe bağlı olan other_data
bazı argümanlarını aldığını hatırlayın):
s1 = Service(['a', 'b']) s2 = Service(['c', 'd']) s1.data.append(1) s1.data ## [1] s2.data ## [1] s2.data.append(2) s1.data ## [1, 2] s2.data ## [1, 2]
Bu iyi değil—sınıf değişkenini bir örnek aracılığıyla değiştirmek, onu diğerleri için değiştirir!
Ad alanı düzeyinde… tüm Service
örnekleri, örnek ad alanlarında kendi data
özniteliklerini oluşturmadan Service.__dict__
içindeki aynı listeye erişiyor ve bunları değiştiriyor.
Bunu atamayı kullanarak çözebiliriz; diğer bir deyişle, listenin değişebilirliğinden yararlanmak yerine, Service
nesnelerimizi kendi listelerine sahip olacak şekilde aşağıdaki gibi atayabiliriz:
s1 = Service(['a', 'b']) s2 = Service(['c', 'd']) s1.data = [1] s2.data = [2] s1.data ## [1] s2.data ## [2]
Bu durumda, s1.__dict__['data'] = [1]
ekliyoruz, bu nedenle orijinal Service.__dict__['data']
değişmeden kalıyor.
Ne yazık ki bu, Service
kullanıcılarının değişkenleri hakkında derin bilgiye sahip olmasını gerektirir ve kesinlikle hatalara açıktır. Bir anlamda, nedenden çok semptomlara değineceğiz. Yapım gereği doğru olan bir şeyi tercih ederiz.
Benim kişisel çözümüm: olası bir Python örnek değişkenine varsayılan bir değer atamak için yalnızca bir sınıf değişkeni kullanıyorsanız, değiştirilebilir değerler kullanmayın . Bu durumda, Service
her örneği sonunda kendi örnek özniteliği ile Service.data
geçersiz kılacaktı, bu nedenle varsayılan olarak boş bir liste kullanmak, kolayca gözden kaçan küçük bir hataya yol açtı. Yukarıdakiler yerine, şunlardan birini yapabilirdik:
- Girişte gösterildiği gibi, tamamen örneğe bağlı nitelikler.
"Varsayılan" olarak boş listeyi (değişken bir değer) kullanmaktan kaçınıldı:
class Service(object): data = None def __init__(self, other_data): self.other_data = other_data ...
Elbette,
None
davasını uygun şekilde ele almamız gerekecek, ancak bu ödenmesi gereken küçük bir bedel.
Peki Python Sınıfı Niteliklerini Ne Zaman Kullanmalısınız?
Sınıf öznitelikleri yanıltıcıdır, ancak işe yarayabilecekleri birkaç duruma bakalım:
Sabitleri saklamak . Sınıf niteliklerine sınıfın kendisinin nitelikleri olarak erişilebildiğinden, bunları Sınıf çapında, Sınıfa özgü sabitleri depolamak için kullanmak genellikle iyidir. Örneğin:
class Circle(object): pi = 3.14159 def __init__(self, radius): self.radius = radius def area(self): return Circle.pi * self.radius * self.radius Circle.pi ## 3.14159 c = Circle(10) c.pi ## 3.14159 c.area() ## 314.159
Varsayılan değerlerin tanımlanması . Önemsiz bir örnek olarak, sınırlı bir liste (yani yalnızca belirli sayıda veya daha az öğeyi tutabilen bir liste) oluşturabilir ve varsayılan sınırı 10 öğe olarak seçebiliriz:
class MyClass(object): limit = 10 def __init__(self): self.data = [] def item(self, i): return self.data[i] def add(self, e): if len(self.data) >= self.limit: raise Exception("Too many elements") self.data.append(e) MyClass.limit ## 10
Daha sonra, örneğin
limit
özniteliğine atayarak, kendi özel sınırlarına sahip örnekler de oluşturabiliriz.foo = MyClass() foo.limit = 50 ## foo can now hold 50 elements—other instances can hold 10
Bu yalnızca, tipik
MyClass
örneğinizin yalnızca 10 veya daha az öğe tutmasını istiyorsanız mantıklıdır - tüm örneklerinize farklı sınırlar veriyorsanız,limit
bir örnek değişkeni olmalıdır. (Yine de unutmayın: değiştirilebilir değerleri varsayılanlarınız olarak kullanırken dikkatli olun.)Belirli bir sınıfın tüm örneklerinde tüm verileri izleme . Bu biraz spesifik, ancak belirli bir sınıfın mevcut her örneğiyle ilgili bir veri parçasına erişmek isteyebileceğiniz bir senaryo görebiliyordum.
Senaryoyu daha somut hale getirmek için diyelim ki bir
Person
sınıfımız var ve her kişinin birname
var. Kullanılan tüm isimlerin kaydını tutmak istiyoruz. Bir yaklaşım, çöp toplayıcının nesne listesi üzerinde yineleme yapmak olabilir, ancak sınıf değişkenlerini kullanmak daha kolaydır.Bu durumda,
names
yalnızca bir sınıf değişkeni olarak erişileceğini unutmayın, bu nedenle değiştirilebilir varsayılan kabul edilebilir.class Person(object): all_names = [] def __init__(self, name): self.name = name Person.all_names.append(name) joe = Person('Joe') bob = Person('Bob') print Person.all_names ## ['Joe', 'Bob']
Hatta bu tasarım modelini, yalnızca bazı ilişkili veriler yerine, belirli bir sınıfın mevcut tüm örneklerini izlemek için kullanabiliriz.
class Person(object): all_people = [] def __init__(self, name): self.name = name Person.all_people.append(self) joe = Person('Joe') bob = Person('Bob') print Person.all_people ## [<__main__.Person object at 0x10e428c50>, <__main__.Person object at 0x10e428c90>]
Performans (bir çeşit… aşağıya bakın).
kaputun altında
Not: Bu düzeyde performans konusunda endişeleniyorsanız, farklılıklar milisaniyenin onda biri kadar olacağından, ilk etapta Python kullanmak istemeyebilirsiniz - ancak biraz kurcalamak yine de eğlencelidir. ve illüstrasyon uğruna yardımcı olur.
Bir sınıfın ad alanının, sınıfın tanımı sırasında oluşturulduğunu ve doldurulduğunu hatırlayın. Bu, belirli bir sınıf değişkeni için yalnızca bir atama yaptığımız anlamına gelirken, her yeni örnek oluşturulduğunda örnek değişkenlerinin atanması gerekir. Bir örnek alalım.
def called_class(): print "Class assignment" return 2 class Bar(object): y = called_class() def __init__(self, x): self.x = x ## "Class assignment" def called_instance(): print "Instance assignment" return 2 class Foo(object): def __init__(self, x): self.y = called_instance() self.x = x Bar(1) Bar(2) Foo(1) ## "Instance assignment" Foo(2) ## "Instance assignment"
Bar.y
yalnızca bir kez atadık, ancak instance_of_Foo.y
__init__
öğesine yapılan her çağrıda.
Daha fazla kanıt olarak, Python ayrıştırıcısını kullanalım:
import dis class Bar(object): y = 2 def __init__(self, x): self.x = x class Foo(object): def __init__(self, x): self.y = 2 self.x = x dis.dis(Bar) ## Disassembly of __init__: ## 7 0 LOAD_FAST 1 (x) ## 3 LOAD_FAST 0 (self) ## 6 STORE_ATTR 0 (x) ## 9 LOAD_CONST 0 (None) ## 12 RETURN_VALUE dis.dis(Foo) ## Disassembly of __init__: ## 11 0 LOAD_CONST 1 (2) ## 3 LOAD_FAST 0 (self) ## 6 STORE_ATTR 0 (y) ## 12 9 LOAD_FAST 1 (x) ## 12 LOAD_FAST 0 (self) ## 15 STORE_ATTR 1 (x) ## 18 LOAD_CONST 0 (None) ## 21 RETURN_VALUE
Bayt koduna baktığımızda, Foo.__init__
'nin iki atama yapması gerektiği, Bar.__init__
ise sadece bir atama yapması gerektiği tekrar açıktır.
Uygulamada, bu kazanç gerçekten neye benziyor? Zamanlama testlerinin genellikle kontrol edilemeyen faktörlere büyük ölçüde bağımlı olduğunu ve aralarındaki farkların genellikle doğru bir şekilde açıklanmasının zor olduğunu kabul eden ilk kişi olacağım.
Ancak, bu küçük parçacıkların (Python timeit modülüyle çalıştırılır) sınıf ve örnek değişkenleri arasındaki farkları göstermeye yardımcı olduğunu düşünüyorum, bu yüzden onları yine de dahil ettim.
Not: OS X 10.8.5 ve Python 2.7.2 yüklü bir MacBook Pro'dayım.
başlatma
10000000 calls to `Bar(2)`: 4.940s 10000000 calls to `Foo(2)`: 6.043s
Bar
başlatılması bir saniyeden daha hızlıdır, bu nedenle buradaki fark istatistiksel olarak anlamlı görünmektedir.
Peki bu durum neden böyle? Bir spekülatif açıklama: Foo.__init__
içinde iki atama yapıyoruz, ancak Bar.__init__
içinde sadece bir atama yapıyoruz.
Atama
10000000 calls to `Bar(2).y = 15`: 6.232s 10000000 calls to `Foo(2).y = 15`: 6.855s 10000000 `Bar` assignments: 6.232s - 4.940s = 1.292s 10000000 `Foo` assignments: 6.855s - 6.043s = 0.812s
Not: Kurulum kodunuzu timeit ile her denemede yeniden çalıştırmanın bir yolu yoktur, bu nedenle denememizde değişkenimizi yeniden başlatmamız gerekir. Zamanların ikinci satırı, daha önce hesaplanan başlatma zamanlarının düşüldüğü yukarıdaki zamanları temsil eder.
Yukarıdan, Foo
görevleri yerine getirmesi Bar
yalnızca yaklaşık %60'ını alıyor gibi görünüyor.
Bu neden böyle? Bir spekülatif açıklama: Bar(2).y
, önce örnek ad alanına bakarız ( Bar(2).__dict__[y]
), y
öğesini bulamaz ve ardından sınıf ad alanına bakarız ( Bar.__dict__[y]
), ardından uygun atamayı yapın. Foo(2).y
, hemen örnek ad alanına ( Foo(2).__dict__[y]
) atadığımızın yarısı kadar arama yaparız.
Özetle, bu performans kazanımları gerçekte önemli olmasa da, bu testler kavramsal düzeyde ilgi çekicidir. Bir şey olursa, umarım bu farklılıklar sınıf ve örnek değişkenler arasındaki mekanik ayrımları göstermeye yardımcı olur.
Sonuç olarak
Python'da sınıf öznitelikleri yeterince kullanılmamış gibi görünüyor; pek çok programcı, nasıl çalıştıkları ve neden yardımcı olabilecekleri konusunda farklı izlenimlere sahiptir.
Benim görüşüm: Python sınıfı değişkenleri, iyi kod okulu içinde yerlerini alır. Dikkatli kullanıldıklarında işleri basitleştirebilir ve okunabilirliği artırabilirler. Ancak belirli bir sınıfa dikkatsizce atıldıklarında, sizi kesinlikle çeldireceklerdir.
Ek : Özel Örnek Değişkenleri
Dahil etmek istediğim ama doğal bir giriş noktası olmayan bir şey…
Python'un tabiri caizse özel değişkenleri yoktur, ancak sınıf ve örnek adlandırma arasındaki bir başka ilginç ilişki, ad yönetimiyle gelir.
Python stil kılavuzunda, sözde özel değişkenlerin önüne çift alt çizgi eklenmesi gerektiği söylenir: '__'. Bu, yalnızca başkalarına değişkeninizin özel olarak ele alınması gerektiğinin bir işareti değil, aynı zamanda ona erişimi engellemenin bir yoludur. İşte demek istediğim:
class Bar(object): def __init__(self): self.__zap = 1 a = Bar() a.__zap ## Traceback (most recent call last): ## File "<stdin>", line 1, in <module> ## AttributeError: 'Bar' object has no attribute '__baz' ## Hmm. So what's in the namespace? a.__dict__ {'_Bar__zap': 1} a._Bar__zap ## 1
Şuna bakın: __zap örnek niteliği, __zap
şekilde otomatik olarak sınıf adının önüne _Bar__zap
.
a._Bar__zap
kullanılarak hala ayarlanabilir ve alınabilir olsa da, bu ad yönetimi, sizin ve başkalarının yanlışlıkla veya cehalet yoluyla ona erişmesini engellediği için 'özel' bir değişken oluşturmanın bir yoludur.
Düzenleme: Pedro Werneck'in nazikçe belirttiği gibi, bu davranış büyük ölçüde alt sınıflamaya yardımcı olmayı amaçlamaktadır. PEP 8 stil kılavuzunda, bunun iki amaca hizmet ettiğini görüyorlar: (1) alt sınıfların belirli niteliklere erişmesini engellemek ve (2) bu alt sınıflarda ad alanı çakışmalarını önlemek. Yararlı olmakla birlikte, değişken yönetme, Java'da olduğu gibi varsayılan bir genel-özel ayrımıyla kod yazmaya davet olarak görülmemelidir.