Używanie @property kontra getters i setters


727

Oto pytanie projektowe dotyczące wyłącznie Pythona:

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

    def set_my_attr(self, value):
        ...

i

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

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

Python pozwala nam to zrobić w obu kierunkach. Jeśli zaprojektowałbyś program w języku Python, jakiego podejścia byś użył i dlaczego?

Odpowiedzi:


613

Preferuj właściwości . Po to są.

Powodem jest to, że wszystkie atrybuty są publiczne w Pythonie. Rozpoczynanie nazw od podkreślenia lub dwóch to tylko ostrzeżenie, że dany atrybut jest szczegółem implementacji, który może nie pozostać taki sam w przyszłych wersjach kodu. Nie przeszkadza ci to w uzyskaniu lub ustawieniu tego atrybutu. Dlatego standardowy dostęp do atrybutów jest normalnym, Pythonicznym sposobem dostępu do atrybutów.

Zaletą właściwości jest to, że są one identyczne pod względem składniowym z dostępem do atrybutów, dzięki czemu można zmieniać je bez żadnych zmian w kodzie klienta. Możesz mieć nawet jedną wersję klasy, która używa właściwości (na przykład do kodowania według umowy lub debugowania) i taką, która nie jest przeznaczona do produkcji, bez zmiany kodu, który z niej korzysta. Jednocześnie nie musisz pisać programów pobierających i ustawiających wszystko, na wypadek gdybyś później musiał lepiej kontrolować dostęp.


90
Nazwy atrybutów z podwójnym podkreśleniem są obsługiwane przez Python; to nie jest zwykła konwencja. Zobacz docs.python.org/py3k/tutorial/classes.html#private-variables
6502

63
Są traktowane inaczej, ale to nie powstrzymuje cię przed dostępem do nich. PS: AD 30 C0
uprzejmie

4
a ponieważ znaki „@” są brzydkie w kodzie python, a dereferencje @decorators dają to samo wrażenie, co kod spaghetti.
Berry Tsakala,

18
Nie zgadzam się W jaki sposób kod strukturalny jest równy kodowi spaghetti? Python to piękny język. Ale byłoby jeszcze lepiej z lepszym wsparciem dla prostych rzeczy, takich jak właściwe kapsułkowanie i ustrukturyzowane klasy.

69
Chociaż w większości przypadków zgadzam się, uważaj na ukrywanie powolnych metod za dekoratorem @property. Użytkownik interfejsu API oczekuje, że dostęp do właściwości działa podobnie jak dostęp do zmiennych, a zbytnie odstępowanie od tego oczekiwania może sprawić, że korzystanie z interfejsu API będzie nieprzyjemne.
defrex

153

W Pythonie nie używasz programów pobierających, ustawiających ani właściwości tylko dla zabawy. Najpierw używasz atrybutów, a później, tylko w razie potrzeby, ostatecznie migrujesz do właściwości bez konieczności zmiany kodu za pomocą swoich klas.

Rzeczywiście jest dużo kodu z rozszerzeniem .py, który używa getterów i seterów oraz dziedziczenia i bezsensownych klas wszędzie tam, gdzie np. Wystarczyłaby prosta krotka, ale jest to kod od ludzi piszących w C ++ lub Javie przy użyciu Pythona.

To nie jest kod Pythona.


46
@ 6502, kiedy powiedziałeś „[…] bezsensowne klasy wszędzie tam, gdzie np. Wystarczyłaby prosta krotka”: przewaga klasy nad krotką polega na tym, że instancja klasy zapewnia jawne nazwy dostępu do jej części, podczas gdy krotka nie . Nazwy są lepsze pod względem czytelności i unikania błędów, niż indeksowanie krotek, szczególnie gdy ma być przekazane poza bieżący moduł.
Hibou57

15
@ Hibou57: Nie mówię, że zajęcia są bezużyteczne. Ale czasami krotka jest więcej niż wystarczająca. Problem polega jednak na tym, że kto mówi, że Java lub C ++ nie ma wyboru, musi tworzyć klasy dla wszystkiego, ponieważ inne możliwości są po prostu denerwujące w tych językach. Innym typowym symptomem programowania Java / C ++ przy użyciu Pythona jest tworzenie klas abstrakcyjnych i złożonych hierarchii klas bez żadnego powodu, w którym w Pythonie można było po prostu używać niezależnych klas dzięki pisaniu kaczek.
6502

