Código de bugs Python: 10 erros mais comuns que os desenvolvedores cometem

Sobre o Python


Python é uma linguagem de programação interpretada, orientada a objetos e de alto nível, com semântica dinâmica. As estruturas de dados de alto nível integradas, combinadas com a digitação dinâmica e a ligação dinâmica, o tornam muito atraente para o BRPS (desenvolvimento rápido de ferramentas de aplicativo), bem como para ser usado como uma linguagem de script e conexão para conectar componentes ou serviços existentes. O Python suporta módulos e pacotes, incentivando a modularidade do programa e a reutilização de código.

Sobre este artigo


A simplicidade e a facilidade de dominar essa linguagem podem ser confusas para os desenvolvedores (especialmente aqueles que estão começando a aprender Python), para que você perca de vista algumas sutilezas importantes e subestime o poder da variedade de soluções possíveis usando o Python.

Com isso em mente, este artigo apresenta o "top 10" de erros sutis e difíceis de encontrar que até mesmo desenvolvedores avançados de Python podem cometer.

Erro nº 1: usar expressões incorretas como valores padrão para argumentos de função


O Python permite que você indique que uma função pode ter argumentos opcionais, definindo um valor padrão para eles. Isso, é claro, é um recurso muito conveniente da linguagem, mas pode levar a conseqüências desagradáveis ​​se o tipo desse valor for mutável. Por exemplo, considere a seguinte definição de função:

>>> def foo(bar=[]): # bar -    #      . ... bar.append("baz") #     ... ... return bar 

Um erro comum nesse caso é pensar que o valor de um argumento opcional será definido como o valor padrão sempre que uma função for chamada sem um valor para esse argumento. No código acima, por exemplo, podemos assumir que, chamando repetidamente a função foo () (ou seja, sem especificar um valor para o argumento bar), ele sempre retornará "baz", pois é assumido que toda vez que foo () é chamado (sem especificando a barra de argumentos), bar é definido como [] (ou seja, uma nova lista vazia).

Mas vamos ver o que realmente acontecerá:

 >>> foo() ["baz"] >>> foo() ["baz", "baz"] >>> foo() ["baz", "baz", "baz"] 

Hein? Por que a função continua a adicionar o valor padrão "baz" à lista existente toda vez que foo () é chamado, em vez de criar uma nova lista a cada vez?

A resposta a esta pergunta será uma compreensão mais profunda do que está acontecendo com o Python "sob o capô". A saber: o valor padrão da função é inicializado apenas uma vez, durante a definição da função. Portanto, o argumento da barra é inicializado por padrão (ou seja, uma lista vazia) somente quando foo () é definido pela primeira vez, mas as chamadas subseqüentes para foo () (ou seja, sem especificar o argumento da barra) continuarão usando a mesma lista que foi criado para a barra de argumentos no momento da primeira definição da função.

Para referência, uma "solução alternativa" comum para esse erro é a seguinte definição:

 >>> 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 # 2: uso indevido de variáveis ​​de classe


Considere o seguinte exemplo:

 >>> class A(object): ... x = 1 ... >>> class B(A): ... pass ... >>> class C(A): ... pass ... >>> print Ax, Bx, Cx 1 1 1 

Tudo parece estar em ordem.

 >>> Bx = 2 >>> print Ax, Bx, Cx 1 2 1 

Sim, tudo estava como o esperado.

 >>> Ax = 3 >>> print Ax, Bx, Cx 3 2 3 

Que diabos ?! Acabamos de mudar o Ax. Por que o Cx também mudou?

No Python, as variáveis ​​de classe são tratadas como dicionários e seguem o que geralmente é chamado MRO (Method Resolution Order). Portanto, no código acima, como o atributo x não é encontrado na classe C, ele será encontrado em suas classes base (apenas A no exemplo acima, embora o Python suporte herança múltipla). Em outras palavras, C não possui sua própria propriedade x independente de A. Portanto, as referências a Cx são na verdade referências a Ax. Isso causará problemas se esses casos não forem tratados adequadamente. Portanto, ao aprender Python, preste atenção especial aos atributos da classe e trabalhe com eles.

Erro nº 3: parâmetros incorretos para o bloco de exceção


Suponha que você tenha o seguinte trecho de 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 expressão de exceção não aceita a lista de exceções especificadas dessa maneira. Em vez disso, no Python 2.x, a expressão “exceto Exceção, e” é usada para vincular a exceção a um segundo parâmetro opcional fornecido (neste caso, e) para disponibilizá-la para uma inspeção adicional. Como resultado, no código acima, uma exceção IndexError não é capturada pela instrução exceto; em vez disso, a exceção termina com a ligação a um parâmetro chamado IndexError.

