Alıcı ve ayarlayıcılara karşı @property kullanma


727

İşte Python'a özgü saf bir tasarım sorusu:

class MyClass(object):
    ...
    def get_my_attr(self):
        ...

    def set_my_attr(self, value):
        ...

ve

class MyClass(object):
    ...        
    @property
    def my_attr(self):
        ...

    @my_attr.setter
    def my_attr(self, value):
        ...

Python bunu her iki şekilde yapmamıza izin veriyor. Bir Python programı tasarlarsanız hangi yaklaşımı kullanırsınız ve neden?

Yanıtlar:


613

Özellikleri tercih edin . Bunun için oradalar.

Bunun nedeni, tüm özelliklerin Python'da herkese açık olmasıdır. İsimleri bir alt çizgi veya iki ile başlatmak, verilen özelliğin kodun gelecekteki sürümlerinde aynı kalmayabilecek bir uygulama ayrıntısı olduğuna dair bir uyarıdır. Bu özelliği almanızı veya ayarlamanızı engellemez. Bu nedenle, standart öznitelik erişimi, özniteliklere erişmenin normal, Pythonic yoludur.

Özelliklerin avantajı, öznitelik erişimi ile sözdizimsel olarak aynı olmalarıdır, böylece istemci kodunda herhangi bir değişiklik yapmadan birinden diğerine geçiş yapabilirsiniz. Hatta sınıfın özelliklerini kullanan bir sürümüne (örneğin, sözleşmeye göre kodlama veya hata ayıklama için) ve bunu kullanan kodu değiştirmeden üretim için olmayan bir sürüme sahip olabilirsiniz. Aynı zamanda, daha sonra erişimi daha iyi kontrol etmeniz gerektiğinde, her şey için alıcı ve ayarlayıcı yazmak zorunda kalmazsınız.


90
Çift alt çizgiye sahip öznitelik adları özellikle Python tarafından işlenir; sadece bir ibadet değil. Bkz. Docs.python.org/py3k/tutorial/classes.html#private-variables
6502

63
Farklı şekilde ele alınırlar, ancak bu onlara erişmenizi engellemez. Not: AD 30 C0
kindall

4
ve "@" karakterleri python kodunda çirkin ve dereferencing @decorators spagetti kodu gibi aynı hissi verir.
Berry Tsakala

18
Kabul etmiyorum. Yapılandırılmış kod spagetti koduna nasıl eşittir? Python güzel bir dildir. Ancak, doğru kapsülleme ve yapılandırılmış sınıflar gibi basit şeylere daha iyi destek vererek daha da iyi olurdu.

69
Çoğu durumda katılıyorum, ancak bir yavaş yavaş dekoratör arkasında yavaş yöntemleri gizlemek konusunda dikkatli olun. API'nızın kullanıcısı, mülk erişiminin değişken erişim gibi bir performans sergilemesini bekler ve bu beklentiden çok uzakta sapmak API'nizin kullanımını rahatsız edici hale getirebilir.
defrex

153

Python'da sadece eğlenmek için alıcıları, ayarlayıcıları veya özellikleri kullanmazsınız. Önce öznitelikleri kullanırsınız ve daha sonra, yalnızca gerekirse, sınıflarınızı kullanarak kodu değiştirmek zorunda kalmadan bir özelliğe geçiş yaparsınız.

Gerçekten basit bir demet yapacak her yerde getters ve setters ve miras ve anlamsız sınıfları kullanan .py uzantısı ile bir sürü kod var, ama Python kullanarak C ++ veya Java ile yazılan insanların kodu.

Bu Python kodu değil.


46
@ 6502, “[…] örneğin basit bir demetin yapacağı her yerde anlamsız sınıflar” dediğinizde: bir sınıfın bir demet üzerine göre avantajı, bir sınıf örneğinin parçalarına erişmek için açık isimler sağlamasıdır. . İsimler okunabilirlik ve hatalardan kaçınmak için, özellikle mevcut modülün dışına geçirilecek olduğunda, aboneliklerden daha iyidir.
Hibou57

15
@ Hibou57: Sınıfın yararsız olduğunu söylemiyorum. Ancak bazen bir demet fazlasıyla yeterlidir. Ancak problemler, Java veya C ++ 'dan gelenlerin her şey için sınıf yaratmaktan başka seçeneği olmadığıdır, çünkü diğer olasılıklar bu dilde kullanmak için can sıkıcıdır. Python kullanarak Java / C ++ programlamanın bir başka tipik belirtisi, Python'da ördek yazımı sayesinde bağımsız sınıfları kullanabileceğiniz hiçbir sebep olmadan soyut sınıflar ve karmaşık sınıf hiyerarşileri oluşturmaktır.
6502

39
@ Bunun için u Hibou57 ayrıca namedtuple kullanabilirsiniz: doughellmann.com/PyMOTW/collections/namedtuple.html
hugo24

5
@JonathonReinhart: 2.6'dan beri standart kütüphanede IS ... bkz. Docs.python.org/2/library/collections.html
6502

1
"Sonunda gerekirse bir özelliğe taşınırken" büyük olasılıkla sınıflarınızı kullanarak bir kesme kodu yaparsınız. Özellikler genellikle kısıtlamalar getirir - bu kısıtlamaları beklemeyen tüm kodlar, siz onları tanıtır getirmez kesilir.
yaccob

