[ 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ł defaultX
i defaultY
się obiektu __init__
metody. Powodem jest to, że w naszym przypadku chcę założyć, że someDefaultComputation
metody 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 x
i . (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 setattr
w 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 value
lub uruchomienie, method
jeś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 method
tutaj jest metoda klasowa, value
jest wartością UberProperty
i dodałem, isSet
ponieważ None
moż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ć uberProperty
i 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, UberProperty
aby przechowywać tylko method
referencję:
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, UberProperty
co oczywiście nie jest możliwe do wywołania, więc nie ma to większego zastosowania.
Potrzebujemy jakiegoś sposobu, aby dynamicznie powiązać UberProperty
instancję 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 UberProperty
do instancji, więc rzeczą oczywistą, aby powrócić byłoby BoundUberProperty. To tutaj faktycznie utrzymamy stan x
atrybutu.
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 UberObject
i ...
e = Example()
print e.x -> <__main__.BoundUberProperty object at 0x104604c90>
Po zmianie x
na:
@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
, setValue
i clearValue
tu. 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.x
wró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.