← tópicos

Específicos · Engenharia de Software

Python: tipagem, estruturas, comprehensions, decorators, geradores, GIL

Em Python o nome é uma etiqueta colada em um objeto — e quase todo item com listagem de código se decide por saber se o objeto é mutável e por quantos nomes está sendo apontado.

Altíssima33 itens no tópico

A ideia que organiza o assunto

Em Python não existe variável no sentido de caixa que guarda um valor. Existe objeto, que vive na memória e carrega o próprio tipo, e existe nome, que é apenas uma etiqueta colada nesse objeto. A atribuição não copia nada: cola outra etiqueta. É por isso que y = x sobre uma lista não produz duas listas, e é por isso que y += [60, 50, 40] altera também o que x enxerga — o += sobre lista modifica o objeto no lugar, e o objeto é um só.

Dessa ideia decorre quase tudo o que a prova cobra. Tipagem dinâmica: como o tipo mora no objeto e não no nome, a verificação só pode acontecer na execução, e a mesma variável recebe hoje um inteiro e amanhã uma string sem declaração nenhuma. Mutável contra imutável: a pergunta não é se o nome pode ser reapontado — sempre pode — e sim se o objeto apontado admite alteração interna. Passagem de argumento: a função recebe outra etiqueta para o mesmo objeto, de modo que alterar a lista lá dentro altera a do chamador, mas reatribuir o parâmetro não.

A segunda ideia é de método, não de linguagem, e vale para a maior parte dos itens deste tópico: eles trazem uma listagem e anunciam o resultado. O resultado anunciado é sempre plausível — tem o formato certo, a quantidade certa de linhas, números na ordem de grandeza certa. Execute o código no papel, escreva a sua saída e só então compare. Se você ler primeiro o resultado anunciado, ele organiza a sua leitura do código e você confirma o que já leu.

E quando a listagem não decide, o que decide é etiqueta de biblioteca: o item descreve corretamente uma capacidade e a pendura no nome errado — describe fazendo o trabalho de head, poly1d fazendo o trabalho de array. Leia a descrição, escolha você o nome, confira o nome por último.

Por que se usa (e o que custa)

Ganha-se densidade. O mesmo programa sai em menos linhas porque a linguagem já traz lista, dicionário, conjunto e tupla como tipos nativos, com sintaxe literal para cada um, e porque a biblioteca padrão cobre arquivo, rede, JSON, expressão regular, data e teste sem dependência externa. Ganha-se também a ausência de compilação separada e de declaração de tipo, que encurta o ciclo de escrever e executar. E ganha-se o ecossistema científico — numpy, Pandas, scikit-learn, TensorFlow —, que é a razão de Python aparecer em edital de análise de dados.

Paga-se em desempenho e em previsibilidade. O interpretador executa bytecode sem compilação prévia para código nativo, e a tipagem dinâmica obriga a consultar o tipo do objeto a cada operação. Erro de tipo que outra linguagem apontaria na compilação só aparece quando aquela linha for executada — o que transfere para o teste automatizado uma responsabilidade que em Java é do compilador.

E paga-se sobretudo no GIL, o bloqueio global do interpretador. Na implementação de referência, o CPython, apenas uma thread executa bytecode por vez dentro do processo, por mais núcleos que a máquina tenha. Isso não impede concorrência de entrada e saída — a thread que espera disco ou rede libera o bloqueio, e o programa progride —, mas impede paralelismo real de cálculo. A saída padrão é trocar thread por processo, com o módulo multiprocessing, e é por isso que numpy, que executa em código nativo compilado fora do interpretador, escapa da limitação.

A formulação honesta para a prova: Python troca velocidade de execução e verificação antecipada por velocidade de escrita. O GIL é o preço concreto dessa troca no CPython.

Como funciona

Tipagem. Python é dinâmica e forte. Dinâmica porque o vínculo entre nome e tipo se estabelece na execução; forte porque não há conversão implícita entre tipos incompatíveis — 1 + "2" levanta TypeError em vez de inventar um resultado. As duas perguntas são independentes e a banca as embaralha: dinâmica contra estática responde quando se verifica; forte contra fraca responde se há conversão silenciosa. As anotações de tipo introduzidas a partir do Python 3.5 não mudam nada disso: são documentação lida por ferramentas externas, e o interpretador não as verifica.

As estruturas nativas, cada uma com uma frase que não serve para nenhuma outra:

O módulo collections acrescenta os especializados que o edital cobra: deque, fila de duas pontas com inserção e remoção em tempo constante nas duas extremidades; Counter, que conta ocorrências; defaultdict, que cria a chave ausente; e OrderedDict, anterior à garantia de ordem do dicionário comum.