118

Özellikleri kullanmak, normal öznitelik erişimleriyle başlamanıza ve ardından gerektiğinde alıcılar ve ayarlayıcılarla yedeklemenize olanak tanır .


3
@GregKrsak Tuhaf görünüyor çünkü öyle. "Rıza yetişkinlerin şey" özellikleri eklenmeden önce bir python meme oldu. Erişim değiştiricilerinin eksikliğinden şikayetçi olan insanlara hisse senedi tepkisidir. Özellikler eklendiğinde, aniden kapsülleme arzu edilir hale gelir. Aynı şey soyut temel sınıflarda da oldu. Diyerek şöyle devam etti: "Python her zaman kapsülleme ile savaştı. Özgürlük köleliktir. Lambdas sadece bir hatta sığmalıdır."
johncip

71

Kısa cevap: özellikler eller kazanır . Her zaman.

Bazen alıcılara ve pasörlere ihtiyaç duyulur, ancak o zaman bile onları dış dünyaya "gizleyecektim". Orada Python bunu yapmanın yollarından (bolluk getattr, setattr, __getattribute__, vb ... ama çok özlü ve temiz biri:

def set_email(self, value):
    if '@' not in value:
        raise Exception("This doesn't look like an email address.")
    self._email = value

def get_email(self):
    return self._email

email = property(get_email, set_email)

İşte Python'da alıcılar ve ayarlayıcılar konusunu tanıtan kısa bir makale .


1
@BasicWolf - Ben çitin mülkiyet tarafında dolaylı olarak açık olduğunu düşündüm! :) Ama cevabımı netleştirmek için bir para ekliyorum.
mac

