O livro “Python. Melhores práticas e ferramentas "

imagemOlá, Habitantes! Python é uma linguagem de programação dinâmica usada em uma ampla variedade de áreas temáticas. Embora seja fácil escrever código em Python, é muito mais difícil tornar o código legível, reutilizável e fácil de manter. Terceira edição do Python. Best Practices and Tools ”fornecerá as ferramentas para resolver com eficácia qualquer problema de desenvolvimento e manutenção de software. Os autores começam falando sobre os novos recursos do Python 3.7 e os aspectos avançados da sintaxe do Python. Eles continuam com conselhos sobre a implementação de paradigmas populares, incluindo programação orientada a objetos, funcional e orientada a eventos. Os autores também falam sobre as melhores práticas de nomenclatura, sobre como você pode automatizar a implantação de programas em servidores remotos. Você vai aprender,como criar extensões úteis do Python em C, C ++, Cython e CFFI.



Para quem é este livro
Python, . , Python. , , , Python.



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



- -, , : .



Padrões de acesso para atributos estendidos



Ao aprender Python, muitos programadores C ++ e Java ficam surpresos com a falta da palavra-chave privada. O conceito mais próximo disso é mutilação de nomes. Cada vez que um atributo é prefixado com __, ele é renomeado dinamicamente pelo intérprete:



class MyClass:
__secret_value = 1
      
      





Acessar o atributo __secret_value por seu nome original lançará uma exceção 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
      
      





Isso é feito especificamente para evitar conflito de nomes por herança, uma vez que o atributo é renomeado pelo nome da classe como um prefixo. Este não é um análogo exato de privado, uma vez que o atributo pode ser acessado por meio de um nome concatenado. Esta propriedade pode ser usada para proteger o acesso a alguns atributos, mas na prática __ nunca é usado. Se o atributo não for público, é comum usar o prefixo _. Ele não invoca o algoritmo de decoração de nome, mas documenta o atributo como um elemento privado da classe e é o estilo predominante.



Python possui outros mecanismos para separar a parte pública da parte privada de uma classe. Descritores e propriedades fornecem uma maneira de organizar essa separação.



Descritores



O descritor permite que você personalize a ação que ocorre quando você faz referência a um atributo em um objeto.



Os descritores estão no centro do acesso a atributos complexos em Python. Eles são usados ​​para implementar propriedades, métodos, métodos de classe, métodos estáticos e supertipos. Essas são as classes que definem como os atributos de outra classe serão acessados. Em outras palavras, uma classe pode delegar o controle de um atributo a outra classe.



As classes do descritor são baseadas em três métodos especiais que formam o protocolo do descritor:



__set __ (self, obj, value) - Chamado sempre que um atributo é definido. Nos exemplos a seguir, iremos nos referir a ele como “setter”;



__get __ (self, obj, owner = None) - chamado sempre que o atributo é lido (doravante o getter);



__delete __ (self, object) - Chamado quando del é chamado por um atributo.



Um descritor que implementa __get__ e __set__ é chamado de descritor de dados. Se apenas implementar __get__, é chamado de descritor sem dados.



Os métodos deste protocolo são realmente chamados pelo método __getattribute __ () (não deve ser confundido com __getattr __ (), que tem um propósito diferente) cada vez que um atributo é pesquisado. Sempre que tal pesquisa é feita usando um ponto ou uma chamada de função direta, o método __getattribute __ () é implicitamente chamado, que procura o atributo na seguinte ordem.



  1. Verifica se um atributo é um descritor de dados em um objeto da classe de instância.
  2. Caso contrário, verifica se o atributo é encontrado no __dict__ do objeto de instância.
  3. Por fim, verifica se o atributo é um identificador sem dados no objeto de classe de instância.


Em outras palavras, os descritores de dados têm precedência sobre __dict__, que por sua vez tem precedência sobre os descritores que não são de dados.



Para maior clareza, aqui está um exemplo da documentação oficial do Python que mostra como os descritores funcionam em 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
      
      





Aqui está um exemplo de como usá-lo interativamente:



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