39
@ Hibou57 do tego możesz również użyć namedtuple: doughellmann.com/PyMOTW/collections/namedtuple.html
hugo24

5
@JonathonReinhart: JEST w standardowej bibliotece od wersji 2.6 ... patrz docs.python.org/2/library/collections.html
6502

1
Podczas „ewentualnej migracji do właściwości w razie potrzeby” jest całkiem prawdopodobne, że łamiesz kod za pomocą swoich klas. Właściwości często wprowadzają ograniczenia - każdy kod, który nie spodziewał się, że ograniczenia zostaną złamane, jak tylko je wprowadzisz.
yaccob

118

Korzystanie z właściwości pozwala rozpocząć od normalnego dostępu do atrybutów, a następnie wykonać kopię zapasową za pomocą metod pobierających i ustawiających, jeśli to konieczne .


3
@GregKrsak To wydaje się dziwne, bo tak jest. „Zgoda dorosłych” była pytonem memu sprzed dodania właściwości. Była to reakcja ludzi, którzy narzekali na brak modyfikatorów dostępu. Po dodaniu właściwości nagle pożądane staje się kapsułkowanie. To samo stało się z abstrakcyjnymi klasami podstawowymi. „Python zawsze toczył wojnę z przełamywaniem enkapsulacji. Wolność jest niewolnictwem. Lambdas powinien pasować tylko do jednej linii”.
johncip

71

Krótka odpowiedź brzmi: właściwości wygrywają . Zawsze.

Czasami istnieje potrzeba osób pobierających i ustawiających, ale nawet wtedy „ukrywałbym” je przed światem zewnętrznym. Istnieje wiele sposobów, aby to zrobić w Pythonie ( getattr, setattr, __getattribute__, itd ..., ale bardzo zwięzłe i czyste nich jest:

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)

Oto krótki artykuł, który wprowadza temat getterów i seterów w Pythonie.


1
@BasicWolf - Myślałem, że domyślnie było jasne, że jestem po stronie ogrodzenia po stronie nieruchomości! :) Ale dodaję parę do mojej odpowiedzi, aby to wyjaśnić.
Mac.

9
WSKAZÓWKA: Słowo „zawsze” jest wskazówką, że autor próbuje przekonać cię twierdzeniem, a nie argumentem. Podobnie obecność czcionki pogrubionej. (Mam na myśli, jeśli zamiast tego widzisz CAPS, to - whoa - musi mieć rację.) Spójrz, funkcja „własności” jest inna niż Java (z jakiegoś powodu de facto nemezis Pythona), a zatem grupa grup dyskusyjnych społeczności Pythona deklaruje, że jest lepiej. W rzeczywistości właściwości naruszają zasadę „Jawne jest lepsze niż niejawne”, ale nikt nie chce tego przyznać. Przeszedł na język, więc teraz jest zadeklarowany jako „pytoniczny” za pomocą argumentu tautologicznego.
Stuart Berg,

3
Żadne uczucia nie bolą. :-P Próbuję tylko wskazać, że konwencje „Pythonic” są niespójne w tym przypadku: „Jawna jest lepsza niż niejawna” jest w bezpośrednim konflikcie z użyciem property. ( Wygląda to jak zwykłe przypisanie, ale wywołuje funkcję.) Dlatego też „Pythonic” jest w zasadzie bez znaczenia, z wyjątkiem tautologicznej definicji: „Konwencje Pythona to rzeczy, które zdefiniowaliśmy jako Pythonic”.
Stuart Berg,

1
Teraz idea posiadania zestawu konwencji, które następują motyw jest wielki . Jeśli istniałby taki zestaw konwencji, można go użyć jako zestawu aksjomatów do kierowania myśleniem, a nie tylko długiej listy kontrolnej sztuczek do zapamiętania, co jest znacznie mniej przydatne. Aksjomaty można wykorzystać do ekstrapolacji i pomóc w podejściu do problemów, których nikt jeszcze nie widział. Szkoda, że ​​ta propertyfunkcja grozi uczynieniem idei pytonicznych aksjomatów niemal bezwartościowymi. Więc pozostaje nam tylko lista kontrolna.
Stuart Berg,

1
Nie zgadzam się Preferuję właściwości w większości sytuacji, ale jeśli chcesz podkreślić, że ustawienie czegoś ma skutki uboczne inne niż modyfikowanie selfobiektu , pomocne mogą być jawne ustawienia. Na przykład user.email = "..."nie wygląda na to, aby mógł zgłosić wyjątek, ponieważ wygląda jak ustawienie atrybutu, a user.set_email("...")jednocześnie wyjaśnia, że ​​mogą wystąpić efekty uboczne, takie jak wyjątki.
bluenote10