9
İPUCU: "Her zaman" kelimesi, yazarın sizi bir iddia ile değil, bir iddiayla ikna etmeye çalıştığı bir ipucudur. Kalın yazı tipinin varlığı da öyle. (Yani, bunun yerine CAPS'i görürseniz, o zaman - whoa - doğru olmalı.) Bakın, "özellik" özelliği Java'dan (Python'un de facto nemesis'i bir nedenle) ve bu nedenle Python'un topluluk grup düşüncesinden farklıdır. daha iyi olduğunu beyan eder. Gerçekte, özellikler "Açık, örtük olmaktan daha iyidir" kuralını ihlal eder, ancak kimse bunu kabul etmek istemez. Dili dile getirdi, şimdi totolojik bir argüman aracılığıyla "Pitonik" olarak ilan edildi.
Stuart Berg

3
Hiçbir duygu acıtmadı. :-P Sadece "Pythonic" kurallarının bu durumda tutarsız olduğuna dikkat çekmeye çalışıyorum: "Açık, örtük olmaktan daha iyidir" a ile doğrudan çatışma içindedir property. ( Basit bir atamaya benziyor , ancak bir işlev çağırıyor.) Bu nedenle, "Pythonic", totolojik tanım dışında, aslında anlamsız bir terimdir: "Pythonic konvansiyonları, Pythonic olarak tanımladığımız şeylerdir."
Stuart Berg

1
Şimdi, bir temayı takip eden bir dizi konvansiyona sahip olma fikri harika . Böyle bir kurallar dizisi varsa, onu ezberlemek için uzun bir hile listesi değil, düşüncelerinizi yönlendirmek için bir aksiyom kümesi olarak kullanabilirsiniz, bu da daha az yararlıdır. Aksiyomlar ekstrapolasyon için kullanılabilir ve henüz kimsenin görmediği sorunlara yaklaşmanıza yardımcı olabilir . propertyÖzelliğin Pythonic aksiyomları fikrini neredeyse değersiz hale getirme tehdidi utanç verici . Geriye kalan tek şey bir kontrol listesi.
Stuart Berg

1
Kabul etmiyorum. Çoğu durumda özellikleri tercih ederim, ancak bir şeyi ayarlamanın nesneyi değiştirmek dışında yan etkileriself olduğunu vurgulamak istediğinizde , açık ayarlayıcılar yardımcı olabilir. Örneğin, user.email = "..."bir istisna oluşturabilir gibi görünmüyor çünkü sadece bir özellik ayarlamak gibi görünüyor, oysa user.set_email("...")istisnalar gibi yan etkilerin olabileceğini açıkça ortaya koyuyor.
bluenote10

65

[ TL; DR? Sen edebilir bir kod örneğin sonuna atlamak .]

Aslında farklı bir deyim kullanmayı tercih ediyorum, ki bu bir kerelik kullanım için biraz dahil, ancak daha karmaşık bir kullanım durumunuz varsa güzel.

Önce biraz arka plan.

Özellikler, hem ayarı hem de değerleri programlı bir şekilde ele almamıza izin verir, ancak yine de özniteliklere öznitelik olarak erişilmesine izin verir. 'Gets'ı' hesaplamalara 'dönüştürebiliriz (esasen) ve' setleri '' olaylara 'dönüştürebiliriz. Diyelim ki Java benzeri alıcılar ve ayarlayıcılarla kodladığım şu sınıfımız var.

class Example(object):
    def __init__(self, x=None, y=None):
        self.x = x
        self.y = y

    def getX(self):
        return self.x or self.defaultX()

    def getY(self):
        return self.y or self.defaultY()

    def setX(self, x):
        self.x = x

    def setY(self, y):
        self.y = y

    def defaultX(self):
        return someDefaultComputationForX()

    def defaultY(self):
        return someDefaultComputationForY()

Neden aramadım defaultXve defaultYnesnenin __init__yöntemini merak ediyor olabilirsiniz . Bunun nedeni, bizim durumumuz için someDefaultComputationyöntemlerin zaman içinde değişen, bir zaman damgası söylediği ve x(veya y) her ayarlanmadığında (bu örneğin amacı için "ayarlanmadı") ayarlandığı anlamına geldiğini varsaymak istiyorum to None ") x's (veya y' s) varsayılan hesaplama değerini istiyorum .

Bu, yukarıda açıklanan bir dizi nedenden dolayı topal. Özellikleri kullanarak yeniden yazacağım:

class Example(object):
    def __init__(self, x=None, y=None):
        self._x = x
        self._y = y

    @property
    def x(self):
        return self.x or self.defaultX()

    @x.setter
    def x(self, value):
        self._x = value

    @property
    def y(self):
        return self.y or self.defaultY()

    @y.setter
    def y(self, value):
        self._y = value

    # default{XY} as before.

Ne kazandık? Her ne kadar sahnelerin arkasında yöntemler çalıştırıyor olsak da, bu niteliklere nitelik olarak gönderme yeteneği kazandık.

Elbette özelliklerin gerçek gücü, bu yöntemlerin sadece değerleri alıp ayarlamaya ek olarak bir şeyler yapmasını istememizdir (aksi takdirde özellikleri kullanmanın bir anlamı yoktur). Bunu alıcı örneğimde yaptım. Temel olarak, değer ayarlanmadığında varsayılanı almak için bir işlev gövdesi çalıştırıyoruz. Bu çok yaygın bir örüntüdür.

Ama ne kaybediyoruz ve ne yapamayız?

Benim görüşüme göre ana sıkıntı, bir alıcıyı tanımlarsanız (burada yaptığımız gibi) bir ayarlayıcı tanımlamanız gerektiğidir. [1] Bu, kodu karmaşıklaştıran ekstra gürültü.

Başka bir sıkıntı ise xve ydeğerlerini hala başlatmamız gerektiğidir __init__. (Elbette bunları kullanarak ekleyebiliriz, setattr()ancak bu daha fazla kod.)

Üçüncüsü, Java benzeri örnekten farklı olarak, alıcılar diğer parametreleri kabul edemez. Şimdi söylediklerini duyabiliyorum, iyi, eğer parametre alıyorsa bir alıcı değil! Resmi anlamda, bu doğrudur. Ancak pratik anlamda, adlandırılmış bir özniteliği - gibi x- parametrelendiremememiz ve değerini bazı belirli parametreler için ayarlamamamız için hiçbir neden yoktur .

Böyle bir şey yapabilirsek iyi olur:

e.x[a,b,c] = 10
e.x[d,e,f] = 20

Örneğin. Alabileceğimiz en yakın şey, bazı özel anlambilimi ima etmek için atamayı geçersiz kılmaktır:

e.x = [a,b,c,10]
e.x = [d,e,f,30]

ve elbette, ayarlayıcının ilk üç değeri bir sözlüğe anahtar olarak nasıl çıkaracağını ve değerini bir sayıya veya başka bir şeye nasıl ayarlayacağını bildiğinden emin olun.

Ancak bunu yapsak bile, onu özelliklerle destekleyemedik çünkü değeri elde etmenin bir yolu yok çünkü parametreleri alıcıya geçiremiyoruz. Bu yüzden her şeyi geri döndürmek zorunda kaldık, bir asimetri getirdik.

Java tarzı alıcı / ayarlayıcı bunu halletmemize izin veriyor, ancak alıcı / ayarlayıcılara ihtiyacımız var.

Aklımda gerçekten istediğimiz şey, aşağıdaki gereksinimleri yakalayan bir şey:

  • Kullanıcılar belirli bir öznitelik için yalnızca bir yöntem tanımlar ve burada özniteliğin salt okunur olup olmadığını belirtebilir. Öznitelik yazılabilirse, özellikler bu sınamada başarısız olur.

  • Kullanıcının işlevin altında yatan fazladan bir değişken tanımlamasına gerek yoktur, bu nedenle kodda __init__veya setattrkodda ihtiyacımız yoktur . Değişken, bu yeni stil niteliğini oluşturduğumuz gerçeğiyle var.

  • Öznitelik için herhangi bir varsayılan kod, yöntem gövdesinin kendisinde yürütülür.

  • Özniteliği öznitelik olarak ayarlayabilir ve öznitelik olarak başvurabiliriz.

  • Özelliği parametrelendirebiliriz.

Kod açısından şunu yazmanın bir yolunu istiyoruz:

def x(self, *args):
    return defaultX()

ve şunları yapabilir:

print e.x     -> The default at time T0
e.x = 1
print e.x     -> 1
e.x = None
print e.x     -> The default at time T1

ve benzerleri.

Ayrıca, parametrelendirilebilir bir özelliğin özel durumu için bunu yapmanın bir yolunu istiyoruz, ancak yine de varsayılan atama vakasının çalışmasına izin veriyoruz. Bunu nasıl ele aldığımı aşağıda göreceksiniz.

Şimdi noktaya (yay! Nokta!). Bunun için bulduğum çözüm şöyledir.

Bir özellik kavramını değiştirmek için yeni bir nesne yaratırız. Nesnenin kendisine ayarlanmış bir değişkenin değerini saklaması amaçlanmıştır, ancak aynı zamanda varsayılanın nasıl hesaplanacağını bilen kod üzerinde bir tanıtıcı tutar. Görevi, kümeyi saklamak valueveya methodbu değer ayarlanmamışsa çalıştırmaktır.

Buna bir diyelim UberProperty.

class UberProperty(object):

    def __init__(self, method):
        self.method = method
        self.value = None
        self.isSet = False

    def setValue(self, value):
        self.value = value
        self.isSet = True

    def clearValue(self):
        self.value = None
        self.isSet = False

methodBurada bir sınıf yöntemi olduğunu varsayalım value, değeri UberProperty, ve ben ekledim isSetçünkü Nonegerçek bir değer olabilir ve bu bize gerçekten "değer" olduğunu bildirmek için temiz bir yol sağlar. Başka bir yol, bir çeşit nöbetçi.

Bu temelde bize istediğimizi yapabilen bir nesne veriyor, ama aslında onu sınıfımıza nasıl koyabiliriz? Özellikler, dekoratörler kullanır; neden yapamıyoruz? Nasıl göründüğüne bakalım (buradan sonra sadece tek bir 'özellik' kullanmaya devam edeceğim x).

class Example(object):

    @uberProperty
    def x(self):
        return defaultX()

Aslında bu henüz işe yaramıyor. uberPropertyHem alır hem de ayarlar işlediğinden emin olmalıyız .

Hadi başlayalım.

İlk denemem sadece yeni bir UberProperty nesnesi oluşturmak ve onu döndürmekti:

def uberProperty(f):
    return UberProperty(f)

Elbette, bunun işe yaramadığını çabucak keşfettim: Python, çağrılabilir nesneyi asla nesneye bağlamaz ve işlevi çağırmak için nesneye ihtiyacım var. Sınıfta dekoratör oluşturmak bile işe yaramaz, çünkü şimdi sınıfa sahip olmamıza rağmen, hala üzerinde çalışacak bir nesnemiz yok.

Yani burada daha fazlasını yapabilmemiz gerekecek. Bir yöntemin yalnızca bir kez temsil edilmesi gerektiğini biliyoruz, bu yüzden dekoratörümüzü koruyalım, ancak UberPropertyyalnızca methodreferansı saklamak için değiştirelim :

class UberProperty(object):

    def __init__(self, method):
        self.method = method

Ayrıca çağrılabilir değil, şu anda hiçbir şey çalışmıyor.

Resmi nasıl tamamlarız? Yeni dekoratörümüzü kullanarak örnek sınıf oluşturduğumuzda neyle sonuçlanırız:

class Example(object):

    @uberProperty
    def x(self):
        return defaultX()

print Example.x     <__main__.UberProperty object at 0x10e1fb8d0>
print Example().x   <__main__.UberProperty object at 0x10e1fb8d0>

her iki durumda UberPropertyda elbette hangisinin çağrılabilir olmadığını geri alırız , bu yüzden bu pek işe yaramaz.

İhtiyacımız olan şey UberProperty, sınıf oluşturulduktan sonra dekoratör tarafından oluşturulan örneği, o nesne kullanım için o kullanıcıya geri gönderilmeden önce sınıfın bir nesnesine dinamik olarak bağlamanın bir yoludur . Um, evet, bu bir __init__telefon ahbap.

Bulma sonucumuzun önce olmasını istediğimizi yazalım. Biz bir bağlayıcı ediyoruz UberPropertydönüşüne açık bir şey BoundUberProperty olacağını bu yüzden, bir örneğine. Bu, xöznitelik için durumu gerçekten koruyacağımız yerdir .

class BoundUberProperty(object):
    def __init__(self, obj, uberProperty):
        self.obj = obj
        self.uberProperty = uberProperty
        self.isSet = False

    def setValue(self, value):
        self.value = value
        self.isSet = True

    def getValue(self):
        return self.value if self.isSet else self.uberProperty.method(self.obj)

    def clearValue(self):
        del self.value
        self.isSet = False

Şimdi temsil ediyoruz; bunları bir nesneye nasıl taşıyabilirim? Birkaç yaklaşım vardır, ancak açıklanması en kolay olanı sadece __init__bu eşlemeyi yapmak için yöntemi kullanır . Zamana göre __init__dekoratörlerimiz çalıştırıldı, bu yüzden sadece nesnenin içine bakmak __dict__ve özniteliğin değerinin tür olduğu herhangi bir özniteliği güncellemek gerekir UberProperty.

Şimdi, uber-özellikleri harika ve muhtemelen onları çok fazla kullanmak isteyeceğiz, bu yüzden bunu tüm alt sınıflar için yapan bir temel sınıf oluşturmak mantıklı. Bence baz sınıfın adı ne olacak.

class UberObject(object):
    def __init__(self):
        for k in dir(self):
            v = getattr(self, k)
            if isinstance(v, UberProperty):
                v = BoundUberProperty(self, v)
                setattr(self, k, v)

Bunu ekliyoruz, miras almak için örneğimizi değiştiriyoruz UberObjectve ...

e = Example()
print e.x               -> <__main__.BoundUberProperty object at 0x104604c90>

Değişiklik yaptıktan sonra x:

@uberProperty
def x(self):
    return *datetime.datetime.now()*

Basit bir test yapabiliriz:

print e.x.getValue()
print e.x.getValue()
e.x.setValue(datetime.date(2013, 5, 31))
print e.x.getValue()
e.x.clearValue()
print e.x.getValue()

Ve istediğimiz çıktıyı elde ediyoruz:

2013-05-31 00:05:13.985813
2013-05-31 00:05:13.986290
2013-05-31
2013-05-31 00:05:13.986310

(Gee, geç saatlere kadar çalışıyorum.)

Ben kullandım o Not getValue, setValueve clearValueburada. Bunun nedeni, bunların otomatik olarak geri döndürülmesi için henüz bağlantı kurmamış olmamdır.

Ama bence burası şimdilik durmak için iyi bir yer, çünkü yoruluyorum. İstediğimiz temel işlevin yerinde olduğunu da görebilirsiniz; gerisi pencere giydirme. Önemli kullanılabilirlik penceresi giyinme, ancak postayı güncellemek için bir değişiklik yapana kadar bekleyebilir.

Bir sonraki gönderideki örneği şu şeylere değinerek bitireceğim:

  • UberObject'lerin __init__her zaman alt sınıflar tarafından çağrıldığından emin olmalıyız .

    • Bu yüzden ya bir yere çağrılmaya zorlanıyoruz ya da uygulanmasını engelliyoruz.
    • Bunu bir metasınıfla nasıl yapacağımızı göreceğiz.
  • Birisinin bir işlevi başka bir şeye `` takma '' ettiği ortak durumu ele almamız gerektiğinden emin olmalıyız:

      class Example(object):
          @uberProperty
          def x(self):
              ...
    
          y = x
  • Varsayılan e.xolarak geri dönmemiz gerekiyor e.x.getValue().

    • Aslında göreceğimiz şey, bu modelin başarısız olduğu bir alandır.
    • Değeri elde etmek için her zaman bir işlev çağrısı kullanmamız gerektiği ortaya çıkıyor.
    • Ancak normal bir işlev çağrısı gibi görünmesini sağlayabilir ve kullanmaktan kaçınabiliriz e.x.getValue(). (Bunu düzeltmediyseniz, bunu yapmak açıktır.)
  • Ayarları e.x directlyolduğu gibi desteklememiz gerekir e.x = <newvalue>. Bunu üst sınıfta da yapabiliriz, ancak bununla __init__başa çıkmak için kodumuzu güncellememiz gerekir.

  • Son olarak, parametreli özellikler ekleyeceğiz. Bunu da nasıl yapacağımız oldukça açık olmalı.

Şimdiye kadar var olan kod İşte:

import datetime

class UberObject(object):
    def uberSetter(self, value):
        print 'setting'

    def uberGetter(self):
        return self

    def __init__(self):
        for k in dir(self):
            v = getattr(self, k)
            if isinstance(v, UberProperty):
                v = BoundUberProperty(self, v)
                setattr(self, k, v)


class UberProperty(object):
    def __init__(self, method):
        self.method = method

class BoundUberProperty(object):
    def __init__(self, obj, uberProperty):
        self.obj = obj
        self.uberProperty = uberProperty
        self.isSet = False

    def setValue(self, value):
        self.value = value
        self.isSet = True

    def getValue(self):
        return self.value if self.isSet else self.uberProperty.method(self.obj)

    def clearValue(self):
        del self.value
        self.isSet = False

    def uberProperty(f):
        return UberProperty(f)

class Example(UberObject):

    @uberProperty
    def x(self):
        return datetime.datetime.now()

[1] Bunun hala böyle olup olmadığı konusunda geride kalabilirim.


53
Evet, bu 'tldr'. Burada ne yapmaya çalıştığınızı özetleyebilir misiniz?
poolie

9
@Adam return self.x or self.defaultX()bu tehlikeli bir kod. Ne zaman olur self.x == 0?
Kelly Thomas

Bilginize, sen yapabilirsiniz sen tür, gaz giderici parametre cinsinden böylece bunu yapmak. Değişkeni, __getitem__yöntemi geçersiz kıldığınız özel bir sınıf haline getirmeyi içerir . Tamamen standart olmayan bir python'a sahip olacağınız için garip olurdu.
will

2
@KellyThomas Örneği basit tutmaya çalışıyorum. Doğru yapmak için x dict girdisini tamamen oluşturmanız ve silmeniz gerekir , çünkü None değeri bile özel olarak ayarlanmış olabilir. Ama evet, kesinlikle haklısınız, bu bir üretim kullanım durumunda dikkate almanız gereken bir şey.
Adam Donahue

Java benzeri alıcılar tam olarak aynı hesaplamayı yapmanızı sağlar, değil mi?
qed

26

Bence ikisinin de yeri var. Kullanmayla ilgili bir sorun, @propertystandart sınıf mekanizmalarını kullanarak alt sınıflarda alıcıların veya ayarlayıcıların davranışını genişletmenin zor olmasıdır. Sorun, gerçek alıcı / ayarlayıcı işlevlerinin özellikte gizlenmiş olmasıdır.

Fonksiyonları gerçekten ele alabilirsiniz, örneğin

class C(object):
    _p = 1
    @property
    def p(self):
        return self._p
    @p.setter
    def p(self, val):
        self._p = val

siz alıcı ve ayarlayıcı işlevleri erişebilir C.p.fgetve C.p.fsetfakat bunları kolayca genişletmek için, normal yöntem miras (örn süper) tesislerini kullanamaz. Süper inceliklerini içine biraz kazma sonra yapabilirsiniz gerçekten bu şekilde süper kullanın:

# Using super():
class D(C):
    # Cannot use super(D,D) here to define the property
    # since D is not yet defined in this scope.
    @property
    def p(self):
        return super(D,D).p.fget(self)

    @p.setter
    def p(self, val):
        print 'Implement extra functionality here for D'
        super(D,D).p.fset(self, val)

# Using a direct reference to C
class E(C):
    p = C.p

    @p.setter
    def p(self, val):
        print 'Implement extra functionality here for E'
        C.p.fset(self, val)

Bununla birlikte, super () kullanmak oldukça karmaşıktır, çünkü özellik yeniden tanımlanmalıdır ve p'nin sınırsız bir kopyasını almak için biraz karşı sezgisel süper (cls, cls) mekanizmasını kullanmanız gerekir.


20

Özellikleri kullanmak benim için daha sezgisel ve çoğu koda daha iyi uyuyor.

karşılaştırma

o.x = 5
ox = o.x

vs.

o.setX(5)
ox = o.getX()

bana göre çok daha kolay okunuyor. Ayrıca özellikler özel değişkenlere çok daha kolay izin verir.


12

Çoğu durumda da kullanmayı tercih etmem. Özelliklerle ilgili sorun, sınıfı daha az şeffaf hale getirmeleridir. Özellikle, bir pasörden bir istisna yaratacak olsaydınız bu bir sorundur. Örneğin, bir Account.email özelliğiniz varsa:

class Account(object):
    @property
    def email(self):
        return self._email

    @email.setter
    def email(self, value):
        if '@' not in value:
            raise ValueError('Invalid email address.')
        self._email = value

sınıf kullanıcısı özelliğe bir değer atamanın bir istisnaya neden olmasını beklemez:

a = Account()
a.email = 'badaddress'
--> ValueError: Invalid email address.

Sonuç olarak, istisna işlenmeyebilir ve çağrı zincirinde düzgün bir şekilde işlenemeyecek kadar yüksek yayılabilir veya program kullanıcısına (python ve java dünyasında ne yazık ki çok yaygın olan) çok yararsız bir izleme ile sonuçlanabilir. ).

