Categorias
Geral

Python Raiz

Logotipo antigo do Python onde se lê "Python Powered" com uma fonte pontilhada em branco com uma borda preta. Esse logotipo fica sobre uma elipse azul

Como todos sabem sou programador Python a quase duas décadas e boa parte da minha carreira foi construída em torno desta linguagem. E isso mostra como ela é excelente. Pra uma pessoa como eu, que adora aprender e estudar linguagens de programação, significa muito priorizar uma só linguagem por quase metade da vida.

Mas o que me motivou a escrever esse texto aqui foram as adições recentes (ou nem tão recentes) à linguagem. Coisas como suporte para desenvolvimento assíncrono (async, await, etc), anotações de tipos, f-strings, operador “walrus” (:=), e a mais recente delas (cuja PEP ainda está em “Draft”): Pattern Matching.

Python Nutella

O conceito de Pattern Matching já existe a bastante tempo (Prolog?) mas ficou mais popular com a recente adoção da excelente linguagem Elixir por muitos desenvolvedores.

O uso mais básico de Pattern Matching pode ser visto nas instruções switch/case presente em várias linguagens de programação estruturadas. Mas Pattern Matching não é “só” um switch/case. Nos tradicionais comandos de switch/case as linguagens avaliam uma expressão (switch expr) e dependendo do resultado da expressão ele busca um bloco de código que dê “match” com esse resultado (case constant-expr).

Mas em linguagens como Elixir (que é a que usarei como exemplo por conhecer melhor) esse “match” pode ser feito com regras muito mais elaboradas e usar estruturas de dados completas no lugar de apenas um valor constante, como nas linguagens que mencionei acima.

Abaixo vou colocar um exemplo de código pseudo-Python que demonstra como uma função pode ser implementada sem e com Pattern Matching:

Esse caso mostra como o conceito é poderoso e prático para resolver uma série de problemas. E a sintaxe parece razoavelmente natural. Mas parece que essa “naturalidade” some quando avançamos pelos exemplos da PEP e começamos a ver coisas como:

O que me assusta em tudo isso são as invenções de coisas que nunca estiveram no Python:

  • _ como caracter coringa: o caracter _ sempre foi usado, por convenção, como uma variável cujo valor pode ser descartado. Um exemplo disso seria fazer coisas como: name, url, *_ = 'name,url,extra,data'.split(','). Porque não usar a cláusula else: com o match?
  • Dotted-names: Os casos de Constant Value Patterns onde tiveram que inventar uma sintaxe nova com dotted-names é outra coisa que nunca vi no Python. Essa sintaxe foi descartada anteriormente para coisas muito mais simplórias como quando pediram algo parecido com with do Pascal.
  • | como operador de alternativa: | já existe em Python e ele faz as vezes de OR bit-a-bit. Também temos o or que faz as vezes de OR lógico. Me parece que, mesmo não sendo semanticamente a mesma coisa, o segundo OR lógico faz mais sentido do que o OR bit-a-bit. Uma opção aceitável, por familiaridade, seria o uso de || que também é usado como OR lógico em diversas linguagens.

Mas o que mais me desagrada nessas recentes adições à linguagem é a preocupação que tenho com “legibilidade natural da linguagem“. E não, essa legibilidade não tem relação direta com escrever código limpo e organizado para programadores Python. Tem relação com a capacidade de um código ser compreendido até mesmo por algum desenvolvedor que não conhece a linguagem.

Aprendendo Python

Python não foi minha primeira linguagem. Já conhecia outras antes dela. Essas linguagens eram majoritariamente estruturadas mas eu já tinha brincado um pouco com desenvolvimento orientado à objetos com Object Pascal do Turbo Pascal 6 e 7 (com Turbo Vision e tal).

Meu primeiro contato com Python se deu por volta do ano 2000 na extinta e saudosa Conectiva (aquela do Conectiva Linux que depois se juntou com a Mandrake pra formar a Mandriva que também sumiu… enfim… uma bagunça 🙂).

Eu estava trabalhando em um projeto de “compilador de configuração para interfaces gráficas”. Esse é o nome chique que dei agora… na época era só um programa escrito em C que geraria os arquivos texto de configuração para Gnome, KDE e WindowMaker à partir de um conjunto de configurações centralizadas.

Mas o importante para a discussão de agora é a parte do “escrito em C” e “arquivos texto”. Todos sabem que C e texto não se harmonizam muito bem, certo? Então… no final tudo daria certo. Só demoraria mais tempo pra ficar pronto.

