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:
- list,
[1, 2, 3]— sequência ordenada, mutável, aceita repetição e tipos misturados, cresce e encolhe depois de criada. - tuple,
(1, 2, 3)— sequência ordenada, imutável; por isso pode ser chave de dicionário e elemento de conjunto. - dict,
{"a": 1}— associa chave única a valor; a chave precisa ser imutável, e desde a versão 3.7 a ordem de inserção é preservada. - set,
{1, 2, 3}— coleção sem duplicatas e sem ordem garantida;frozenseté a versão imutável. - str — sequência imutável de caracteres: todo método que parece alterar a string na verdade devolve outra.
- range — sequência imutável de inteiros gerada sob demanda, sem materializar a lista.
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áveis | imutáveis |
|---|---|
list, dict, set, bytearray | tuple, 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:
| escrita | produz |
|---|---|
(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:
| pergunta | Python | |
|---|---|---|
| dinâmica × estática | quando o tipo é verificado | na execução |
| forte × fraca | há conversão implícita entre tipos incompatíveis | não há |
Lista × gerador:
| lista | gerador | |
|---|---|---|
| quando calcula | tudo de uma vez | um item por vez |
| memória | proporcional ao tamanho | constante |
| percurso | quantas vezes quiser | uma só |
índice e len | sim | não |
Nome, objeto e cópia:
| escrita | efeito |
|---|---|
y = x | dois 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ção | verdadeira? |
|---|---|
| impede paralelismo de cálculo entre threads no mesmo processo | sim, no CPython |
| impede concorrência de entrada e saída | não — a thread que espera libera o bloqueio |
é contornado por multiprocessing | sim, porque cada processo tem o seu interpretador |
| existe em toda implementação de Python | não — é do CPython |
Método → papel, nas bibliotecas:
| faz | não faz | |
|---|---|---|
describe() | resumo estatístico das colunas | mostrar linhas |
head() / tail() | primeiras / últimas linhas | estatística |
info() | tipos, não nulos e memória | estatística descritiva |
shape | dimensões | contagem de nulos |
np.dot / @ | produto matricial | produto elemento a elemento |
a * b (numpy) | produto elemento a elemento | produto matricial |
poly1d | polinômio de uma variável | array multidimensional |
Series | uma dimensão, rotulada | tabela |
DataFrame | tabela de linhas e colunas | vetor 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ência | de 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 dict | a partir do Python 3.7 (detalhe de implementação no 3.6) |
| f-strings | Python 3.6 |
| anotações de tipo | Python 3.5 (typing) |
operador de matriz @ | Python 3.5 |
| fim do suporte ao Python 2 | 2020 |
| threads que executam bytecode por vez no CPython | 1, por causa do GIL |
| superclasses diretas permitidas | ilimitadas (herança múltipla) |
len de um dicionário | número de chaves |
True e False em conta aritmética | valem 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.
LidoPraticado