Ayrıca getters ve setters kullanmaktan kaçınır:

  • çünkü bunları tüm mülkler için önceden tanımlamak çok zaman alıcıdır,
  • kodun miktarını gereksiz yere uzatır, bu da kodun anlaşılmasını ve korunmasını zorlaştırır,
  • bunları yalnızca özellikler için tanımlasaydınız, sınıfın arabirimi değişerek sınıfın tüm kullanıcılarına zarar verirdi

Özellikleri ve getters / setters yerine bir doğrulama yöntemi gibi iyi tanımlanmış yerlerde karmaşık mantık yapmayı tercih ederim:

class Account(object):
    ...
    def validate(self):
        if '@' not in self.email:
            raise ValueError('Invalid email address.')

veya benzer bir Account.save yöntemidir.

Özellikler yararlı olduğunda hiçbir durumda olduğunu söylemeye çalışmıyorum, sadece sınıfları onlara ihtiyacınız olmayan kadar basit ve şeffaf yapabilirsiniz eğer daha iyi olabilir.


3
@ user2239734 Bence özellikler kavramını yanlış anlıyorsunuz. Özelliği ayarlarken değeri doğrulayabilmenize rağmen, buna gerek yoktur. validate()Bir sınıfta hem özelliklere hem de bir yönteme sahip olabilirsiniz . Bir özellik, basit bir obj.x = yatamanın arkasında karmaşık bir mantığınız olduğunda kullanılır ve mantığın ne olduğuna bağlıdır.
Zaur Nasibov

