Ce sera une réponse de longue haleine qui ne servira peut-être qu'à être complémentaire ... mais votre question m'a amené à faire un tour dans le terrier du lapin, donc j'aimerais partager mes conclusions (et ma douleur) également.
Vous pourriez finalement trouver cette réponse non utile à votre problème réel. En fait, ma conclusion est que - je ne ferais pas ça du tout. Cela dit, le contexte de cette conclusion pourrait vous divertir un peu, car vous recherchez plus de détails.
Remédier à certaines idées fausses
La première réponse, bien que correcte dans la plupart des cas, n'est pas toujours le cas. Par exemple, considérez cette classe:
class Foo:
def __init__(self):
self.name = 'Foo!'
@property
def inst_prop():
return f'Retrieving {self.name}'
self.inst_prop = inst_prop
inst_prop
, tout en étant un property
, est irrévocablement un attribut d'instance:
>>> 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!'
Tout dépend où votre property
est défini en premier lieu. Si votre @property
est défini dans la classe "scope" (ou vraiment, la namespace
), il devient un attribut de classe. Dans mon exemple, la classe elle-même n'en connaît aucune inst_prop
jusqu'à ce qu'elle soit instanciée. Bien sûr, ce n'est pas du tout utile en tant que propriété ici.
Mais d'abord, abordons votre commentaire sur la résolution d'héritage ...
Alors, comment l'héritage entre-t-il exactement dans ce problème? Cet article suivant plonge un peu dans le sujet, et l' ordre de résolution des méthodes est quelque peu lié, bien qu'il traite principalement de l'étendue de l'héritage au lieu de la profondeur.
Combiné avec notre constat, compte tenu de la configuration ci-dessous:
@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"
Imaginez ce qui se passe lorsque ces lignes sont exécutées:
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}')
Le résultat est donc:
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>
Remarquez comment:
self.world_view
a pu être appliqué, mais a self.culture
échoué
culture
n'existe pas dans Child.__dict__
(le mappingproxy
de la classe, à ne pas confondre avec l'instance __dict__
)
- Même s'il
culture
existe en c.__dict__
, il n'est pas référencé.
Vous pourriez peut-être deviner pourquoi - a world_view
été écrasé par la Parent
classe en tant que non-propriété, vous avez donc Child
pu le remplacer également. Pendant ce temps, culture
étant hérité, il n'existe qu'au sein mappingproxy
deGrandparent
:
Grandparent.__dict__ is: {
'__module__': '__main__',
'culture': <property object at 0x00694C00>,
'world_view': <property object at 0x00694C00>,
...
}
En fait, si vous essayez de supprimer Parent.culture
:
>>> del Parent.culture
Traceback (most recent call last):
File "<pyshell#67>", line 1, in <module>
del Parent.culture
AttributeError: culture
Vous remarquerez qu'il n'existe même pas pour Parent
. Parce que l'objet fait directement référence à Grandparent.culture
.
Alors, qu'en est-il de l'ordre de résolution?
Nous sommes donc intéressés à observer l'ordre de résolution réel, essayons Parent.world_view
plutôt de le supprimer :
del Parent.world_view
print(f'c.world_view is: {c.world_view}')
print(f'Child.world_view is: {Child.world_view}')
Vous vous demandez quel est le résultat?
c.world_view is: Family property
Child.world_view is: <property object at 0x00694C00>
Il est revenu à celui de Grands-parents world_view
property
, même si nous avions réussi à assigner l' self.world_view
avant! Mais que se passe-t-il si nous changeons avec force world_view
au niveau de la classe, comme l'autre réponse? Et si nous le supprimons? Et si nous attribuons l'attribut de classe actuel à une propriété?
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}')
Le résultat est:
# 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>
Ceci est intéressant car c.world_view
est restauré à son attribut d'instance, tandis que Child.world_view
c'est celui que nous avons attribué. Après avoir supprimé l'attribut d'instance, il revient à l'attribut de classe. Et après avoir réaffecté le Child.world_view
à la propriété, nous perdons instantanément l'accès à l'attribut d'instance.
Par conséquent, nous pouvons supposer l'ordre de résolution suivant :
- Si un attribut de classe existe et qu'il s'agit d'un
property
, récupérez sa valeur via getter
ou fget
(plus de détails plus loin). La classe actuelle en premier à la classe de base en dernier.
- Sinon, s'il existe un attribut d'instance, récupérez la valeur d'attribut d'instance.
- Sinon, récupérez l'
property
attribut non- classe. La classe actuelle en premier à la classe de base en dernier.
Dans ce cas, supprimons la racine property
:
del Grandparent.culture
print(f'c.culture is: {c.culture}')
print(f'Child.culture is: {Child.culture}')
Qui donne:
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'
Et voilà! Child
a maintenant leur propre culture
basé sur l'insertion énergique dans c.__dict__
. Child.culture
n'existe pas, bien sûr, car il n'a jamais été défini dans Parent
ou l' Child
attribut class, et celui Grandparent
-ci a été supprimé.
Est-ce la cause première de mon problème?
En fait, non . L'erreur que vous obtenez, que nous observons toujours lors de l'attribution self.culture
, est totalement différente . Mais l'ordre d'héritage définit la toile de fond de la réponse - qui est le property
lui - même.
Outre la getter
méthode mentionnée précédemment , property
ayez également quelques astuces soignées dans ses manches. La plus pertinente dans ce cas est la méthode, setter
ou fset
, qui est déclenchée par la self.culture = ...
ligne. Étant donné que vous property
n'avez implémenté aucune fonction setter
ni aucune fget
fonction, python ne sait pas quoi faire et lance AttributeError
plutôt une (c'est-à-dire can't set attribute
).
Si toutefois vous avez implémenté une setter
méthode:
@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...
Lors de l'instanciation du Child
cours, vous obtiendrez:
Instantiating Child class...
property setter is called!
Au lieu de recevoir un AttributeError
, vous appelez maintenant la some_prop.setter
méthode. Ce qui vous donne plus de contrôle sur votre objet ... avec nos résultats précédents, nous savons que nous devons avoir un attribut de classe écrasé avant qu'il n'atteigne la propriété. Cela pourrait être implémenté dans la classe de base comme déclencheur. Voici un nouvel exemple:
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)
Ce qui se traduit par:
Fine, have your own culture
I'm a millennial!
ET VOILÀ! Vous pouvez maintenant remplacer votre propre attribut d'instance sur une propriété héritée!
Alors, problème résolu?
... Pas vraiment. Le problème avec cette approche est que vous ne pouvez plus avoir de setter
méthode appropriée . Dans certains cas, vous souhaitez définir des valeurs sur votre property
. Mais maintenant, chaque fois que vous le définissez, self.culture = ...
il remplacera toujours la fonction que vous avez définie dans le getter
(qui, dans ce cas, n'est vraiment que la @property
partie encapsulée. Vous pouvez ajouter des mesures plus nuancées, mais d'une manière ou d'une autre, cela impliquera toujours plus que juste self.culture = ...
. par exemple:
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
C'est waaaaay plus compliqué que l'autre réponse, size = None
au niveau de la classe.
Vous pouvez également envisager d'écrire votre propre descripteur à la place pour gérer les méthodes __get__
and __set__
ou supplémentaires. Mais à la fin de la journée, quand self.culture
est référencé, le __get__
sera toujours déclenché en premier, et quand self.culture = ...
est référencé, __set__
sera toujours déclenché en premier. Il n'y a pas moyen de le contourner autant que j'ai essayé.
Le nœud du problème, l'OMI
Le problème que je vois ici est - vous ne pouvez pas avoir votre gâteau et le manger aussi. property
est conçu comme un descripteur avec un accès pratique à partir de méthodes comme getattr
ou setattr
. Si vous souhaitez également que ces méthodes atteignent un objectif différent, vous demandez simplement des ennuis. Je repenserais peut-être l'approche:
- Ai-je vraiment besoin d'un
property
pour ça?
- Une méthode pourrait-elle me servir différemment?
- Si j'ai besoin d'un
property
, y a-t-il une raison pour laquelle je devrais l'écraser?
- La sous-classe appartient-elle vraiment à la même famille si celles
property
- ci ne s'appliquent pas?
- Si j'ai besoin d'écraser un / tous les
property
s, une méthode distincte me servirait-elle mieux qu'une simple réaffectation, car la réaffectation peut accidentellement annuler le property
s?
Pour le point 5, mon approche serait d'avoir une overwrite_prop()
méthode dans la classe de base qui écraserait l'attribut de classe actuel afin que la property
volonté ne soit plus déclenchée:
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
Comme vous pouvez le voir, bien que toujours un peu artificiel, il est au moins plus explicite qu'un cryptique size = None
. Cela dit, en fin de compte, je n'écraserais pas du tout la propriété et reconsidérerais ma conception à la racine.
Si vous êtes arrivé jusqu'ici - merci d'avoir fait ce voyage avec moi. C'était un petit exercice amusant.
len(self.elements)
en tant qu'implémentation. Cette implémentation très spécifique impose un contrat à toutes les instances de la classe, à savoir qu'elles fournissentelements
ce qui est un conteneur de taille . Cependant, votreSquare_Integers_Below
classe ne semble pas se comporter de cette façon (peut-être qu'elle génère ses membres de manière dynamique), elle doit donc définir son propre comportement.