Python WAT

por Flávio Juvenal

  • Consultor e Sócio em Vinta Software: vinta.com.br
  • 6 anos de experiência em Python
  • Boas práticas!
  • Django, JavaScript, React, etc

Estes slides sabem interpretar Python!

In [1]:
l = [1,2,3]
l[1:]
Out[1]:
[2, 3]

Sobrescrevendo a Biblioteca Padrão

você sabe o que acontece quando você cria uma variável com um nome igual a um built-in?

In [5]:
sum([1,2,3])
Out[5]:
6
In [6]:
sum = 1 + 2  # SyntaxError?
sum([1,2,3])
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-6-426f9aa1ca06> in <module>()
      1 sum = 1 + 2  # SyntaxError?
----> 2 sum([1,2,3])

TypeError: 'int' object is not callable

Use algum editor com syntax-highlighting para saber quando está sobrescrevendo um built-in! Quando precisar dar a uma variável um nome built-in, coloque underline no fim, como em sum_.

Identidade

você sabe quando um objeto em Python é (is) outro?

In [7]:
a = 256
b = 256
a is b
Out[7]:
True
In [8]:
a = 257
b = 257
a is b
Out[8]:
False

Python tem uma cache de inteiros de [-5, 256], o que faz com que a e b sejam referências para o mesmo objeto. Como ints em Python são imutáveis, essa cache não introduz nenhum bug.

Note que isto é um detalhe da implementação CPython e pode ser diferente em outras implementações. [1]

In [9]:
!cat int_example.py
a = 257
b = 257
print(a is b)
In [10]:
!python int_example.py
True

Python faz otimizações com as literais que ocorrem em um mesmo arquivo. Se quisermos comparar valor, devemos sempre usar ==
[2]

In [11]:
me = {'name': 'Fulano', 'age': 30}
people = [me] * 3
people
Out[11]:
[{'age': 30, 'name': 'Fulano'},
 {'age': 30, 'name': 'Fulano'},
 {'age': 30, 'name': 'Fulano'}]
In [12]:
people[0]['name'] = 'Sicrano'
people
Out[12]:
[{'age': 30, 'name': 'Sicrano'},
 {'age': 30, 'name': 'Sicrano'},
 {'age': 30, 'name': 'Sicrano'}]

[me] * 3 é equivalente a [me, me, me], ou seja, 3 referências para o mesmo objeto.

In [13]:
line = [' '] * 3
line
Out[13]:
[' ', ' ', ' ']
In [14]:
game_table = [line] * 3
game_table
Out[14]:
[[' ', ' ', ' '], [' ', ' ', ' '], [' ', ' ', ' ']]
In [15]:
def print_game_table():
    for l in game_table:
        print(l)
print_game_table()
[' ', ' ', ' ']
[' ', ' ', ' ']
[' ', ' ', ' ']
In [16]:
game_table[2][0] = 'X'
print_game_table()
['X', ' ', ' ']
['X', ' ', ' ']
['X', ' ', ' ']

In [17]:
game_table[0] is game_table[1] and \
game_table[1] is game_table[2]
Out[17]:
True
In [18]:
id(game_table[0]) == id(game_table[1]) == id(game_table[2])
Out[18]:
True

id representa a identidade do objeto. Se duas variáveis tem o mesmo id, elas representam o mesmo objeto.
Em CPython, id é o endereço do objeto na memória. [3]

In [19]:
# Nice 🙂
game_table = [[' ' for j in range(3)] for i in range(3)]
print_game_table()
[' ', ' ', ' ']
[' ', ' ', ' ']
[' ', ' ', ' ']
In [20]:
# Nice 🙂
game_table[2][0] = 'O'
print_game_table()
[' ', ' ', ' ']
[' ', ' ', ' ']
['O', ' ', ' ']
In [21]:
game_table = [[' '] * 3 for i in range(3)]
print_game_table()
[' ', ' ', ' ']
[' ', ' ', ' ']
[' ', ' ', ' ']
In [22]:
game_table = [[{}] * 3 for i in range(3)]
game_table[0][0]['name'] = 'Fulano'
print_game_table()
[{'name': 'Fulano'}, {'name': 'Fulano'}, {'name': 'Fulano'}]
[{}, {}, {}]
[{}, {}, {}]

