To będzie długa, wyczerpująca odpowiedź, która może być tylko komplementarna ... ale twoje pytanie zabrało mnie na przejażdżkę w dół króliczej nory, więc chciałbym również podzielić się swoimi odkryciami (i bólem).
Ostatecznie może się okazać, że ta odpowiedź nie jest pomocna w rzeczywistym problemie. W rzeczywistości doszedłem do wniosku, że wcale tego nie zrobiłbym. To powiedziawszy, tło do tego wniosku może cię trochę zabawić, ponieważ szukasz więcej szczegółów.
Rozwiązanie niektórych nieporozumień
Pierwsza odpowiedź, choć poprawna w większości przypadków, nie zawsze tak jest. Rozważmy na przykład tę klasę:
class Foo:
def __init__(self):
self.name = 'Foo!'
@property
def inst_prop():
return f'Retrieving {self.name}'
self.inst_prop = inst_prop
inst_prop
, będąc jednocześnie property
, jest nieodwołalnie atrybutem instancji:
>>> Foo.inst_prop
Traceback (most recent call last):
File "<pyshell#60>", line 1, in <module>
Foo.inst_prop
AttributeError: type object 'Foo' has no attribute 'inst_prop'
>>> Foo().inst_prop
<property object at 0x032B93F0>
>>> Foo().inst_prop.fget()
'Retrieving Foo!'
Wszystko zależy od tego, gdzie Twoje property
jest zdefiniowane. Jeśli twoje @property
jest zdefiniowane w ramach klasy „scope” (lub tak naprawdę tonamespace
), staje się atrybutem klasy. W moim przykładzie sama klasa nie zdaje sobie z tego sprawy, inst_prop
dopóki nie zostanie utworzona . Oczywiście tutaj wcale nie jest bardzo przydatna.
Ale najpierw zajmijmy się twoim komentarzem na temat rozwiązania dziedziczenia ...
Więc w jaki sposób dziedziczenie uwzględnia ten problem? W tym artykule omówiono nieco temat i kolejność rozwiązywania metod jest w pewnym stopniu powiązana, chociaż omawia ona głównie szerokość dziedziczenia zamiast głębokości.
W połączeniu z naszym ustaleniem, biorąc pod uwagę poniższe ustawienia:
@property
def some_prop(self):
return "Family property"
class Grandparent:
culture = some_prop
world_view = some_prop
class Parent(Grandparent):
world_view = "Parent's new world_view"
class Child(Parent):
def __init__(self):
try:
self.world_view = "Child's new world_view"
self.culture = "Child's new culture"
except AttributeError as exc:
print(exc)
self.__dict__['culture'] = "Child's desired new culture"
Wyobraź sobie, co się dzieje, gdy te wiersze są wykonywane:
print("Instantiating Child class...")
c = Child()
print(f'c.__dict__ is: {c.__dict__}')
print(f'Child.__dict__ is: {Child.__dict__}')
print(f'c.world_view is: {c.world_view}')
print(f'Child.world_view is: {Child.world_view}')
print(f'c.culture is: {c.culture}')
print(f'Child.culture is: {Child.culture}')
Wynik jest zatem następujący:
Instantiating Child class...
can't set attribute
c.__dict__ is: {'world_view': "Child's new world_view", 'culture': "Child's desired new culture"}
Child.__dict__ is: {'__module__': '__main__', '__init__': <function Child.__init__ at 0x0068ECD8>, '__doc__': None}
c.world_view is: Child's new world_view
Child.world_view is: Parent's new world_view
c.culture is: Family property
Child.culture is: <property object at 0x00694C00>
Zauważ jak:
self.world_view
był w stanie być zastosowany, podczas gdy self.culture
nie udało się
culture
nie istnieje w Child.__dict__
(mappingproxy
klasie, nie należy mylić z instancją __dict__
)
- Mimo że
culture
istnieje c.__dict__
, nie jest przywoływany.
Być może potrafisz zgadnąć, dlaczego - world_view
został nadpisany przez Parent
klasę jako nie-własność, więc również Child
był w stanie go zastąpić. Tymczasem, ponieważ culture
jest dziedziczona, to istnieje tylko w obrębie mappingproxy
odGrandparent
:
Grandparent.__dict__ is: {
'__module__': '__main__',
'culture': <property object at 0x00694C00>,
'world_view': <property object at 0x00694C00>,
...
}
W rzeczywistości, jeśli spróbujesz usunąć Parent.culture
:
>>> del Parent.culture
Traceback (most recent call last):
File "<pyshell#67>", line 1, in <module>
del Parent.culture
AttributeError: culture
Zauważysz, że nawet nie istnieje Parent
. Ponieważ obiekt bezpośrednio odwołuje się doGrandparent.culture
.
A co z nakazem rozstrzygnięcia?
Dlatego jesteśmy zainteresowani obserwowaniem rzeczywistego zlecenia rozdzielczości, spróbujmy usunąć Parent.world_view
:
del Parent.world_view
print(f'c.world_view is: {c.world_view}')
print(f'Child.world_view is: {Child.world_view}')
Zastanawiam się, jaki jest wynik?
c.world_view is: Family property
Child.world_view is: <property object at 0x00694C00>
Wrócił do Dziadka world_view
property
, mimo że udało nam się wcześniej przypisać self.world_view
! Ale co, jeśli zmienimy siłą world_view
na poziomie klasy, podobnie jak inna odpowiedź? Co jeśli go usuniemy? Co jeśli przypisamy bieżący atrybut klasy jako właściwość?
Child.world_view = "Child's independent world_view"
print(f'c.world_view is: {c.world_view}')
print(f'Child.world_view is: {Child.world_view}')
del c.world_view
print(f'c.world_view is: {c.world_view}')
print(f'Child.world_view is: {Child.world_view}')
Child.world_view = property(lambda self: "Child's own property")
print(f'c.world_view is: {c.world_view}')
print(f'Child.world_view is: {Child.world_view}')
Wynik to:
# Creating Child's own world view
c.world_view is: Child's new world_view
Child.world_view is: Child's independent world_view
# Deleting Child instance's world view
c.world_view is: Child's independent world_view
Child.world_view is: Child's independent world_view
# Changing Child's world view to the property
c.world_view is: Child's own property
Child.world_view is: <property object at 0x020071B0>
Jest to interesujące, ponieważ c.world_view
jest przywracane do atrybutu wystąpienia, podczas gdy Child.world_view
jest to ten, który przypisaliśmy. Po usunięciu atrybutu instancji następuje powrót do atrybutu klasy. Po ponownym przypisaniu Child.world_view
do właściwości natychmiast tracimy dostęp do atrybutu instancji.
Dlatego możemy założyć następującą kolejność rozwiązywania :
- Jeśli istnieje atrybut klasy i jest to
property
, pobierz jego wartość za pomocą getter
lubfget
(więcej na ten temat później). Bieżąca klasa od pierwszej do podstawowej jako ostatnia.
- W przeciwnym razie, jeśli istnieje atrybut instancji, pobierz wartość atrybutu instancji.
- W przeciwnym razie pobierz
property
atrybut nieklasowy. Bieżąca klasa pierwsza do klasy podstawowej ostatnia.
W takim przypadku usuńmy root property
:
del Grandparent.culture
print(f'c.culture is: {c.culture}')
print(f'Child.culture is: {Child.culture}')
Co daje:
c.culture is: Child's desired new culture
Traceback (most recent call last):
File "<pyshell#74>", line 1, in <module>
print(f'Child.culture is: {Child.culture}')
AttributeError: type object 'Child' has no attribute 'culture'
Ta dah! Child
teraz ma swoją własną culture
opartą na silnym wstawianiu do c.__dict__
. Child.culture
nie istnieje oczywiście, ponieważ nigdy nie został zdefiniowany w atrybucie Parent
lub Child
class, i Grandparent
został usunięty.
Czy to jest podstawowa przyczyna mojego problemu?
Właściwie nie . Występujący błąd, który wciąż obserwujemy podczas przypisywania self.culture
, jest zupełnie inny . Ale kolejność dziedziczenia stanowi tło dla odpowiedzi - która jestproperty
samą sobą.
Oprócz poprzednio wspomnianej getter
metody, property
masz również kilka zgrabnych sztuczek w rękawach. W tym przypadku najbardziej istotna jest metoda setter
lub fset
, która jest uruchamiana przez self.culture = ...
linię. Ponieważ property
nie zaimplementowałeś żadnej setter
ani fget
funkcji, python nie wie, co robić, i AttributeError
zamiast tego rzuca (tj can't set attribute
.).
Jeśli jednak zastosowałeś setter
metodę:
@property
def some_prop(self):
return "Family property"
@some_prop.setter
def some_prop(self, val):
print(f"property setter is called!")
# do something else...
Podczas tworzenia Child
klasy otrzymasz:
Instantiating Child class...
property setter is called!
Zamiast odbierać AttributeError
, aktualnie wywołujesz some_prop.setter
metodę. Co daje ci większą kontrolę nad twoim obiektem ... dzięki naszym wcześniejszym ustaleniom wiemy, że musimy nadpisać atrybut klasy, zanim dotrze on do właściwości. Można to zaimplementować w klasie bazowej jako wyzwalacz. Oto nowy przykład:
class Grandparent:
@property
def culture(self):
return "Family property"
# add a setter method
@culture.setter
def culture(self, val):
print('Fine, have your own culture')
# overwrite the child class attribute
type(self).culture = None
self.culture = val
class Parent(Grandparent):
pass
class Child(Parent):
def __init__(self):
self.culture = "I'm a millennial!"
c = Child()
print(c.culture)
Co skutkuje w:
Fine, have your own culture
I'm a millennial!
TA DAH! Możesz teraz zastąpić własny atrybut instancji dziedziczoną właściwością!
Więc problem rozwiązany?
... Nie całkiem. Problem z tym podejściem polega na tym, że nie możesz mieć właściwej setter
metody. Są przypadki, w których chcesz ustawić swoje wartości property
. Ale teraz, gdy ustawisz self.culture = ...
to będzie zawsze zastąpić cokolwiek działać zdefiniowano w getter
(który w tym przypadku naprawdę jest po prostu @property
owinięta część. Ty możesz dodać w środkach bardziej zróżnicowany, ale tak czy inaczej będzie to zawsze obejmować więcej niż tylko self.culture = ...
. na przykład:
class Grandparent:
# ...
@culture.setter
def culture(self, val):
if isinstance(val, tuple):
if val[1]:
print('Fine, have your own culture')
type(self).culture = None
self.culture = val[0]
else:
raise AttributeError("Oh no you don't")
# ...
class Child(Parent):
def __init__(self):
try:
# Usual setter
self.culture = "I'm a Gen X!"
except AttributeError:
# Trigger the overwrite condition
self.culture = "I'm a Boomer!", True
To waaaaay bardziej skomplikowane niż w innych odpowiedzi, size = None
na poziomie klasy.
Możesz również rozważyć napisanie własnego deskryptora zamiast obsługi metod __get__
i __set__
lub dodatkowych metod. Ale pod koniec dnia, gdy self.culture
jest przywoływane, __get__
zawsze będzie uruchamiane jako pierwsze, a kiedy self.culture = ...
jest przywoływane, __set__
zawsze będzie uruchamiane jako pierwsze. O ile próbowałem, nie można tego obejść.
Sedno problemu, IMO
Problem, który tu widzę, polega na tym, że nie możesz mieć ciasta i też jesz. property
oznacza deskryptor z wygodnym dostępem z metod takich jak getattr
lub setattr
. Jeśli chcesz, aby te metody osiągnęły inny cel, po prostu prosisz o kłopoty. Być może przemyślę to podejście:
- Czy naprawdę potrzebuję
property
do tego?
- Czy metoda może mi służyć inaczej?
- Jeśli potrzebuję
property
, czy jest jakiś powód, dla którego musiałbym go zastąpić?
- Czy podklasa naprawdę należy do tej samej rodziny, jeśli
property
nie mają one zastosowania?
- Jeśli muszę zastąpić dowolne / wszystkie
property
s, czy osobna metoda byłaby dla mnie lepsza niż zwykłe ponowne przypisanie, ponieważ ponowne przypisanie może przypadkowo unieważnić te property
?
W przypadku punktu 5 moim podejściem byłaby overwrite_prop()
metoda w klasie bazowej, która nadpisze aktualny atrybut klasy, aby property
nie był już wyzwalany:
class Grandparent:
# ...
def overwrite_props(self):
# reassign class attributes
type(self).size = None
type(self).len = None
# other properties, if necessary
# ...
# Usage
class Child(Parent):
def __init__(self):
self.overwrite_props()
self.size = 5
self.len = 10
Jak widać, choć wciąż nieco wymyślony, jest co najmniej bardziej jawny niż tajemniczy size = None
. To powiedziawszy, ostatecznie nie zastąpiłbym w ogóle właściwości i ponownie rozważyłbym mój projekt od podstaw.
Jeśli dotarłeś tak daleko - dziękuję za podróż ze mną. To było zabawne, małe ćwiczenie.