O exemplo mostra claramente que se a classe tem um descritor de dados para aquele atributo, então __get __ () é chamado para retornar um valor toda vez que um atributo de instância é recuperado, e __set __ () é chamado sempre que esse atributo recebe um valor. O uso do método __del__ não é mostrado no exemplo anterior, mas deveria ser óbvio: ele é chamado sempre que um atributo de instância é removido usando a instrução del instance.attribute ou delattr (instance, 'attribute').



A diferença entre descritores de dados e não dados é importante pelos motivos que mencionamos no início desta subseção. Python usa o protocolo descritor para vincular funções de classe a instâncias por meio de métodos. Eles também se aplicam aos decoradores de método de classe e método estático. Isso ocorre porque os objetos funcionais são essencialmente também descritores sem dados:



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





O mesmo é verdadeiro para funções criadas usando expressões lambda:



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





Portanto, a menos que __dict__ tenha precedência sobre os descritores sem dados, não poderemos substituir dinamicamente métodos específicos de instâncias já instanciadas em tempo de execução. Felizmente, graças à maneira como os descritores funcionam em Python, isso é possível; portanto, os desenvolvedores podem escolher quais instâncias funcionam sem usar subclasses.



Exemplo da vida real: avaliação preguiçosa de atributos. Um exemplo de uso de descritores é atrasar a inicialização de um atributo de classe quando ele é acessado de uma instância. Isso pode ser útil se a inicialização de tais atributos depender do contexto global do aplicativo. Outro caso é quando essa inicialização é muito cara e não se sabe se o atributo será usado após a importação da classe. Esse descritor pode ser implementado da seguinte forma:



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
      
      





Abaixo está um exemplo 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']
      
      





A biblioteca oficial PyPI OpenGL Python chamada PyOpenGL usa uma técnica como esta para implementar um objeto lazy_property que é um decorador e um descritor de dados:



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
      
      





Essa implementação é semelhante ao uso do decorador de propriedade (falaremos sobre isso mais tarde), mas a função que está envolvida no decorador é executada apenas uma vez e, em seguida, o atributo de classe é substituído pelo valor retornado por esta propriedade de função. Este método é frequentemente útil quando você precisa atender simultaneamente a dois requisitos:



  • uma instância de objeto deve ser salva como um atributo de classe, que é compartilhado entre suas instâncias (para economizar recursos);
  • este objeto não pode ser inicializado no momento da importação, pois o processo de sua criação depende de algum estado global da aplicação / contexto.


No caso de aplicativos escritos usando OpenGL, você frequentemente encontrará essa situação. Por exemplo, criar sombreadores em OpenGL é caro porque requer a compilação de código escrito em OpenGL Shading Language (GLSL). Faz sentido criá-los apenas uma vez e, ao mesmo tempo, manter sua descrição próxima às classes que precisam deles. Por outro lado, as compilações de sombreador não podem ser realizadas sem inicializar o contexto OpenGL, portanto, é difícil defini-los e montá-los no namespace do módulo global no momento da importação.



O exemplo a seguir mostra um possível uso de uma versão modificada do decorador lazy_property PyOpenGL (aqui lazy_class_attribute) em algum aplicativo OpenGL abstrato. Mudanças no decorador lazy_property original são necessárias para permitir que o atributo seja compartilhado entre diferentes instâncias da 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
      )
   )
      
      





Como todos os recursos avançados de sintaxe do Python, este também deve ser usado com cuidado e bem documentado no código. Para desenvolvedores inexperientes, o comportamento alterado de uma classe pode ser uma surpresa porque os descritores afetam o comportamento da classe. Portanto, é muito importante certificar-se de que todos os membros de sua equipe estejam familiarizados com os descritores e entendam esse conceito, se ele desempenhar um papel importante na base de código do projeto.



Propriedades



Propriedades fornecem um tipo de descritor integrado que sabe como associar um atributo a um conjunto de métodos. A propriedade aceita quatro argumentos opcionais: fget, fset, fdel e doc. O último pode ser fornecido para definir a docstring associada ao atributo como se fosse um método. Abaixo está um exemplo de uma classe Rectangle que pode ser manipulada acessando diretamente os atributos que prendem dois pontos de canto ou usando as propriedades de largura e altura:



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
     )

      
      





