Usando comando with para evitar acoplamento temporal

Hoje vamos falar sobre como melhorar um padrão que você provavelmente já viu em muito código:

    f = open(...)
    ....
    f.close()

 

    a = A(...)
    a.start(...)
    ...
    a.end()

 

    c = Coisa(...)
    c.cria(...)
    ....
    c.destroi()

 

    test = Test()
    test.setUp()
    test.run()
    test.tearDown()

Os trechos de código acima possuem em comum o que chamamos acoplamento temporal (do inglês, temporal coupling).

Acoplamento mede o quanto uma coisa depende de outra em software, e é comumente medida entre módulos ou componentes – tipo, o quanto certas classes ou funções dependem uma da outra. Em geral, é desejável que haja poucas interdependências, para evitar que a complexidade se espalhe em um projeto.

O acoplamento temporal acontece quando uma coisa precisa ser feita depois de outra, mesmo que seja dentro do mesmo módulo ou função. A relação de interdependência é com o tempo, isto é, o momento em que as coisas precisam acontecer. Alguns exemplos seriam: fechar um arquivo depois de terminar de carregar o conteúdo, liberar memória quando acabar de usar, etc.

Reduzindo o acoplamento temporal

Desde a versão 2.5, Python possui o comando with para lidar exatamente com este tipo de situação. Com ele, podemos fazer:

    with open(...) as f:
        dados = f.read()
    # processa dados aqui

Ao usar o bloco with para abrir um arquivo, o método close() é chamado por trás dos panos pelo gerenciador de contexto incondicionalmente (isto é, mesmo que ocorra alguma exceção no código de dentro do bloco).

O código equivalente seria:

    f = open(...)
    try:
        dados = f.read()
    finally;
        f.close()

Repare como o primeiro código é mais curto e mais simples que o segundo, pois há menos interdependências entre as coisas que estão acontecendo.

A ideia é não precisar lembrar de ter que escrever try/finally e chamar o close(), utilizando por trás dos panos um código que se certifique de que as coisas aconteçam na ordem esperada. A este código, chamamos de gerenciadores de contexto, ou context managers.

Implementando um gerenciador de contexto

A maneira mais simples de implementar um gerenciador de contexto Python é utilizar o decorator contextlib.contextmanager. Vejamos um exemplo:

import os
import contextlib

@contextlib.contextmanager
def roda_em_dir(dir):
    orig_dir = os.getcwd()
    os.chdir(dir)
    try:
        yield
    finally:
        os.chdir(orig_dir)

Esse gerenciador de contexto nos permite rodar um código em um diretório qualquer, e ao fim dele voltar para o diretório que estávamos antes.

A função os.getcwd() devolve o diretório atual e a função os.chdir() entra no diretório passado como argumento, e depois os.chdir() é usada novamente para voltar para o diretório original.

Veja como usá-lo:

print('Comecei no diretorio: %s' % os.getcwd())

with roda_em_dir('/etc'):
    print('Agora estou no diretorio: %s' % os.getcwd())

print('E agora, de volta no diretorio: %s' % os.getcwd())

Rodando esse código na minha máquina, a saída é:

Comecei no diretorio: /home/elias
Agora estou no diretorio: /etc
E agora, de volta no diretorio: /home/elias

A função roda_em_dir() é o que chamamos de uma corotina, pois utiliza o comando yield para “pausar” sua execução, entregando-a para o código que está dirigindo-a. Neste caso, isso é o trabalho do decorator contextlib.contextmanager, que entrega a execução para o código que está dentro do bloco with até que este termine, o que devolverá a execução para a corotina roda_em_dir(), que irá executar o código dentro do finally.

Não se preocupe se for um pouco difícil de entender como a coisa toda funciona, estamos passando por cima de alguns tópicos avançados aqui (decorators, corotinas, etc). O importante é que você se dê conta de que pode implementar um gerenciador de contexto rapidamente usando o contextlib.contextmanager com uma função que faça yield dentro de um bloco try/finally.

Vejamos um outro exemplo, desta vez vamos fazer um gerenciador de contexto que cria um arquivo temporário para utilizarmos em um código de teste, e deleta o arquivo automaticamente ao fim do bloco with:

import os
import contextlib
import tempfile

@contextlib.contextmanager
def arquivo_temp():
    _, arq = tempfile.mkstemp()
    try:
        yield arq
    finally:
        os.remove(arq)

Repare como desta vez, o comando yield não está mais sozinho, desta vez ele está enviando a variável arq que contém o nome do arquivo temporário para ser usado no with, como segue:

with arquivo_temp() as arq:
    print('Usando arquivo temporario %s' % arq)
    print('Arquivo existe? %s' % os.path.exists(arq))

print('E agora, arquivo existe? %s' % os.path.exists(arq))

Rodando na minha máquina, a saída ficou:

Usando arquivo temporario /tmp/tmp2IUF7H
Arquivo existe? True
E agora, arquivo existe? False

Note como a variável arq pode ser usada depois do with também: isto mostra que o contexto está sendo gerenciado de maneira especial, mas o espaço de nomes de variáveis ainda é o mesmo (ou seja, o comando with é mais parecido com if e for, do que com o comando def, por exemplo).

Para concluir

Bem, apesar do mecanismo por trás ser um pouquinho complicado de entender inicialmente, você pode perceber que implementar um gerenciador de contexto não é muito difícil. Você precisa usar o decorator contextlib.contextmanager em uma função geradora fazendo yield dentro de um bloco try/finally – moleza!

Você também pode implementar um gerenciador de contexto escrevendo uma classe que implemente o protocolo do comando with, que envolve basicamente implementar dois métodos especiais chamados __enter__ e __exit__ que sinalizam respectivamente entrar e sair do bloco with.

Em geral é mais conveniente utilizar o @contextlib.contextmanager, mas em alguns casos é melhor implementar os métodos. Por exemplo, caso queira compartilhar o próprio objeto gerenciador do contexto dentro do with, você pode usar return self no método __enter__.

Agora, vá refatorar aqueles códigos com acoplamento temporal e se divirta!

Tente outra vez

Imagine que você está escrevendo um código que dependa de recursos externos e que podem falhar caso um servidor não esteja disponível (tipo, baixar uma página ou acionar uma API) ou caso algum recurso (pendrive, drive de DVD, etc) ainda não esteja pronto para operar. Digamos que você queira fazer isso funcionar de maneira robusta, tentando de novo quando uma operação falhar.

Há um tempo atrás mostramos aqui no blog um pequeno exemplo disso, explicando como baixar uma URL com tolerância a falhas simples em Python só com a biblioteca padrão. Acontece que tem um jeito melhor de fazer esse tipo de coisa, que resulta em código limpo e fácil de mudar: usar a biblioteca retrying.

Para começar, instale o pacote retrying com:

pip install retrying

Agora, para usar no seu código, importe o decorator retry (aprenda aqui sobre decorators) e use-o para alguma função que você deseja tentar de novo quando falhar (isto é, quando levantar alguma exceção):

from retrying import retry

@retry
def faz_algo_nao_confiavel():
    print("Tentando...")
    import random
    if random.randint(0, 10) > 1:
        raise RuntimeError("Putz, deu zica!")
    else:
        return "Funcionou!"

A função acima só funciona sem levantar exceção 20% das vezes que é chamada. Colocando o decorator, ela fica envolta em uma lógica que tenta de novo até que funcione sem a exceção. Rodando algumas vezes no meu laptop, a saída fica:

>>> faz_algo_nao_confiavel()
Tentando...
'Funcionou!'
>>> faz_algo_nao_confiavel()
Tentando...
Tentando...
Tentando...
Tentando...
Tentando...
Tentando...
Tentando...
'Funcionou!'
>>> faz_algo_nao_confiavel()
Tentando...
Tentando...
'Funcionou!'

Esse é apenas o jeito mais simples de usar. Em problemas do mundo real, você provavelmente vai querer configurar um intervalo de espera entre cada tentativa e também um limite máximo:

@retry(stop_max_attempt_number=7, wait_fixed=2000)
def busca_algo_na_rede():
    ....

A função acima será tentada no máximo 7 vezes, esperando 2 segundos (2000 ms) entre cada tentativa.

Outra opção interessante é o backoff exponencial, útil quando você quer ajustar a taxa de tentativas para não exigir demais de um sistema remoto que pode estar tendo dificuldades de sobrecarga. Veja um exemplo:

@retry(wait_exponential_multiplier=500, wait_exponential_max=30000)
def aciona_outro_sistema():
    ...

Nesse exemplo, caso a função falhar, será tentada novamente com um intervalo de espera calculado usando uma fórmula exponencial, tentando a primeira vez após 1s, na segunda 2s, na próxima 4s, 8s, 16s até o limite de 30s e então seguirá tentando com esse limite.

Quando não é adequado usar @retry?

Essa ideia de tentar de novo só funciona bem para operações que sejam idempotentes, isto é, que você pode acionar várias vezes sem alterar o resultado.

Por isso, quando quiser adicionar @retry em uma função existente, tenha cuidado para que seja uma função segura de ser chamada mais de uma vez. Em alguns casos, você terá que criar uma nova função com a parte que pode falhar e que funcione dependendo apenas dos próprios parâmetros (tornando-a assim idempotente) e colocar o @retry nessa função.

Baixar página de servidor com tolerância a falhas simples

Um dia desses precisei fazer um script Python que baixava uma cacetada de páginas HTML de um servidor, que às vezes respondia com um erro para algumas das requisições. As requisições estavam corretas, as páginas existiam, mas por alguma falha no servidor elas não respondiam no momento exato da execução do script.

A solução foi fazer uma função que tenta carregar a URL, e caso não consiga, espera alguns segundos e tenta de novo:

import time
import urllib2

def get_url(url, retry=3, timeout=3):
    try:
        return urllib2.urlopen(url).read()
    except:
        if retry > 0:
            time.sleep(timeout)
            return get_url(url, retry - 1, timeout)
        else:
            raise

Python é lento? Que Python?

Já vi muita gente falando que Java é ruim porque é lento. Eu mesmo, há tempos atrás, falava isso. Esse é um dos muitos mitos que se propagam entre as pessoas, sem uma análise crítica mais aprofundada. Isso já foi verdade, láááá no começo. Hoje em dia, é possível conseguir melhor desempenho com programas escritos em Java do que programas escritos em C, que é o rei da performance, de acordo com o senso comum.

Hoje em dia, se fala muito que Python é uma ótima linguagem, com sintaxe e recursos excelentes, mas que possui desempenho ruim. Ou seja, dizem que é lento. Mas calma aí, vamos pensar um pouquinho e esclarecer algumas coisas.

Temos dois conceitos separados que muitas vezes são confundidos. Uma coisa é Python, a linguagem. Outra coisa é Python, o interpretador. A linguagem em si nada mais é do que a especificação, com as regras léxicas, sintáticas e semânticas. Já o interpretador Python é o programa que irá ler e executar os programas escritos usando a linguagem Python. Existem várias implementações, e não somente um único interpretador Python. Assim, não faz sentido afirmar que “Python é lento”. O que poderia ser dito é “todos os interpretadores Python existentes são lentos”, mas isso seria uma mentira.

Os interpretadores

O interpretador Python mais conhecido é o CPython, que é a implementação de referência da linguagem. É ele que vem instalado por padrão no Ubuntu, é ele que a grande maioria das pessoas instala quando vai aprender Python e é ele que é usado pelos desenvolvedores quando vão escrever programas em Python. É bem provável que você tenha o CPython instalado aí na sua máquina. Porém, apesar da popularidade, ele não é o único e também não é o mais performático.

Um interpretador que tem ganhado visibilidade é o PyPy. Diferentemente do CPython, que é escrito em linguagem C, o PyPy é escrito em um subconjunto de Python, o RPython. O que mais chama a atenção no PyPy é o desempenho. Ele consegue obter um desempenho bem melhor do que o CPython em muitos casos. Veja o gráfico abaixo, comparando a versão 2.2 do PyPy ao CPython em vários benchmarks (para a versão mais atual, acesse: speed.pypy.org).

Desempenho do PyPy vs CPython

Desempenho do PyPy vs CPython

Talvez você esteja pensando: como o PyPy, escrito em Python, pode ser mais rápido do que o CPython, que é escrito em C e compilado direto para código de máquina? O PyPy é escrito em Python, mas isso não quer dizer que ele é executado sobre um interpretador Python. O código do PyPy, escrito em RPython, é compilado para linguagem de máquina, podendo então ser executado diretamente sobre o hardware (sobre o SO, na verdade). Mas isso por si só não justifica o bom desempenho dele, afinal o CPython também é compilado para código de máquina.

O que diferencia o PyPy é o fato de ele utilizar um mecanismo chamado JIT (Just In Time compilation) durante a interpretação dos programas. O JIT fica analisando a execução do programa, pega as partes que são executadas com mais frequência e, dentre outras coisas, faz uma tradução em tempo de execução daquelas partes para código de máquina. Assim essas partes do programa não precisarão ser decodificadas e executadas pelo interpretador toda vez que tiverem que ser executadas. Além disso, o JIT pode fazer outros tipos de otimização no código, sempre em tempo de execução. Essa técnica é bem antiga, e é usada também em algumas implementações da JVM (Java Virtual Machine).

