Código Python bugado: os 10 erros mais comuns que os desenvolvedores Python cometem
Publicados: 2022-03-11Sobre Python
Python é uma linguagem de programação interpretada, orientada a objetos e de alto nível com semântica dinâmica. Suas estruturas de dados integradas de alto nível, combinadas com tipagem dinâmica e vinculação dinâmica, o tornam muito atraente para o Desenvolvimento Rápido de Aplicativos, bem como para uso como script ou linguagem de colagem para conectar componentes ou serviços existentes. Python suporta módulos e pacotes, incentivando assim a modularidade do programa e a reutilização de código.
Sobre este artigo
A sintaxe simples e fácil de aprender do Python pode induzir os desenvolvedores Python – especialmente aqueles que são mais novos na linguagem – a perder algumas de suas sutilezas e subestimar o poder da linguagem Python diversificada.
Com isso em mente, este artigo apresenta uma lista dos “10 principais” de erros um tanto sutis e mais difíceis de detectar que podem atrapalhar até mesmo alguns desenvolvedores Python mais avançados.
(Observação: este artigo é destinado a um público mais avançado do que Erros comuns de programadores Python, que é mais voltado para aqueles que são mais novos na linguagem.)
Erro comum nº 1: uso indevido de expressões como padrões para argumentos de função
Python permite que você especifique que um argumento de função é opcional fornecendo um valor padrão para ele. Embora esse seja um ótimo recurso da linguagem, pode causar alguma confusão quando o valor padrão é mutável . Por exemplo, considere esta definição de função Python:
>>> def foo(bar=[]): # bar is optional and defaults to [] if not specified ... bar.append("baz") # but this line could be problematic, as we'll see... ... return bar
Um erro comum é pensar que o argumento opcional será definido para a expressão padrão especificada toda vez que a função for chamada sem fornecer um valor para o argumento opcional. No código acima, por exemplo, pode-se esperar que chamar foo()
repetidamente (ou seja, sem especificar um argumento bar
) sempre retornaria 'baz'
, já que a suposição seria que cada vez que foo()
fosse chamado (sem uma bar
argumento especificado) bar
é definido como []
(ou seja, uma nova lista vazia).
Mas vamos ver o que realmente acontece quando você faz isso:
>>> foo() ["baz"] >>> foo() ["baz", "baz"] >>> foo() ["baz", "baz", "baz"]
Huh? Por que ele continuou anexando o valor padrão de "baz"
a uma lista existente toda vez que foo()
era chamado, em vez de criar uma nova lista a cada vez?
A resposta de programação Python mais avançada é que o valor padrão para um argumento de função é avaliado apenas uma vez, no momento em que a função é definida. Assim, o argumento bar
é inicializado com seu padrão (isto é, uma lista vazia) somente quando foo()
é definido pela primeira vez, mas então as chamadas para foo()
(isto é, sem um argumento bar
especificado) continuarão a usar a mesma lista para qual bar
foi inicializada originalmente.
Para sua informação, uma solução comum para isso é a seguinte:
>>> def foo(bar=None): ... if bar is None: # or if not bar: ... bar = [] ... bar.append("baz") ... return bar ... >>> foo() ["baz"] >>> foo() ["baz"] >>> foo() ["baz"]
Erro comum nº 2: usar variáveis de classe incorretamente
Considere o seguinte exemplo:
>>> class A(object): ... x = 1 ... >>> class B(A): ... pass ... >>> class C(A): ... pass ... >>> print Ax, Bx, Cx 1 1 1
Faz sentido.
>>> Bx = 2 >>> print Ax, Bx, Cx 1 2 1
Sim, novamente como esperado.
>>> Ax = 3 >>> print Ax, Bx, Cx 3 2 3
O que $%#!& ?? Nós apenas mudamos Ax
. Por que Cx
também mudou?
Em Python, as variáveis de classe são tratadas internamente como dicionários e seguem o que é frequentemente chamado de Method Resolution Order (MRO). Portanto, no código acima, como o atributo x
não é encontrado na classe C
, ele será pesquisado em suas classes base (somente A
no exemplo acima, embora o Python suporte heranças múltiplas). Em outras palavras, C
não tem sua própria propriedade x
, independente de A
. Assim, as referências a Cx
são de fato referências a Ax
. Isso causa um problema do Python, a menos que seja tratado corretamente. Saiba mais sobre atributos de classe em Python.
Erro comum nº 3: Especificar parâmetros incorretamente para um bloco de exceção
Suponha que você tenha o seguinte código:
>>> try: ... l = ["a", "b"] ... int(l[2]) ... except ValueError, IndexError: # To catch both exceptions, right? ... pass ... Traceback (most recent call last): File "<stdin>", line 3, in <module> IndexError: list index out of range
O problema aqui é que a instrução except
não recebe uma lista de exceções especificadas dessa maneira. Em vez disso, no Python 2.x, a sintaxe except Exception, e
é usada para vincular a exceção ao segundo parâmetro opcional especificado (neste caso e
), a fim de disponibilizá-lo para inspeção adicional. Como resultado, no código acima, a exceção IndexError
não está sendo capturada pela instrução except
; em vez disso, a exceção acaba sendo vinculada a um parâmetro chamado IndexError
.
A maneira correta de capturar várias exceções em uma instrução except
é especificar o primeiro parâmetro como uma tupla contendo todas as exceções a serem capturadas. Além disso, para portabilidade máxima, use a palavra as
chave as, pois essa sintaxe é suportada pelo Python 2 e pelo Python 3:
>>> try: ... l = ["a", "b"] ... int(l[2]) ... except (ValueError, IndexError) as e: ... pass ... >>>
Erro comum nº 4: entender mal as regras de escopo do Python
A resolução do escopo do Python é baseada no que é conhecido como regra LEGB, que é uma abreviação de L ocal, Enclosing , G lobal, B uilt-in. Parece bastante simples, certo? Bem, na verdade, existem algumas sutilezas na maneira como isso funciona em Python, o que nos leva ao problema comum de programação Python mais avançado abaixo. Considere o seguinte:
>>> x = 10 >>> def foo(): ... x += 1 ... print x ... >>> foo() Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 2, in foo UnboundLocalError: local variable 'x' referenced before assignment
Qual é o problema?
O erro acima ocorre porque, quando você faz uma atribuição a uma variável em um escopo, essa variável é automaticamente considerada pelo Python como local para esse escopo e oculta qualquer variável com nome semelhante em qualquer escopo externo.
Muitos ficam surpresos ao obter um UnboundLocalError
no código de trabalho anterior quando ele é modificado pela adição de uma instrução de atribuição em algum lugar no corpo de uma função. (Você pode ler mais sobre isso aqui.)
É particularmente comum que isso atrapalhe os desenvolvedores ao usar listas. Considere o seguinte exemplo:
>>> lst = [1, 2, 3] >>> def foo1(): ... lst.append(5) # This works ok... ... >>> foo1() >>> lst [1, 2, 3, 5] >>> lst = [1, 2, 3] >>> def foo2(): ... lst += [5] # ... but this bombs! ... >>> foo2() Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 2, in foo UnboundLocalError: local variable 'lst' referenced before assignment
Huh? Por que foo2
bomba enquanto foo1
funcionou bem?
A resposta é a mesma do problema do exemplo anterior, mas é reconhecidamente mais sutil. foo1
não está fazendo uma atribuição para lst
, enquanto foo2
está. Lembrando que lst += [5]
é realmente apenas uma abreviação de lst = lst + [5]
, vemos que estamos tentando atribuir um valor a lst
(portanto, presumido pelo Python como no escopo local). No entanto, o valor que estamos procurando atribuir a lst
é baseado no próprio lst
(novamente, agora presumivelmente no escopo local), que ainda não foi definido. Estrondo.
Erro comum nº 5: Modificando uma lista ao iterar sobre ela
O problema com o código a seguir deve ser bastante óbvio:
>>> odd = lambda x : bool(x % 2) >>> numbers = [n for n in range(10)] >>> for i in range(len(numbers)): ... if odd(numbers[i]): ... del numbers[i] # BAD: Deleting item from a list while iterating over it ... Traceback (most recent call last): File "<stdin>", line 2, in <module> IndexError: list index out of range
Excluir um item de uma lista ou array durante a iteração sobre ele é um problema do Python que é bem conhecido por qualquer desenvolvedor de software experiente. Mas enquanto o exemplo acima pode ser bastante óbvio, mesmo desenvolvedores avançados podem ser involuntariamente mordidos por isso em um código que é muito mais complexo.
Felizmente, o Python incorpora vários paradigmas de programação elegantes que, quando usados corretamente, podem resultar em um código significativamente simplificado e otimizado. Um benefício colateral disso é que o código mais simples é menos propenso a ser mordido pelo bug de exclusão acidental de um item de lista enquanto iterando sobre ele. Um desses paradigmas é o das compreensões de lista. Além disso, as compreensões de lista são particularmente úteis para evitar esse problema específico, como mostrado por esta implementação alternativa do código acima que funciona perfeitamente:

