El libro “Python. Mejores prácticas y herramientas "

imagen¡Hola, habitantes! Python es un lenguaje de programación dinámico que se utiliza en una amplia variedad de áreas temáticas. Si bien es fácil escribir código en Python, es mucho más difícil hacer que el código sea legible, reutilizable y fácil de mantener. Tercera edición de Python. Best Practices and Tools ”le brindará las herramientas para resolver eficazmente cualquier problema de desarrollo y mantenimiento de software. Los autores comienzan hablando sobre las nuevas características de Python 3.7 y los aspectos avanzados de la sintaxis de Python. Continúan con el asesoramiento sobre la implementación de paradigmas populares, incluida la programación orientada a objetos, funcional y dirigida por eventos. Los autores también hablan sobre las mejores prácticas de nomenclatura, sobre cómo puede automatizar la implementación de programas en servidores remotos. Aprenderás,cómo crear extensiones Python útiles en C, C ++, Cython y CFFI.



Para quién es este libro
Python, . , Python. , , , Python.



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



- -, , : .



Patrones de acceso para atributos extendidos



Al aprender Python, muchos programadores de C ++ y Java se sorprenden de la falta de la palabra clave privada. El concepto más cercano es la alteración de nombres. Cada vez que un atributo tiene el prefijo __, el intérprete lo renombra dinámicamente:



class MyClass:
__secret_value = 1
      
      





Acceder al atributo __secret_value por su nombre original arrojará una excepción 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
      
      





Esto se hace específicamente para evitar conflictos de nombres por herencia, ya que el nombre de la clase cambia el nombre del atributo como prefijo. Este no es un análogo exacto de privado, ya que se puede acceder al atributo a través de un nombre concatenado. Esta propiedad se puede usar para proteger el acceso a algunos atributos, pero en la práctica, __ nunca se usa. Si el atributo no es público, es habitual utilizar el prefijo _. No invoca el algoritmo de decoración de nombres, pero documenta el atributo como un elemento privado de la clase y es el estilo predominante.



Python tiene otros mecanismos para separar la parte pública de la privada de una clase. Los descriptores y las propiedades proporcionan una forma de arreglar esta separación.



Descriptores



El descriptor le permite personalizar la acción que ocurre cuando hace referencia a un atributo en un objeto.



Los descriptores están en el corazón del acceso a atributos complejos en Python. Se utilizan para implementar propiedades, métodos, métodos de clase, métodos estáticos y supertipos. Estas son las clases que definen cómo se accederá a los atributos de otra clase. En otras palabras, una clase puede delegar el control de un atributo a otra clase.



Las clases de descriptor se basan en tres métodos especiales que forman el protocolo de descriptor:



__set __ (self, obj, value): se llama siempre que se establece un atributo. En los siguientes ejemplos, nos referiremos a él como "setter";



__get __ (self, obj, owner = None): se llama cada vez que se lee el atributo (en adelante, el getter);



__delete __ (self, object): se llama cuando un atributo llama a del.



Un descriptor que implementa __get__ y __set__ se llama descriptor de datos. Si solo implementa __get__, se denomina descriptor sin datos.



Los métodos de este protocolo en realidad son llamados por el método __getattribute __ () (que no debe confundirse con __getattr __ (), que tiene un propósito diferente) cada vez que se busca un atributo. Siempre que se realiza una búsqueda de este tipo utilizando un punto o una llamada de función directa, se llama implícitamente al método __getattribute __ (), que busca el atributo en el siguiente orden.



  1. Comprueba si un atributo es un descriptor de datos en un objeto de la clase de instancia.
  2. De lo contrario, busca para ver si el atributo se encuentra en el __dict__ del objeto de instancia.
  3. Finalmente, verifica si el atributo es un identificador sin datos en el objeto de clase de instancia.


En otras palabras, los descriptores de datos tienen prioridad sobre __dict__, que a su vez tiene prioridad sobre los descriptores que no son de datos.



Para mayor claridad, aquí hay un ejemplo de la documentación oficial de Python que muestra cómo funcionan los descriptores en código real:



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
      
      





Aquí hay un ejemplo de cómo usarlo de forma interactiva:



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





