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 (defoulambda), e não declarados globais nessa função); Enclosing(nomes no escopo local de qualquer função que encerra estaticamente (defoulambda), de dentro para fora);Global(nomes atribuídos no nível superior de um arquivo de módulo, ou pela execução da instruçãoglobalemdefdentro 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.
GO TO FULL VERSION