CodeGym /Cursos /Python SELF PT /Erros padrões, parte 2

Erros padrões, parte 2

Python SELF PT
Nível 20 , Lição 2
Disponível

8.1 Falta de compreensão das regras de escopo do Python

O escopo em Python é baseado na chamada regra LEGB, que é uma abreviação:

  • Local (nomes, atribuídos de qualquer forma dentro de uma função (def ou lambda), e não declarados globais nessa função);
  • Enclosing (nomes no escopo local de qualquer função que encerra estaticamente (def ou lambda), de dentro para fora);
  • Global (nomes atribuídos no nível superior de um arquivo de módulo, ou pela execução da instrução global em def dentro do arquivo);
  • Built-in (nomes previamente atribuídos no módulo de nomes embutidos: open, range, SyntaxError, e outros).
  • Parece bem simples, né?

    No entanto, existem algumas sutilezas em como isso funciona no Python, que nos leva a um problema complexo ao programar em Python. Vamos considerar 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 um valor a uma variável no escopo, o Python automaticamente a considera local para esse escopo e esconde qualquer variável com nome semelhante em qualquer escopo superior.

    Assim, muitos se surpreendem ao obter um UnboundLocalError em um código que anteriormente funcionava, quando ele é modificado adicionando-se uma atribuição em algum lugar no corpo da função.

    Essa característica é especialmente confusa para desenvolvedores ao usar listas. Considere o seguinte exemplo:

    
    lst = [1, 2, 3]
    def foo1():
        lst.append(5)  # Isso funciona bem...
            
    foo1()
    print(lst)
    [1, 2, 3, 5]
            
    lst = [1, 2, 3]
    def foo2():
        lst += [5]  # ... mas isso não funciona!
            
    foo2()
    Traceback (most recent call last):
        File "
           
       
           
       
           
       
         ", line 1, in 
        
          File " 
         
           ", line 2, in foo UnboundLocalError: local variable 'lst' referenced before assignment 
          
         
       

    Por que foo2 falha enquanto foo1 funciona bem?

    A resposta é a mesma do exemplo anterior, mas é amplamente considerado que a situação aqui é mais sutil. foo1 não usa um operador de atribuição em lst, enquanto foo2 sim. Lembrando que lst += [5] é na verdade apenas uma forma abreviada de lst = lst + [5], vemos que estamos tentando atribuir um valor a lst (portanto, o Python supõe que ele está no escopo local). No entanto, o valor que queremos atribuir a lst é baseado no próprio lst (novamente, agora assumido como estando no escopo local), que ainda não foi definido. E nós obtemos um erro.

    8.2 Alterando uma lista durante a iteração

    Este problema no próximo pedaço 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]  # RUIM: Deletando item de uma lista enquanto itera sobre ela
        
    Traceback (most recent call last):
            File "
           
       
           
       
           
       
         ", line 2, in 
        
          IndexError: list index out of range 
         
       

    Deletar um elemento de uma lista ou array enquanto itera sobre ela é um problema em Python que é bem conhecido por qualquer desenvolvedor de software experiente. Mas, embora o exemplo acima possa ser bastante óbvio, mesmo desenvolvedores experientes podem cair nessa armadilha em um código muito mais complexo.

    Felizmente, o Python inclui várias elegantes paradigmas de programação que, quando usados corretamente, podem levar a uma simplificação e otimização significativas do código. Um efeito colateral agradável disso é que, em um código mais simples, a probabilidade de se deparar com o erro de deletar acidentalmente um elemento da lista enquanto se itera sobre ela é significativamente reduzida.

    Uma dessas paradigmas são as list comprehensions. Além disso, entender como funcionam as list comprehensions é especialmente útil para evitar esse problema específico, como 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)]  # apenas mantendo novos elementos
    print(numbers)
    # [0, 2, 4, 6, 8]

    Importante! Aqui não ocorre atribuição de um novo objeto lista. Usar numbers[:] é uma forma de atribuição coletiva de novos valores a todos os elementos da lista.

    8.3 Falta de compreensão de como o Python vincula variáveis em closures

    Considere o seguinte exemplo:

    
    def create_multipliers():
        return [lambda x: i * x for i in range(5)]  #Retorna uma lista de funções!
    
    for multiplier in create_multipliers():
        print(multiplier(2))

    Você pode esperar a seguinte saída:

    
    0
    2
    4
    6
    8
    

    Mas na realidade você terá o seguinte:

    
    8
    8
    8
    8
    8
    

    Surpresa!

    Isso acontece por causa do late binding no Python, que significa que os valores das variáveis usadas em closures são procurados durante a chamada da função interna.

    Assim, no código acima, sempre que uma das funções retornadas é chamada, o valor de i é procurado no escopo envolvente no momento de sua chamada (e a essa altura o loop já terminou, então i já foi atribuído o resultado final — o valor 4).

    A solução para esse problema comum em Python é a seguinte:

    
    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

    E voilà! Estamos usando aqui argumentos padrão para gerar funções anônimas para obter o comportamento desejado. Alguns chamariam essa solução de elegante. Alguns de sutil. Alguns odeiam esse tipo de coisa. Mas se você é um desenvolvedor Python, é importante entender isso de qualquer forma.

Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION