Le livre «Python. Bonnes pratiques et outils "

imageBonjour, Habitants! Python est un langage de programmation dynamique utilisé dans une grande variété de domaines. Bien qu'il soit facile d'écrire du code en Python, il est beaucoup plus difficile de rendre le code lisible, réutilisable et facile à maintenir. Troisième édition de Python. Best Practices and Tools »vous donnera les outils nécessaires pour résoudre efficacement tout problème de développement et de maintenance de logiciels. Les auteurs commencent par parler des nouvelles fonctionnalités de Python 3.7 et des aspects avancés de la syntaxe Python. Ils continuent avec des conseils sur la mise en œuvre de paradigmes populaires, y compris la programmation orientée objet, fonctionnelle et événementielle. Les auteurs parlent également des meilleures pratiques de dénomination, de la façon dont vous pouvez automatiser le déploiement de programmes sur des serveurs distants. Tu vas apprendre,comment créer des extensions Python utiles en C, C ++, Cython et CFFI.



Pour qui est ce livre
Python, . , Python. , , , Python.



, . , Python. , , . Python 3.7 , Python 2.7 .



- -, , : .



Modèles d'accès pour les attributs étendus



Lors de l'apprentissage de Python, de nombreux programmeurs C ++ et Java sont surpris de l'absence du mot-clé private. Le concept le plus proche est celui de la mutilation des noms. Chaque fois qu'un attribut est préfixé avec __, il est renommé dynamiquement par l'interpréteur:



class MyClass:
__secret_value = 1
      
      





L'accès à l'attribut __secret_value par son nom d'origine lèvera une exception AttributeError:



>>> instance_of = MyClass()
>>> instance_of.__secret_value
Traceback (most recent call last):
   File "<stdin>", line 1, in <module>
AttributeError: 'MyClass' object has no attribute '__secret_value'
>>> dir(MyClass)
['_MyClass__secret_value', '__class__', '__delattr__', '__dict__',
'__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__',
'__gt__', '__hash__', '__init__', '__le__', '__lt__', '__module__',
'__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__',
'__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__']
>>> instance_of._MyClass__secret_value
1
      
      





Ceci est fait spécifiquement pour éviter les conflits de nom par héritage, puisque l'attribut est renommé par le nom de classe comme préfixe. Ce n'est pas un analogue exact de private, puisque l'attribut est accessible via un nom concaténé. Cette propriété peut être utilisée pour protéger l'accès à certains attributs, mais en pratique __ n'est jamais utilisée. Si l'attribut n'est pas public, il est d'usage d'utiliser le préfixe _. Il n'appelle pas l'algorithme de décoration de nom, mais documente l'attribut comme un élément privé de la classe et est le style prédominant.



Python a d'autres mécanismes pour séparer la partie publique de la partie privée d'une classe. Les descripteurs et les propriétés permettent de mettre de l'ordre dans cette séparation.



Descripteurs



Le descripteur vous permet de personnaliser l'action qui se produit lorsque vous référencez un attribut sur un objet.



Les descripteurs sont au cœur de l'accès aux attributs complexes en Python. Ils sont utilisés pour implémenter des propriétés, des méthodes, des méthodes de classe, des méthodes statiques et des supertypes. Ce sont les classes qui définissent comment les attributs d'une autre classe seront accessibles. En d'autres termes, une classe peut déléguer le contrôle d'un attribut à une autre classe.



Les classes de descripteur sont basées sur trois méthodes spéciales qui forment le protocole de descripteur:



__set __ (self, obj, value) - Appelé chaque fois qu'un attribut est défini. Dans les exemples suivants, nous l'appellerons «setter»;



__get __ (self, obj, owner = None) - appelé chaque fois que l'attribut est lu (ci-après le getter);



__delete __ (self, object) - Appelé lorsque del est appelé par un attribut.



Un descripteur qui implémente __get__ et __set__ est appelé un descripteur de données. S'il implémente simplement __get__, il est appelé un descripteur sans données.



Les méthodes de ce protocole sont en fait appelées par la méthode __getattribute __ () (à ne pas confondre avec __getattr __ (), qui a un but différent) à chaque fois qu'un attribut est recherché. Chaque fois qu'une telle recherche est effectuée à l'aide d'un point ou d'un appel direct de fonction, la méthode __getattribute __ () est implicitement appelée, qui recherche l'attribut dans l'ordre suivant.



  1. Vérifie si un attribut est un descripteur de données sur un objet de la classe d'instance.
  2. Sinon, il cherche à voir si l'attribut se trouve dans le __dict__ de l'objet d'instance.
  3. Enfin, vérifie si l'attribut est un handle sans données sur l'objet de classe d'instance.


En d'autres termes, les descripteurs de données ont priorité sur __dict__, qui à son tour a la priorité sur les descripteurs sans données.



Pour plus de clarté, voici un exemple de la documentation officielle Python qui montre comment les descripteurs fonctionnent dans du code réel:



class RevealAccess(object):
   """ ,     
           
   """
   def __init__(self, initval=None, name='var'):
      self.val = initval
      self.name = name
   def __get__(self, obj, objtype):
      print('Retrieving', self.name)
      return self.val
   def __set__(self, obj, val):
      print('Updating', self.name)
      self.val = val
class MyClass(object):
   x = RevealAccess(10, 'var "x"')
   y = 5
      
      





Voici un exemple d'utilisation interactive:



>>> m = MyClass()
>>> m.x
Retrieving var "x"
10
>>> m.x = 20
Updating var "x"
>>> m.x
Retrieving var "x"
20
>>> m.y
5
      
      





L'exemple montre clairement que si la classe a un descripteur de données pour cet attribut, alors __get __ () est appelé pour renvoyer une valeur chaque fois qu'un attribut d'instance est récupéré, et __set __ () est appelé chaque fois qu'une valeur est attribuée à cet attribut. L'utilisation de la méthode __del__ n'est pas montrée dans l'exemple précédent, mais elle devrait être évidente: elle est appelée chaque fois qu'un attribut d'instance est supprimé à l'aide de l'instruction del instance.attribute ou delattr (instance, 'attribute').



La différence entre les descripteurs de données et non-données est importante pour les raisons que nous avons mentionnées au début de cette sous-section. Python utilise le protocole descripteur pour lier les fonctions de classe aux instances via des méthodes. Ils s'appliquent également aux décorateurs de méthode de classe et de méthode statique. En effet, les objets fonctionnels sont essentiellement également des descripteurs sans données:



>>> def function(): pass
>>> hasattr(function, '__get__')
True
>>> hasattr(function, '__set__')
False
      
      





Il en va de même pour les fonctions créées à l'aide d'expressions lambda:



>>> hasattr(lambda: None, '__get__')
True
>>> hasattr(lambda: None, '__set__')
False
      
      





Ainsi, à moins que __dict__ ait la priorité sur les descripteurs sans données, nous ne serons pas en mesure de remplacer dynamiquement des méthodes spécifiques d'instances déjà instanciées lors de l'exécution. Heureusement, grâce au fonctionnement des descripteurs en Python, cela est possible; par conséquent, les développeurs peuvent choisir les instances qui fonctionnent sans utiliser de sous-classes.



Exemple réel: évaluation paresseuse des attributs. Un exemple d'utilisation de descripteurs consiste à retarder l'initialisation d'un attribut de classe lorsqu'il est accédé à partir d'une instance. Cela peut être utile si l'initialisation de ces attributs dépend du contexte global de l'application. Un autre cas est celui où une telle initialisation est trop coûteuse, et on ne sait pas si l'attribut sera utilisé après l'importation de la classe. Un tel descripteur peut être implémenté comme suit:



class InitOnAccess:
   def __init__(self, klass, *args, **kwargs):
      self.klass = klass
      self.args = args
      self.kwargs = kwargs
      self._initialized = None
   def __get__(self, instance, owner):
      if self._initialized is None:
         print('initialized!')
         self._initialized = self.klass(*self.args, **self.kwargs)
      else:
         print('cached!')
      return self._initialized
      
      