65

[ TL; DR? Możesz przejść do końca, aby uzyskać przykładowy kod .]

Właściwie wolę używać innego idiomu, który jest trochę zaangażowany w używanie go jako jednorazowego użytku, ale jest fajny, jeśli masz bardziej złożony przypadek użycia.

Najpierw trochę tła.

Właściwości są przydatne, ponieważ pozwalają nam obsługiwać zarówno ustawianie, jak i uzyskiwanie wartości w sposób zautomatyzowany, ale nadal umożliwiają dostęp do atrybutów jako atrybutów. Możemy przekształcić „przekształca się” w „obliczenia” (zasadniczo) i możemy przekształcić „zbiory” w „zdarzenia”. Powiedzmy, że mamy następującą klasę, którą kodowałem przy użyciu programów pobierających i ustawiających podobnych do języka Java.

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()

Być może zastanawiasz się, dlaczego nie zadzwonił defaultXi defaultYsię obiektu __init__metody. Powodem jest to, że w naszym przypadku chcę założyć, że someDefaultComputationmetody zwracają wartości, które zmieniają się w czasie, powiedzmy znacznik czasu i zawsze, gdy x(lub y) nie jest ustawiony (gdzie dla celów tego przykładu „nie ustawiono” oznacza „zestaw” na Brak ”) Chcę wartości domyślnego obliczenia x(lub y).

Jest to więc kiepskie z wielu powodów opisanych powyżej. Przepiszę go za pomocą właściwości:

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.

Co zyskaliśmy? Zdobyliśmy możliwość odwoływania się do tych atrybutów jako atrybutów, mimo że za kulisami używamy metod.

Oczywiście prawdziwą potęgą właściwości jest to, że generalnie chcemy, aby te metody działały oprócz pobierania i ustawiania wartości (w przeciwnym razie nie ma sensu używanie właściwości). Zrobiłem to w moim przykładzie gettera. Zasadniczo uruchamiamy ciało funkcji, aby wybrać wartość domyślną, gdy wartość nie jest ustawiona. Jest to bardzo powszechny wzór.

Ale co tracimy, a czego nie możemy zrobić?

Moim zdaniem główną irytacją jest to, że jeśli definiujesz gettera (tak jak my tutaj), musisz także zdefiniować setera. [1] To dodatkowy szum, który zaśmieca kod.

Inną irytacją jest to, że wciąż musimy inicjować wartości xi . (Cóż, oczywiście możemy dodać je za pomocą, ale to jest dodatkowy kod).y__init__setattr()

Po trzecie, w przeciwieństwie do przykładu podobnego do Javy, pobierające nie mogą zaakceptować innych parametrów. Teraz słyszę, jak mówisz, cóż, jeśli przyjmuje parametry, to nie jest getter! W oficjalnym sensie to prawda. Ale w sensie praktycznym nie ma powodu, abyśmy nie byli w stanie sparametryzować nazwanego atrybutu - podobnego x- i ustawić jego wartość dla niektórych określonych parametrów.

Byłoby miło, gdybyśmy mogli zrobić coś takiego:

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

na przykład. Najbliższe, co możemy uzyskać, to przesłonić przypisanie, aby sugerować jakąś specjalną semantykę:

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

i oczywiście upewnij się, że nasz ustawiający umie wyodrębnić pierwsze trzy wartości jako klucz do słownika i ustawić jego wartość na liczbę lub coś takiego.

Ale nawet gdybyśmy to zrobili, nadal nie moglibyśmy wesprzeć go właściwościami, ponieważ nie ma sposobu na uzyskanie wartości, ponieważ nie możemy w ogóle przekazać parametrów do gettera. Musieliśmy więc zwrócić wszystko, wprowadzając asymetrię.

Getter / setter w stylu Java pozwala nam sobie z tym poradzić, ale wróciliśmy do potrzeby getter / setter.