Certo dia meu chefe me trouxe um artigo que ensinava Python. Ele havia traduzido o artigo original para o português e pediu pra que eu desse uma revisada (acho que a intenção dele era outra já nem sabia inglês direito). O artigo original não está mais no ar mas tenho uma cópia dele (a tradução se perdeu).

A versão do Python, naquela época, era 1.5.2 (o artigo foi atualizado depois que eu li para acrescentar coisas do Python 2). E lendo só esse artigo eu aprendi Python em 1, uma, UMA f*cking noite! Com um artigo de blog!

Eu aprendi Python em uma noite porque eu sou inteligente? Esperto? Super-humano? Não! Dêem uma olhada no post.

Eu espero.

Viram como a linguagem do artigo é simples? Ela é legível, as construções dela são intuitivas: v = 1 atribui valor 1 pra v, v == 2 compara valor v com 2, v[0] acessa o primeiro elemento de um array/lista, class Person: ... define uma classe, e assim se segue. Ou seja, para uma pessoa que programa minimamente em algo conseguia aprender a linguagem muito rápido.

Tinham pouca coisa estranha nessa linguagem que não existia em outras. Talvez o fato de usar indentação pra delimitar os blocos de código e aquele parâmetro self nos métodos de instância. Mas tirando isso é tudo bem normalzinho.

Agora imaginem uma pessoa aprendendo Python com coisas como:

Imagina esbarrar com um código desses logo de primeira? O cara volta pro Perl 🙂 Just kidding…

Eu sei que o código do artigo é praticamente o mesmo para rodar no Python de hoje (talvez só o uso de print() e input() tenha mudado com o Python 3).

Também sei que o artigo não ensina tudo sobre a linguagem (mesmo naquela versão da época). Ele é só uma introdução. E de fato, quando decidi me aprofundar mais no aprendizado da linguagem eu fui atrás de outros materiais. Li o The Python Tutorial que vem com a própria linguagem e na seqüencia importei um livro que realmente ensinava a linguagem toda: Learning Python da editora O’Reilly.

Naquela época eu comprei a primeira edição do livro que, hoje, já está na quinta edição que cobre até o Python 3.3.

Finalmente eu pude estudar tudo o que Python tinha. E até hoje eu recomendo esse livro. Mas vou falar uma curiosidade sobre ele: a 1ª edição tinha 384 páginas. A 5ª edição tem 1648 páginas! Mais de 4 vezes maior. E nem descrevem as (muitas) novidades do Python 3.4, 3.5, 3.6, 3.7, etc.

Ou seja, se você realmente pretende dominar tudo o que a linguagem oferece vai levar uma vida. Além do Learning Python eu ainda recomendo o Fluent Python (assim que a segunda edição sair eu atualizo o link) do meu querido amigo Luciano Ramalho.

Python para não-programadores

Todo mundo sabe que Python tem crescido muito na comunidade científica. Isso aconteceu muito graças à iniciativas como SciPy e de projetos que nasceram dentro dessa iniciativa e ganharam vida própria como o Jupyter, matplotlib, pandas, scikit-learn, etc.

Por outro lado Python também se tornou uma linguagem muito usada no ensino de programação de diversas escolas e universidades em todo o mundo. Usam ela para ensinar programação para todo mundo e não só para alunos dos cursos de computação e afins.

E essas duas coisas estão relacionadas. Se você ensina programação com Python para um aluno de biologia qual linguagem ele vai usar para escrever uma ferramenta que auxilia num trabalho de genética?

Porque essas escolas escolheram Python para ensinar programação Porque projetos como SciPy e Jupiter escolheram usar Python? Porque não escolheram outras linguagens?

Eu suspeito de que seja um conjunto de atributos da linguagem, entre eles:

  1. Educacional: Python nasceu de um projeto de linguagem educacional (ABC), mas também nasceu porque seu criador (Guido van Rossum) acreditava que linguagens educacionais não precisavam ser de brinquedo (toy languages) e que deveria ser possível usá-las no dia-a-dia.
  2. Multiplataforma: instalação, implementação e uso fácil nas principais plataformas disponíveis.
  3. Multipropósito: você consegue desenvolver software de linha de comando mas também consegue implementar uma interface gráfica ou um servidor Web.
  4. Multiparadigma: sabe programação estruturada? ok. Sabe modelagem OO? ok também. Sou craque em lambda functions? tá lá também um básico pra você usar.
  5. Facilidade de integração com bibliotecas em de outras linguagens como C, Fortran, etc. Vocês devem imaginar o imenso número de bibliotecas científicas implementadas em outras linguagens. Porque reescrever tudo?