12

Mülkleri sadece gerçekten ihtiyacınız olduğunda getters ve setters yazma yükü izin izin hakkında olduğunu hissediyorum.

Java Programlama kültürü, mülklere asla erişim vermemenizi ve bunun yerine alıcılar ve ayarlayıcılar ve yalnızca gerçekten ihtiyaç duyulanlar arasında ilerlemenizi şiddetle tavsiye eder. Her zaman bu açık kod parçalarını yazmak ve zamanın% 70'inin hiçbir zaman önemsiz olmayan bir mantıkla değiştirilmediğini fark etmek biraz ayrıntılıdır.

Python'da, insanlar aslında bu tür bir yükü önemsiyorlar, böylece aşağıdaki uygulamayı kucaklayabilirsiniz:

  • İhtiyaç duyulmamışsa, ilk olarak alıcıları ve ayarlayıcıları kullanmayın
  • @propertyKodunuzun geri kalanının sözdizimini değiştirmeden uygulamak için kullanın .

1
"ve zamanın% 70'inin hiçbir zaman önemsiz olmayan bir mantıkla değiştirilmediğine dikkat edin." - bu oldukça spesifik bir sayı, bir yerden mi geliyor, yoksa bir el dalgası olarak "büyük çoğunluk" gibi bir şey mi planlıyorsunuz? okumaya gerçekten ilgi duyuyor)
Adam Parkin

