Attributs et protocole de gestion en Python

Considérez le code suivant:


class Foo: def __init__(self): self.bar = 'hello!' foo = Foo() print(foo.bar) 

Aujourd'hui, nous analyserons la réponse à la question: «Que se passe-t-il exactement lorsque nous écrivons foo.bar




Vous savez peut-être déjà que la plupart des objets ont un dictionnaire interne __dict__ contenant tous leurs attributs. Et ce qui est particulièrement agréable, c'est à quel point il est facile d'étudier ces détails de bas niveau en Python:


 >>> foo = Foo() >>> foo.__dict__ {'bar': 'hello!'} 

Commençons par essayer de formuler cette hypothèse (incomplète):


foo.bar est équivalent à foo .__ dict __ ['bar'] .


Bien que cela ressemble à la vérité:


 >>> foo = Foo() >>> foo.__dict__['bar'] 'hello!' 


Supposons maintenant que vous savez déjà que les attributs dynamiques peuvent être déclarés dans les classes:


 >>> class Foo: ... def __init__(self): ... self.bar = 'hello!' ... ... def __getattr__(self, item): ... return 'goodbye!' ... ... foo = Foo() >>> foo.bar 'hello!' >>> foo.baz 'goodbye!' >>> foo.__dict__ {'bar': 'hello!'} 

Hmm ... d'accord. On peut voir que __getattr__ peut émuler l'accès aux "faux" attributs, mais ne fonctionnera pas s'il existe déjà une variable déclarée (comme foo.bar qui retourne "bonjour!" Et non "au revoir!" ). Tout semble être un peu plus compliqué qu'il n'y paraissait au début.


Et en effet: il y a une méthode magique qui est appelée chaque fois que nous essayons d'obtenir un attribut, mais, comme l'exemple ci-dessus l'a montré, ce n'est pas __getattr__ . La méthode appelée s'appelle __getattribute__ , et nous essaierons de comprendre comment elle fonctionne exactement en observant diverses situations.


Jusqu'à présent, nous modifions notre hypothèse comme suit:


foo.bar est équivalent à foo .__ getattribute __ ('bar') , qui fonctionne à peu près comme ceci:



 def __getattribute__(self, item): if item in self.__dict__: return self.__dict__[item] return self.__getattr__(item) 

Nous allons le tester en implémentant cette méthode (sous un nom différent) et en l'appelant directement:


 >>> class Foo: ... def __init__(self): ... self.bar = 'hello!' ... ... def __getattr__(self, item): ... return 'goodbye!' ... ... def my_getattribute(self, item): ... if item in self.__dict__: ... return self.__dict__[item] ... return self.__getattr__(item) >>> foo = Foo() >>> foo.bar 'hello!' >>> foo.baz 'goodbye!' >>> foo.my_getattribute('bar') 'hello!' >>> foo.my_getattribute('baz') 'goodbye!' 

Ça a l'air bien, non?



Eh bien, il ne reste plus qu'à vérifier que l'affectation des variables est prise en charge, après quoi vous pouvez rentrer chez vous ... -


 >>> foo.baz = 1337 >>> foo.baz 1337 >>> foo.my_getattribute('baz') = 'h4x0r' SyntaxError: can't assign to function call 

L'enfer


my_getattribute renvoie un objet. Nous pouvons le changer s'il est mutable, mais nous ne pouvons pas le remplacer par un autre en utilisant l'opérateur d'affectation. Que faire? Après tout, si foo.baz équivaut à appeler une fonction, comment peut-on attribuer une nouvelle valeur à un attribut en principe?


Lorsque nous regardons une expression comme foo.bar = 1 , il y a plus que d'appeler une fonction pour obtenir la valeur de foo.bar . L'attribution d'une valeur à un attribut semble être fondamentalement différente de l' obtention d'une valeur d' attribut. Et la vérité: nous pouvons implémenter __setattr__ pour voir ceci:


 >>> class Foo: ... def __init__(self): ... self.__dict__['my_dunder_dict'] = {} ... self.bar = 'hello!' ... ... def __setattr__(self, item, value): ... self.my_dunder_dict[item] = value ... ... def __getattr__(self, item): ... return self.my_dunder_dict[item] >>> foo = Foo() >>> foo.bar 'hello!' >>> foo.bar = 'goodbye!' >>> foo.bar 'goodbye!' >>> foo.baz Traceback (most recent call last): File "<pyshell#75>", line 1, in <module> foo.baz File "<pyshell#70>", line 10, in __getattr__ return self.my_dunder_dict[item] KeyError: 'baz' >>> foo.baz = 1337 >>> foo.baz 1337 >>> foo.__dict__ {'my_dunder_dict': {'bar': 'goodbye!', 'baz': 1337}} 

Quelques points à noter concernant ce code:


  1. __setattr__ n'a pas d'équivalent à __getattribute__ (c'est-à-dire que la méthode magique __setattribute__ n'existe pas).
  2. __setattr__ est appelé à l'intérieur de __init__ , c'est pourquoi nous sommes obligés de faire soi - même .__ dict __ ['my_dunder_dict'] = {} au lieu de self.my_dunder_dict = {} . Sinon, nous rencontrerions une récursion infinie.


Mais nous avons aussi des biens (et ses amis). Un décorateur qui permet aux méthodes d'agir comme des attributs.


Essayons de comprendre comment cela se produit.


 >>> class Foo(object): ... def __getattribute__(self, item): ... print('__getattribute__ was called') ... return super().__getattribute__(item) ... ... def __getattr__(self, item): ... print('__getattr__ was called') ... return super().__getattr__(item) ... ... @property ... def bar(self): ... print('bar property was called') ... return 100 >>> f = Foo() >>> f.bar __getattribute__ was called bar property was called 

