Foto com o zoom de uma tela exibindo um trecho de código escrito em Python em um editor de textos com syntax highlight.

Personal Python Style Guide

No lugar onde trabalho usamos Github e usamos a funcionalidade de Pull Request para sugerirmos melhoria no código da empresa.

Eu adoro esse sistema porque ensinamos e aprendemos a programar melhor. Mas algumas sugestões eu evito colocar porque elas são baseadas em minhas preferências pessoais e não em fundamentações técnicas.

Essas eu vou colocar aqui e apontar esse post para meus colegas de trabalho (e amigos). Quem gostar pode adotar.

São escolhas e opções de estilo que vão além do que a PEP-8 propõe.

Múltiplos pontos de retorno

Vejo muito código assim:

def f(x):
    if x == "spam":
       do_something()
       do_something_else()

Não tem nada de errado com esse código. Mas eu fico incomodado com o fato de que todo o código dessa função está dentro do bloco do if. Eu prefiro, quando possível, inverter a lógica do if e inserir um ponto de retorno extra na função:

def f(x):
    if x != "spam":
       return

    do_something()
    do_something_else()

Tenho amigos programadores que não gostam de inserir pontos de retorno extras na função. Eles tem argumentos bons e fortes para defender o “jeito deles” e eu tenho os meus argumentos para defender o “meu jeito”.

if/elif tem que ter um else

É claro que um bom sistema de objetos com interfaces claras e polimorficas eliminaria toneladas de lógica condicional do seu código. Mas, vamos lá, no mundo real os ifs estão por aí.

Junto com os ifs temos os elifs que podem ser usados (com moderação) para nos ajudar a colocar um pouco de vida ao nosso código.

Mas quando eu vejo isso:

def f(x):
    if x == "spam":
       do_something()
    elif x == "eggs":
       do_something_else()

O meu TOC (Transtorno Obsessivo Compulsivo) “apita” e eu tenho que “consertar” esse código pra algo mais ou menos assim:

def f(x):
    if x == "spam":
       do_something()
    elif x == "eggs":
       do_something_else()
    else:
       return

Já teve casos onde fiz else: pass só pra poder dormir de noite. 🙂

Deixando a brincadeira de lado, colocar um else em todas as construções que tenham elif é uma prática de programação defensiva. É bastante comum encontrar algo parecido com o código abaixo nos sistemas que desenvolvo:

def f(x):
    if x == "spam":
       do_something()
    elif x == "eggs":
       do_something_else()
    else:
       raise SystemError("Invalid argument")

Esse tipo de código evita que erros passem desapercebidos pois uma exceção será gerada sempre que um argumento incorreto for passado para essa função. Com isso eu posso corrigir o problema ou acrescentar um elif novo para tratar esse novo argumento.

Consistência, retornos e erros

Esse talvez seja um caso mais sério do que apenas uma “questão de estilo” porque afeta a qualidade geral do código se não adotada de forma correta.

Em linguagens dinâmicas não declaramos os tipos dos identificadores e isso traz uma série de benefícios mas também força o programador a seguir uma série de regras para evitar dores de cabeça. Uma delas é a de que as interfaces (métodos ou funções) precisam retornar valores consistentes.

def is_odd(n):
    try:
       return n % 2 == 1
    except TypeError:
       return

O código acima é o de uma função que retorna True se o número passado como parâmetro for ímpar. Se o valor passado como parâmetro não for um número o retorno é None

Essa função tem um problema de estilo: quando um valor incorreto é passado para uma função ela deveria avisar o programador sobre esse erro. Como fazemos isso? Com o sistema de exceção!

Então não tem problema receber um TypeError exception quando passamos uma string para uma função que deveria receber apenas números. O código que está chamando essa função claramente tem um bug que precisa ser corrigido.

O outro problema vai um pouco além da questão estilo. Essa função deveria ter um retorno booleano, ou seja, deveria retornar um valor do conjunto (True, False). Mas isso não está acontecendo. Ela pode retornar None que é um não-valor que não faz parte do conjunto dos booleanos.

E pior: apesar de não fazer parte do conjunto dos booleanos, o None é interpretado como False pelo Python e isso pode fazer com que erros fiquem ocultos por muito tempo em nosso código.

Essa função, para mim, deveria ser implementada assim:

def is_odd(n):
   return n % 2 == 1

Singular para um, plural para muitos

O nome que escolho para os identificadores no meu código refletem (parcialmente) o tipo de dado que eles referenciam.

def is_odd(n):
   return n % 2 == 1

Se a função tem um nome iniciado com is_* ela retornará um valor booleano.

def odds(r):
   return [n for n in range(r) if n % 2]

Se o identificador tem plural no nome (odds) ele vai retornar uma coleção (collection) ou um iterador (iterator).

def next_odd(n):
   return n + (2 if n % 2 else 1)

Se o nome do identificador estiver no singular ele vai retornar um valor único.

class Odds(object):
   def __init__(self):
      self.odds = []

   def load_odds(r):
      self.odds = [n for n in range(r) if n % 2]

Quando o identificador tem um verbo “impositivo” no nome ele não retorna nada (eu raramente uso métodos get_* que violam essa regra). O mesmo, neste caso, quando o método faz mudanças inplace no objeto.

Essa regra que diz que métodos que fazem mudanças inplace nos objetos não devem retornar valor é adotada pelo próprio Python, por exemplo, nos casos dos métodos list.sort() ou list.reverse().

To be continued…

Assim que eu me lembrar de outras coisas vou atualizar esse artigo. Se você tem sugestões de estilo que vocês adotam no dia-a-dia escrevam nos comentários.