Padrões de design e estilo em python

Quanto mais funcionalidades seu código possuir, maior e mais complexo ele vai ficar. Pensando em tornar os scripts em python mais compreensíveis pela comunidade, foram criadas convenções de estilo (“style guides”) para os desenvolvedores seguirem. Para também melhorar a manutenção e escalabilidade, existem padrões de design (“design pattern”) que podem dar um trabalho extra no início da escrita do código, mas que permitirão “debugar” o código mais facilmente e expandir suas ações com menos dor de cabeça, aumentando sua produtividade.

Guia de estilo para Python

O documento “PEP 8 – Style Guide for Python Code” oferece algumas convenções de estilo. Nele, são implementadas regras como:

  • Limite todas as linhas a um máximo de 79 caracteres
  • Separe funções e definições de classe com duas linhas em branco; métodos dentro de uma classe, com uma única linha em branco
  • Use linhas em branco para separar blocos lógicos dentro de métodos e funções
  • Imports devem sempre ser feitos em linhas separadas
  • Imports devem ser sempre colocados no topo do arquivo, logo depois de quaisquer comentários ou docstrings, e antes de constantes ou globais. Eles devem ser agrupados seguindo a ordem: módulos da biblioteca padrão, módulos grandes relacionados entre si e módulos específicos da aplicação
  • Não usar espaço imediatamente antes e após parêntese, colchete ou chave, logo antes de uma vírgula, ponto-e-vírgula ou dois-pontos – e fuja de espaços múltiplos
  • Sempre circunde os seguintes operadores binários com um único espaço de cada lado, exceto quando usado para indicar um valor padrão de um argumento
  • Use o seu julgamento na hora de inserir espaços entre operadores aritméticos
  • Sempre escrever os comentários em inglês, começando por #, espaço e letra maiúscula – comentários na mesma linha devem ser usados esporadicamente
  • Escreva docstrings para todo módulo, função, classe e método público – cada uma deve sempre usar aspas triplas (string multiline), com as aspas triplas que finalizam uma dosctring em uma linha separada
  • Partes do nome de uma variável podem ser separadas por underscore (com todas as letras maiúsculas ou minúsculas) ou todas juntas com a primeira letra de cada parte em maiúscula – underscore no início costuma indicar que o atributo é de uso interno, dois indicam atributo privado da classe e dois no início e no fim são de atributos ou objetos especiais
  • Nomes de classe devem usar o padrão de PalavrasComeçandoPorMaiúscula – exceto no caso de classes para uso interno, que devem começar com um underscore
  • Não compare valores booleanos com True e False com “==”, buscando usar somente “if var:” para verificar se é verdadeira

Essa e outras PEPs (Python Enhancement Proposals) podem ser vistas no link.

Modularização

Ao escrever programas maiores, é fundamental dividir o trabalho em módulos. Ou seja, deve-se evitar uma solução “monolítica”, onde o programa inteiro está expresso em uma única sequência de comandos. A modularização atua com os os conceitos de função, objeto, classe, módulo e pacote. Um programa modularizado facilita o planejamento, a distribuição de tarefas entre vários programadores, o controle de qualidade e a reutilização de soluções. Ao evitar a redigitação, não só economizamos tempo, mas ainda limitamos a propagação de “bugs”, ou falhas de programação.

A peça-chave da programação estruturada é o conceito de subprograma, um fragmento com começo, meio e fim, que desempenha um papel bem definido dentro de um programa maior. Na linguagem Python, um subprograma é definido através do comando de bloco “def” e chamado de método. Ele pode ser uma função (produz/retorna valor) ou uma procedure (não retorna nada).

Os módulos são coleções de métodos. Existem vários módulo prontos, como o math, que contém funções matemáticas. Eles podem ser chamados no script principal através do comando “import math” – ou “from math import sqrt” para importar somente uma funcionalidade dele. Quando os módulos ficam grandes demais, eles podem ser quebrados em vários outros módulos, sendo que o conjunto deles é chamado de pacote. Em termos de armazenamento, enquanto módulos são estruturados em arquivos, pacotes são estruturados em pastas.

Uma classe define uma estrutura de dados que contenha instância de atributos (tipo, valor, etc), instância de métodos e classes aninhadas. Quando a classe é atribuída a outro objeto, este objeto é chamado de instância da classe.

Por padrão, todo método criado em uma classe pertence ao objeto. Isso significa que é necessária uma instância do objeto para se chamar um método, e essa instância é normalmente associada ao primeiro parâmetro da função (que convenciona-se chamar de self). Para se criar métodos que pertençam à classe, e não ao objeto, pode-se usar os decoradores @staticmethod ou @classmethod. Ambos são semelhantes, embora o primeiro não faça referência alguma à classe e o segundo o faça (de forma semelhante ao método comum, embora nesse caso convenciona-se chamar de cls em vez de self). A principal diferença entre @classmethod e @staticmethod é que o primeiro pode ser herdado de forma a fazer alguma coisa útil, levando a classe em consideração ao exercer sua função.