Voici un exemple d'utilisation:



>>> class MyClass:
... lazily_initialized = InitOnAccess(list, "argument")
...
>>> m = MyClass()
>>> m.lazily_initialized
initialized!
['a', 'r', 'g', 'u', 'm', 'e', 'n', 't']
>>> m.lazily_initialized
cached!
['a', 'r', 'g', 'u', 'm', 'e', 'n', 't']
      
      





La bibliothèque officielle PyPI OpenGL Python appelée PyOpenGL utilise une technique comme celle-ci pour implémenter un objet lazy_property qui est à la fois un décorateur et un descripteur de données:



class lazy_property(object):
   def __init__(self, function):
      self.fget = function
   def __get__(self, obj, cls):
      value = self.fget(obj)
      setattr(obj, self.fget.__name__, value)
      return value
      
      





Cette implémentation est similaire à l'utilisation du décorateur de propriété (nous en reparlerons plus tard), mais la fonction qui est enveloppée dans le décorateur n'est exécutée qu'une seule fois, puis l'attribut class est remplacé par la valeur retournée par cette propriété de fonction. Cette méthode est souvent utile lorsque vous devez répondre simultanément à deux exigences:



  • une instance d'objet doit être enregistrée en tant qu'attribut de classe, qui est partagé entre ses instances (pour économiser les ressources);
  • cet objet ne peut pas être initialisé au moment de l'importation, car le processus de sa création dépend d'un état global de l'application / du contexte.


Dans le cas des applications écrites à l'aide d'OpenGL, vous rencontrerez souvent cette situation. Par exemple, la création de shaders dans OpenGL est coûteuse car elle nécessite la compilation de code écrit en OpenGL Shading Language (GLSL). Il est logique de les créer une seule fois et en même temps de garder leur description à proximité des classes qui en ont besoin. D'autre part, les compilations de shaders ne peuvent pas être effectuées sans initialiser le contexte OpenGL, il est donc difficile de les définir et de les assembler dans l'espace de noms du module global au moment de l'importation.



L'exemple suivant montre une utilisation possible d'une version modifiée du décorateur lazy_property PyOpenGL (ici lazy_class_attribute) dans une application OpenGL abstraite. Des modifications du décorateur lazy_property d'origine sont nécessaires pour permettre à l'attribut d'être partagé entre différentes instances de la classe:



import OpenGL.GL as gl
from OpenGL.GL import shaders
class lazy_class_attribute(object):
   def __init__(self, function):
      self.fget = function
   def __get__(self, obj, cls):
      value = self.fget(obj or cls)
      # :   - 
      #    
      setattr(cls, self.fget.__name__, value)
      return value
class ObjectUsingShaderProgram(object):
   #   -
    VERTEX_CODE = """
      #version 330 core
      layout(location = 0) in vec4 vertexPosition;
      void main(){
         gl_Position = vertexPosition;
      }
"""
#  ,    
FRAGMENT_CODE = """
   #version 330 core
   out lowp vec4 out_color;
   void main(){
      out_color = vec4(1, 1, 1, 1);
   }
"""
@lazy_class_attribute
def shader_program(self):
   print("compiling!")
   return shaders.compileProgram(
      shaders.compileShader(
         self.VERTEX_CODE, gl.GL_VERTEX_SHADER
      ),
      shaders.compileShader(
         self.FRAGMENT_CODE, gl.GL_FRAGMENT_SHADER
      )
   )
      
      





Comme toutes les fonctionnalités avancées de la syntaxe Python, celle-ci doit également être utilisée avec précaution et bien documentée dans le code. Pour les développeurs inexpérimentés, le changement de comportement d'une classe peut être une surprise car les descripteurs affectent le comportement de la classe. Par conséquent, il est très important de s'assurer que tous les membres de votre équipe connaissent les descripteurs et comprennent ce concept s'il joue un rôle important dans la base de code du projet.



Propriétés