>>> odd = lambda x : bool(x % 2) >>> numbers = [n for n in range(10)] >>> numbers[:] = [n for n in numbers if not odd(n)] # ahh, the beauty of it all >>> numbers [0, 2, 4, 6, 8]
Erro comum nº 6: Confundir como o Python vincula variáveis em encerramentos
Considerando o seguinte exemplo:
>>> def create_multipliers(): ... return [lambda x : i * x for i in range(5)] >>> for multiplier in create_multipliers(): ... print multiplier(2) ...
Você pode esperar a seguinte saída:
0 2 4 6 8
Mas você realmente obtém:
8 8 8 8 8
Surpresa!
Isso acontece devido ao comportamento de ligação tardia do Python, que diz que os valores das variáveis usadas nos encerramentos são consultados no momento em que a função interna é chamada. Portanto, no código acima, sempre que qualquer uma das funções retornadas é chamada, o valor de i
é pesquisado no escopo ao redor no momento em que é chamado (e, então, o loop foi concluído, então i
já recebeu seu final valor de 4).
A solução para este problema comum do Python é um pouco hack:
>>> def create_multipliers(): ... return [lambda x, i=i : i * x for i in range(5)] ... >>> for multiplier in create_multipliers(): ... print multiplier(2) ... 0 2 4 6 8
Voilá! Estamos aproveitando os argumentos padrão aqui para gerar funções anônimas para obter o comportamento desejado. Alguns chamariam isso de elegante. Alguns chamariam de sutil. Alguns odeiam. Mas se você é um desenvolvedor Python, é importante entender em qualquer caso.
Erro comum nº 7: criando dependências de módulos circulares
Digamos que você tenha dois arquivos, a.py
e b.py
, cada um importando o outro, da seguinte forma:
Em a.py
:
import b def f(): return bx print f()
E em b.py
:
import a x = 1 def g(): print af()
Primeiro, vamos tentar importar a.py
:
>>> import a 1
Funcionou muito bem. Talvez isso o surpreenda. Afinal, temos uma importação circular aqui que presumivelmente deveria ser um problema, não deveria?
A resposta é que a mera presença de uma importação circular não é por si só um problema em Python. Se um módulo já foi importado, o Python é inteligente o suficiente para não tentar reimportá-lo. No entanto, dependendo do ponto em que cada módulo está tentando acessar funções ou variáveis definidas no outro, você pode realmente ter problemas.
Então, voltando ao nosso exemplo, quando importamos a.py
, não tivemos problemas em importar b.py
, pois b.py
não exige que nada de a.py
seja definido no momento da importação . A única referência em b.py
para a
é a chamada para af()
. Mas essa chamada está em g()
e nada em a.py
ou b.py
invoca g()
. Então a vida é boa.
Mas o que acontece se tentarmos importar b.py
(sem ter importado previamente a.py
, ou seja):
>>> import b Traceback (most recent call last): File "<stdin>", line 1, in <module> File "b.py", line 1, in <module> import a File "a.py", line 6, in <module> print f() File "a.py", line 4, in f return bx AttributeError: 'module' object has no attribute 'x'
Uh-oh. Isso não é bom! O problema aqui é que, no processo de importação de b.py
, ele tenta importar a.py
, que por sua vez chama f()
, que tenta acessar bx
. Mas bx
ainda não foi definido. Daí a exceção AttributeError
.
Pelo menos uma solução para isso é bastante trivial. Simplesmente modifique b.py
para importar a.py
dentro de g()
:
x = 1 def g(): import a # This will be evaluated only when g() is called print af()
Não, quando importamos, está tudo bem:
>>> import b >>> bg() 1 # Printed a first time since module 'a' calls 'print f()' at the end 1 # Printed a second time, this one is our call to 'g'
Erro comum nº 8: conflito de nomes com os módulos da biblioteca padrão do Python
Uma das belezas do Python é a riqueza de módulos de biblioteca que ele vem “fora da caixa”. Mas, como resultado, se você não estiver evitando isso conscientemente, não é tão difícil encontrar um conflito de nomes entre o nome de um de seus módulos e um módulo com o mesmo nome na biblioteca padrão fornecida com o Python (por exemplo , você pode ter um módulo chamado email.py
em seu código, que estaria em conflito com o módulo de biblioteca padrão de mesmo nome).
Isso pode levar a problemas complicados, como importar outra biblioteca que, por sua vez, tenta importar a versão da biblioteca padrão do Python de um módulo, mas, como você tem um módulo com o mesmo nome, o outro pacote importa erroneamente sua versão em vez da que está dentro a biblioteca padrão do Python. É aqui que acontecem erros ruins do Python.
Portanto, deve-se tomar cuidado para evitar usar os mesmos nomes dos módulos da biblioteca padrão do Python. É muito mais fácil para você alterar o nome de um módulo em seu pacote do que enviar uma Proposta de Aprimoramento do Python (PEP) para solicitar uma alteração de nome upstream e tentar obter a aprovação.
Erro comum nº 9: não abordar as diferenças entre o Python 2 e o Python 3
Considere o seguinte arquivo foo.py
:
import sys def bar(i): if i == 1: raise KeyError(1) if i == 2: raise ValueError(2) def bad(): e = None try: bar(int(sys.argv[1])) except KeyError as e: print('key error') except ValueError as e: print('value error') print(e) bad()
No Python 2, isso funciona bem:
$ python foo.py 1 key error 1 $ python foo.py 2 value error 2
Mas agora vamos dar uma olhada no Python 3:
$ python3 foo.py 1 key error Traceback (most recent call last): File "foo.py", line 19, in <module> bad() File "foo.py", line 17, in bad print(e) UnboundLocalError: local variable 'e' referenced before assignment
O que acabou de acontecer aqui? O “problema” é que, no Python 3, o objeto de exceção não é acessível além do escopo do bloco except
. (A razão para isso é que, caso contrário, ele manteria um ciclo de referência com o quadro de pilha na memória até que o coletor de lixo seja executado e limpe as referências da memória. Mais detalhes técnicos sobre isso estão disponíveis aqui).
Uma maneira de evitar esse problema é manter uma referência ao objeto de exceção fora do escopo do bloco except
para que ele permaneça acessível. Aqui está uma versão do exemplo anterior que usa essa técnica, produzindo assim um código compatível com Python 2 e Python 3:
import sys def bar(i): if i == 1: raise KeyError(1) if i == 2: raise ValueError(2) def good(): exception = None try: bar(int(sys.argv[1])) except KeyError as e: exception = e print('key error') except ValueError as e: exception = e print('value error') print(exception) good()
Executando isso no Py3k:
$ python3 foo.py 1 key error 1 $ python3 foo.py 2 value error 2
Yippee!
(Aliás, nosso Guia de Contratação do Python discute várias outras diferenças importantes a serem observadas ao migrar o código do Python 2 para o Python 3.)
Erro comum nº 10: uso indevido do método __del__
Digamos que você tenha isso em um arquivo chamado mod.py
:
import foo class Bar(object): ... def __del__(self): foo.cleanup(self.myhandle)
E você tentou fazer isso de another_mod.py
:
import mod mybar = mod.Bar()
Você receberia uma exceção AttributeError
feia.
Por quê? Porque, conforme relatado aqui, quando o interpretador é encerrado, as variáveis globais do módulo são todas definidas como None
. Como resultado, no exemplo acima, no momento em que __del__
é invocado, o nome foo
já foi definido como None
.
Uma solução para este problema de programação Python um pouco mais avançado seria usar atexit.register()
em vez disso. Dessa forma, quando seu programa terminar de ser executado (ou seja, ao sair normalmente), seus manipuladores registrados serão iniciados antes que o interpretador seja desligado.
Com esse entendimento, uma correção para o código mod.py
acima pode ser algo assim:
import foo import atexit def cleanup(handle): foo.cleanup(handle) class Bar(object): def __init__(self): ... atexit.register(cleanup, self.myhandle)
Essa implementação fornece uma maneira limpa e confiável de chamar qualquer funcionalidade de limpeza necessária no término normal do programa. Obviamente, cabe a foo.cleanup
decidir o que fazer com o objeto vinculado ao nome self.myhandle
, mas você entendeu.
Embrulhar
Python é uma linguagem poderosa e flexível com muitos mecanismos e paradigmas que podem melhorar muito a produtividade. Como acontece com qualquer ferramenta de software ou linguagem, porém, ter uma compreensão ou apreciação limitada de suas capacidades pode às vezes ser mais um impedimento do que um benefício, deixando a pessoa no estado proverbial de “saber o suficiente para ser perigoso”.
Familiarizar-se com as principais nuances do Python, como (mas não limitado a) os problemas de programação moderadamente avançados levantados neste artigo, ajudará a otimizar o uso da linguagem, evitando alguns de seus erros mais comuns.
Você também pode conferir nosso Guia do Insider para Entrevistas em Python para obter sugestões sobre perguntas de entrevista que podem ajudar a identificar especialistas em Python.
Esperamos que você tenha achado as dicas deste artigo úteis e agradecemos seus comentários.