1
Üzgünüm. Bu sayıyı yedeklemek için biraz çalışmam var gibi görünüyor, ama sadece "çoğu zaman" demek istedim.
fulmicoton

7
İnsanların genel masrafları önemsememesi değil, Python'da istemci kodunu değiştirmeden doğrudan erişimci yöntemlerine geçiş yapabileceğinizden, ilk başta özellikleri doğrudan açığa çıkararak kaybedecek bir şeyiniz yok.
Neil G

10

Kimsenin özelliklerin bir tanımlayıcı sınıfın bağlı yöntemleri olduğunu söylemediğine şaşırdım, Adam Donohue ve NeilenMarais yazılarında tam olarak bu fikri alıyorlar - alıcılar ve ayarlayıcılar işlevler ve bunlar için kullanılabilir:

  • doğrulamak
  • verileri değiştir
  • ördek türü (başka bir türe zorlama tipi)

Bu bir akıllı sunar , bloklar, iddialar veya hesaplanan değerler hariç olmak üzere, uygulama ayrıntılarını gizlemek yol ve normal ifade, tür yayınları, deneme .. gibi kod hamlelerini kodlar.

Genel olarak bir nesne üzerinde CRUD yapmak genellikle oldukça sıradan olabilir, ancak ilişkisel bir veritabanında kalıcı olacak veri örneğini göz önünde bulundurun. ORM'ler, fah, fset, fdel'e bağlı yöntemlerde belirli SQL dillerinin uygulama ayrıntılarını, eğer OO kodunda çok çirkin olan merdiveleri yönetecek bir özellik sınıfında tanımlanmış yöntemlerde gizleyebilir - basit ve zarif self.variable = somethingve ORM kullanarak geliştirici için ayrıntıları ortadan kaldırmak .