El ejemplo muestra claramente que si la clase tiene un descriptor de datos para ese atributo, entonces se llama a __get __ () para devolver un valor cada vez que se recupera un atributo de instancia, y se llama a __set __ () cada vez que se asigna un valor a ese atributo. El uso del método __del__ no se muestra en el ejemplo anterior, pero debería ser obvio: se llama cada vez que se elimina un atributo de instancia utilizando la declaración del instance.attribute o delattr (instancia, 'atributo').



La diferencia entre descriptores de datos y no datos es importante por las razones que mencionamos al comienzo de esta subsección. Python usa el protocolo descriptor para vincular funciones de clase a instancias a través de métodos. También se aplican a los decoradores classmethod y staticmethod. Esto se debe a que los objetos funcionales son esencialmente también descriptores sin datos:



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





Lo mismo ocurre con las funciones creadas con expresiones lambda:



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





Por lo tanto, a menos que __dict__ tenga prioridad sobre los descriptores sin datos, no podremos anular dinámicamente métodos específicos de instancias ya instanciadas en tiempo de ejecución. Afortunadamente, gracias a la forma en que funcionan los descriptores en Python, esto es posible; por lo tanto, los desarrolladores pueden elegir qué instancias funcionan sin usar subclases.



Ejemplo de la vida real: evaluación perezosa de atributos. Un ejemplo de uso de descriptores es retrasar la inicialización de un atributo de clase cuando se accede a él desde una instancia. Esto puede resultar útil si la inicialización de dichos atributos depende del contexto global de la aplicación. Otro caso es cuando dicha inicialización es demasiado cara y no se sabe si el atributo se utilizará después de importar la clase. Dicho descriptor se puede implementar de la siguiente manera:



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
      
      





A continuación se muestra un ejemplo de uso:



>>> 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 biblioteca oficial PyPI OpenGL Python llamada PyOpenGL usa una técnica como esta para implementar un objeto lazy_property que es tanto decorador como descriptor de datos:



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
      
      





Esta implementación es similar a usar el decorador de propiedades (hablaremos de ello más adelante), pero la función que está envuelta en el decorador se ejecuta solo una vez, y luego el atributo de clase se reemplaza con el valor devuelto por esta propiedad de función. Este método suele ser útil cuando necesita cumplir dos requisitos simultáneamente:



  • una instancia de objeto debe guardarse como un atributo de clase, que se comparte entre sus instancias (para ahorrar recursos);
  • este objeto no se puede inicializar en el momento de la importación, ya que el proceso de su creación depende de algún estado global de la aplicación / contexto.


En el caso de aplicaciones escritas con OpenGL, a menudo se encontrará con esta situación. Por ejemplo, crear sombreadores en OpenGL es costoso porque requiere compilar código escrito en OpenGL Shading Language (GLSL). Tiene sentido crearlos solo una vez y al mismo tiempo mantener su descripción cerca de las clases que los necesitan. Por otro lado, las compilaciones de sombreadores no se pueden realizar sin inicializar el contexto OpenGL, por lo que es difícil definirlas y ensamblarlas en el espacio de nombres del módulo global en el momento de la importación.



El siguiente ejemplo muestra un posible uso de una versión modificada del decorador lazy_property PyOpenGL (aquí lazy_class_attribute) en alguna aplicación OpenGL abstracta. Se requieren cambios en el decorador de lazy_property original para permitir que el atributo se comparta entre diferentes instancias de la clase:



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
      )
   )
      
      





Como todas las funciones avanzadas de sintaxis de Python, esta también debe usarse con cuidado y bien documentada en el código. Para los desarrolladores sin experiencia, el comportamiento cambiado de una clase puede ser una sorpresa porque los descriptores afectan el comportamiento de la clase. Por lo tanto, es muy importante asegurarse de que todos los miembros de su equipo estén familiarizados con los descriptores y comprendan este concepto si juega un papel importante en la base de código del proyecto.



Propiedades



Las propiedades proporcionan un tipo de descriptor incorporado que sabe cómo asociar un atributo con un conjunto de métodos. La propiedad toma cuatro argumentos opcionales: fget, fset, fdel y doc. Este último se puede proporcionar para definir la cadena de documentos asociada con el atributo como si fuera un método. A continuación se muestra un ejemplo de una clase Rectangle que se puede manipular accediendo directamente a los atributos que contienen dos puntos de esquina o utilizando las propiedades de ancho y alto:



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
     )

      
      





El siguiente fragmento de código proporciona un ejemplo de dichas propiedades definidas en una sesión interactiva:



>>> 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
      
      





Estas propiedades facilitan la escritura de los descriptores, pero deben manejarse con cuidado al usar la herencia de clases. El atributo se crea dinámicamente utilizando los métodos de la clase actual y no aplicará métodos que se reemplacen en clases derivadas.



El código del siguiente ejemplo no podrá anular la implementación del método fget de la propiedad width de la clase principal (Rectangle):



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





Para resolver este problema, se debe sobrescribir toda la propiedad en la clase derivada:



>>> 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'
      
      





Desafortunadamente, el código tiene algunos problemas de mantenimiento. Puede surgir confusión si un desarrollador decide cambiar la clase principal pero se olvida de actualizar la llamada de propiedad. Es por eso que no se recomienda anular solo partes del comportamiento de las propiedades. En lugar de depender de la implementación de la clase principal, es una buena idea reescribir todos los métodos de propiedad en las clases derivadas si desea cambiar la forma en que funcionan. Por lo general, no hay otras opciones, ya que cambiar las propiedades del comportamiento del setter implica un cambio en el comportamiento del getter.



La mejor forma de crear propiedades es utilizar la propiedad como decorador. Esto reducirá la cantidad de firmas de métodos dentro de la clase y hará que el código sea más legible y fácil de mantener:



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
      
      





Tragamonedas



Una característica interesante que los desarrolladores rara vez utilizan son las tragamonedas. Le permiten establecer una lista estática de atributos para una clase usando el atributo __slots__ y omitir la creación de un diccionario __dict__ en cada instancia de la clase. Fueron creados para ahorrar espacio de memoria para clases con pocos atributos, ya que __dict__ no se crea en todas las instancias.



También pueden ayudar a crear clases cuyas firmas deben congelarse. Por ejemplo, si necesita restringir las propiedades dinámicas de un idioma para una clase específica, las ranuras pueden ayudar:



>>> 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'
      
      





Esta función debe utilizarse con precaución. Cuando el conjunto de atributos disponibles se limita a las ranuras, es mucho más difícil agregar algo a un objeto de forma dinámica. Algunos trucos conocidos, como el parche de monos, no funcionarán con instancias de clases que tengan espacios específicos. Afortunadamente, se pueden agregar nuevos atributos a las clases derivadas si no tienen sus propios espacios definidos:



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





Acerca de los autores



Michal Jaworski es un programador de Python con diez años de experiencia. Ha ocupado diversos puestos en varias empresas: desde desarrollador full-stack habitual, luego arquitecto de software y, finalmente, hasta vicepresidente de desarrollo en una empresa emergente dinámica. Michal es actualmente ingeniero de backend senior en Showpad. Tiene una amplia experiencia en el desarrollo de servicios distribuidos de alto rendimiento. Además, es un colaborador activo de muchos proyectos Python de código abierto.

Tarek Ziade es un desarrollador de Python. Vive en el campo cerca de Dijon, Francia. Trabaja en Mozilla, en el equipo de servicios. Tarek fundó el grupo de usuarios francés de Python (llamado Afpy) y ha escrito varios libros sobre Python en francés e inglés. En su tiempo libre de piratería y fiestas, se dedica a sus pasatiempos favoritos: trotar o tocar la trompeta.



Puede visitar su blog personal (Fetchez le Python) y seguirlo en Twitter (tarek_ziade).



Sobre el editor científico



Cody Jackson es un Ph.D., fundador de Socius Consulting, una firma de consultoría de gestión empresarial y de TI con sede en San Antonio, y cofundador de Top Men Technologies. Actualmente trabaja para CACI International como ingeniero jefe de modelado ICS / SCADA. En la industria de las tecnologías de la información desde 1994, desde su etapa en la Armada como químico nuclear e ingeniero de radio. Antes de CACI, trabajó en la universidad en ECPI como Profesor Asistente de Sistemas de Información Computacional. Aprendí a programar Python por mi cuenta, escribí los libros Aprender a programar usando Python y Recetas secretas del Python Ninja.



Se pueden encontrar más detalles sobre el libro en el sitio web de la editorial

" Tabla de contenido

". Extracto



para los habitantes de un 25% de descuento en el cupón - Python



Tras el pago de la versión impresa del libro, se envía un libro electrónico al correo electrónico.



All Articles