Juste pour le plaisir, qu'avons -nous dans f .__ dict__ ?


 >>> f.__dict__ __getattribute__ was called {} 

Il n'y a pas de clé de barre dans __dict__ , mais __getattr__ n'est pas appelé pour une raison quelconque. Wat?


bar est une méthode qui prend également self en paramètre, seule cette méthode se trouve dans la classe et non dans l'instance de classe. Et c'est facile à voir:


 >>> Foo.__dict__ mappingproxy({'__dict__': <attribute '__dict__' of 'Foo' objects>, '__doc__': None, '__getattr__': <function Foo.__getattr__ at 0x038308A0>, '__getattribute__': <function Foo.__getattribute__ at 0x038308E8>, '__module__': '__main__', '__weakref__': <attribute '__weakref__' of 'Foo' objects>, 'bar': <property object at 0x0381EC30>}) 

La touche de barre se trouve en effet dans le dictionnaire d'attributs de classe. Pour comprendre comment fonctionne __getattribute__ , nous devons répondre à la question: dont __getattribute__ est appelé auparavant - une classe ou une instance?


 >>> f.__dict__['bar'] = 'will we see this printed?' __getattribute__ was called >>> f.bar __getattribute__ was called bar property was called 100 

On peut voir que la première chose que la vérification est dans la classe __dict__ , c'est-à-dire il a priorité sur l'instance.



Attendez une minute, quand avons-nous appelé la méthode du bar ? Je veux dire, notre pseudo-code pour __getattribute__ n'appelle jamais un objet. Que se passe-t-il?


Rencontrez le protocole descripteur :


descr .__ get __ (self, obj, type = None) -> valeur


descr .__ set __ (self, obj, value) -> Aucun


descr .__ delete __ (self, obj) -> Aucun



Tout est là. Implémentez l'une de ces trois méthodes afin que l'objet devienne un descripteur et puisse modifier le comportement par défaut lorsqu'il est traité comme un attribut.



Si un objet déclare à la fois __get __ () et __set __ () , il est alors appelé descripteur de données. Les descripteurs qui implémentent uniquement __get __ () sont appelés des descripteurs non liés aux données.



Les deux types de descripteurs diffèrent dans la façon dont les éléments du dictionnaire d'attributs d'objet sont remplacés. Si le dictionnaire contient une clé portant le même nom que le descripteur de données, le descripteur de données est prioritaire (c'est-à-dire que __set __ () est appelé). Si le dictionnaire contient une clé du même nom que le descripteur sans données, le dictionnaire a la priorité (c'est-à-dire que l'élément du dictionnaire est écrasé).



Pour créer un descripteur de données en lecture seule, déclarez à la fois __get __ () et __set __ () , où __set __ () lance une AttributeError lors de son appel. L'implémentation de ce __set __ () suffit pour créer un descripteur de données.


En bref, si vous avez déclaré l'une de ces méthodes __get__ , __set__ ou __delete__ , vous avez implémenté la prise en charge du protocole de descripteur. Et c'est exactement ce que fait le décorateur de propriétés : il déclare un descripteur en lecture seule qui sera appelé dans __getattribute__ .


Dernier changement dans notre implémentation:


foo.bar est équivalent à foo .__ getattribute __ ('bar') , qui fonctionne à peu près comme ceci:



 def __getattribute__(self, item): if item in self.__class__.__dict__: v = self.__class__.__dict__[item] elif item in self.__dict__: v = self.__dict__[item] else: v = self.__getattr__(item) if hasattr(v, '__get__'): v = v.__get__(self, type(self)) return v 

Essayons de démontrer en pratique:


 class Foo: class_attr = "I'm a class attribute!" def __init__(self): self.dict_attr = "I'm in a dict!" @property def property_attr(self): return "I'm a read-only property!" def __getattr__(self, item): return "I'm dynamically returned!" def my_getattribute(self, item): if item in self.__class__.__dict__: print('Retrieving from self.__class__.__dict__') v = self.__class__.__dict__[item] elif item in self.__dict__: print('Retrieving from self.__dict__') v = self.__dict__[item] else: print('Retrieving from self.__getattr__') v = self.__getattr__(item) if hasattr(v, '__get__'): print("Invoking descriptor's __get__") v = v.__get__(self, type(self)) return v 


 >>> foo = Foo() ... ... print(foo.class_attr) ... print(foo.dict_attr) ... print(foo.property_attr) ... print(foo.dynamic_attr) ... ... print() ... ... print(foo.my_getattribute('class_attr')) ... print(foo.my_getattribute('dict_attr')) ... print(foo.my_getattribute('property_attr')) ... print(foo.my_getattribute('dynamic_attr')) I'm a class attribute! I'm in a dict! I'm a read-only property! I'm dynamically returned! Retrieving from self.__class__.__dict__ I'm a class attribute! Retrieving from self.__dict__ I'm in a dict! Retrieving from self.__class__.__dict__ Invoking descriptor's __get__ I'm a read-only property! Retrieving from self.__getattr__ I'm dynamically returned! 


Nous venons de gratter un peu la surface de l'implémentation des attributs en Python. Bien que notre dernière tentative d'émuler foo.bar soit généralement correcte, gardez à l'esprit qu'il peut toujours y avoir de petits détails implémentés différemment.


J'espère qu'en plus de savoir comment fonctionnent les attributs, j'ai aussi réussi à transmettre la beauté du langage qui vous encourage à expérimenter. Remboursez aujourd'hui une partie de la dette de connaissances .

Source: https://habr.com/ru/post/fr479824/


All Articles