Mas o que eu acho mais importante para essa escolha é a de que todos os cientistas que já programaram alguma coisa para seu trabalho conseguem ver um código Python e ter uma ideia, mesmo que superficial, do que aquilo faz. É a tal “legibilidade natural da linguagem” que já mencionei.

Multipropósito

Se você quer um carro que comporte toda a família, seja potente, veloz, econômico, tenha porta-malas grande, seja espaçoso, confortável, etc você provavelmente vai acabar com isso aqui:

Python nasceu pra ser fácil de aprender e usar. Ter uma sintaxe limpa, clara e familiar. Sem coisas esdrúxulas como símbolos em excesso ou coisas menos convencionais como a sintaxe object message do Smalltalk (que inspira a sintaxe do Ruby).

Python também nasceu sem tipos ou anotações de tipos. Tudo nela foi pensada para abstrair esse conceito “mundano” da computação. Isso facilita o aprendizado das pessoas.

Python nunca pretendeu ter uma performance absurda. Só a performance necessária para permitir o seu uso em problemas reais. Se você precisasse de performance absurda você provavelmente escreveria código em C ou Assembly e “colaria” ele com Python.

Python nunca foi pensada para concorrência ou paralelismo. Então não dá pra “competir” com linguagens como Go ou Elixir (Erlang) nesse quesito. Essas linguagens nasceram pra lidar com esse tipo de problema. Acrescentar um punhado de palavras reservadas e algumas bibliotecas não torna Python ideal para esse propósito.

Python não nasceu como linguagem funcional. Mesmo ela tendo ferramentas que te permitam expressar algumas ideias de modo funcional ela não é uma linguagem funcional de verdade. Python nasceu como linguagem majoritariamente orientada à objetos.

Colocar um map() aqui e um lambda acolá não torna a linguagem própria para resolver problemas que são mais facilmente solucionáveis com linguagens funcionais. Python tem objetos mutáveis. Python não tem suporte nativo à tail-recursion. Macros? O próprio Guido já disse que “nem morto” (não achei a referência, mas acredite, eu vi isso).

Guarda-Chuva

Python já não é mais uma linguagem de nicho ou underground. Python já é mainstream e centenas ou milhares de empresas de diversos portes já usam ela em seus negócios.

Como consequência disso o número de vagas de emprego e trabalhos com Python cresceu vertiginosamente nos últimos anos e por isso, inevitavelmente, programadores de outras linguagens acabam tendo que lidar com Python em algum momento de suas carreiras.

Por conta dessa situação eu tenho a sensação de que a necessidade de adicionar certas funcionalidades no Python vem do desejo desses programadores de usar parte favorita da outra linguagem também em Python porque eles não conseguem pensar “do jeito Python”.

Lembro bem quando Java estava na moda e todo programador Python criava getters/setters nas classes. Inventavam mil maluquices como Interfaces (no Zope), protocols, … para terem algo parecido com o que Java oferecia. Um baita esforço pra programar Java em Python.

Hoje em dia parece que tá todo mundo querendo programar Elixir em Python, JS/Node em Python, …

Sem Conclusão

Vocês devem estar imaginando que estou ficando desgostoso com a linguagem ou que vou abandoná-la. Não estou não. As coisas novas que não gosto de usar irei ignorar (ex. operador :=). Outras que me fizeram até torcer o nariz mas passei a gostar entraram pro meu repertório (ex. f-strings).

Tem aquelas funcionalidades que me fazem torcer o nariz, e quando dei uma chance pra elas descobri que podem ser úteis se forem usadas com muita moderação (ex. type annotation) serão usadas com moderação.

Enfim… Vou continuar a usar e a gostar de Python. Até porque ela tem tantas qualidades que me deixou preguiçoso para mudar. Mas pretendo continuar a programar Python em Python. Daquele jeito raiz. Daquele jeito moleque… aquela programação Python de várzea.

Update: corrigido um erro no operador “OR lógico” para or e adicionei uma sugestão de uso do ||.

Categorias
Geral

Personal Python Style Guide

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

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.