Mas o CPython e o PyPy não são as únicas implementações do interpretador Python. O Jython é um interpretador que roda sobre a JVM (que por sua vez roda sobre o SO, que roda sobre o hardware, o que acaba gerando problemas de desempenho). Outras implementações populares são: IronPython (implementação em C#), Stackless Python, Unladen Swallow (uma implementação da Google, que foi deixada meio de lado), dentre outros.

Enfim, o importante é perceber que não existe o tal interpretador Python. O que existe é uma variedade de implementações, cada uma com um objetivo. Enquanto para algumas o principal objetivo é oferecer integração com outras plataformas (Jython e IronPython), para outras o objetivo é levar o desempenho ao topo (PyPy, Stackless Python e Unladen Swallow).

O Django é um projeto de grande porte que é suportado pelos principais interpretadores existentes (CPython por padrão, PyPyJythonIronPython). A troca do interpretador que está por debaixo do Django irá impactar na performance dos aplicativos que estiverem rodando sobre o mesmo. Dá pra ter uma ideia mais prática do impacto que a troca do interpretador pode ter no desempenho de uma aplicação lendo esse post: http://tomvn.com/posts/load-testing-and-pypy-smoking-the-competition.html. No texto, o autor relata que implementou uma API e que obteve uma média de 600 requisições por segundo rodando a mesma sobre o CPython. Ao trocar para o PyPy, este número subiu para impressionantes 2k requisições por segundo.

Então Python não é lento?

Não necessariamente, embora em geral os interpretadores ainda não sejam tão bons de performance quanto código escrito em C/C++ ou Java (em 2013). Mas, para poder falar mais, seria necessário realizar experimentos comparando programas escritos em Python e interpretados pelo PyPy com programas escritos em C, por exemplo. Mas esse tipo de comparação é sempre muito subjetiva, pois depende dos recursos usados em cada implementação. De nada adianta comparar uma hashtable escrita em Java com uma hashtable escrita em Python, se uma delas for thread-safe e a outra não, por exemplo.

Acima de tudo, é importante perceber que a perda de performance é compensada pela agilidade possibilitada à equipe de desenvolvimento na hora de escrever uma aplicação e colocá-la em produção. Nem sempre o desempenho é o mais importante em um projeto, afinal, as horas de trabalho dos desenvolvedores também custam muito. Além disso, experimentos mostram que com PyPy é possível melhorar de forma significativa o desempenho de algumas aplicações que rodam no CPython.

Só não se esqueça do famoso mantra do Donald Knuth: “Premature optimization is the root of all evil“.

Comportamento inesperado na divisão inteira

Alerta de versão: esse post foi escrito com base na versão 2 da linguagem Python. Na versão 3, o operador de divisão inteira é o //.

Para quem já estudou um pouco de programação, o seguinte resultado não é surpresa alguma:

>>> 3 / 2
1

Por se tratar de uma divisão de números inteiros, o resultado é truncado em um número inteiro também. Até aí, está tudo dentro do esperado, não? Então, abra um shell Python e teste a seguinte operação:

>>> -3 / 2
-2

Quem imaginava que o resultado seria -1, levante a mão: \o_

Por que -2 ?!

Em Python, a divisão inteira arredonda o resultado para baixo, ou seja, sempre para o menor número inteiro mais próximo. Por exemplo: 3 / 2 seria 1.5, mas o resultado é arredondado para 1 (e não 2), pois 1 < 2. Já no caso de -3 / 2, o resultado seria -1.5, mas por se tratar de uma divisão inteira, ele é arredondado para -2 e não para -1, pois -2 < -1.

Isso não é muito comum nas linguagens de programação. Em C e Java, por exemplo, uma divisão inteira tem o seu resultado sempre arredondado em direção ao 0. Python, como já vimos, faz com que o resultado de uma divisão inteira seja arredondado para baixo. Veja a ilustração abaixo:

drawing

Mas por que Python faz dessa forma? Ninguém melhor para explicar isso do que o criador da linguagem, o Guido Van Rossum. Em um post no blog Python History, ele explica que resultados negativos de divisão inteira são arredondados em direção a -∞ para que a seguinte relação entre as operações de divisão (/) e de módulo (%) se mantenha também para as operações com resultados negativos:

quociente = numerador / denominador
resto = numerador % denominador
denominador * quociente + resto == numerador

Vamos testar?

>>> numerador = -3
>>> denominador = 2
>>> quociente = numerador / denominador
>>> resto = numerador % denominador
>>> print quociente, resto
-2 1
>>> print denominador * quociente + resto == numerador
True
# e agora, com numerador positivo
>>> numerador = 3
>>> quociente = numerador / denominador
>>> resto = numerador % denominador
>>> print quociente, resto
1 1
>>> print denominador * quociente + resto == numerador
True

Perceba que se o resultado fosse arredondado em direção ao zero, a propriedade não seria satisfeita.

Esse é um detalhe de implementação muito importante e que todo desenvolvedor Python deve conhecer para não introduzir bugs em seus códigos, para evitar de perder horas depurando algo que parecia fugir comportamento esperado e também para evitar sentimentos de “esse intepretador está errado!”.

Leia mais sobre o assunto no post do Guido Van Rossum no blog The History of PythonWhy Python’s Integer Division Floors.

Sugestão de livro: Two Scoops of Django

2scoops

O Django é uma baita ferramenta que auxilia muitos desenvolvedores a concretizar seus projetos web com agilidade e simplicidade impressionantes. A documentação do framework é bastante vasta. São blogs de desenvolvedores, listas de email, livros bem completos, a trilha no StackOverflow, além de muitos e muitos projetos abertos no GitHub e BitBucket, e é claro, a excelente e completíssima documentação oficial. Até aí, tudo perfeito. Material para iniciantes querendo aprender Django existe de monte, mas quando as dúvidas começam a ficar um pouco mais específicas, ou questões relacionadas à boas práticas em projetos Django, a coisa começa a ficar mais escassa. Felizmente para nós, Djangonautas, o Daniel Greenfeld e a Audrey Roy começaram a resolver um pouco desse problema escrevendo o excelente Two Scoops of Django: Best Practices for Django 1.5.

O livro não é um tutorial e tampouco uma documentação exaustiva do Django, mas sim uma valiosa coleção de dicas e conselhos sobre boas práticas em projetos Django, atualizada para a versão 1.5. Já nos primeiros capítulos, fiquei com aquela sensação de “putz, eu tô fazendo tudo do jeito mais difícil nos meus projetos!”. Os autores vão mostrando os problemas e apresentando as soluções de uma forma bem prática, passando dicas, alertas, e, o que achei mais legal de tudo, as Package Tips, que são dicas sobre pacotes de terceiros que os autores costumam usar em seus projetos e que são uma verdadeira mão-na-roda.

Talvez você esteja pensando consigo próprio: “ah, eu já vi várias coisas dessas espalhadas pela web…”. Aí é que está o ponto principal, pois os autores pegaram a vasta experiência que possuem e compilaram uma série de dicas em um só lugar. E quando falo de dicas, não pense que são trechinhos pequenos de texto com links para outros recursos. Pelo contrário, os autores se preocuparam em explicar bem o porquê das coisas, sem cansar o leitor.

Outra coisa que achei interessante é que, diferentemente de um monte de livros que a gente vê por aí, parece que os autores deixaram de lado a preocupação de que o livro deles possa ficar obsoleto por passar dicas pontuais de pacotes específicos para resolver determinados problemas. Me parece que muitos autores limitam a abrangência de seus livros por medo de abordar um assunto mais específico, que poderia sofrer mudanças em breve (talvez o sentimento de estar sendo eternizado pelo livro deixe alguns autores meio confusos). Os autores do Two Scoops of Django não se preocuparam muito com isso e até se comprometeram em publicar erratas caso alguns elementos sofram mudanças nos próximos tempos.

O livro em si é muito bem organizado, com um formato muito bom para a leitura. Os autores se preocuparam MUITO e conseguiram fazer um layout excelente para ser lido em e-readers. Eu comprei a versão para Kindle, e esse é o primeiro livro técnico que leio em que não é preciso ficar diminuindo o tamanho da fonte para conseguir ler decentemente os trechos de código. Parabéns aos autores pela preocupação com os leitores da versão digital do livro!

O conteúdo

Não vou fazer aqui uma análise completa do livro. Vou listar apenas algumas coisas importantes que aprendi com o livro:

  • Como estruturar meus projetos Django;
  • Que as class-based-views são muito fáceis de usar;
  • Que na versão 1.5 do Django ficou barbada estender o modelo User;
  • Que realizar processamento nos templates é roubada;
  • Que dá pra manter configurações (settings.py) específicas para diferentes ambientes;
  • Que import relativo existe; (isso mesmo, eu não conhecia esse recurso)
  • Que select_related() quebra um galhão pra consultas grandes;
  • E muitas outras coisas! (muitas mesmo!) 🙂

Enfim, o conteúdo do livro é fantástico! Recomendo a todo mundo que tem um pouquinho de experiência com o Django que compre e leia esse livro. Não é preciso ser especialista no framework para se aproveitar do conteúdo dele. Se você está na dúvida se o livro é adequado para você, dê uma conferida no conteúdo dele na página oficial.

Eu recomendo!

De 0 a 10, dou nota 10 para esse livro. Li ele apenas uma vez, mas já vou começar a reler para fixar bem as dicas, pois são muitas coisas novas.

Se quiser seguir minha dica, estão à venda as versões impressa e digital do livro. Comprando direto pela página do livro, é possível comprar o pacote digital (formatos PDF, mobi e ePub, tudo DRM-free) por 17 dólares (preço em 22/06/2013). Na Amazon americana, está à venda a versão impressa. E ainda, se quiser comprar pela Amazon Brasil, eles estão vendendo a versão para Kindle.

Se ainda estiver na dúvida se o livro vale mesmo a pena, leia os reviews dos leitores na Amazon.

Entendendo os decorators

Hoje me deparei com um excelente texto sobre decorators que me inspirou a escrever algo sobre o assunto que para muita gente ainda é um tanto quanto nebuloso. Vou tentar aqui explicar o funcionamento de um decorator e mostrar algumas possíveis aplicações.

Aviso aos iniciantes: esse assunto pode ser um pouco confuso para quem ainda está iniciando em programação. Caso sinta dificuldades, não desanime e pule antes para a seção que contém as referências para melhor entendimento do texto.

O que é um decorator?

Um decorator é uma forma prática e reusável de adicionarmos funcionalidades às nossas funções/métodos/classes, sem precisarmos alterar o código delas.

O framework para desenvolvimento web Django oferece diversos decorators prontos para os desenvolvedores. Por exemplo, para exigir que o acesso a determinada view seja feito somente por usuários autenticados, basta preceder o código da view (que em geral é uma funçãozinha ou classe) pelo decorator @login_required. Exemplo:

@login_required
def boas_vindas(request):
    return HttpResponse("Seja bem-vindo!")

É claro que isso não é mágica. Como a gente pode ver no código-fonte do decorator login_required, os detalhes estão apenas sendo ocultados do código-fonte do nosso projeto. Assim, ao invés de ter que, a cada view, escrever o código que verifica se determinado usuário está autenticado, basta usar o decorator. Isso faz com que adicionemos a funcionalidade de verificar se um usuário está ou não logado no site, com uma linha de código apenas. Que barbada, não?

O decorator é um açúcar sintático que Python oferece aos desenvolvedores desde a versão 2.4, facilitando o desenvolvimento de códigos reusáveis.

OK, mas como implementar um decorator?

Você já sabe como um decorator pode ser usado, então agora vamos entender as internas desse recurso do Python.

Um decorator é implementado como uma função que recebe uma função como parâmetro, faz algo, então executa a função-parâmetro e retorna o resultado desta. O algo é a funcionalidade que adicionamos a nossa função original através do decorator.

Vamos escrever um decorator que sirva para escrever na tela o nome da função a ser executada, antes da execução da mesma. Como descrito acima, precisamos definir uma função que receba outra função como parâmetro, imprima o nome dessa, execute a função e retorne o seu resultado. Veja o código:

def echo_funcname(func):

    def finterna(*args, **kwargs):
        print "Chamando funcao: %s()"  % (func.__name__)
        return func(*args, **kwargs)

    return finterna

@echo_funcname
def dobro(x):
    return x*2

dobro(10)

Antes de mais nada, observe atentamente a função echo_funcname, pois existem alguns conceitos importantes dentro dela.

def echo_funcname(func):

    def finterna(*args, **kwargs):
        print "Chamando funcao: %s()"  % (func.__name__)
        return func(*args, **kwargs)

    return finterna

Veja que ela receba um parâmetro func (que espera-se que seja uma função) e retorna outra função (finterna). A função retornada, finterna, é “configurada” para executar ao seu final a função recebida como argumento pela função externa (echo_funcname), bem como retornar o valor de retorno da função recebida. Em outras palavras, echo_funcname() cria dentro de si próprio uma função chamada finterna(), que no final (linha 5) chama a função recebida como parâmetro. Mas, é importante perceber que a palavra-chave def somente cria a função (isto é, instancia um objeto do tipo função), não executando ela. Ou seja, echo_funcname cria uma função, configura ela para executar func() ao seu final, não a executa, mas sim somente retorna o objeto função, que então poderá ser chamada por quem recebê-la. (um assunto muito importante para o entendimento desse conceito de função dentro de função é o conceito de closures).

Caso tenha ficado confuso, perceba que finterna é um objeto como qualquer outro que estamos acostumados a criar dentro de nossas funções, como uma lista, por exemplo. A diferença é que esse objeto é uma função, o que pode parecer um pouco estranho, em um primeiro momento. Sendo um objeto qualquer, a função é instanciada, recebe um nome (finterna), e pode ser retornada, assim como todo objeto (tudo isso sem ser executada, pois não chamamos finterna).

Veja um exemplo de visualização de uma função que define outra função internamente (visualização gerada pelo excepcional pythontutor.com):

func

Se quiser visualizar a versão interativa, clique aqui (powered by PythonTutor.com).

Tendo a função echo_funcname() definida, agora poderíamos fazer o seguinte:

def echo_funcname(func):

    def finterna(*args, **kwargs):
        print "Chamando funcao: %s()"  % (func.__name__)
        return func(*args, **kwargs)

    return finterna

def dobro(x):
    """ Uma funcao exemplo qualquer.
    """
    return 2*x

dobro_com_print = echo_funcname(dobro)
print dobro_com_print(10)

Ao executar o código acima, teremos como resposta na tela:

Chamando funcao: dobro()
20

Criamos uma função chamada dobro(), que recebe um número e retorna o dobro desse número. Depois, passamos esse objeto do tipo function para a função echo_funcname() e recebemos como retorno outro objeto do tipo function, ao qual referenciamos como dobro_com_print. Perceba que dobro_com_print nada mais é do que uma referência a uma função mais ou menos assim:

def finterna(*args, **kwargs):
    print "Chamando funcao: %s()"  % (dobro.__name__)
    return dobro(*args, **kwargs)

Essa função foi gerada dentro de echo_funcname() e retornada, já com dobro no lugar de func. Assim, quando chamamos a função como em print dobro_com_print(10), estamos chamando a função acima, e passando 10 como argumento.

Mas, esse negócio todo de passar uma função como parâmetro e receber uma função como retorno de uma chamada de função é um pouco confuso. Para abstrair um pouco esses detalhes, Python oferece a sintaxe do @nome_do_decorator que precede a definição de funções. Assim, ao invés de:

dobro_com_print = echo_funcname(dobro)
print dobro_com_print(10)

Poderíamos apenas preceder a definição da função dobro() com o decorator @echo_funcname:

@echo_funcname
def dobro(x):
    """ Uma funcao exemplo qualquer.
    """
    return 2*x

Agora, ao chamar a função dobro(), estaríamos chamando a função decorada (isto é, acrescida de funcionalidades). No nosso caso, o decorator apenas adiciona a impressão na tela de um aviso sobre a chamada da função.

Enfim, um decorator nada mais é do que uma função que recebe outra função como parâmetro, gera uma nova função que adiciona algumas funcionalidades à função original e a retorna essa nova função.

Concluindo …

Os decorators formam um recurso muito importante para diminuir a duplicação e aumentar o reuso de código em um projeto. O conceito pode ser um pouquinho complicado para entender de primeira, mas uma vez que você o domine, você começará a perceber diversas oportunidades para implementar e usar decorators em seus projetos.

Leia mais

Por se tratar de um assunto mais complicado para iniciantes, segue aqui uma lista de textos que poderiam ser lidos, possibilitando um melhor entendimento sobre o assunto.

Funções como objetos:

Closures:

Os métodos mágicos de Python

Obs.: Códigos testados com a versão 2.7 do Python.

Quem está chegando em Python normalmente fica um pouco confuso ao ler o código de uma classe e perceber um monte de métodos com underscores (__) no nome. Para entender o que são esses métodos, vamos ver um exemplo.

Uma classe para números binários

Suponha que, por algum motivo, você receba a tarefa de implementar uma classe para representação de números em base binária.

class Binario(object):

    def __init__(self, valor_dec):
        self.valor_dec = valor_dec
        self.valor_bin = bin(self.valor_dec)  #bin() é uma função builtin

b = Binario(5)
print b

Se executarmos o código acima, teremos em b um objeto do tipo Binario, que representa o valor 5 em base-2. 5 em base binária é 101. Porém, a execução da linha print b mostrou a seguinte saída na tela:

<__main__.Binario object at 0x28286d0>


Isso porque o print executa a função str() no objeto recebido. Essa função, por sua vez, procura por um método chamado __str__() no objeto a ser impresso. Como não definimos um método com esse nome em nossa classe, o interpretador continua sua busca pelo método na classe que está acima de Binario na hierarquia de classes, que é object. Lá ele encontra o método __str__, que então retorna o texto <__main__.Binario object at 0x28286d0>, contendo o endereço do objeto na memória.

O método __str__() deve retornar uma representação em forma de string do valor do objeto. Para personalizar essa mensagem, ou seja, para fazer com que o print em objetos do tipo Binario mostre uma sequência de 0s e 1s representando o número binário em questão, vamos adicionar um método __str__() à nossa classe:

class Binario(object):

    def __init__(self, valor_dec):
        self.valor_dec = valor_dec
        self.valor_bin = bin(self.valor_dec)

    def __str__(self):
        return "%s" % (self.valor_bin)

b = Binario(5)
print b

Agora, o resultado da execução do código acima é o seguinte:


0b101

Que é o formato retornado pela função bin() quando chamada. O prefixo 0b é adicionado para indicar que se trata de um número binário. Podemos facilmente nos livrar desse prefixo para representar o número binário na tela usando operadores de slicing:

def __str__(self):
    return "%s" % (self.valor_bin[2:])

Beleza! Agora nosso número binário pode ser impresso corretamente na tela! 🙂

Sem perceber e sem chamá-los em lugar algum, já utilizamos dois métodos mágicos de Python:

  • __init__: método chamado para inicialização do objeto, logo após a sua construção;
  • __str__: método chamado pela função str() para obter o valor do objeto em forma de string;

Chamamos eles de métodos mágicos porque eles resolvem o nosso problema sem sequer termos que chamá-los. Quem os chama são códigos de outras classes/programas, que esperam que nossos objetos forneçam tais métodos.

E se agora quisermos comparar objetos do tipo Binario em operações relacionais como >, <, >=, <=, != e ==? Se tentarmos comparar duas instâncias de Binario usando algum desses operadores, podemos ter respostas inesperadas, visto que eles não irão fazer o que esperamos. O esperado é que a > b retorne True se o valor de a for maior do que o valor de b. Porém, onde definimos qual valor será usado para comparação dos objetos? Como não fizemos isso, o interpretador irá usar o id de ambos os objetos para a comparação.

Para definir como os objetos de nossa classe serão comparados, podemos implementar o método mágico __cmp__. Na documentação oficial, vemos instruções sobre como implementar esse método para que nossos objetos possam ser comparados e usados em operações relacionais:


object.__cmp__(self, other)
Deve retornar um inteiro negativo se self < other, zero se self == other, ou um número positivo se self > other.

Vamos então implementar o nosso método __cmp__. Podemos, para isso, usar o valor em decimal, que armazenamos internamente na variável self.valor_dec:

def __cmp__(self, other):
    if self.valor_dec > other.valor_dec:
        return 1
    elif self.valor_dec < other.valor_dec:
        return -1
    else:
        return 0

Que poderia também ser escrito como:

def __cmp__(self, other):
    return self.valor_dec - other.valor_dec

Tendo adicionado o código acima à classe Binario, agora podemos utilizar nossos objetos em operações relacionais:

b = Binario(1)
c = Binario(2)
if b < c:
    print "OK"

Mais uma vez, nosso método é executado sem que o chamemos explicitamente. Além dos métodos que vimos aqui, existem vários outros métodos mágicos que podemos implementar em nossos objetos para que o comportamento deles se pareça mais com o comportamento de objetos nativos da linguagem. Vou listar alguns deles a seguir:

  • __add__(self, other): para adicionarmos a possibilidade de aplicação do operador + aos nossos objetos. Para os outros operadores, também existem métodos mágicos (subtração(-): __sub__; multiplicação(*): __mul__, divisão(/): __div__, módulo(%): __mod__, potência(**): __pow__);
  • __call__(self): faz com que o objeto seja chamável (executável), assim como uma função é;
  • __len__: retorna o comprimento do objeto (se for um container);
  • __getitem__(self, key): para containers, retorna o elemento correspondente à chave key;

São muitos os métodos. Se você quiser conhecê-los melhor, sugiro dar uma olhada nesse texto (em inglês): http://www.rafekettler.com/magicmethods.html.

Os métodos mágicos (magic methods), também chamados de métodos dunderscore (double-underscore) ou de métodos especiais, são muito úteis pois permitem que objetos de nossas classes possuam uma interface de acesso semelhante aos objetos nativos da linguagem. A função sorted(), por exemplo, ordena os elementos de um iterável de acordo com o valor dos objetos que a compõe. Se definirmos nosso método de comparação, a função sorted() irá usá-lo para fazer a ordenação dos elementos da lista. Assim, é possível que códigos de terceiros lidem com nosso código sem sequer conhecê-lo. Veja mais sobre esse conceito em: Polimorfismo.

Variáveis, valores e referências

Já vi muita gente tendo problemas em código Python simplesmente por não entender direito como Python lida com os conceitos de variáveis. Considere o exemplo abaixo:

>>> x = [1, 2, 3]
>>> y = x
>>> x.append(4)
>>> print y
************

Qual o resultado esperado pela execução da última linha (print y)? Se você ficou na dúvida, mesmo que por um curto período de tempo, leia este post até o fim, que você irá entender melhor.

Nome

Grave o seguinte:

Em Python, uma variável é apenas um NOME que REFERENCIA a um OBJETO.

Veja o exemplo abaixo:

>>> x = 42

O código acima é muitas vezes lido como “atribui o valor 42 à variável x”. Mas, o que Python faz é o seguinte: cria um objeto do tipo int que possui 42 como valor, cria o nome x e faz com que o nome x referencie o objeto (do tipo int) 42. Assim, toda vez que o nome x for usado em seu código, ele será automaticamente substituído pelo valor do objeto que este nome referencia (42). A imagem abaixo ilustra melhor a relação entre x e 42.

var1

Continuando o exemplo anterior, o que acontece se fizermos o seguinte?

>>> x = x + 1

É simples, o nome x passa a fazer referência a um novo objeto do tipo int, cujo valor é 43.

A imagem a seguir dá uma idéia melhor sobre o que acontece.

var2

Objetos do tipo int são imutáveis. x = x + 1 cria um novo objeto do tipo int (cujo valor é determinado pela soma de x com 1) e faz com que x passe a referenciar esse novo objeto. Se você observar a imagem acima, verá que não há mais seta alguma apontando para o valor 42, isto é, não há mais nenhum nome fazendo referência àquele objeto. Normalmente, um objeto que não possui nome algum o referenciando vira candidato a coleta de lixo, que é um mecanismo que elimina da memória objetos que não são mais necessários. Mas, o interpretador Python não realiza esse processo em objetos do tipo int e do tipo str (quando pequenos). Ao invés disso, ele mantém esses objetos em uma espécie de cache, para não ter que recriá-los em um futuro próximo e a todo momento em que forem necessários. Se quiser confirmar isso:

>>> 42 is 42  # ambos são o mesmo objeto
True
>>> 'ola' is 'ola'
True
>>> [] is []  # o mesmo já não vale para listas
False

Como comentei anteriormente, toda vez que um nome de variável aparece em uma expressão, esse nome é substituído pelo valor do objeto ao qual ele faz referência. Sabendo disso, considere a expressão abaixo:

>>> y = x

O interpretador cria um novo nome y e faz com que ele referencie o objeto referenciado por x, como mostra a figura abaixo:

var3

Agora, o que acontece se fizermos o seguinte?

>>> x = 10

É criado um objeto int com valor 10, e x então passa a referenciar a esse novo objeto.

var4

Tá, e daí?

E daí que entendendo isso tudo, você achará mais natural alguns comportamentos em Python. Por exemplo, teste o seguinte código e tente entender o que acontece:

>>> x = [1, 2, 3]
>>> y = x
>>> x.append(4)
>>> print x
[1, 2, 3, 4]
>>> print y
[1, 2, 3, 4]

Como mostra a imagem abaixo, y = x faz com que y passe a referenciar o mesmo objeto que x referencia.

var5

Assim, x.append(4) tem efeito sobre o objeto referenciado agora pelas duas variáveis (x e y).

Isso ocorre porque listas, em Python, são objetos mutáveis. O método append() modifica a lista de modo in-place, isto é, as modificações são feitas no próprio objeto, sem a necessidade de criação de uma nova lista, como ocorreria com objetos imutáveis, como strings ou ints, por exemplo.

Mutável vs Imutável

Vamos ver agora um exemplo da diferença entre um objeto mutável (lista) e um objeto imutável (string). Temos dois objetos, l e s:

>>> l = [1, 2, 3]
>>> s = 'abc'

Queremos adicionar um novo elemento ao fim de cada um deles. Com a lista podemos usar o método append():

>>> l.append(4)

Que adiciona o valor 4 ao final de l, modificando-a. Com a string, não temos esse método disponível, então vamos usar o operador de concatenação:

>>> s = s + 'd'

O lado direito da expressão acima cria uma nova string com o conteúdo de s acrescido do caractere 'd' e faz com que o nome s passe a referenciar essa nova string. Ou seja, ao invés de modificar, foi criado um novo objeto. O antigo valor de s ('abc') passa então a ficar sem referência alguma a ele.

Quem é quem?

Para confirmar se duas variáveis referenciam o mesmo objeto, podemos usar o operador de identidade is que verifica se duas variáveis possuem como valor o mesmo objeto.

>>> x = [1, 2, 3]
>>> y = x
>>> x is y
True

Outra forma de verificar se duas variáveis se referem ao mesmo objeto é usando o comando id(), que retorna o identificador do objeto, que nada mais é do que um número inteiro que cada objeto possui para ser unicamente identificado:

>>> print id(x)
30008456
>>> print id(y)
30008456

Tanto x quanto y se referem ao objeto com o identificador 30008456, isto é, ao mesmo objeto ([1, 2, 3]).

Agora, se o que você deseja é fazer uma cópia do objeto lista, de modo que uma modificação na cópia não interfira no objeto original, existem algumas formas de fazer isso: usando o operador de fatiamento (slicing) ou o método copy(), disponível no módulo copy.

>>> x = [1, 2, 3]
>>> y = x[:] # cria uma nova lista com todo conteúdo de x e atribui a y
>>> x is y
False
>>> import copy
>>> y = copy.copy(x)
>>> x is y
False

Perceba que o método copy.copy() faz apenas o que chamamos de cópia rasa da lista, pois se a lista em questão possuir outras listas aninhadas, estas não serão copiadas, sendo somente suas referências copiadas. Para cópias profundas, use copy.deepcopy().

Passagem de parâmetros

Outra confusão muito comum é a passagem de parâmetros para funções. Veja o exemplo abaixo e tente descobrir o resultado da execução:

def func(x, y):
    x = x + 1
    y.append(4)

x = 1
y = [1, 2, 3]
func(x, y)
print 'x:', x
print 'y:', y

(Pare por um momento se for necessário antes de seguir a leitura e analise o código acima para descobrir o resultado.)

O resultado pode ser visto abaixo:

x: 1
y: [1, 2, 3, 4]

A função func recebeu dois argumentos. De forma leiga, poderíamos dizer que ambos sofreram alterações dentro da função, mas somente na lista a alteração persistiu fora do escopo da função. Isso tem uma explicação bem clara: para qualquer objeto passado para uma função, é feita uma cópia da referência do objeto para o escopo local da função. Assim, se o objeto for mutável, uma operação como append(), por exemplo, vai afetar o objeto referenciado pela variável e assim a alteração persiste fora do escopo da função. Com um objeto imutável, não é possível alterar o objeto em si. O que se faz é alterar a referência (x = x + 1, por exemplo). Como a referência é apenas uma cópia, qualquer alteração feita sobre ela não irá ter efeito na referência do escopo de fora. Por isso, x permanece referenciando o mesmo objeto (1) no escopo global após a função ter terminado.

Resumindo: para entender o comportamento da passagem de parâmetros em Python, basta entender a diferença entre objetos mutáveis e imutáveis, e também lembrar que toda variável em Python nada mais é do que uma referência para algum objeto da memória.

Além disso, é conveniente deixar de lado um pouco aquela noção de que x = x + 1 (ou operações parecidas) altera(m) o objeto. Lembre sempre que o lado direito da expressão (x + 1) cria um novo objeto e que o lado esquerdo da expressão (x =) faz com que o nome x passe a referenciar o novo objeto. Mais nada. 🙂

Pronto!

O conceito de variáveis e objetos em Python é bem simples e consistente. Fique atento ao seu código, principalmente quando você precisa realizar cópias de objetos, o que em Python não é feito usando o operador =. Ah, não se esqueça outras linguagens implementam esses conceitos de formas diferentes. Veja: Other languages have “variables”… Python has “names” .

Deu erro! E agora, o que eu faço?

Traceback (most recent call last):
  File "novo.py", line 12, in <module>
    hello()
  File "novo.py", line 10, in hello
    monta_lista(10)
  File "novo.py", line 5, in monta_lista
    return x[max]
IndexError: list index out of range

Se você sente calafrios só de ver a mensagem de erro acima, este post é para você. Você já ficou p*** da vida com o interpretador porque aquele programa recém escrito, perfeitinho, começou a apresentar mensagens de erro indecifráveis? Vá com calma, na maioria esmagadora dos casos, a culpa não é do interpretador, da linguagem, do sistema operacional ou do hardware. A culpa é quase sempre sua! (lembre-se sempre disso). Frases como “esse compilador está errado!” são muito comuns nos momentos de frustração, mesmo para programadores experientes. Mas caia na real, o compilador/interpretador só está fazendo o trabalho dele, o errado da história, em geral, é você*.

No momento de desespero, quando você não está entendendo o que está acontecendo para que o seu programa tenha problemas, é preciso que você conheça os mecanismos adequados para simplificar a tarefa de descobrir os problemas em seu código. Neste post nós vamos ver como podemos proceder para facilitar a descoberta de erros no nosso código. Vamos começar pelo basicão: como encontrar e identificar o erro em nosso código.

*É claro que não é impossível que você realmente tenha encontrado algum defeito no compilador ou no sistema operacional, mas esgote todas as possibilidades antes de partir para tal hipótese.

Entendendo melhor as mensagens de erro do Python

O traceback é a informação que o interpretador Python fornece para nos ajudar a identificar o problema em nosso código. Ele contém algumas informações úteis:

  • a sequência de chamadas de funções até o ponto onde ocorreu o problema;
  • o número da linha onde o erro foi gerado;
  • o tipo de erro que ocorreu, bem como uma pequena mensagem informativa sobre o ocorrido.

Às vezes o traceback parece meio enigmático, mas você verá que é bem fácil de entendê-lo. Vejamos um exemplo de um traceback bem simples:

Traceback (most recent call last):
  File "erro1.py", line 4, in <module>
    print (z+y)/x
ZeroDivisionError: integer division or modulo by zero

Mesmo sem olhar o código que o gerou, já podemos extrair algumas informações úteis:

  1. o erro foi gerado na linha 4 do arquivo erro1.py;
  2. o erro que ocorreu é chamado de ZeroDivisionError, indicando que ocorreu uma tentativa de dividir um valor por zero;
  3. o código da linha que gerou o problema é print (z+y)/x

Olhando agora para o código-fonte do programa que gerou o erro acima, podemos identificar logo o erro:

1  x = 0
2  y = 10
3  z = 3
4  print (z+y)/x

Tá na cara, estamos dividindo um valor por x, sendo que x é zero. Divisão por zero é uma indefinição matemática e nem o computador sabe lidar com isso.

Outro exemplo

Agora você está escrevendo um programinha que vai manipular uma lista com valores aleatórios.

#! -*- encoding:utf-8 -*-
from random import shuffle
TAM = 5
lista = range(TAM)
shuffle(lista)
print 'Primeiro:', lista[0]
print 'Último:', lista[TAM]

Ao executar, se depara com o seguinte saída, incluindo um traceback:

Primeiro: 3
Último:
Traceback (most recent call last):
  File "/home/user/src/erro2.py", line 7, in <module>
    print 'Último:', lista[TAM]
IndexError: list index out of range

Perceba que as duas primeiras linhas acima fazem parte da saída gerada pelo seu programinha. Vamos agora encontrar e identificar o erro:

  • De acordo com o traceback, o erro foi gerado na linha 7 do nosso arquivo. Essa é a última linha (print 'Último:', lista[TAM]).
  • Outra informação muito importante na saída apresentada acima é o tipo de erro que ocorreu: IndexError: list index out of range (adaptado em pt-br: Erro de índice: índice fora da faixa da lista). O IndexError significa que estamos tentando acessar alguma posição da lista que vai além do tamanho dela. Por exemplo, em uma lista de dez elementos, você não pode acessar o décimo-primeiro elemento, pois ele não existe. Se você tentar, vai receber um IndexError na cabeça.

Agora você já tem alguma informação sobre o que pode estar acontecendo: é na última linha e é algum problema relacionado a acesso a posições inexistentes da lista.

Vamos, antes de mais nada lembrar rapidinho como se dá a indexação de listas (e outras sequências) em Python. Veja a lista abaixo, e seus respectivos índices:

lis
+-----+-----+-----+-----+-----+
|  2  |  9  |  6  |  8  |  7  |
+-----+-----+-----+-----+-----+
   0     1     2     3     4

Você já sabe que o índice para acesso ao primeiro elemento da lista lis é 0. Para imprimir o primeiro elemento, você faria print lis[0]. Sabendo que o primeiro índice é 0, fica óbvio que o último não é o tamanho da lista, mas sim o tamanho da lista – 1. Ou seja, em uma lista de 5 elementos (como lis), para imprimir o último elemento, você deve utilizar print lis[4] ao invés de print lis[5]. Agora reveja o traceback e o código-fonte do programa e descubra o erro. 😛

Mais um exemplo

Agora você vai fazer um programa para montar e imprimir URLs de acordo com os parâmetros de protocol, host e path. Veja abaixo o exemplo:

def URL(protocol, host, path):
    return protocol + "://" + host + "/" + path 

def print_urls():
    print URL('http', 'www.example.com', 'path/to/file.html')
    print URL('ftp', 'www.example.com', 'path/to/file.html')
    print URL('http', 'www.example.com', 2)

print_urls()

Porém, pra variar, ao executar, lá vem o maldito traceback (apesar de odiá-lo, acredite, seria muito pior sem ele).

http://www.example.com/path/to/file.html
ftp://www.example.com/path/to/file.html
Traceback (most recent call last):
File "/home/user/src/erro3.py", line 11, in <module>
  print_urls()
File "/home/user/src/erro3.py", line 8, in print_urls
  print URL('http', 'www.example.com', 2)
File "/home/user/src/erro3.py", line 2, in URL
  return protocol + "://" + host + "/" + path 
TypeError: cannot concatenate 'str' and 'int' objects

E agora, o que diabos está acontecendo? Primeiramente, você deve tentar entender a saída do programa. Veja que as duas primeiras linhas indicam que as duas primeiras tentativas de impressão de URLs ocorreram com sucesso. Então, mesmo sem olhar o traceback, você já imagina que algo ocorreu de errado na terceira URL (que deveria mostrar http://www.example.com/2). Analise o traceback, seguindo o procedimento que vimos anteriormente (encontrar onde pode estar o erro e então identificá-lo, para depois corrigí-lo).

A antepenúltima linha do traceback diz que o erro está na linha 2, na função URL (File "/home/user/src/erro3.py", line 2, in URL). A linha 2 é return protocol + "://" + host + "/" + path. Mas peraí, se você olhar o traceback de cima para baixo, verá que ele aponta várias linhas como fontes do erro: 11, 8 e 2. Observe que ele mostra o rastro (trace) de chamadas de funções que levaram ao código com defeito. Como o próprio traceback nos mostra, ocorreu a seguinte chamada de funções que levou ao erro apresentado acima:

print_urls()  -->  print URL('http', 'www.example.com', 2)  -->  return protocol + "://" + host + "/" + path 
    11                                 8                                                2

Talvez você não esteja conseguindo descobrir o erro. Então, vamos analisar mais uma informação do traceback: o tipo do erro. De acordo com o que foi apresentado na última linha do erro, temos um erro do tipo TypeError (erro de tipagem), com a mensagem de erro: cannot concatenate 'str' and 'int' objects (não é possível concatenar objetos dos tipos ‘str’ e ‘int’). Ou seja, estamos em algum lugar passando um número inteiro para ser concatenado às strings que são utilizadas para compor a URL. Agora ficou moleza, pois na linha 8 estamos passando o argumento 2 (um valor do tipo int) para o parâmetro path da função URL() (que é usado nessa função para ser concatenado a valores do tipo str). Para corrigir é fácil, basta substituir o 2 do tipo inteiro pelo '2' do tipo string. A linha 8 ficaria assim:

print URL('http', 'www.example.com', '2')

Mas às vezes os erros não estão tão explícitos assim, e somente analisar o traceback não é o suficiente para descobrirmos o problema de nosso código. Então teremos que partir para outras abordagens, como veremos a seguir.

(Se você quiser conhecer os principais erros e exceções que podem ocorrer em Python, consulte a documentação oficial.)

Muito além do traceback

Muitas vezes é preciso que analisemos a execução do programa em questão. Essa análise envolve descobrirmos os valores que as variáveis estão assumindo e quais caminhos no código o programa está percorrendo durante sua execução. A forma mais comum que existe para fazermos isso e que para muitos é a solução definitiva para o problema, é encher o código de prints, mostrando assim no console os valores das variáveis, para que possamos ver se o programa está se comportando da forma desejada. Existem outras formas também, usando ferramentas específicas, que permitem que o programa seja pausado, executado linha por linha e inspecionado. Tanto o uso dos prints quanto o uso de ferramentas específicas é chamado de depuração de código (debugging, eliminação de bugs).

O print

Quem nunca encheu o código de prints para tentar entender o que está se passando no programa atire a primeira pedra. Acaba que essa é a solução mais natural para o cara que está iniciando. A regra é simples: imprima o valor das variáveis que lhe interessam, principalmente em pontos como entradas de expressões condicionais (if-elif-else); antes, durante e depois da execução de um laço de repetição (for, while); antes e depois da chamada de uma função que modifica valores de nossas variáveis; e em toda situação que ficamos na dúvida sobre o que está acontecendo com nossas variáveis.

Veja um exemplo do uso de prints para depurar um programa:

x = 0
x = faca_algo_com(x)
print 'pre-while: %s' % (x, )
while x > 0:
    x = faca_algo_com(x)
    if x % 2 == 0:
        x += 1
    print 'while: %s' % (x, )
print 'pos-while: %s' % (x, )

No exemplo acima, usamos 3 prints para termos melhor entendimento do que está acontecendo durante a execução do programa. Em programas que já lançam muitos valores na saída padrão, pode ser útil lançar as mensagens de depuração na saída padrão de erros do shell, para que possamos, se preciso for, analisá-las em separado do resto do programa:

import sys
x = 0
x = faca_algo_com(x)
sys.stderr.write('pre-while: %s\n' % (x, ))
while x > 0:
    x = faca_algo_com(x)
    if x % 2 == 0:
        x += 1
    sys.stderr.write('while: %s\n' % (x, ))
sys.stderr.write('pos-while: %s\n' % (x, ))

Mas, tenha cuidado, pois às vezes o excesso de informações vai atrapalhar mais do que ajudar.

Os loggers

Uma alternativa interessante aos prints é o uso de loggers. O log de um programa é, em geral, um objeto no qual registramos informações, para uma possível análise posterior. Em geral, utilizamos o log para registro da ocorrência de eventos durante a execução de um programa. A ideia principal é basicamente a mesma do que usar os prints, mas na prática existem várias vantagens no uso de um logger.

O recurso mais interessante dos loggers é a possibilidade de “categorizar” as informações que vamos jogando na tela, de acordo com o seu grau de importância. Por exemplo, o mecanismo de logging padrão do Python possui 5 níveis: DEBUG, INFO, WARNING (nível padrão), ERROR e CRITICAL. Veja o exemplo:


import logging

logging.debug('depurando ...')
logging.info('informando ...')
logging.warning('alertando ...')
logging.error('assustando ...')
logging.critical('apavorando ...')

Se o programa acima for executado, a saída será:

WARNING:root:alertando ...
ERROR:root:assustando ...
CRITICAL:root:apavorando ...

Perceba que as duas primeiras linhas do logging não foram refletidas na saída do programa. Isso porque o nível padrão do logger é o warning, o que significa que somente mensagens de warning e de categorias mais importantes (no caso, error e critical) serão impressas.

Usando o logger, podemos incluir várias mensagens de depuração no código, e ativar seu aparecimento na saída do programa somente quando tivermos interesse.

Veja mais em: http://docs.python.org/2/library/logging

O debugger

Depuradores são ferramentas feitas para auxiliar o desenvolvedor a descobrir falhas em seu código. Eles permitem que executemos um programa passo-a-passo, que a cada passo inspecionemos as variáveis, que verifiquemos em que seção do código está a execução do programa, além de outros recursos. Para mim, o melhor dos recursos é poder inspecionar os valores aos quais as variáveis referenciam durante a execução passo-a-passo do programa.

O depurador que vamos ver em seguida é o pdb, que é o depurador padrão distribuído com Python. Basta que executemos o depurador, informando a ele qual o programa que queremos depurar. Por exemplo, para depurar um programa chamado prog.py, invocamos o pdb
da seguinte forma (em um shell do sistema operacional):

$ python -m pdb prog.py

Teste você mesmo, baixando o arquivo prog.py. Veja na imagem a seguir um exemplo de execução de um programa usando o pdb:

pdb

Ao iniciar a execução, o depurador para e aguarda por comandos do usuário, que podem ser: execute a atual e pule para a próxima linha, entre dentro da função, mostre o valor de uma variável/expressão, mostre o código fonte, etc.

Alguns comandos úteis do pdb:

  • list: mostra um trecho do código em execução, com destaque para a linha atual;
  • next: comando que indica ao pdb que ele deve seguir para a próxima linha de execução, sem adentrar em funções quando houverem chamadas;
  • step: indica ao pdb para que ele siga em frente, entrando na função se a linha atual for uma chamada de função;
  • pp <nome_de_variável>: mostra na tela o valor referenciado pela variável nome_de_variável.

A seguir, vou apresentar um processo de depuração, para que você entenda melhor do que se trata e como podemos fazê-lo. Vamos testar um programa que faz a multiplicação de dois valores através de sucessivas somas:

def multiplica(x, y):
  result = 0
  while y > 0:
      result = result + x
      y = y - 1
  return result

z = 10
k = 2
r = multiplica(z, k)
print r

Veja o vídeo abaixo, onde demonstro uma pequena sessão de depuração do programa acima:

Como você pode perceber, o mais interessante do uso do depurador é a análise detalhada que é possível que façamos enquanto o programa está em execução. No vídeo, inspecionamos os valores de variáveis (com o comando pp), verificamos o código em execução do programa (o list mostra a linha em execução atualmente, bem como algumas linhas acima e abaixo), passamos de linha em linha (usando o comando next) e até mesmo entramos em funções, para verificar o que é feito dentro delas (com o comando step). Normalmente, é disso que se trata uma sessão de depuração, uma análise do programa em execução, inspeção de valores, verificação de caminhos percorridos pela execução no código do programa. Assim, o programador pode obter boas pistas sobre o problema que está enfrentando.

A seguir, uma tabela contendo os principais comandos do pdb e suas funcionalidades:

Comando Atalho Uso
help h Mostra a ajuda do comando.
break x b Insere um breakpoint na linha x, um ponto de parada, até o qual o programa será executado sem interatividade. A partir dali, a execução fica sob o controle do usuário do depurador.
step s Prossegue a execução até a próxima linha de código, inclusive entrando em funções.
next n Parecido com step, mas executa chamadas a funções como comandos comuns, sem entrar nelas.
pp exp pp Imprime o valor da expressão exp, podendo exp ser uma simples variável ou uma expressão complexa.
quit q Fecha o depurador

Mais comandos em: http://docs.python.org/2/library/pdb.html#debugger-commands

Veja mais sobre o pdb em (em inglês):

Breakpoints

Para que você não precise excutar o programa todo, linha por linha, next por next, desde o começo até o ponto que lhe interesse, você pode usar um breakpoint. Um breakpoint é um ponto de parada, de forma que o programa é executado normalmente (sem interatividade) até o momento em que encontrar o breakpoint. A partir dali, o depurador passa a aguardar pelos comandos do usuário. Breakpoints são muito úteis, pois em geral estamos interessados na depuração de somente um trecho do código, onde desconfiamos que esteja ocorrendo o erro.

winpdb

Se você prefere um ambiente gráfico, vários IDEs fornecem a integração com depurador na sua interface. Basta escolher um IDE e usar a depuração sempre que for preciso.

Mas, se você não quiser usar um IDE só por isso, você pode utilizar o winpdb, que é um depurador gráfico para Python. Em uma janela só, você vê o código, uma marcação indicando a linha em execução, uma tabela contendo todas as informações que estão no escopo local e global, além de suportar avaliação de expressões, e até mesmo, mudanças nos valores das variáveis do programa em tempo de execução.

No vídeo a seguir eu mostro um exemplo de execução de um programa usando o winpdb:

Enfim…

Se você sofre muito para encontrar os problemas existentes no seu código, aprenda a usar um depurador, seja ele gráfico ou em texto, e se acostume a usá-lo. Usar um depurador acaba sendo muito mais eficiente do que encher o código com prints, pois ele permite que paremos a execução em um determinado ponto, que alteremos valores, e que analisemos com mais calma a execução do programa, tendo sempre uma visão melhor do estado atual deste.

Atenção: os códigos apresentados neste post foram elaborados com o único propósito de mostrar uma situação em que erros podem ocorrer. Assim, de forma alguma eles podem ser considerados exemplos de boas práticas.