Armadilhas comuns e o estilo de questão da Python Institute

TL;DR

As notas 02 e 03 deste galho mapearam os blocos oficiais do syllabus às notas-fonte dos Galhos 1-6. Esta nota faz outra coisa: isola o padrão de pegadinha que se repete de questão em questão na Python Institute, independente de qual bloco ela está testando. Toda prova PCEP/PCAP é dominada por uma pergunta: “o que este código imprime?” — e a Python Institute tem um catálogo relativamente pequeno de armadilhas que reaparecem disfarçadas em sintaxes diferentes: mutação por referência disfarçada de “só li a variável”, escopo LEGB armado pra explodir com UnboundLocalError, precedência de operador que separa bool de int no resultado, slicing com passo negativo, o cache de inteiros pequenos do CPython fazendo is “funcionar por acidente”, o argumento default mutável, += custando O(n²) sem avisar, comparação encadeada, e type() vs isinstance() sob herança. Cada armadilha aqui é nova em relação às notas 02/03 (que já cobriram else de loop, finally/return, find/index, comparação de strings) — o objetivo é fechar o catálogo antes do simulado final da nota 08.

Por que a Python Institute pergunta assim

Questões de múltipla escolha que testam “o que aparece na tela” são baratas de corrigir automaticamente e caras de acertar no chute — exigem simular o interpretador na cabeça, sem rodar nada. É o formato dominante nas provas PCEP-30-02 e PCAP-31-03 (confirmado nas notas 02 e 03 deste galho, syllabus oficial pythoninstitute.org). O padrão se repete porque as armadilhas exploram um número pequeno de comportamentos “não óbvios mas documentados” do Python — a mesma dezena de armadilhas reaparece vestida com nomes de variável diferentes, tipos diferentes, valores numéricos diferentes. Decorar a lista de armadilhas vale mais, ponto a ponto, do que decorar sintaxe: quem já fez os Galhos 1-6 sabe a sintaxe; o que falta é o reflexo de “opa, isso é uma das armadilhas conhecidas”.

flowchart TD
    A["Pegadinha Python Institute"] --> B["Referência e mutação"]
    A --> C["Escopo e nomes"]
    A --> D["Avaliação de expressão"]
    A --> E["Sequências e slicing"]
    A --> F["Identidade vs igualdade"]
    A --> G["Tipos e checagem"]

    B --> B1["mutar lista/dict dentro de função"]
    B --> B2["argumento default mutável"]

    C --> C1["UnboundLocalError sem global"]
    C --> C2["closure tardia em loop"]

    D --> D1["precedência aritmética/lógica/bitwise"]
    D --> D2["+= em string dentro de loop"]
    D --> D3["comparação encadeada 1 < x < 10"]

    E --> E1["lista[::-1] — passo negativo"]
    E --> E2["lista[-3:-1] — dois negativos"]

    F --> F1["is vs == e o cache -5..256"]

    G --> G1["type() vs isinstance() com herança"]

    style A fill:#4a5568,color:#fff

Mutação por referência disfarçada de leitura

O erro mental mais caro em prova: olhar def f(lista): e assumir que, como Python “passa tudo por valor” (mito comum de quem vem de C), a função não pode afetar o chamador. Python passa referências a objetos — se o objeto é mutável (lista, dict, set) e a função chama um método que muta in-place (.append(), .pop(), [chave] = valor), o objeto original muda, porque não existe cópia nenhuma no meio do caminho. Isso já foi explicado em profundidade em Core 02 (mutabilidade) — o ângulo novo aqui é o formato exato como a prova testa isso: uma função que parece “só processar” a lista, sem return, e a pergunta é sobre o estado da variável depois da chamada.

def processa(dados):
    dados.append(99)
    dados = dados + [100]   # reatribuição LOCAL — não afeta o chamador
    dados.append(101)
 
numeros = [1, 2, 3]
processa(numeros)
print(numeros)

O mesmo padrão aparece com dicionários — e a prova gosta de combinar com .get() pra criar uma armadilha dupla:

def atualiza(cache, chave):
    valor = cache.get(chave, 0)
    valor += 1
    cache[chave] = valor
 
contagem = {"a": 1}
atualiza(contagem, "a")
atualiza(contagem, "b")
print(contagem)

Argumento default mutável — a pegadinha clássica, com uma volta a mais

Core 06 já documenta a regra: um valor default é avaliado uma única vez, no momento em que a def é executada (não a cada chamada), e fica gravado no objeto função — se esse default é mutável, toda chamada que não passa o argumento explicitamente compartilha o mesmo objeto. A versão que a prova mais gosta de testar não é o exemplo isolado — é o efeito acumulado ao longo de várias chamadas em sequência, porque é isso que separa quem decorou a regra de quem entende o mecanismo por trás dela:

def historico(item, log=[]):
    log.append(item)
    return log
 