Özelliklerin sadece bir Esaret ve Disiplin dilinin (yani Java) kasvetli bir izlemesi olduğunu düşünmesi durumunda, tanımlayıcıların noktasını kaçırırlar.


6

Karmaşık projelerde, açık ayarlayıcı işleviyle salt okunur özellikleri (veya alıcıları) kullanmayı tercih ederim:

class MyClass(object):
...        
@property
def my_attr(self):
    ...

def set_my_attr(self, value):
    ...

Uzun yaşam projelerinde hata ayıklama ve yeniden düzenleme, kodun kendisini yazmaktan daha fazla zaman alır. @property.setterHata ayıklamayı daha da zorlaştıran birkaç dezavantajı vardır :

1) python, mevcut bir nesne için yeni özellikler oluşturulmasına izin verir. Bu, aşağıdaki yanlış baskıların izlenmesini zorlaştırır:

my_object.my_atttr = 4.

Nesneniz karmaşık bir algoritma ise, neden birleşmediğini bulmaya çalışmak için biraz zaman harcayacaksınız (yukarıdaki satırda fazladan bir 't' dikkat edin)

2) ayarlayıcı bazen karmaşık ve yavaş bir yönteme dönüşebilir (örneğin bir veritabanına vurmak). Başka bir geliştiricinin aşağıdaki işlevin neden çok yavaş olduğunu anlaması oldukça zor olacaktır. Profilleme do_something()yöntemine çok zaman harcayabilirken my_object.my_attr = 4., aslında yavaşlamanın nedeni budur:

def slow_function(my_object):
    my_object.my_attr = 4.
    my_object.do_something()

5

Hem @propertygeleneksel alıcılar hem de pasiflerin avantajları vardır. Kullanım durumunuza bağlıdır.

Avantajları @property

  • Veri erişiminin uygulamasını değiştirirken arabirimi değiştirmeniz gerekmez. Projeniz küçük olduğunda, muhtemelen bir sınıf üyesine erişmek için doğrudan öznitelik erişimini kullanmak istersiniz. Örneğin, üyesi olan footürde bir nesneniz olduğunu Foovarsayalım num. Sonra bu üyeyi kolayca alabilirsiniz num = foo.num. Projeniz büyüdükçe, basit özellik erişiminde bazı kontroller veya hata ayıklamaların yapılması gerektiğini düşünebilirsiniz. Sonra bunu sınıf @property içinde bir ile yapabilirsiniz . Veri erişim arabirimi aynı kalır, böylece istemci kodunu değiştirmeye gerek kalmaz.

    PEP-8'den alıntı :

    Basit genel veri özellikleri için, karmaşık erişimci / mutasyon aracı yöntemleri olmadan yalnızca özellik adını göstermek en iyisidir. Basit bir veri özelliğinin işlevsel davranışı arttırması gerektiğini fark ederseniz, Python'un gelecekteki geliştirmelere kolay bir yol sağladığını unutmayın. Bu durumda, basit veri özniteliği erişim sözdiziminin arkasındaki işlevsel uygulamayı gizlemek için özellikleri kullanın.

  • Kullanılması @propertyolarak kabul edilir Python veri erişimi için Pythonic :

    • Bir Python (Java değil) programcısı olarak kendini tanımlamanızı güçlendirebilir.

    • Görüşmeciniz Java tarzı alıcıların ve ayarlayıcıların anti-kalıp olduğunu düşünüyorsa iş görüşmenize yardımcı olabilir .