Moim zdaniem naprawdę chcemy, aby spełnić następujące wymagania:

  • Użytkownicy definiują tylko jedną metodę dla danego atrybutu i mogą tam wskazać, czy atrybut jest tylko do odczytu, czy do odczytu i zapisu. Właściwości nie przejdą tego testu, jeśli atrybut można zapisać.

  • Użytkownik nie musi definiować dodatkowej zmiennej leżącej u podstaw funkcji, więc nie potrzebujemy __init__ani setattrw kodzie. Zmienna istnieje tylko dlatego, że stworzyliśmy ten atrybut nowego stylu.

  • Każdy domyślny kod atrybutu jest wykonywany w samym ciele metody.

  • Możemy ustawić atrybut jako atrybut i odwołać się do niego jako atrybutu.

  • Możemy sparametryzować atrybut.

Jeśli chodzi o kod, chcemy sposób na napisanie:

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

i być w stanie wykonać:

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

i tak dalej.

Chcemy również tego dokonać w specjalnym przypadku atrybutu parametryzowalnego, ale nadal pozwalamy na działanie domyślnego przypadku przypisania. Zobaczysz, jak sobie z tym poradziłem poniżej.

Teraz do rzeczy (tak! Do rzeczy!). Rozwiązanie, dla którego wpadłem na to, jest następujące.

Tworzymy nowy obiekt, aby zastąpić pojęcie własności. Obiekt przeznaczony jest do przechowywania wartości zestawu zmiennych, ale zachowuje również uchwyt kodu, który wie, jak obliczyć wartość domyślną. Jego zadaniem jest zapisanie zestawu valuelub uruchomienie, methodjeśli ta wartość nie jest ustawiona.

Nazwijmy to an 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

Zakładam, że methodtutaj jest metoda klasowa, valuejest wartością UberPropertyi dodałem, isSetponieważ Nonemoże to być prawdziwa wartość, a to pozwala nam na czysty sposób zadeklarowania, że ​​tak naprawdę „nie ma wartości”. Innym sposobem jest jakiś wartownik.

Zasadniczo daje nam to obiekt, który może robić to, co chcemy, ale w jaki sposób możemy umieścić go w naszej klasie? Cóż, właściwości używają dekoratorów; dlaczego nie możemy Zobaczmy, jak to może wyglądać (odtąd będę się trzymał tylko jednego „atrybutu” x).

class Example(object):

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

Oczywiście to jeszcze nie działa. Musimy wdrożyć uberPropertyi upewnić się, że obsługuje on zarówno zestawy, jak i zestawy.

Zacznijmy od dostaje.

Moja pierwsza próba polegała na utworzeniu nowego obiektu UberProperty i zwróceniu go:

def uberProperty(f):
    return UberProperty(f)

Oczywiście szybko odkryłem, że to nie działa: Python nigdy nie wiąże wywoływanego obiektu z obiektem i potrzebuję go do wywołania funkcji. Nawet tworzenie dekoratora w klasie nie działa, ponieważ chociaż teraz mamy klasę, nadal nie mamy obiektu do pracy.

Musimy więc być w stanie zrobić więcej tutaj. Wiemy, że metoda musi być reprezentowana tylko raz, więc chodźmy dalej i zachowaj dekoratora, ale zmodyfikuj, UberPropertyaby przechowywać tylko methodreferencję:

class UberProperty(object):

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

Nie można go również wywoływać, więc w tej chwili nic nie działa.

Jak uzupełniamy obraz? Cóż, z czym skończymy, tworząc klasę przykładową za pomocą naszego nowego dekoratora:

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>

w obu przypadkach odzyskujemy to, UberPropertyco oczywiście nie jest możliwe do wywołania, więc nie ma to większego zastosowania.

Potrzebujemy jakiegoś sposobu, aby dynamicznie powiązać UberPropertyinstancję utworzoną przez dekorator po utworzeniu klasy z obiektem klasy, zanim ten obiekt zostanie zwrócony do tego użytkownika do użycia. Um, tak, to __init__połączenie, koleś.

Napiszmy, co chcemy, aby nasz wynik wyszukiwania był pierwszy. Jesteśmy wiążące UberPropertydo instancji, więc rzeczą oczywistą, aby powrócić byłoby BoundUberProperty. To tutaj faktycznie utrzymamy stan xatrybutu.

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

Teraz my reprezentacja; jak przenieść je na obiekt? Istnieje kilka podejść, ale najłatwiejszym do wyjaśnienia jest po prostu użycie __init__metody do wykonania tego mapowania. Do czasu, gdy __init__nazywa się to, nasze dekoratory już działają, więc wystarczy przejrzeć obiekty __dict__i zaktualizować wszystkie atrybuty, których wartość jest typu UberProperty.