Les propriétés fournissent un type de descripteur intégré qui sait comment associer un attribut à un ensemble de méthodes. La propriété prend quatre arguments facultatifs: fget, fset, fdel et doc. Ce dernier peut être fourni pour définir la docstring associée à l'attribut comme s'il s'agissait d'une méthode. Vous trouverez ci-dessous un exemple de classe Rectangle qui peut être manipulée soit en accédant directement aux attributs contenant deux points d'angle, soit en utilisant les propriétés width et height:



class Rectangle:
   def __init__(self, x1, y1, x2, y2):
      self.x1, self.y1 = x1, y1
      self.x2, self.y2 = x2, y2
   def _width_get(self):
      return self.x2 - self.x1
      def _width_set(self, value):
      self.x2 = self.x1 + value
   def _height_get(self):
      return self.y2 - self.y1
   def _height_set(self, value):
      self.y2 = self.y1 + value
   width = property(
       _width_get, _width_set,
       doc="rectangle width measured from left"
   )
   height = property(
       _height_get, _height_set,
       doc="rectangle height measured from top"
   )
   def __repr__(self):
      return "{}({}, {}, {}, {})".format(
         self.__class__.__name__,
         self.x1, self.y1, self.x2, self.y2
     )

      
      





L'extrait de code suivant fournit un exemple de telles propriétés définies dans une session interactive:



>>> rectangle.width, rectangle.height
(15, 24)
>>> rectangle.width = 100
>>> rectangle
Rectangle(10, 10, 110, 34)
>>> rectangle.height = 100
>>> rectangle
Rectangle(10, 10, 110, 110)
>>> help(Rectangle)
Help on class Rectangle in module chapter3:
class Rectangle(builtins.object)
| Methods defined here:
|
| __init__(self, x1, y1, x2, y2)
| Initialize self. See help(type(self)) for accurate signature.
|
| __repr__(self)
| Return repr(self).
|
| --------------------------------------------------------
| Data descriptors defined here:
| (...)
|
| height
| rectangle height measured from top
|
| width
| rectangle width measured from left
      
      





Ces propriétés facilitent l'écriture des descripteurs, mais doivent être manipulées avec précaution lors de l'utilisation de l'héritage de classe. L'attribut est créé dynamiquement à l'aide des méthodes de la classe actuelle et n'appliquera pas les méthodes qui sont remplacées dans les classes dérivées.



Le code de l'exemple suivant ne pourra pas remplacer l'implémentation de la méthode fget à partir de la propriété width de la classe parent (Rectangle):



>>> class MetricRectangle(Rectangle):
... def _width_get(self):
... return "{} meters".format(self.x2 - self.x1)
...
>>> Rectangle(0, 0, 100, 100).width
100
      
      





Pour résoudre ce problème, la propriété entière doit être écrasée dans la classe dérivée:



>>> class MetricRectangle(Rectangle):
... def _width_get(self):
... return "{} meters".format(self.x2 - self.x1)
... width = property(_width_get, Rectangle.width.fset)
...
>>> MetricRectangle(0, 0, 100, 100).width
'100 meters'
      
      





Malheureusement, le code présente des problèmes de maintenabilité. Une confusion peut survenir si un développeur décide de changer la classe parente mais oublie de mettre à jour l'appel de propriété. C'est pourquoi il n'est pas recommandé de remplacer uniquement certaines parties du comportement des propriétés. Au lieu de s'appuyer sur l'implémentation de la classe parente, il est judicieux de réécrire toutes les méthodes de propriété dans les classes dérivées si vous souhaitez modifier leur mode de fonctionnement. Il n'y a généralement pas d'autres options, car la modification des propriétés du comportement du setter entraîne un changement du comportement du getter.



La meilleure façon de créer des propriétés est d'utiliser la propriété comme décorateur. Cela réduira le nombre de signatures de méthode à l'intérieur de la classe et rendra le code plus lisible et maintenable:



class Rectangle:
   def __init__(self, x1, y1, x2, y2):
      self.x1, self.y1 = x1, y1
      self.x2, self.y2 = x2, y2
   @property
   def width(self):
      """    """
      return self.x2 - self.x1
   @width.setter
   def width(self, value):
      self.x2 = self.x1 + value
   @property
   def height(self):
      """   """
      return self.y2 - self.y1
   @height.setter
   def height(self, value):
      self.y2 = self.y1 + value
      
      