A maneira correta de capturar várias exceções com a expressão de exceção é especificar o primeiro parâmetro como uma tupla contendo todas as exceções que você deseja capturar. Além disso, para obter compatibilidade máxima, use a palavra-chave as, pois essa sintaxe é suportada no Python 2 e no Python 3:

 >>> try: ... l = ["a", "b"] ... int(l[2]) ... except (ValueError, IndexError) as e: ... pass ... >>> 

Erro # 4: interpretando mal as regras de escopo do Python


O escopo no Python é baseado na regra LEGB, que é uma abreviação de Local (nomes atribuídos de qualquer forma a uma função (def ou lambda) e não declarada global nesta função), Enclosing (nome no escopo local de qualquer função que inclua estaticamente) ( def ou lambda), de interno para externo), Global (nomes atribuídos no nível superior do arquivo do módulo ou executando instruções globais em def dentro do arquivo), Interno (nomes previamente atribuídos no módulo de nome interno: aberto, intervalo, SyntaxError, ...). Parece bastante simples, certo? Bem, na verdade, existem algumas sutilezas sobre como isso funciona no Python, o que nos leva ao problema de programação Python geral mais complexo abaixo. Considere o seguinte exemplo:

 >>> 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ê atribui uma variável no escopo, o Python automaticamente a considera local para esse escopo e oculta qualquer variável com o mesmo nome em qualquer escopo pai.

Assim, muitos ficam surpresos quando recebem UnboundLocalError no código em execução anterior, quando são modificados adicionando um operador de atribuição em algum lugar do corpo da função.

Esse recurso é especialmente confuso para os desenvolvedores ao usar listas. Considere o seguinte exemplo:

 >>> lst = [1, 2, 3] >>> def foo1(): ... lst.append(5) #   ... ... >>> foo1() >>> lst [1, 2, 3, 5] >>> lst = [1, 2, 3] >>> def foo2(): ... lst += [5] # ...    ! ... >>> 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 

Hein? Por que o foo2 trava enquanto o foo1 está funcionando bem?

A resposta é a mesma do exemplo anterior, mas, segundo a crença popular, a situação aqui é mais sutil. foo1 não aplica o operador de atribuição a lst, enquanto foo2 não. Tendo em mente que lst + = [5] é apenas uma abreviação de lst = lst + [5], vemos que estamos tentando atribuir o valor lst (então o Python assume que está no escopo local). No entanto, o valor que queremos atribuir ao lst é baseado no próprio lst (novamente, agora é assumido que esteja no escopo local), que ainda não foi determinado. E temos um erro.

Erro # 5: alterando uma lista durante a iteração sobre ela


O problema no seguinte trecho de código 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 

A remoção de um item de uma lista ou matriz durante a iteração é um problema do Python que é bem conhecido por qualquer desenvolvedor de software experiente. Mas, embora o exemplo acima possa ser bastante óbvio, até desenvolvedores experientes podem embarcar nesse rake em um código muito mais complexo.

Felizmente, o Python inclui vários paradigmas de programação elegantes que, quando usados ​​corretamente, podem levar a uma simplificação e otimização significativa do código. Uma conseqüência agradável adicional disso é que, no código mais simples, a probabilidade de cair no erro de excluir acidentalmente um item da lista durante a iteração é muito menor. Um desses paradigmas é o de geradores de lista. Além disso, entender a operação dos geradores de lista é especialmente útil para evitar esse problema específico, conforme mostrado nesta 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 nº 6: mal entendendo como o Python vincula variáveis ​​nos fechamentos


Considere 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 na verdade você entende o seguinte:

 8 8 8 8 8 

Surpresa!

Isso ocorre devido à ligação tardia no Python, o que significa que os valores das variáveis ​​usadas nos fechamentos são consultados durante a chamada para a função interna. Assim, no código acima, sempre que qualquer uma das funções retornadas é chamada, o valor de i é pesquisado no escopo circundante durante sua chamada (e nessa época o ciclo já havia sido concluído, portanto, o resultado final já havia sido atribuído - valor 4) .

A solução para esse problema comum do Python seria:

 >>> 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 

Voila! Usamos os argumentos padrão aqui para gerar funções anônimas e obter o comportamento desejado. Alguns chamariam essa solução de elegante. Alguns são
fino. Algumas pessoas odeiam essas coisas. Mas se você é um desenvolvedor de Python, é importante entender.

Erro # 7: criando dependências de módulo cíclico


Suponha que você tenha dois arquivos, a.py e b.py, cada um deles importando o outro, da seguinte maneira:

No a.py:

 import b def f(): return bx print f() 

Em b.py:

 import a x = 1 def g(): print af() 

Primeiro, tente importar a.py:

 >>> import a 1 

Funcionou muito bem. Isso pode surpreendê-lo. Afinal, os módulos se importam ciclicamente e isso provavelmente deve ser um problema, certo?