Teraz właściwości uber są fajne i prawdopodobnie będziemy chcieli z nich często korzystać, więc sensowne jest po prostu utworzenie klasy podstawowej, która robi to dla wszystkich podklas. Myślę, że wiesz, jak będzie nazywana klasa podstawowa.

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)

Dodajemy to, zmieniamy nasz przykład na dziedziczenie UberObjecti ...

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

Po zmianie xna:

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

Możemy przeprowadzić prosty test:

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()

I otrzymujemy pożądaną wydajność:

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

(Rany, pracuję do późna.)

Zauważ, że użyłem getValue, setValuei clearValuetu. Wynika to z faktu, że nie podłączyłem jeszcze środków, aby automatycznie je zwrócić.

Ale myślę, że jest to dobre miejsce na teraz, aby się zatrzymać, ponieważ jestem zmęczony. Możesz również zobaczyć, że podstawowa funkcjonalność, której chcieliśmy, jest już dostępna; reszta to dekoracja okien. Ważne opatrunek okna użyteczności, ale to może poczekać, aż będę mieć zmianę, aby zaktualizować post.

Skończę przykład w następnym poście, odnosząc się do następujących rzeczy:

  • Musimy upewnić się, że UberObject __init__jest zawsze wywoływany przez podklasy.

    • Więc albo wymuszamy, aby gdzieś się nazywał, albo uniemożliwiamy jego wdrożenie.
    • Zobaczymy, jak to zrobić za pomocą metaklasy.
  • Musimy upewnić się, że zajmiemy się powszechnym przypadkiem, w którym ktoś „aliasy” funkcji do czegoś innego, na przykład:

      class Example(object):
          @uberProperty
          def x(self):
              ...
    
          y = x
  • Domyślnie musimy e.xwrócić e.x.getValue().

    • To, co faktycznie zobaczymy, to ten obszar, w którym model zawodzi.
    • Okazuje się, że zawsze będziemy musieli użyć wywołania funkcji, aby uzyskać wartość.
    • Ale możemy sprawić, że będzie wyglądać jak zwykłe wywołanie funkcji i uniknąć konieczności używania e.x.getValue(). (Wykonanie tego jest oczywiste, jeśli jeszcze go nie naprawiłeś.)
  • Musimy wesprzeć ustawienie e.x directly, jak w e.x = <newvalue>. Możemy to zrobić również w klasie nadrzędnej, ale będziemy musieli zaktualizować nasz __init__kod, aby go obsłużyć.

  • Na koniec dodamy sparametryzowane atrybuty. To powinno być dość oczywiste, jak to zrobimy.

Oto kod, jaki istnieje do tej pory:

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] Mogę być opóźniony, czy nadal tak jest.


53
Tak, to jest „tldr”. Czy możesz streścić to, co próbujesz tutaj zrobić?
poolie

9
@Adam return self.x or self.defaultX()to niebezpieczny kod. Co się stanie kiedy self.x == 0?
Kelly Thomas,

Do Twojej wiadomości, możesz to zrobić, abyś mógł sparametryzować moduł pobierający. Wymagałoby to uczynienia ze zmiennej klasy niestandardowej, której __getitem__metodę zastąpiliśmy . To byłoby dziwne, ponieważ miałbyś wtedy zupełnie niestandardowy python.
będzie

2
@KellyThomas Po prostu staram się, aby przykład był prosty. Aby to zrobić poprawnie, musisz całkowicie utworzyć i usunąć wpis x dict , ponieważ nawet wartość None mogła zostać specjalnie ustawiona. Ale tak, masz absolutną rację, jest to coś, co należy rozważyć w przypadku zastosowania produkcyjnego.
Adam Donahue,

Programy pobierające w stylu Java pozwalają wykonywać dokładnie takie same obliczenia, prawda?
qed

26

Myślę, że oboje mają swoje miejsce. Jednym z problemów związanych z używaniem @propertyjest to, że trudno jest rozszerzyć zachowanie modułów pobierających lub ustawiających w podklasach za pomocą standardowych mechanizmów klas. Problem polega na tym, że rzeczywiste funkcje pobierające / ustawiające są ukryte we właściwości.

Możesz faktycznie uzyskać funkcje, np. Za pomocą

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

możesz uzyskać dostęp do funkcji pobierających i ustawiających tak jak C.p.fgeti C.p.fset, ale nie możesz łatwo użyć zwykłych metod dziedziczenia metod (np. super) w celu ich rozszerzenia. Po pewnym kopanie w zawiłości super, to może rzeczywiście wykorzystać bardzo w ten sposób:

# 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)

Używanie super () jest jednak dość niezręczne, ponieważ właściwość musi zostać zdefiniowana na nowo i musisz użyć nieco sprzecznego z intuicją mechanizmu super (cls, cls), aby uzyskać niezwiązaną kopię p.


20

Korzystanie z właściwości jest dla mnie bardziej intuicyjne i lepiej pasuje do większości kodów.

Porównywanie

o.x = 5
ox = o.x

vs.

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

jest dla mnie dość oczywiste i łatwiejsze do odczytania. Również właściwości znacznie ułatwiają prywatne zmienne.


12

W większości przypadków wolałbym nie używać żadnego z nich. Problem z właściwościami polega na tym, że powodują one, że klasa jest mniej przezroczysta. Jest to szczególnie problem, jeśli chcesz zgłosić wyjątek od setera. Na przykład jeśli masz właściwość Account.email:

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

wówczas użytkownik klasy nie oczekuje, że przypisanie wartości do właściwości może spowodować wyjątek:

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

W rezultacie wyjątek może nie zostać obsłużony i albo rozprzestrzenić się zbyt wysoko w łańcuchu wywołań, aby można go było odpowiednio obsłużyć, lub spowodować bardzo nieprzydatne śledzenie użytkownika programu (co jest niestety zbyt częste w świecie python i java ).

Unikałbym również używania programów pobierających i ustawiających:

  • ponieważ ich wcześniejsze zdefiniowanie dla wszystkich właściwości jest bardzo czasochłonne,
  • sprawia, że ​​ilość kodu jest niepotrzebnie większa, co utrudnia zrozumienie i utrzymanie kodu,
  • gdybyś zdefiniował je dla właściwości tylko w razie potrzeby, interfejs klasy zmieniłby się, szkodząc wszystkim użytkownikom klasy

Zamiast właściwości i obiektów pobierających / ustawiających wolę wykonywać złożoną logikę w dobrze określonych miejscach, takich jak metoda sprawdzania poprawności:

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

lub podobna metoda Account.save.

Zauważ, że nie próbuję powiedzieć, że nie ma przypadków, w których właściwości są użyteczne, ale lepiej, jeśli uczynisz swoje zajęcia tak prostymi i przejrzystymi, że nie będziesz ich potrzebować.


3
@ user2239734 Myślę, że źle rozumiesz pojęcie właściwości. Chociaż można sprawdzić wartość podczas ustawiania właściwości, nie trzeba tego robić. Możesz mieć zarówno właściwości, jak i validate()metodę w klasie. Właściwość jest używana tylko wtedy, gdy za prostym obj.x = yprzypisaniem stoi złożona logika i zależy ona od logiki.
Zaur Nasibov

12

Wydaje mi się, że właściwości polegają na tym, aby pozwolić ci na obciążenie pisarzami ustawiającymi i ustawiającymi tylko wtedy, gdy ich potrzebujesz.

Kultura programowania Java zdecydowanie zaleca, aby nigdy nie dawać dostępu do właściwości, a zamiast tego przechodzić przez moduły pobierające i ustawiające oraz tylko te, które są rzeczywiście potrzebne. Zawsze jest trochę gadatliwe, aby zawsze pisać te oczywiste fragmenty kodu i zauważyć, że w 70% przypadków nigdy nie są one zastępowane jakąś nietrywialną logiką.

W Pythonie ludzie faktycznie dbają o tego rodzaju koszty ogólne, abyś mógł zastosować następującą praktykę:

  • Nie używaj pobieraczy i ustawiaczy na początku, jeśli nie są potrzebne
  • Użyj, @propertyaby je zaimplementować bez zmiany składni reszty kodu.

1
„i zauważ, że w 70% przypadków nie zastępuje ich żadna nietrywialna logika”. - to dość konkretna liczba, czy pochodzi ona z jakiegoś miejsca, czy masz zamiar traktować ją jako coś w rodzaju „zdecydowanej większości” (nie jestem żartobliwy, jeśli istnieje badanie, które określa tę liczbę, naprawdę zainteresowany lekturą)
Adam Parkin

1
O nie przepraszam Wygląda na to, że mam trochę badań, aby wykonać kopię zapasową tego numeru, ale miałem na myśli to tylko „przez większość czasu”.
fulmicoton

7
Nie chodzi o to, że ludzie dbają o narzut, ale o to, że w Pythonie możesz zmienić bezpośredni dostęp do metod akcesorów bez zmiany kodu klienta, więc nie musisz nic tracić, bezpośrednio ujawniając właściwości.
Neil G,

