À propos du décorateur @property
En Python, nous utilisons le décorateur @property pour déguiser les appels de fonction en accès aux propriétés.
Alors pourquoi tu fais ça ? Parce que @property nous permet d'associer du code personnalisé à un accès/paramètre de variable, tout en conservant une interface simple pour accéder aux propriétés de votre classe.
Par exemple, disons que nous avons une classe qui doit représenter un film :
class Movie(object): def __init__(self, title, description, score, ticket): self.title = title self.description = description self.score = scroe self.ticket = ticket
Vous commencez à l'utiliser ailleurs dans le catégorie de projet, mais vous réalisez ensuite : et si vous attribuiez accidentellement une note négative au film ? Vous pensez qu’il s’agit d’un mauvais comportement et espérez que la classe Movie pourra empêcher cette erreur. Votre première pensée est de modifier la classe Movie pour qu'elle ressemble à ceci :
class Movie(object): def __init__(self, title, description, score, ticket): self.title = title self.description = description self.ticket = ticket if score < 0: raise ValueError("Negative value not allowed:{}".format(score)) self.score = scroe
Mais cela ne fonctionnera pas. Parce que d'autres parties du code sont attribuées directement via Movie.score. Cette classe nouvellement modifiée capturera uniquement les données erronées dans la méthode __init__, mais elle ne pourra rien faire sur les instances de classe existantes. Si quelqu'un essaie d'exécuter m.scrore= -100, vous ne pouvez rien faire pour l'arrêter. Ce qu'il faut faire?
La propriété Python résout ce problème.
Nous pouvons le faire
class Movie(object): def __init__(self, title, description, score): self.title = title self.description = description self.score = score self.ticket = ticket @property def score(self): return self.__score @score.setter def score(self, score): if score < 0: raise ValueError("Negative value not allowed:{}".format(score)) self.__score = score @score.deleter def score(self): raise AttributeError("Can not delete score")
De cette façon, modifier le score n'importe où détectera s'il est inférieur à 0.
Inconvénients des propriétés
Le plus gros inconvénient des propriétés est qu'elles ne peuvent pas être réutilisées. Par exemple, disons que vous souhaitez également ajouter un chèque non négatif au champ du ticket.
Ce qui suit est la nouvelle classe modifiée :
class Movie(object): def __init__(self, title, description, score, ticket): self.title = title self.description = description self.score = score self.ticket = ticket @property def score(self): return self.__score @score.setter def score(self, score): if score < 0: raise ValueError("Negative value not allowed:{}".format(score)) self.__score = score @score.deleter def score(self): raise AttributeError("Can not delete score") @property def ticket(self): return self.__ticket @ticket.setter def ticket(self, ticket): if ticket < 0: raise ValueError("Negative value not allowed:{}".format(ticket)) self.__ticket = ticket @ticket.deleter def ticket(self): raise AttributeError("Can not delete ticket")
Vous pouvez voir que le code a beaucoup augmenté, mais la logique répétée également apparaît. Bien que la propriété puisse donner à l’interface d’une classe une apparence soignée et belle de l’extérieur, elle ne peut pas être aussi soignée et belle de l’intérieur.
Des descripteurs apparaissent
Que sont les descripteurs ?
De manière générale, un descripteur est une propriété d'objet avec un comportement de liaison, et l'accès à ses propriétés est remplacé par les méthodes du protocole de descripteur. Ces méthodes sont __get__(), __set__() et __delete__(). Tant qu'un objet contient au moins une de ces trois méthodes, il est appelé un descripteur. À quoi sert le descripteur
?
Le comportement par défaut pour l'accès aux attributs consiste à obtenir, définir ou supprimer l'attribut du dictionnaire d'un objet. Par exemple, a.x a une chaîne de recherche commençant par a.__dict__['x'], puis tapez (a. ) .__dict__['x'], et en continuant à travers les classes de base de type(a) à l'exclusion des métaclasses. Si la valeur recherchée est un objet définissant l'une des méthodes du descripteur, alors Python peut remplacer le comportement par défaut et appeler le descripteur. L'endroit où cela se produit dans la chaîne de priorité dépend des méthodes de descripteur définies. —–Extrait de la documentation officielle
En termes simples, les descripteurs changeront la manière de base d'obtenir, de définir et de supprimer un attribut.
Voyons d'abord comment utiliser les descripteurs pour résoudre le problème de la logique de propriété répétée ci-dessus.
class Integer(object): def __init__(self, name): self.name = name def __get__(self, instance, owner): return instance.__dict__[self.name] def __set__(self, instance, value): if value < 0: raise ValueError("Negative value not allowed") instance.__dict__[self.name] = value class Movie(object): score = Integer('score') ticket = Integer('ticket')
Parce que le descripteur a une priorité élevée et modifiera le comportement d'obtention et de définition par défaut, donc lorsque nous accédons ou définissons Movie Score(), ce sera le cas. limité par le descripteur Integer.
Cependant, nous ne pouvons pas toujours créer des instances de la manière suivante.
a = Movie() a.score = 1 a.ticket = 2 a.title = ‘test' a.descript = ‘…'
C'est trop direct, il nous manque donc toujours un constructeur.
class Integer(object): def __init__(self, name): self.name = name def __get__(self, instance, owner): if instance is None: return self return instance.__dict__[self.name] def __set__(self, instance, value): if value < 0: raise ValueError('Negative value not allowed') instance.__dict__[self.name] = value class Movie(object): score = Integer('score') ticket = Integer('ticket') def __init__(self, title, description, score, ticket): self.title = title self.description = description self.score = score self.ticket = ticket
De cette façon, les __get__ et __set__ d'Integer seront saisis lors de l'obtention, de la définition et de la suppression du score et du ticket, réduisant ainsi la logique répétée.
Maintenant que le problème est résolu, vous pourriez être curieux de savoir comment fonctionne exactement ce descripteur. Plus précisément, ce qui est accessible dans la fonction __init__ est son propre self.score et self.ticket. Comment est-il lié aux attributs de classe score et ticket ?
Comment fonctionnent les descripteurs
Voir la description officielle
Si un objet définit à la fois __get__() et __set__(), il est considéré comme un descripteur de données qui définit uniquement des descripteurs __get__. () sont appelés descripteurs de non-données (ils sont généralement utilisés pour les méthodes mais d'autres utilisations sont possibles).
Les descripteurs de données et de non-données diffèrent dans la manière dont les remplacements sont calculés par rapport aux entrées du dictionnaire d'une instance If. le dictionnaire d'une instance a une entrée avec le même nom qu'un descripteur de données, le descripteur de données est prioritaire. Si le dictionnaire d'une instance a une entrée avec le même nom qu'un descripteur de données, l'entrée du dictionnaire est prioritaire.
Les points importants à retenir sont :
les descripteurs sont invoqués par la méthode __getattribute__()
la substitution de __getattribute__() empêche les appels automatiques de descripteurs
object.__getattribute__() et type.__getattribute__() font la différence les appels à __get__().
les descripteurs de données remplacent toujours les dictionnaires d'instance.
les descripteurs non-données peuvent être remplacés par les dictionnaires d'instance.
Lorsqu'une classe appelle __getattribute__(), c'est probablement le cas. ressemble à ceci :
def __getattribute__(self, key): "Emulate type_getattro() in Objects/typeobject.c" v = object.__getattribute__(self, key) if hasattr(v, '__get__'): return v.__get__(None, self) return v
Ce qui suit est extrait d'un blog étranger.
Étant donné une classe « C » et une instance « c » où « c = C(…) », appeler « c.name » signifie rechercher un attribut « nom » sur l'instance « c » comme ceci :
Get the Class from Instance
Call the Class's special method getattribute__. All objects have a default __getattribute
Inside getattribute
Get the Class's mro as ClassParents
For each ClassParent in ClassParents
If the Attribute is in the ClassParent's dict
If is a data descriptor
Return the result from calling the data descriptor's special method __get__()
Break the for each (do not continue searching the same Attribute any further)
If the Attribute is in Instance's dict
Return the value as it is (even if the value is a data descriptor)
For each ClassParent in ClassParents
If the Attribute is in the ClassParent's dict
If is a non-data descriptor
Return the result from calling the non-data descriptor's special method __get__()
If it is NOT a descriptor
Return the value
If Class has the special method getattr
Return the result from calling the Class's special method__getattr__.
我对上面的理解是,访问一个实例的属性的时候是先遍历它和它的父类,寻找它们的__dict__里是否有同名的data descriptor如果有,就用这个data descriptor代理该属性,如果没有再寻找该实例自身的__dict__ ,如果有就返回。任然没有再查找它和它父类里的non-data descriptor,最后查找是否有__getattr__
描述符的应用场景
python的property、classmethod修饰器本身也是一个描述符,甚至普通的函数也是描述符(non-data discriptor)
django model和SQLAlchemy里也有描述符的应用
class User(db.Model): id = db.Column(db.Integer, primary_key=True) username = db.Column(db.String(80), unique=True) email = db.Column(db.String(120), unique=True) def __init__(self, username, email): self.username = username self.email = email def __repr__(self): return '<User %r>' % self.username
总结
只有当确实需要在访问属性的时候完成一些额外的处理任务时,才应该使用property。不然代码反而会变得更加啰嗦,而且这样会让程序变慢很多。以上就是本文的全部内容,由于个人能力有限,文中如有笔误、逻辑错误甚至概念性错误,还请提出并指正。
更多Python属性和描述符的使用相关文章请关注PHP中文网!