Machines à sous



Les machines à sous sont une fonctionnalité intéressante que les développeurs utilisent rarement. Ils vous permettent de définir une liste statique d'attributs pour une classe à l'aide de l'attribut __slots__ et d'ignorer la création d'un dictionnaire __dict__ dans chaque instance de la classe. Ils ont été créés pour économiser de l'espace mémoire pour les classes avec peu d'attributs, puisque __dict__ n'est pas créé dans chaque instance.



Ils peuvent également aider à créer des classes dont les signatures doivent être figées. Par exemple, si vous devez restreindre les propriétés dynamiques d'un langage pour une classe spécifique, les slots peuvent vous aider:



>>> class Frozen:
... __slots__ = ['ice', 'cream']
...
>>> '__dict__' in dir(Frozen)
False
>>> 'ice' in dir(Frozen)
True
>>> frozen = Frozen()
>>> frozen.ice = True
>>> frozen.cream = None
>>> frozen.icy = True
Traceback (most recent call last): File "<input>", line 1, in <module>
AttributeError: 'Frozen' object has no attribute 'icy'
      
      





Cette fonctionnalité doit être utilisée avec précaution. Lorsque l'ensemble des attributs disponibles est limité aux emplacements, il est beaucoup plus difficile d'ajouter quelque chose de manière dynamique à un objet. Certaines astuces bien connues, telles que le patching de singe, ne fonctionneront pas avec des instances de classes qui ont des emplacements spécifiques. Heureusement, de nouveaux attributs peuvent être ajoutés aux classes dérivées si elles n'ont pas leurs propres emplacements définis:



>>> class Unfrozen(Frozen):
... pass
...
>>> unfrozen = Unfrozen()
>>> unfrozen.icy = False
>>> unfrozen.icy
False
      
      





À propos des auteurs



Michal Jaworski est un programmeur Python avec dix ans d'expérience. Il a occupé différents postes dans diverses entreprises: d'un développeur full-stack régulier, puis d'un architecte logiciel et, enfin, à un vice-président du développement dans une start-up dynamique. Michal est actuellement Senior Backend Engineer chez Showpad. Possède une vaste expérience dans le développement de services distribués haute performance. De plus, il contribue activement à de nombreux projets Python open source.

Tarek Ziade est un développeur Python. Vit à la campagne près de Dijon, France. Travaille chez Mozilla, dans l'équipe des services. Tarek a fondé le groupe d'utilisateurs français Python (appelé Afpy) et a écrit plusieurs livres sur Python en français et en anglais. Pendant son temps libre de piratage et de fête, il s'adonne à ses passe-temps préférés: faire du jogging ou jouer de la trompette.



Vous pouvez visiter son blog personnel (Fetchez le Python) et le suivre sur Twitter (tarek_ziade).



À propos de l'éditeur scientifique



Cody Jackson est titulaire d'un doctorat, fondateur de Socius Consulting, une société de conseil en informatique et en gestion d'entreprise basée à San Antonio, et co-fondateur de Top Men Technologies. Il travaille actuellement pour CACI International en tant qu'ingénieur principal pour la modélisation ICS / SCADA. Dans l'industrie informatique depuis 1994, depuis son passage dans la Marine en tant que chimiste nucléaire et ingénieur radio. Avant de rejoindre le CACI, il a travaillé à l'université de l'ECPI en tant que professeur assistant de systèmes d'information informatique. J'ai appris la programmation Python par moi-même, j'ai écrit les livres Apprendre à programmer en utilisant Python et Recettes secrètes du Python Ninja.



Plus de détails sur le livre peuvent être trouvés sur le site de la maison d'édition

" Table of Contents

" Extrait



Pour Habitants une réduction de 25% sur le coupon - Python



Lors du paiement de la version papier du livre, un e-book est envoyé à l'e-mail.



All Articles