10

Dziwi mnie, że nikt nie wspominał, że właściwości są metodami związanymi z klasą deskryptorów, Adam Donohue i NeilenMarais doszli do tego pomysłu w swoich postach - że funkcje pobierające i ustawiające są funkcjami i mogą być używane do:

  • uprawomocnić
  • zmienić dane
  • typ kaczki (typ przymusowy na inny typ)

Jest to inteligentny sposób na ukrycie szczegółów implementacji i cruft kodu, takich jak wyrażenia regularne, rzutowania typów, try .. oprócz bloków, asercji lub wartości obliczanych.

Ogólnie rzecz biorąc, wykonywanie CRUD na obiekcie może być często dość przyziemne, ale należy wziąć pod uwagę przykład danych, które zostaną utrwalone w relacyjnej bazie danych. ORM może ukryć szczegóły implementacji poszczególnych języków SQL w metodach powiązanych z fget, fset, fdel zdefiniowanych w klasie właściwości, która będzie zarządzać okropnymi, jeśli ... elif .. innymi drabinkami, które są tak brzydkie w kodzie OO - odsłaniając proste i elegancki self.variable = somethingi pomiń szczegóły dla programisty używającego ORM.

Jeśli ktoś myśli o właściwościach tylko jako ponurym śladzie języka Bondage and Discipline (tj. Java), nie ma sensu w deskryptorach.


6

W złożonych projektach wolę używać właściwości tylko do odczytu (lub getterów) z jawną funkcją ustawiającą:

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

def set_my_attr(self, value):
    ...

W długich projektach debugowanie i refaktoryzacja zajmuje więcej czasu niż napisanie samego kodu. Istnieje kilka wad używania, @property.setterktóre sprawiają, że debugowanie jest jeszcze trudniejsze:

1) Python pozwala tworzyć nowe atrybuty dla istniejącego obiektu. To bardzo utrudnia śledzenie następującego błędu:

my_object.my_atttr = 4.

Jeśli twój obiekt jest skomplikowanym algorytmem, poświęcisz sporo czasu na sprawdzenie, dlaczego się nie zbiega (zauważ dodatkowe „t” w powyższej linii)

2) seter może czasem ewoluować do skomplikowanej i powolnej metody (np. Trafienie do bazy danych). Innemu deweloperowi byłoby bardzo trudno zrozumieć, dlaczego następująca funkcja jest bardzo wolna. Może spędzać dużo czasu na do_something()metodzie profilowania , podczas gdy w my_object.my_attr = 4.rzeczywistości jest przyczyną spowolnienia:

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

5

Zarówno @propertytradycyjny, jak i ustawiający mają swoje zalety. To zależy od twojego przypadku użycia.

Zalety @property

  • Nie musisz zmieniać interfejsu podczas zmiany implementacji dostępu do danych. Kiedy twój projekt jest mały, prawdopodobnie chcesz użyć bezpośredniego dostępu do atrybutu, aby uzyskać dostęp do członka klasy. Załóżmy na przykład, że masz obiekt footypu Foo, który ma element członkowski num. Następnie możesz po prostu zdobyć tego członka num = foo.num. W miarę rozwoju projektu możesz odczuwać potrzebę sprawdzenia lub debugowania prostego dostępu do atrybutów. Następnie można zrobić z @property wewnątrz klasy. Interfejs dostępu do danych pozostaje taki sam, więc nie ma potrzeby modyfikowania kodu klienta.

    Cytowany z PEP-8 :

    W przypadku prostych publicznych atrybutów danych najlepiej jest ujawnić tylko nazwę atrybutu, bez skomplikowanych metod dostępu / mutatora. Należy pamiętać, że Python zapewnia łatwą ścieżkę do przyszłego rozszerzenia, jeśli okaże się, że prosty atrybut danych musi zwiększyć funkcjonalne zachowanie. W takim przypadku użyj właściwości, aby ukryć funkcjonalną implementację kryjącą się za prostą składnią dostępu do atrybutów danych.

  • Używanie @propertydostępu do danych w Pythonie jest uważane za Pythonic :

    • Może wzmocnić twoją samoidentyfikację jako programista w języku Python (nie Java).

    • Może pomóc w rozmowie o pracę, jeśli ankieter uważa, że ​​osoby pobierające i ustawiające w stylu Java są anty-wzorcami .