Geleneksel alıcıların ve ayarlayıcıların avantajları

  • Geleneksel alıcılar ve ayarlayıcılar, basit özellik erişiminden daha karmaşık veri erişimine izin verir. Örneğin, bir sınıf üyesi ayarlarken, bazen bir şey mükemmel görünmese bile bu işlemi nereye zorlamak istediğinizi belirten bir bayrağa ihtiyacınız olabilir. Doğrudan üye erişiminin nasıl artırılacağı belli olmasa da foo.num = num, geleneksel ayarlayıcınızı ek bir forceparametre ile kolayca artırabilirsiniz :

    def Foo:
        def set_num(self, num, force=False):
            ...
  • Geleneksel alıcılar ve ayarlayıcılar, sınıf üyesi erişiminin bir yöntemle açık olduğunu gösterir. Bunun anlamı:

    • Sonuç olarak elde ettiğiniz şey, o sınıfta tam olarak depolananla aynı olmayabilir.

    • Erişim basit bir özellik erişimi gibi görünse bile, performans bundan çok farklı olabilir.

    Sınıf kullanıcılarınız @propertyher özellik erişim ifadesinin arkasında saklanmayı beklemedikçe , bu tür şeyleri açık yapmak sınıf kullanıcılarınızın sürprizlerini en aza indirmeye yardımcı olabilir.

  • @NeilenMarais ve bu yazıda bahsedildiği gibi , alt sınıflarda geleneksel alıcıları ve ayarlayıcıları genişletmek, özellikleri genişletmekten daha kolaydır.

  • Geleneksel alıcılar ve ayarlayıcılar farklı dillerde uzun süredir yaygın olarak kullanılmaktadır. Ekibinizde farklı geçmişlere sahip kişiler varsa, daha tanıdık geliyorlar @property. Ayrıca, projeniz büyüdükçe, Python'dan sahip olmayan başka bir dile geçmeniz gerekebiliyorsa @property, geleneksel alıcıları ve ayarlayıcıları kullanmak taşımayı daha pürüzsüz hale getirecektir.

Uyarılar

  • @propertyAdından önce çift alt çizgi kullansanız bile, ne geleneksel ne de geleneksel alıcılar ve ayarlayıcılar sınıf üyesini özel yapmaz:

    class Foo:
        def __init__(self):
            self.__num = 0
    
        @property
        def num(self):
            return self.__num
    
        @num.setter
        def num(self, num):
            self.__num = num
    
        def get_num(self):
            return self.__num
    
        def set_num(self, num):
            self.__num = num
    
    foo = Foo()
    print(foo.num)          # output: 0
    print(foo.get_num())    # output: 0
    print(foo._Foo__num)    # output: 0

2

İşte "Etkili Python: Daha İyi Python Yazmanın 90 Özel Yolu" (Şaşırtıcı bir kitap. Kesinlikle tavsiye ederim) bir alıntı .

Hatırlanacak şeyler

Public Basit ortak nitelikleri kullanarak yeni sınıf arayüzleri tanımlayın ve ayarlayıcı ve alıcı yöntemlerini tanımlamaktan kaçının.

Necessary Gerekirse, nesnelerinizdeki özniteliklere erişildiğinde özel davranışı tanımlamak için @property komutunu kullanın.

Least En az sürpriz kuralına uyun ve @property yöntemlerinizdeki garip yan etkilerden kaçının.

@ @Property yöntemlerinin hızlı olduğundan emin olun; yavaş veya karmaşık işler için - özellikle G / Ç içeren veya yan etkilere neden olan - normal yöntemleri kullanın.

@Property'nin gelişmiş ancak yaygın bir kullanımı, bir zamanlar basit bir sayısal özellik olanı anında hesaplamaya dönüştürmektir. Bu son derece yararlıdır, çünkü herhangi bir çağrı sitesinin yeniden yazılmasına gerek kalmadan yeni davranışlar için bir sınıfın tüm kullanımını taşımanıza izin verir (özellikle kontrol etmeyen arama kodu varsa önemlidir). @property ayrıca zaman içinde arayüzleri geliştirmek için önemli bir durma aralığı sağlar.

Özellikle @property'yi seviyorum çünkü zamanla daha iyi bir veri modeline doğru aşamalı ilerleme kaydetmenizi sağlar.
@property, gerçek dünya kodunda karşılaşacağınız sorunları çözmenize yardımcı olacak bir araçtır. Fazla kullanmayın. Kendinizi @property yöntemlerini tekrar tekrar genişlettiğinizde bulduğunuzda, muhtemelen kodunuzun zayıf tasarımına daha fazla yol açmak yerine sınıfınızı yeniden düzenleme zamanı gelmiştir.

Existing Mevcut örnek niteliklerine yeni işlevler vermek için @property komutunu kullanın.

@ @Property kullanarak daha iyi veri modellerine doğru aşamalı ilerleme sağlayın.

@ @Property'yi çok fazla kullandığınızda bir sınıfı ve tüm çağrı sitelerini yeniden düzenlemeyi düşünün.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.