A resposta é que simplesmente ter importação cíclica de módulos não é, por si só, um problema no Python. Se o módulo já foi importado, o Python é inteligente o suficiente para não tentar importá-lo novamente. No entanto, dependendo do ponto em que cada módulo está tentando acessar funções ou variáveis ​​definidas em outro, você pode realmente ter problemas.

Portanto, voltando ao nosso exemplo, quando importamos o a.py, não houve problemas ao importar o b.py, pois o b.py não exige que nenhum dos a.py seja definido durante a importação. A única referência em b.py a a é uma chamada para af (). Mas essa chamada em g () e nada em a.py ou b.py não chama g (). Então, tudo funciona bem.

Mas o que acontece se tentarmos importar o b.py (sem antes importar o a.py):

 >>> 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' 

Oh oh Isso não é bom! O problema aqui é que, durante o processo de importação do b.py, ele tenta importar o a.py, que por sua vez chama f (), que tenta acessar o bx. Mas o bx ainda não foi definido. Daí a exceção AttributeError.

Pelo menos uma solução para esse problema é bastante trivial. Basta modificar b.py para importar a.py para g ():

 x = 1 def g(): import a # This will be evaluated only when g() is called print af() 

Agora, quando o importamos, tudo está 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 # 8: cruzando nomes com nomes de módulos na biblioteca padrão do Python


Um dos encantos do Python são seus muitos módulos que saem da caixa. Mas, como resultado, se você não seguir conscientemente isso, poderá descobrir que o nome do seu módulo pode ter o mesmo nome que o módulo na biblioteca padrão que acompanha o Python (por exemplo, no seu código, pode haver um módulo com o nome email.py, que entrará em conflito com o módulo de biblioteca padrão com o mesmo nome).

Isso pode levar a problemas sérios. Por exemplo, se algum dos módulos tentar importar a versão do módulo da biblioteca padrão do Python, e você tiver um módulo com o mesmo nome no projeto, que será importado por engano em vez do módulo da biblioteca padrão.

Portanto, deve-se tomar cuidado para não usar os mesmos nomes dos módulos da biblioteca padrão do Python. É muito mais fácil alterar o nome do módulo em seu projeto do que enviar uma solicitação para alterar o nome do módulo na biblioteca padrão e obter aprovação para ele.

Erro # 9: falha em levar em consideração 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, funcionará bem:

 $ python foo.py 1 key error 1 $ python foo.py 2 value error 2 

Mas agora vamos ver como funcionará 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 aconteceu aqui? O "problema" é que, no Python 3, um objeto em um bloco de exceção não está disponível fora dele. (A razão para isso é que, caso contrário, os objetos nesse bloco serão armazenados na memória até que o coletor de lixo seja iniciado e remova as referências a partir daí).

Uma maneira de evitar esse problema é manter a referência ao objeto do bloco de exceção fora desse bloco, para que ele permaneça disponível. Aqui está a versão do exemplo anterior que usa essa técnica, obtendo assim um código adequado para o Python 2 e o 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() 

Execute-o no Python 3:

 $ python3 foo.py 1 key error 1 $ python3 foo.py 2 value error 2 

Viva!

Erro # 10: uso inadequado do método __del__


Digamos que você tenha um arquivo mod.py como este:

 import foo class Bar(object): ... def __del__(self): foo.cleanup(self.myhandle) 

E você está tentando fazer isso de outro another_mod.py:

 import mod mybar = mod.Bar() 

E obtenha um terrível AttributeError.

Porque Como, conforme relatado aqui , quando o intérprete é desligado, todas as variáveis ​​globais do módulo têm o valor Nenhum. Como resultado, no exemplo acima, quando __del__ foi chamado, o nome foo já estava definido como Nenhum.

A solução para esta "tarefa com um asterisco" é usar atexit.register (). Portanto, quando seu programa conclui a execução (ou seja, quando sai normalmente), seus identificadores são excluídos antes que o intérprete conclua seu trabalho.

Com isso em mente, a correção para o código mod.py acima pode ser algo como isto:

 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 simples e confiável de chamar qualquer limpeza necessária após o término normal do programa. Obviamente, a decisão sobre como lidar com o objeto que está associado ao nome self.myhandle é deixada para fo.cleanup, mas acho que você entende a idéia.

Conclusão


Python é uma linguagem poderosa e flexível, com muitos mecanismos e paradigmas que podem melhorar significativamente o desempenho. No entanto, como em qualquer ferramenta ou idioma de software, com uma compreensão ou avaliação limitada de seus recursos, problemas imprevistos podem surgir durante o desenvolvimento.

Uma introdução às nuances do Python abordadas neste artigo ajudará a otimizar o uso da linguagem, evitando alguns erros comuns.

Source: https://habr.com/ru/post/pt466441/


All Articles