Zalety tradycyjnych pobierających i ustawiających

  • Tradycyjne metody pobierające i ustawiające umożliwiają bardziej skomplikowany dostęp do danych niż prosty dostęp do atrybutów. Na przykład, gdy ustawiasz członka klasy, czasami potrzebujesz flagi wskazującej, gdzie chcesz wymusić tę operację, nawet jeśli coś nie wygląda idealnie. Chociaż nie jest oczywiste, jak zwiększyć bezpośredni dostęp do elementu członkowskiego foo.num = num, możesz z łatwością rozszerzyć tradycyjnego setera za pomocą dodatkowego forceparametru:

    def Foo:
        def set_num(self, num, force=False):
            ...
  • Tradycyjne metody pobierające i ustawiające wyraźnie pokazują, że dostęp członków klasy odbywa się za pomocą metody. To znaczy:

    • Wynik, który otrzymasz, może nie być taki sam, jak dokładnie przechowywany w tej klasie.

    • Nawet jeśli dostęp wygląda jak zwykły dostęp do atrybutu, wydajność może się znacznie od tego różnić.

    O ile użytkownicy klasy nie oczekują, że @propertykryją się za każdym oświadczeniem o dostępie do atrybutów, wyraźne określenie takich rzeczy może pomóc zminimalizować niespodzianki użytkowników klasy.

  • Jak wspomniano w @NeilenMarais i w tym poście , rozszerzenie tradycyjnych metod pobierających i ustawiających w podklasach jest łatwiejsze niż rozszerzanie właściwości.

  • Tradycyjne pobierające i ustawiające są od dawna szeroko stosowane w różnych językach. Jeśli masz w zespole ludzi z różnych środowisk, wyglądają bardziej znajomo niż @property. Ponadto, w miarę rozwoju projektu, jeśli zajdzie potrzeba migracji z Pythona do innego języka, którego nie ma @property, użycie tradycyjnych programów pobierających i ustawiających sprawiłoby, że migracja przebiegałaby płynniej.

Ostrzeżenia

  • Ani @propertytradycyjne metody pobierające i ustawiające nie czynią członka klasy prywatnym, nawet jeśli użyjesz podwójnego podkreślenia przed jego nazwą:

    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

Oto fragmenty „Skutecznego Pythona: 90 konkretnych sposobów na lepsze pisanie w Pythonie” (Niesamowita książka. Bardzo go polecam).

Rzeczy do zapamiętania

✦ Zdefiniuj nowe interfejsy klasowe za pomocą prostych atrybutów publicznych i unikaj definiowania metod ustawiających i pobierających.

✦ Użyj @property, aby zdefiniować specjalne zachowanie, gdy atrybuty są dostępne dla twoich obiektów, jeśli to konieczne.

✦ Postępuj zgodnie z zasadą najmniejszego zaskoczenia i unikaj dziwnych efektów ubocznych w metodach @property.

✦ Upewnij się, że metody @property są szybkie; do powolnej lub złożonej pracy - szczególnie związanej z We / Wy lub powodującej skutki uboczne - użyj normalnych metod.

Jednym z zaawansowanych, ale powszechnych zastosowań @property jest przekształcenie tego, co kiedyś było prostym atrybutem numerycznym, w obliczenie „w locie”. Jest to niezwykle pomocne, ponieważ umożliwia migrację całego istniejącego użycia klasy w celu uzyskania nowych zachowań bez konieczności przepisywania jakichkolwiek witryn wywołujących (co jest szczególnie ważne, jeśli istnieje kod wywołujący, którego nie kontrolujesz). @property zapewnia również ważną lukę w ulepszaniu interfejsów w miarę upływu czasu.

Szczególnie podoba mi się @property, ponieważ pozwala to na stopniowy postęp w kierunku lepszego modelu danych w czasie.
@property to narzędzie, które pomoże Ci rozwiązać problemy napotkane w kodzie rzeczywistym. Nie nadużywaj tego. Kiedy wielokrotnie poszerzasz metody @property, prawdopodobnie nadszedł czas, aby refaktoryzować klasę zamiast dalej brnąć w kiepski projekt kodu.

✦ Użyj @property, aby nadać istniejącym atrybutom instancji nową funkcjonalność.

✦ Postępuj stopniowo w kierunku lepszych modeli danych, używając @property.

✦ Zastanów się nad refaktoryzacją klasy i wszystkich witryn wywołujących, jeśli zbyt często używasz @property.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.