Resumindo:

  • cuidado ao atribuir variáveis, atribuição != cópia
  • cuidado ao multiplicar listas, multiplicação de listas != cópia
  • use id para ver qual objeto uma variável representa
  • use is para ver se duas variáveis representam o mesmo objeto
  • para tabelas, faça dois fors

Atribuição em tuplas

tuplas são imutáveis, mas tem alguma maneira de mudar seus elementos?

In [23]:
a = ([42],)
a[0] += [43]
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-23-a335abb8a98e> in <module>()
      1 a = ([42],)
----> 2 a[0] += [43]

TypeError: 'tuple' object does not support item assignment
In [24]:
a
Out[24]:
([42, 43],)

+= é operador de adição in-place. Ele faz duas coisas:

  • ele chama obj.__iadd__(rhs), que pode modificar o objeto caso ele seja mutável.
  • ele atribui o retorno de obj.__iadd__(rhs) para a variável.

Apenas esta segunda ação falha no a[0] += [43] quando a é uma tupla. A primeira é feita! E, para listas, a primeira ação modifica a lista. [8] [9]

In [25]:
l = [42]
l + [43]
l
Out[25]:
[42]

Não confunda! O + não tem comportamento in-place. Ele retorna um novo objeto (pelo menos para os tipos built-in do Python).

Variáveis de classe

você está usando variáveis de classe corretamente?

In [26]:
class Dog:
    tricks = []

    def __init__(self, name):
        self.name = name
    
    def add_trick(self, trick):
        self.tricks.append(trick)

    def print_tricks(self):
        print(self.name, ' tricks:')
        for trick in self.tricks:
            print(trick)
In [27]:
teddy = Dog("Teddy")
teddy.add_trick("wat")
teddy.print_tricks()
Teddy  tricks:
wat
In [28]:
kika = Dog("Kika")
kika.add_trick("pray")
kika.print_tricks()
Kika  tricks:
wat
pray

In [29]:
teddy.print_tricks()
Teddy  tricks:
wat
pray

In [30]:
# Nice 🙂
class Dog:
    def __init__(self, name):
        self.tricks = []
        self.name = name

    # ...

Cuidado com a diferença entre variáveis de classe e de instância. [10]

Exemplo de bug real: [11]

In [31]:
class A:
    x = 1

class B(A):
    pass

class C(A):
    pass

print(A.x, B.x, C.x)
1 1 1
In [32]:
B.x = 2
print(A.x, B.x, C.x)
1 2 1
In [33]:
A.x = 3
print(A.x, B.x, C.x)
3 2 3

Quando um atributo ou método não é encontrado em uma classe, ele é procurado nas suas classes mães, seguindo o MRO (Method Resolution Order).

In [34]:
class Homer:
    x = 1

class Pikachu(Homer):
    pass

homer_instance = Homer()
homer_instance.x = 2
print("Homer.x", Homer.x)
print("Pikachu.x", Pikachu.x)
Homer.x 1
Pikachu.x 1

Se uma variável é atribuída através da instância, ela é uma variável de instância. Se é atribuída através da classe, ela é uma variável de classe. [12] [13]

Escopo

você entende as regras de escopo de Python?

In [35]:
x = 10
def next_x():
    return x + 1
next_x()
Out[35]:
11
In [36]:
x = 10
def increment():
    x += 1
    return x
increment()
---------------------------------------------------------------------------
UnboundLocalError                         Traceback (most recent call last)
<ipython-input-36-3416d537e798> in <module>()
      3     x += 1
      4     return x
----> 5 increment()

<ipython-input-36-3416d537e798> in increment()
      1 x = 10
      2 def increment():
----> 3     x += 1
      4     return x
      5 increment()

UnboundLocalError: local variable 'x' referenced before assignment

Quando você faz uma atribuição para uma variável em um escopo, essa variável é automaticamente considerada como local nesse escopo e esconde qualquer variável com o mesmo nome no escopo externo. [4]