O seguinte snippet de código fornece um exemplo de tais propriedades definidas em uma sessão interativa:



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





Essas propriedades tornam os descritores mais fáceis de escrever, mas devem ser tratadas com cuidado ao usar a herança de classe. O atributo é criado dinamicamente usando os métodos da classe atual e não aplicará métodos que são substituídos nas classes derivadas.



O código no exemplo a seguir não será capaz de substituir a implementação do método fget da propriedade width da classe pai (Rectangle):



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





Para resolver esse problema, toda a propriedade deve ser substituída na classe 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'
      
      





Infelizmente, o código tem alguns problemas de manutenção. A confusão pode surgir se um desenvolvedor decidir mudar a classe pai, mas se esquecer de atualizar a chamada de propriedade. É por isso que não é recomendado substituir apenas partes do comportamento das propriedades. Em vez de depender da implementação da classe pai, é uma boa ideia reescrever todos os métodos de propriedade em classes derivadas se você quiser alterar a maneira como funcionam. Normalmente não há outras opções, uma vez que alterar as propriedades do comportamento do setter acarreta uma mudança no comportamento do getter.



A melhor maneira de criar propriedades é usá-las como decorador. Isso reduzirá o número de assinaturas de método dentro da classe e tornará o código mais legível e sustentável:



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
      
      





Slots



Um recurso interessante que os desenvolvedores raramente usam são os slots. Eles permitem que você defina uma lista estática de atributos para uma classe usando o atributo __slots__ e pule a criação de um dicionário __dict__ em cada instância da classe. Eles foram criados para economizar espaço de memória para classes com poucos atributos, uma vez que __dict__ não é criado em todas as instâncias.



Eles também podem ajudar na criação de classes cujas assinaturas precisam ser congeladas. Por exemplo, se você precisa restringir as propriedades dinâmicas de um idioma para uma classe específica, os slots podem ajudar:



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





Este recurso deve ser usado com cautela. Quando o conjunto de atributos disponíveis é limitado a slots, é muito mais difícil adicionar algo a um objeto dinamicamente. Alguns truques bem conhecidos, como o monkey patching, não funcionarão com instâncias de classes que possuem slots específicos. Felizmente, novos atributos podem ser adicionados às classes derivadas se eles não tiverem seus próprios slots definidos:



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





Sobre os autores



Michal Jaworski é um programador Python com dez anos de experiência. Ele ocupou vários cargos em várias empresas: de desenvolvedor full-stack regular, depois arquiteto de software e, finalmente, vice-presidente de desenvolvimento em uma empresa startup dinâmica. Michal é atualmente um Engenheiro de Backend Sênior na Showpad. Possui vasta experiência no desenvolvimento de serviços distribuídos de alto desempenho. Além disso, ele é um contribuidor ativo de muitos projetos Python de software livre.

Tarek Ziade é desenvolvedor Python. Vive na zona rural perto de Dijon, França. Trabalha na Mozilla, na equipe de serviços. Tarek fundou o grupo francês de usuários Python (chamado Afpy) e escreveu vários livros sobre Python em francês e inglês. Em seu tempo livre de hackers e festas, ele se dedica a seus passatempos favoritos: correr ou tocar trompete.



Você pode visitar seu blog pessoal (Fetchez le Python) e segui-lo no Twitter (tarek_ziade).



Sobre o editor científico



Cody Jackson é Ph.D., fundador da Socius Consulting, uma empresa de consultoria de TI e gestão de negócios com sede em San Antonio, e co-fundador da Top Men Technologies. Ele atualmente trabalha para a CACI International como Engenheiro Líder para Modelagem ICS / SCADA. No setor de TI desde 1994, desde sua passagem pela Marinha como químico nuclear e engenheiro de rádio. Antes do CACI, ele trabalhou na universidade na ECPI como Professor Assistente de Sistemas de Informação de Computador. Aprendi programação em Python por conta própria, escrevi os livros Learning to Program Using Python e Secret Recipes of the Python Ninja.



Mais detalhes sobre o livro podem ser encontrados no site da editora

" Índice

" Trecho



Para Habitantes desconto de 25% no cupom - Python



Mediante o pagamento da versão em papel do livro, um e-book é enviado para o e-mail.



All Articles