Padrões de design

Um tipo de estrutura comumente usada na programação em python contempla um script principal que chama as classes e módulos. Nesse diretório, chamado de raiz, é criado um sub-diretório, que deve conter os arquivos com as classes e suas funções, além de um arquivo em branco de nome “__init.py__”. Muitas vezes, esse sub-diretório é chamado de “helpers”.

Cada arquivo deve conter uma classe, com o seguinte padrão de nomes:

  • arquivo – mesmo nome da classe do arquivo, tudo em minúsculo e junto (sem usar delimitadores entre as palavras). Ex: myclass.py
  • classe – tudo junto, mas primeira letra de cada nome em maiúsculo. Ex: MyClass
  • método – tudo em minúsculo, usando underline para separar palavras. Ex: my_method

Em cada arquivo, seguir a seguinte estrutura (nessa ordem):

  • shebang line (opcional): localização do interpretador (permite executar o script como “./myscript.py”, em vez de “python myscript.py”). Ex: #!/usr/bin/python3 (python do sistema) ou #!/usr/bin/env python3 (python do ambiente virtual)
  • definição da codificação. Ex.: # -*- Coding: UTF-8 -*-
  • Docstrings: strings explicando, de modo geral, sobre o arquivo. A primeira linha e a última deve conter três aspas (duplas ou simples). É costume conter também o nome do autor, uma forma de contato, versão e última data de modificação.
  • imports de bibliotecas (se for o caso): deve-se evitar o import dentro dos métodos, principalmente se ela fizer parte de um loop. Buscando otimizar o código, deve-se fazer o import no arquivo principal ou no arquivo da classe (ou nos dois, dependendo de onde for necessário seu uso) logo no início, antes de declarar a classe. Também deve-se ordenar os imports alfabeticamente, mas respeitando a seguinte ordem: bibliotecas padrão, de terceiros e locais.
  • classe, seguindo a estrutura “class MyClass(parameters):”

Dentro da classe, deve haver outro “docstring” geral para a classe e os métodos (linhas começadas com “def” e os respectivos parâmetros de entrada), com seus específicos “docstrings”.

Aplicação – Quais classes devo criar?

O número de classes e seus nomes variam de projeto para projeto. Um padrão existente é o MVC (Model-View-Controller), em que são separadas as funções de modelagem, visualização e controle em classes. Nesse exemplo disponível no Github (viniroger/pythondesign), foram criadas três classes:

  • InputFiles – métodos relacionados com leitura e manipulação de arquivos e dados de entrada
  • Formulas – cálculos e manipulações dos dados
  • OutputFiles – gráficos e arquivos de saída, com os resultados gerados na classe “Formulas”

Cada uma das classes tem um método, conforme descrito em sua respectiva docstring. Basicamente, o script principal (myscript.py) deve ler dois arquivos CSV, combinar as linhas com a mesma data e fazer um gráfico de dispersão de duas variáveis.

Na classe “Formulas”, supõe-se que todos os métodos sejam independentes dentro da classe, ou seja, não precisam receber nem retornar valores para outros métodos, somente para o código principal (ou mesmo nem retornar alguma coisa). Por isso, existe uma linha “@staticmethod” antes da definição desse método. Sua importação e uso no script principal é direta, por exemplo:

Já na classe “InputFiles”, existe uma variável definida para todos os seus métodos. Nesse caso, deve ser criado um objeto para a classe receber uma variável (ou mais) como parâmetro:

No código da classe, deve ser criado um método chamado “__init__”, que recebe como argumentos ele mesmo (self) e o parâmetro passado pelo código principal. Todo método dentro dessa classe que for fazer uso dessa variável, deve receber “self” como primeiro argumento e referenciar essa variável com “self.” antes do nome dela. No exemplo, a variável é a “self.path”.

Note que a biblioteca pandas pode ser importada dentro do próprio método (como em “InputFiles”) ou no arquivo da classe (como em “Formulas”). Se a biblioteca for usada para a maior parte dos métodos da classe, é interessante que ela seja importada somente uma vez no início.

Outra forma de importar funções de um arquivo (functions.py) em um diretório “helpers” especificado:

Quanto à ordem que os métodos devem estar dispostos dentro de uma classe, existe um conceito interessante de distância vertical. Sua ideia principal é que você deseja evitar que as pessoas “pulem” ao redor do seu código-fonte apenas para entendê-lo. Ou seja, se os métodos estiverem relacionados entre si, elas devem estar mais próximos. Coisas não relacionadas podem estar mais afastadas. Se não tiverem muita relação entre si, podem estar simplesmente ordenadas alfabeticamente.

Any fool can write code that a computer can understand. Good programmers write code that humans can understand.
Martin Fowler

Fontes