a = historico("x")
b = historico("y")
c = historico("z", log=[])
d = historico("w")
print(a, b, c, d)

Escopo LEGB armado pra explodir

Core 06 já explica a regra LEGB e o UnboundLocalError por reatribuição sem global. O ângulo novo desta nota é o formato de closure tardia dentro de loop — uma armadilha de escopo diferente, que não aparece na nota-fonte porque pertence mais à interseção entre loops e funções do que a escopo básico isolado:

funcoes = []
for i in range(3):
    funcoes.append(lambda: i)
 
resultados = [f() for f in funcoes]
print(resultados)

Precedência de operadores além da tabela decorada

Core 03 já tem a tabela completa de precedência e cobre a mistura de bitwise com comparação. O que a prova gosta de fazer — e que vale destacar isolado — é misturar aritmética com exponenciação e criar a ilusão de associatividade errada, porque ** é o único operador aritmético binário em Python que associa à direita:

print(2 + 3 * 2 ** 2)
print(2 ** 3 ** 2)

A segunda armadilha de precedência mistura operador lógico (and/or, que trabalha com truthiness e devolve um dos operandos) com operador bitwise (&/|, que trabalha bit a bit em inteiros e sempre devolve int ou bool conforme o tipo dos operandos):

a = 6   # 0b110
b = 3   # 0b011
print(a and b)
print(a & b)
print(bool(a) and bool(b))

+= em string dentro de loop

Strings são imutáveis — isso já está em Core 07. O que a prova testa aqui não é o valor final (que costuma ser óbvio), mas o custo e o mecanismo: cada += numa string dentro de um loop não modifica nada in-place, porque não existe “in-place” possível para um str — cria uma string totalmente nova a cada iteração e reatribui o nome pra ela.

resultado = ""
for c in "abc":
    resultado += c.upper()
print(resultado)

Comparação encadeada além do básico

Core 03 já menciona 1 < x < 10 na seção “Em entrevista” como equivalente a 1 < x and x < 10. A armadilha nova aqui é o que acontece quando a cadeia mistura tipos ou quando um dos operandos tem efeito colateral — porque x só é avaliado uma vez, mesmo aparecendo logicamente duas vezes na comparação:

def registra(n):
    print(f"avaliando {n}")
    return n
 
if 1 < registra(5) < 10:
    print("dentro do intervalo")

Slicing com passo negativo em coleções, não só strings

A nota 03 já cobriu s[::-1] e s[-3:-1] para strings. A mesma sintaxe se aplica identicamente a listas e tuplas (todas são sequências no mesmo sentido do data model — ver Collections 01), e a prova gosta de testar o passo negativo combinado com limites explícitos, que é onde a intuição costuma falhar de verdade:

lista = [10, 20, 30, 40, 50]
print(lista[::-1])
print(lista[-3:-1])
print(lista[::-2])
print(lista[4:1:-1])

is vs == — o cache de inteiros pequenos do CPython

Este é o item mais citado em qualquer fórum de preparação pra PCEP/PCAP, e a razão de fundo está em CPython internals, nota 02 (Galho 6) — esta nota não repete a explicação de por que o cache existe (interning de inteiros de -5 a 256, decisão de implementação do CPython, não parte da especificação da linguagem), só o efeito observável que a prova cobra:

a = 100
b = 100
print(a is b)
 
c = 300
d = 300
print(c is d)
 
e, f = 300, 300
print(e is f)

O intervalo exato não é garantia de linguagem

-5 a 256 é o comportamento do CPython especificamente (a implementação de referência, a que a prova assume) — não está na especificação da linguagem Python e pode variar em outras implementações (PyPy, por exemplo, cacheia de forma diferente). A prova testa o comportamento observável do CPython porque é nele que ela roda, mas a lição de fundo — nunca usar is pra comparar valor de int/str/float — vale universalmente, independente do intervalo exato.

type() vs isinstance() sob herança

Território de OO (Galho 3), mas a prova testa como pegadinha de sintaxe isolada, então vale o exemplo aqui: type(obj) == Classe compara o tipo exato, enquanto isinstance(obj, Classe) também aceita subclasses — a diferença só aparece quando existe hierarquia.

class Animal:
    pass
 
class Cachorro(Animal):
    pass
 
rex = Cachorro()
print(type(rex) == Animal)
print(type(rex) == Cachorro)
print(isinstance(rex, Animal))
print(isinstance(rex, Cachorro))

Atributo de classe mutável — a mesma armadilha, um nível acima

O default mutável de função tem uma prima próxima em OO (Galho 3): um atributo mutável definido no corpo da classe (não dentro de __init__) é compartilhado por todas as instâncias, pelo mesmo motivo — ele é avaliado uma única vez, quando a classe é definida, e vive como atributo da classe até que alguma instância o sobrescreva explicitamente.

class Carrinho:
    itens = []   # atributo de CLASSE, não de instância
 
    def adiciona(self, item):
        self.itens.append(item)
 
c1 = Carrinho()
c2 = Carrinho()
c1.adiciona("maçã")
c2.adiciona("pão")
print(c1.itens)
print(c2.itens)

Simulado rápido de aquecimento

Seis perguntas curtas, misturando os padrões desta nota, no estilo “single-choice” da prova real — sem consultar as respostas antes de tentar prever o resultado mentalmente:

1. print(True + True + False)

2. x = [1, 2, 3] y = x y.append(4) print(x)

3. print(10 // 3 ** 2)

4. def f(a, b=[]): b.append(a) return b print(f(1) is f(2))

5. print(list(range(10, 0, -3)))

6. class A: pass class B(A): pass print(issubclass(B, A), issubclass(A, B), isinstance(B(), A))

Desempacotamento com número errado de valores

Collections 02 cobre desempacotamento em profundidade — o ângulo de prova que vale isolar é o que acontece quando a contagem de nomes à esquerda não bate com a de valores à direita, incluindo o uso do operador * pra capturar “o resto”:

a, b, *c = [1, 2, 3, 4, 5]
print(a, b, c)
 
x, *y, z = "abcde"
print(x, y, z)

Mapa de decisão rápido pra revisão

flowchart LR
    Q["Vi uma questão 'o que isso imprime'"] --> R1{"Envolve função<br/>mutando algo?"}
    R1 -->|sim| S1["Checar: mutação in-place<br/>ou reatribuição de nome?"]
    R1 -->|não| R2{"Envolve is/==<br/>com números?"}
    R2 -->|sim| S2["Lembrar: is é identidade,<br/>cache só -5..256"]
    R2 -->|não| R3{"Envolve slicing<br/>com passo?"}
    R3 -->|sim| S3["a:b sempre inclusive-exclusive,<br/>passo negativo inverte leitura de a/b"]
    R3 -->|não| R4{"Envolve operador<br/>ambíguo (and/or/&/|)?"}
    R4 -->|sim| S4["Lógico = truthiness/curto-circuito;<br/>bitwise = bit a bit"]
    R4 -->|não| R5["Simular linha a linha,<br/>sem assumir atalho"]

    style Q fill:#4a5568,color:#fff

Vocabulário PT/EN

Termo PTTermo EN
mutação por referênciamutation by reference
argumento default mutávelmutable default argument
escopo envolventeenclosing scope
captura tardia (de variável)late binding (closure)
associatividade (de operador)associativity
associa à direitaright-associative
operador bitwisebitwise operator
curto-circuito (avaliação)short-circuit evaluation
passo (de slice)step
interning de inteirosinteger interning
cache de inteiros pequenossmall integer cache
identidade de objetoobject identity
checagem de tipotype checking
tipo exatoexact type

Armadilhas comuns

  • Achar que “Python passa por valor” — não passa; passa referência a objeto, e o efeito observável depende só de o objeto ser mutável e de a operação dentro da função ser mutação in-place ou reatribuição de nome local.
  • Confiar no intervalo exato do small int cache (-5 a 256) como se fosse parte da linguagem — é detalhe de implementação do CPython, não garantia da especificação Python.
  • Usar type() == em vez de isinstance() quando existe (ou pode vir a existir) hierarquia de classes — quebra silenciosamente com subclasses.
  • Esquecer que ** associa à direita — é o único operador aritmético binário comum com essa propriedade em Python.
  • Assumir que uma expressão dentro de comparação encadeada (a < f(x) < b) é avaliada duas vezes — é avaliada uma vez só, e isso importa quando f(x) tem efeito colateral.

Em entrevista

  • “Por que def f(lista=[]) é considerado um anti-padrão em Python?” Porque o valor default é avaliado uma única vez, na definição da função, e objetos mutáveis usados como default ficam compartilhados entre todas as chamadas que não passam o argumento — o fix padrão é usar None como sentinela e criar o objeto mutável dentro do corpo da função.
  • “Quando is e == podem divergir para dois inteiros com o mesmo valor?” Fora do intervalo cacheado pelo CPython (-5 a 256), cada literal pode virar um objeto int distinto na memória — == continua True (mesmo valor), mas is pode dar False (identidade diferente), a não ser que o compilador funda os literais por otimização dentro do mesmo bloco de código.

How to explain in English

“A classic Python Institute exam pattern is testing whether a function mutates a shared object or just rebinds a local name — the two look identical at a glance but behave completely differently when the object is passed by reference.”

O que vem a seguir

Com o catálogo de armadilhas fechado, a nota 07 vira pra outro problema: não o que estudar, mas como se preparar e atacar a prova sob tempo — gestão de tempo, ordem de ataque das questões, recursos oficiais de prática.

Veja também

Fontes