Comprehensions. São a forma idiomática de construir coleção a partir de outra em uma expressão, na ordem o que produzir, de onde vem, sob que condição:

quadrados = [n * n for n in range(10) if n % 2 == 0]
por_nome   = {p.nome: p.idade for p in pessoas}
distintos  = {p.setor for p in pessoas}

O delimitador decide o tipo do resultado: colchete produz lista, chave com dois-pontos produz dicionário, chave sem dois-pontos produz conjunto. E parêntese não produz tupla — produz um gerador, que é outra coisa.

Geradores. Uma função que contém yield não devolve valor: devolve um objeto gerador. A cada chamada de next, o corpo executa até o próximo yield, entrega o valor e congela o próprio estado — variáveis locais e ponto de parada — até a chamada seguinte. Daí a propriedade que importa: o gerador é preguiçoso e produz um item por vez, consumindo memória constante mesmo sobre sequência infinita, enquanto a lista materializa todos os elementos de uma vez. Em compensação, ele é de passagem única — percorrido até o fim, esgota-se — e não aceita índice nem len.

Decoradores. São açúcar sintático para uma substituição de função. Escrever

@registra
def processa(x): ...

é exatamente escrever processa = registra(processa). O decorador recebe a função original, devolve outra função no lugar dela e normalmente envolve a chamada com algo acrescentado antes e depois — registro em log, medição de tempo, verificação de permissão, cache. É o mecanismo por trás de @property, @staticmethod, @classmethod e das anotações de rota dos frameworks web. O ponto que decide item: o decorador atua no momento da definição, não no da chamada.

Orientação a objetos. Tudo em Python é objeto, inclusive função, classe e módulo. class define a classe, a herança vem entre parênteses no cabeçalho e herança múltipla é permitida, com a ordem de resolução de métodos decidindo qual implementação vale. O polimorfismo não exige superclasse comum nem interface declarada: basta o objeto responder ao método chamado. Encapsulamento é por convenção — um sublinhado sinaliza uso interno, dois sublinhados provocam alteração do nome do atributo, e nada disso é proibição imposta pelo interpretador.

Sintaxe que a prova cobra diretamente. O bloco é delimitado por endentação, não por chaves — os dois-pontos abrem a estrutura e o recuo diz o que pertence a ela. Chaves em Python constroem dicionário ou conjunto. A substituição de valores dentro de texto exige mecanismo explícito: prefixo f na string ou chamada a .format; sem um dos dois, {nome} é texto literal. str.join é método do separador, não da lista. E break, continue e pass não são intercambiáveis: encerrar o laço, pular para a próxima repetição e não fazer nada são três coisas.

As bibliotecas do edital. numpy oferece o ndarray, array homogêneo de n dimensões, com operações vetorizadas elemento a elemento, métodos de agregação estatística embutidos e broadcasting, que alinha formas compatíveis para operar arrays de dimensões diferentes sem copiar dados. Pandas oferece a Series, unidimensional e rotulada, e o DataFrame, tabela de linhas e colunas, com leitura de CSV, Excel, JSON e SQL, agrupamento, junção e tratamento de ausentes.

O que decide os itens

Mutável × imutável — a distinção que decide mais itens de estrutura de dados:

mutáveisimutáveis
list, dict, set, bytearraytuple, range, str, frozenset, bytes, numéricos

Só o objeto imutável pode ser chave de dicionário ou elemento de conjunto.

Os delimitadores, por tipo produzido:

escritaproduz
(1, 2)tupla
[1, 2]lista
{1, 2}conjunto
{"a": 1}dicionário
[x for x in s]lista
{x for x in s}conjunto
{k: v for ...}dicionário
(x for x in s)gerador, não tupla

Tipagem, nos dois eixos — Python é dinâmica e forte:

perguntaPython
dinâmica × estáticaquando o tipo é verificadona execução
forte × fracahá conversão implícita entre tipos incompatíveisnão há

Lista × gerador:

listagerador
quando calculatudo de uma vezum item por vez
memóriaproporcional ao tamanhoconstante
percursoquantas vezes quiseruma só
índice e lensimnão

Nome, objeto e cópia:

escritaefeito
y = xdois nomes, um objeto
y += [ ... ] (lista)altera o objeto; os dois nomes veem a mudança
y = y + [ ... ]cria objeto novo; desfaz o compartilhamento
y = x.copy() / x[:]cópia rasa: o contêiner é novo, os elementos são os mesmos
copy.deepcopy(x)cópia profunda

O GIL, em uma linha cada:

afirmaçãoverdadeira?
impede paralelismo de cálculo entre threads no mesmo processosim, no CPython
impede concorrência de entrada e saídanão — a thread que espera libera o bloqueio
é contornado por multiprocessingsim, porque cada processo tem o seu interpretador
existe em toda implementação de Pythonnão — é do CPython

Método → papel, nas bibliotecas:

faznão faz
describe()resumo estatístico das colunasmostrar linhas
head() / tail()primeiras / últimas linhasestatística
info()tipos, não nulos e memóriaestatística descritiva
shapedimensõescontagem de nulos
np.dot / @produto matricialproduto elemento a elemento
a * b (numpy)produto elemento a elementoproduto matricial
poly1dpolinômio de uma variávelarray multidimensional
Seriesuma dimensão, rotuladatabela
DataFrametabela de linhas e colunasvetor simples

Nos itens com listagem de código, o que decide quase sempre é uma destas seis coisas: aliasing e += sobre lista; ordem de inserção do dicionário na saída; {x} sem prefixo f (é texto literal, não substituição); a saída de um método herdado que o item esquece de mostrar; int * str, que é repetição e não erro; e dict.fromkeys, que remove duplicata sem ordenar.

Números que caem

índices de sequênciade 0 a len − 1; negativos contam do fim, −1 é o último
fatia a[i:j]inclui i, exclui j
range(n)de 0 a n − 1, n elementos
ordem de inserção garantida no dicta partir do Python 3.7 (detalhe de implementação no 3.6)
f-stringsPython 3.6
anotações de tipoPython 3.5 (typing)
operador de matriz @Python 3.5
fim do suporte ao Python 22020
threads que executam bytecode por vez no CPython1, por causa do GIL
superclasses diretas permitidasilimitadas (herança múltipla)
len de um dicionárionúmero de chaves
True e False em conta aritméticavalem 1 e 0
divisão / × /// devolve float sempre; // é divisão inteira para baixo

Como a CEBRASPE derruba você aqui

Os 30 itens do tópico estão explicados. Sobre os 18 de gabarito Errado: troca de termo 44% (8 itens), inversão 22% (4), número errado 17% (3), e escopo ampliado, atribuição errada e generalização com 6% cada. Doze itens são Certo.

Dois avisos sobre o recorte, antes das famílias. Primeiro: o título do tópico promete mais do que a prova cobra. Entre os 30 itens medidos não há nenhum de comprehension, nenhum de gerador ou yield, nenhum de decorador, nenhum de lambda e nenhum de GIL. Tipagem aparece uma única vez, e num item simples e Certo. O que a prova cobra de fato são estruturas nativas e, muito, as bibliotecas de dados: 9 dos 30 itens (30%) são numpy ou Pandas — describe, shape, poly1d, ndarray, broadcasting, np.dot, DataFrame, Series. Se você tiver uma noite, gaste-a na tabela de métodos das bibliotecas e em mutável × imutável, não em comprehension.

Segundo, e é o achado central: em Python o erro quase nunca é uma definição, é uma saída. Dezoito dos 30 itens trazem uma listagem de código, e dez dos dezoito itens errados consistem exatamente nisto — o código está correto, o resultado anunciado é plausível e é falso por um detalhe. A taxonomia espalha esses dez por três rótulos diferentes (troca de termo, inversão, número errado), mas para quem resolve a prova é uma única habilidade: executar o trecho no papel, escrever a sua saída e só então olhar a que o item oferece. Ler primeiro o resultado anunciado é entregar o item.

A saída anunciada, trocada — 44%, a família dominante. Dos oito itens, cinco são saída falsa de listagem. Duas vezes a mesma causa, que é o mecanismo mais repetido do tópico inteiro: chave sem f e sem .format é texto literal. print(“{i}”,j) dentro de um enumerate imprime {i} abacaxi, e o item anuncia 0 abacaxi; num par de print, o segundo é escrito sem .format e o item simplesmente repete a saída do primeiro, “Meu nome é Pedro, meu instagram é @pedro”. As outras três: dict.fromkeys(['I','N','P','I']) devolve ['I', 'N', 'P'] e o item anuncia ['I', 'I', 'N', 'P'] — duplicata mantida e ainda ordenada, duas coisas que a construção não faz; type sobre tupla e lista devolve tuple e list e o item anuncia <class 'list'> e <class 'array'>, invertendo a ordem e inventando um tipo que não existe no Python nativo; e um objeto que chama dois métodos, um deles herdado, com o item mostrando só a saída do segundo (“Meu nome é Garfield.”) e omitindo o “Tenho quatro patas” que vem antes. Os três itens restantes dessa família trocam termo no sentido literal: describe() que “retorna as linhas superiores e inferiores do DataFrame” (isso é head e tail); “os blocos de código são separados por chaves”, que é regra de C e Java e não de Python; e break, continue e pass declarados “sinônimos” dentro do while. Defesa: nas listagens, escreva a sua saída primeiro — e confira quantas chamadas o código faz antes de aceitar quantas linhas a saída tem.