In [37]:
def counter():
    x = 10
    def increment_aux():
        x += 1
        return x
    return increment_aux()
counter()
---------------------------------------------------------------------------
UnboundLocalError                         Traceback (most recent call last)
<ipython-input-37-991a07a4bcf3> in <module>()
      5         return x
      6     return increment_aux()
----> 7 counter()

<ipython-input-37-991a07a4bcf3> in counter()
      4         x += 1
      5         return x
----> 6     return increment_aux()
      7 counter()

<ipython-input-37-991a07a4bcf3> in increment_aux()
      2     x = 10
      3     def increment_aux():
----> 4         x += 1
      5         return x
      6     return increment_aux()

UnboundLocalError: local variable 'x' referenced before assignment

A solução para não criar uma nova variável local e fazer uma atribuição é usar as keywords global ou nonlocal

In [38]:
# Nice 🙂
x = 10
def increment():
    global x
    x += 1
    return x
increment()
Out[38]:
11
In [39]:
# Nice 🙂
def counter():
    x = 10
    def increment_aux():
        nonlocal x
        x += 1
        return x
    return increment_aux()
counter()
Out[39]:
11

LEGB rule:

  • atribuições em Local, Enclosing, Global, Built-in definem variáveis no seu escopo
  • se uma variável for usada, ela será procurada na ordem LEGB
In [40]:
# Nice 🙂
def heads_or_tails(is_head):
    if is_head:
        coin = 'heads'
    else:
        coin = 'tails'
    print(coin)
heads_or_tails(is_head=False)
tails
In [41]:
print(type)  # built-in
type = 'global'
def generate_fn():
    type = 'enclosing'
    def fn():
        type = 'local'
        return type
    return fn
print(generate_fn()())
<class 'type'>
local
In [42]:
import dis
a = 1
def my_a():
    return a
dis.dis(my_a)
  4           0 LOAD_GLOBAL              0 (a)
              3 RETURN_VALUE
In [43]:
a = 1
def my_a():
    a += 1
    return a

dis.dis(my_a)
  3           0 LOAD_FAST                0 (a)
              3 LOAD_CONST               1 (1)
              6 INPLACE_ADD
              7 STORE_FAST               0 (a)

  4          10 LOAD_FAST                0 (a)
             13 RETURN_VALUE

LOAD_GLOBAL: Loads the global onto the stack.
LOAD_FAST: Pushes a reference to the local onto the stack. [5]

Evaluation de Default Arguments

você sabe quando default arguments são avaliados?

In [44]:
def add_samoieda(dogs=[]):
    dogs.append("samoieda")
    return dogs
In [45]:
add_samoieda(['samoieda'])
Out[45]:
['samoieda', 'samoieda']
In [46]:
print(add_samoieda())
print(add_samoieda())
['samoieda']
['samoieda', 'samoieda']

Default arguments são avaliados no momento da definição da função. Se eles forem mutáveis (como uma lista), vão compartilhar estado durante diferentes chamadas, o que normalmente é indesejado. [6]

In [47]:
add_samoieda.__defaults__
Out[47]:
(['samoieda', 'samoieda'],)
In [48]:
add_samoieda.__defaults__[0].append('ohh')
add_samoieda()
Out[48]:
['samoieda', 'samoieda', 'ohh', 'samoieda']

Late Binding em Closures

closures acessam variáveis como você espera?

In [49]:
def create_multipliers():
    return [lambda x : i * x for i in range(5)]

for multiplier in create_multipliers():
    print(multiplier(2), end=" ")
8 8 8 8 8 

As closures em Python são late-binding. Ou seja, os valores de variáveis são acessados só no momento em que a closure é chamada.

Isso não é exclusivo para lambda, o mesmo ocorre com def [7]

In [50]:
# Nice 🙂
def create_multipliers():
    return [lambda x, i=i : i * x for i in range(5)]

for multiplier in create_multipliers():
    print(multiplier(2), end=" ")
0 2 4 6 8 

A solução é usar o WTF anterior, já que Default Arguments são avaliados no momento de definição!

Dúvidas?

Confuso? Todos Estamos! 😧😧😧