O erro anunciado que não acontece — 22%. Metade das inversões é isto, e vale isolar porque a leitura apressada aceita a falha sem conferir. “A execução do código Python precedente gerará o seguinte erro: TypeError: unsupported operand type(s) for *: ‘int’ and ‘str’” — não gera: inteiro vezes string é repetição de sequência; o TypeError existe de verdade, mas na soma, e é essa vizinhança que torna a mentira plausível. “O código em Python a seguir apresentará um erro de execução” sobre complex(10,7) + (6+3j) — não apresenta: complexo é tipo nativo e j é o literal imaginário. As outras duas inversões viram obrigatório em opcional e dinâmico em fixo: “não é necessário que a estrutura if dentro da estrutura for esteja endentada” (em Python a endentação é sintaxe, não estilo — sem ela, IndentationError); e “a quantidade de objetos que terão essas listas só poderá ser alterada durante a criação delas”, que descreve array de tamanho fixo de outra linguagem, não a lista de Python. Defesa: item que anuncia erro é afirmação de comportamento como qualquer outra — execute antes de aceitar; e tudo que em C é opcional por ser estético costuma ser obrigatório em Python por ser sintático.

O número dentro da saída — 17%, e os três são de numpy e Pandas. Um describe() sobre uma Series de três elementos anunciado com “count 4.000000” e média 38, quando o count é 3 e a média é 37 — os rótulos x, y, z do índice apenas nomeiam posições, não acrescentam elemento, e um count inflado contamina média, desvio e quartis de uma vez. Um array estruturado com dtype=[('x','i4'), ('y','i4')] cuja forma é anunciada como (3, 2), quando a.shape devolve (3,): campos nomeados não são uma segunda dimensão. E y += [60, 50, 40] sobre uma lista compartilhada com x, anunciado como [10, 20, 30] e [60, 60, 60], quando os dois nomes apontam para o mesmo objeto estendido. Defesa: em saída de describe, confira o count antes de qualquer outra linha; em shape, conte dimensões e não colunas lógicas; e, diante de += sobre lista, pergunte quantos nomes apontam para o objeto.

Os três rótulos de 6%, um item cada. Propriedade esticada até onde não vale: “três tipos de sequências — list, tuple e range —, que são imutáveis” acerta em dois e erra exatamente no primeiro; diante de enumeração seguida de uma propriedade comum, teste a propriedade em cada elemento. Capacidade pendurada no nome errado: “A classe numpy.poly1d() permite a criação de arrays multidimensionais” — poly e 1d estão no nome dizendo o que a classe faz; quem cria array é np.array. Negação absoluta: “A execução do código Python a seguir não terá qualquer tipo de parada” num trecho que contém breakpoint(), que existe exatamente para parar; basta um contraexemplo, e ele está numa linha só.

Erros clássicos

Achar que y = x copia a lista. Copia a etiqueta. Para obter outro objeto é preciso x.copy(), x[:] ou list(x) — e ainda assim a cópia é rasa, com os mesmos elementos internos.

Confundir tipagem dinâmica com tipagem fraca. Python é dinâmica e forte: não declara tipo, mas também não converte string em número por conta própria.

Achar que tupla imutável significa conteúdo imutável. A tupla não admite trocar, acrescentar ou remover elementos, mas se um dos elementos for uma lista, essa lista continua alterável.

Tratar (x for x in s) como tupla. Parêntese em comprehension produz gerador. Tupla a partir de comprehension exige tuple(...).

Esperar ordem do set. Conjunto garante ausência de duplicatas, não ordem. Quem preserva a ordem de inserção é o dicionário, desde a versão 3.7.

Achar que o GIL impede qualquer concorrência. Ele impede paralelismo de cálculo entre threads do mesmo processo. Espera por rede, disco e banco progride normalmente, e cálculo paralelo se obtém com multiprocessing ou com bibliotecas nativas como o numpy.

Escrever {nome} esperando substituição. Sem o prefixo f ou sem .format, a chave é caractere literal e sai impressa como está.

Chamar join na lista. É separador.join(lista), método da string separadora — e ela só aceita elementos do tipo str.

Confundir np.dot com * no numpy. O asterisco é elemento a elemento; produto matricial é np.dot, np.matmul ou @.

Achar que Python só é orientado a objetos. É multiparadigma: aceita código procedural e funcional na mesma base, e nada obriga a criar classe.

Praticar30 itens