Este é o primeiro curso de Ciência da Computação Universitária: fundamentos de programação
, ministrado com Python 3. Pressupõe nenhuma experiência prévia em programação. Todo programa neste
material é completo e executável — digite-o, execute-o, altere uma linha e previamente
antecipe as alterações antes de executar novamente. Esse ciclo de prever-então-executar é toda a habilidade.
O curso é uma escada:
As unidades 1.1 a 1.3 estabelecem as bases: valores e tipos, seleção e iteração.
A unidade 1.4 organiza o trabalho em funções.
As unidades 1.5 e 1.6 adicionam coleções padrão e arquivos com tratamento de erros.
A unidade 1.7 é a disciplina superior: decompor problemas, testar e depurar.
Como ler cada unidade: uma breve narrativa explica a importância; uma imagem ou regra prática
vem antes da sintaxe; depois as regras precisas; então exemplos resolvidos que ficam mais difíceis,
cada um com um rastreamento verificável executando o código; depois os erros reais
que as pessoas cometem; e por fim um resumo de três linhas. Cada exemplo aqui pode ser verificado de outra forma —
executando-o. Ler código sem executá-lo é como ler partitura musical sem
ouví-la.
1.1
Valores, tipos e entrada/saída
Um aplicativo de compras mostra "4.20" — isso é texto ou número? Se for texto, somar dois
determinados fornecem "4.203.50"; se for um número, fornece 8.7. O que acontece depende
do tipo, e todo o bloco 1.1 aprende a identificar tipos antes de você
executar qualquer coisa.
A value has a type, and the type decides what you can do with it: int (whole
inteiros), float (decimais), str (texto), bool (True/False). O type(...)
ela nunca edita o valor antigo. A imagem mental, importante manter durante todo o curso:
age = 21 # int
price = 4.20 # float
name = "Ada" # str
open_ = True # bool
print(type(age)) # <class 'int'>
Variáveis são nomes vinculados a valores; a atribuição age = 22 re vincula o nome —
nunca edita o valor antigo. A imagem mental, valiosa durante toda a graduação:
Nomes são etiquetas em caixas; expressões leem caixas e criam novas caixas
Ler uma expressão é "abrir as caixas, calcular, fechar uma nova caixa": count * 3.50
com count = 6 abre a caixa int 6 e a caixa float 3.50, e a mesclagem do Python
promove a regra para o int — o resultado é a caixa float21.0, nunca a caixa int 21.
Conversão é explícita: int("21") é 21, mas "4" + 2 é um TypeError — o Python
recusa adivinhar se você quis dizer "42" ou 6. Saída com print(...); entrada com
input(...), que sempre retorna uma string, então números precisam de conversão:
Exemplo resolvido 1 (o caixa do café).count = int(input("Coffees? ")), depois
print("Total:", count * 3.50). Em 6 cafés isso imprime Total: 21.0 — a caixa .0
marca um float, porque int * float promove para float. Observar qual tipo aparece na
saída de uma expressão é todo o hábito da unidade 1.1. Rastreie pelo estado: input retorna
a string "6"; int(...) abre uma nova caixa contendo a caixa int 6; a multiplicação
cria uma nova caixa float21.0; print a exibe.
Exemplo resolvido 2 (o acidente clássico, reparado). Uma impressora de faixas junta uma contagem
a uma mensagem:
Ao executar, o Python levanta TypeError: can only concatenate str (not "int") to str —
a regra recusa adivinhar. O reparo nomeia a conversão explicitamente:
print("Sold " + str(count) + " today"), ou a forma mais limpa print("Vendido", count, "hoje"), which inserts the spaces for you. The lesson to carry: + significa adição
para números, junção para strings, e nunca uma mesclagem silenciosa.
Erros comuns. Esquecer que input() sempre retorna uma string, portanto,
input() * 2 repete texto em vez de duplicar um número. Escrever 4.20 + 6 e
esperar uma resposta do tipo int — qualquer valor flutuante na expressão gera um float. Confiando
0.1 + 0.2 == 0.3 (é False; arredondamento binário — a unidade 1.2 mostra o ajuste).
Resumo: quatro tipos básicos determinam toda operação; nomes são rótulos, atribuição rebinda;
expressões misturam tipos por promoção com floats; conversões são explícitas e
input() requer uma.
Um catraca de parque temático deve cobrar crianças, adultos e idosos diferentemente — um
programa, vários caminhos. Seleção é como um programa escolhe: if, elif,
else executa um bloco apenas quando uma condição é satisfeita.
Idade 70, estudante: o primeiro ramo verdadeiro define o preço
A condição é qualquer expressão booleana; comparações (==, !=, <, >=) e
lógica (and, or, not) a constroem. Duas regras tornam cada decisão legível:
Ramos são testados de cima para baixo; o primeiro ramo verdadeiro vence e os demais são
ignorados.
Recuo não é decoração — ele É o bloco. Um espaço extra altera quais
linhas pertencem ao ramo.
temp = 37.5
if temp >= 38:
print("Fever")
elif temp >= 36 and temp < 38:
print("Normal")
else:
print("Low")
Exemplo resolvido 1 (preço do cinema). Para 12 ou 65+ paga-se 5, estudantes pagam 7,
todos os outros 10:
age = int(input("Age? "))
student = input("Student? y/n ") == "y"
if age < 12 or age >= 65:
price = 5
elif student:
price = 7
else:
price = 10
print("Pay", price)
A ordem importa: o primeiro ramo verdadeiro é escolhido, razão pela qual a verificação de idade precede a
verificação do estudante — um estudante de 70 anos deve pagar 5, e essa ordem o estabelece. Execute
com 70 e y para ver o caminho mais à esquerda do fluxograma ser selecionado.
Exemplo resolvido 2 (faixas de pontuação, escalonado). A mesma estrutura, agora com uma cadeia
na qual cada elif requer apenas uma comparação, pois os testes anteriores já falharam:
falhou:
score = int(input("Score? "))
if score >= 80:
band = "A"
elif score >= 70:
band = "B"
elif score >= 60:
band = "C"
else:
band = "F"
print("Band:", band)
notar: o 79 não precisa também de Band: B — chegar a essa
note: o elif score >= 70 também não precisa do and score < 80 — chegar àquela
linha já prova que a pontuação está abaixo de 80. Confiar no fluxo contínuo assim é
o que torna as cadeias curtas; retestar tudo é a versão mais longa e errada do iniciante.
version.
Comparações de ponto flutuante exigem cuidado: 0.1 + 0.2 == 0.3 é False (binário
abs(x - y) < 1e-9 com uma condição que nunca será alcançada porque um ramo anterior já a capturou.
Erros comuns. Escrever = onde se pretende == — o erro clássico, e Python
recusa isso em condições (SyntaxError) justamente porque é tão comum. Testar
o caso especial vem depois do geral, então o geral sempre prevalece. Usando
elif com uma condição que nunca é alcançada porque um ramo anterior já
a capturou.
Resumo: condições escolhem caminhos, primeiro-verdadeiro-vence, indentação define o bloco; ordem
testes de específico para geral; floats comparam por tolerância, nunca por ==.
1.3
Iteração: fazendo novamente
Pagar faturas 5,000 manualmente é uma carreira; com um laço, é um programa. Iteração
repete trabalho: um laço for percorre uma sequência conhecida; um laço while executa enquanto uma
condição se mantém. A questão de design é sempre: você sabe quantas vezes vai repetir, ou uma
condição decide?
total = 0
for i in range(1, 101): # 1..100 — range stops BEFORE its end
total += i
print(total) # 5050
Use for quando a contagem de repetições for conhecida; use while quando uma condição decidir. Todo
laço while precisa de três elementos visíveis no código: o valor inicial da variável, a condição,
e algo dentro que leve à saída — caso contrário, é um laço infinito.
Laço
Tabela de rastreamento para o laço de fatorial
n = 5
factorial = 1
while n > 1:
factorial *= n
n -= 1 # the step toward exiting
print(factorial) # 120
A tabela de rastreamento é o hábito de verificação do curso para laços: siga as
variáveis rodada por rodada, primeiro no papel. Para o laço de fatorial:
(n=5, f=1) → (4, 5) → (3, 20) → (2, 60) → (1, 120). Construa a tabela manualmente, depois
adicione um print(n, factorial) dentro do laço e deixe o Python corrigir.
Exemplo resolvido 2 (uma busca com break, escalonado). Encontre o primeiro múltiplo de
7 que seja maior que 100 — a contagem de repetições é desconhecida, a condição decide:
n = 100
while True:
if n % 7 == 0:
break # found it — leave the loop now
n += 1
print(n) # 105
O rastreamento é curto: 100, 101, …, 104 não passam no teste; 105 passa e break
sai. break deixa o laço cedo; continue pula para a próxima rodada. Ambos são
ferramentas de legibilidade — um laço que deve percorrer até encontrar algo, depois parar, lê-se
melhor com break. A escalada seguinte, dobrando dinheiro: a 5% ao ano, quanto tempo
o 1000 leva para chegar ao 2000?
balance = 1000.0
years = 0
while balance < 2000.0: # the condition itself is the goal
balance *= 1.05
years += 1
print(years, round(balance, 2)) # 15 2078.93
Quinze anos, e o loop para no momento em que a condição inverte — a tabela de traçagem
mostraria o saldo cruzando a linha no 2078.93, sem pousar exatamente no 2000.
Erros comuns. Editar a lista (unidade 1.5) ou a variável do loop que você está
for-percorrendo. range(1, 100) quando deveria ser 100 — o final é excluído.
Um while cuja etapa de saída esteja ausente ou comentada: a condição nunca se inverte,
e o programa congela.
Resumo: for para contagens de viagem conhecidas, while para condições; todo loop mostra início,
condição e passo; trace no papel, depois imprima na nota; break e continue para
saídas antecipadas limpas.
O mesmo cálculo de preço aparece em cinco locais; a regra do imposto muda; quatro dos
cinco são atualizados. Uma função empacota um trabalho atrás de um nome para que seja escrita
uma vez e confiada em todos os lugares: parâmetros entram, um valor de retorno sai. def
define; chamar executa.
def total_price(count, unit=3.50):
"""Return the total for count items at unit price."""
return count * unit
print(total_price(6)) # 21.0
print(total_price(6, unit=4.0)) # 24.0
A definição fornece um parâmetro (count, unit); a chamada fornece um
argumento (6). Um valor padrão (unit=3.50) torna um argumento opcional.
return devolve um valor e encerra a chamada; imprimir não é retornar — uma
função sem return retorna None, e o chamador que tenta aritmética sobre ela
recebe um erro de TypeError.
Chamadas aninhadas: o chamador pausa até o valor retornar
Cada chamada possui seu próprio escopo local: os nomes internos são invisíveis
externamente e desaparecem quando a chamada termina. Um rastreamento de chamadas aninhadas lê-se:
chamador pausa → chamado executa → valor de retorno chega ao chamador.
Exemplo resolvido 1 (o pedido com taxa inclusa).
def add_fee(amount):
"""Return amount plus a fixed 3 pence fee."""
return amount + 3
def order_total(unit_pence, quantity):
"""Return the order total: subtotal plus the fixed fee."""
return add_fee(unit_pence * quantity)
print(order_total(250, 4)) # 1003
O contrato no docstring importa: pence inteiros dentro, pence inteiros fora. Rastrear
order_total(250, 4): subtotal 1000, add_fee retorna 1003 — e o quadro
diagrama acima mostra a pausa e a devolução.
Exemplo resolvido 2 (um algoritmo real dentro de uma pequena função, expandido).is_prime(n) retorna True quando nada de 2 até $\sqrt{n}$ divide n:
def is_prime(n):
if n < 2:
return False
d = 2
while d * d <= n: # only test up to the square root
if n % d == 0:
return False # found a divisor — answer known, stop
d += 1
return True
print([p for p in range(2, 30) if is_prime(p)])
# [2, 3, 5, 7, 11, 13, 17, 19, 23, 29]
Dois hábitos de função se encontram aqui: um retorno antecipado de return encerra a chamada assim que a resposta
é conhecida (sem necessidade de variável flag), e o limite do laço d * d <= n vem da
dedução — se $n = a \times b$ com $a \le b$, então $a \le \sqrt{n}$, portanto testar além de $\sqrt{n}$
re-testa divisores já vistos. Execute-o: a lista contém exatamente os primos abaixo de 30.
Erros comuns. Imprimir dentro de uma função em vez de retornar, para que o resultado
não possa ser reutilizado. Esquecer o return em um ramo — esse caminho silenciosamente gera
None. Acessar locais de uma função de fora; eles somem quando a chamada
termina.
Resumo: def uma vez, chamar em qualquer lugar; parâmetros entram, um valor sai via return;
retornos precoces encerram a chamada; variáveis locais nascem e morrem com ela; strings de documentação descrevem o
contrato.
O caixa de um café precisa dos pedidos do dia em ordem, do menu por nome, de uma localização que
não deve se alterar e das bebidas únicas de hoje. Quatro tarefas, quatro recipientes — escolher
entre eles é a decisão de design da unidade 1.5.
Uma lista é uma sequência ordenada e mutável: marks = [72, 85, 61], indexada
de 0, fatiado com marks[1:3]. Um dicionário mapeia chaves para valores:
price = {"flat white": 3.5, "tea": 2.8}, lido e escrito por chave. Uma tupla
é uma sequência fixa (53.1, -1.5) — ideal para coordenadas que não devem mudar. Um
conjunto armazena itens únicos sem ordem: {"a", "b"}.
marks = [72, 85, 61]
marks.append(90) # [72, 85, 61, 90]
best = max(marks) # 90
average = sum(marks) / len(marks) # 77.0
Alienação: dois nomes, uma lista
Atribuição nunca copia: a = [1]; b = a; b.append(2) faz a referenciar [1, 2] — a lista
é compartilhada, os nomes não. Essa alienação são as caixas da unidade 1.1 novamente, agora
com duas etiquetas em uma única caixa, e também explica a próxima regra: mutar uma lista enquanto
percorre seus elementos salta itens — percorra uma cópia (for m in marks[:]) ou construa uma
nova lista.
O .get(key, 0) do dicionário fornece 0 para uma primeira observação — a idioma que
torna a contagem em uma única linha. Verifique manualmente: "tea" aparece três vezes em orders,
e o total impresso confirma isso.
Exemplo resolvido 2 (o best-seller do dia, ampliado). Baseando-se no total,
relate o item mais frequente e sua contagem:
best = max(tally, key=tally.get)
print(best, tally[best]) # tea 3
O argumento key=tally.get instrui max a comparar itens por suas contagens em vez de
ordem alfabética — o mesmo truque key= ordena qualquer coleção por qualquer critério. O
ampliamento é relevante: programas reais encadeiam essas ferramentas (contar, depois classificar) em vez de
reescanear orders duas vezes.
Escolher a coleção é a decisão de design: ordem + duplicatas → lista; consulta por
nome → dicionário; agrupamento fixo → tupla; unicidade → conjunto.
Erros comuns. Acessar ⟨tally[item] += 1⟩ em uma chave que ainda não está no dicionário
(KeyError) — usar ⟨.get⟩ é a correção. Esperar que um conjunto ou dicionário lembre a ordem.
Assumir que ⟨b = a⟩ copia uma lista; ele cria apenas um alias.
Resumo: quatro coleções para quatro tarefas; atribuição compartilha, nunca copia; ⟨.get⟩
torna a contagem segura; ⟨key=⟩ torna o ranking uma única linha.
O café fecha, a luz apaga — e o total da manhã sobrevive apenas se o
programa o tiver gravado em disco. Um arquivo persiste após o programa. with open(path) as f: opens, closes on exit, and reads text; open(path, "w") grava
(e esvazia primeiro — o erro clássico).
with open("log.txt", "w") as f:
f.write("opened\n")
with open("log.txt") as f:
for line in f:
print(line.rstrip()) # rstrip removes the trailing newline
O bloco with é a barreira de segurança: qualquer coisa que aconteça dentro dele, o arquivo é fechado.
A leitura fornece linhas com seu caractere de nova linha, então .rstrip() pertence a todo loop.
Erros são valores que Python levanta como exceções: FileNotFoundError,
ValueError, KeyError, TypeError. try/except captura um tipo nomeado — nunca um
except: genérico que esconde bugs reais.
Exemplo resolvido ⟨1⟩ (arquivo de configuração ausente). Ler um tamanho de texto de um arquivo de
configuração que pode estar faltando, ou pode não conter um número inteiro:
try:
with open("settings.txt") as f:
size = int(f.read().strip())
except FileNotFoundError:
size = 12 # no file yet: a sensible default
except ValueError:
size = 12 # the file holds "big", not a number
print("Text size", size)
Execute três vezes — com ⟨16⟩ no arquivo, sem arquivo, e com ⟨big⟩ no
arquivo — e ele imprimirá um tamanho utilizável cada vez, por três rotas diferentes. Essa é a estrutura de código robusto: uma solução sensível
várias rotas para ele. Trate o erro esperado, onde você pode responder a
ele; deixe o restante falhar com ruído alto.
Exemplo resolvido 2 (um loop de entrada robusto, escalonado). Pergunte até que o usuário digite um
número inteiro:
Alimente-o x depois 4: a primeira passagem levanta, é capturada, imprime a mensagem, e o
while True pergunta novamente; a segunda passagem de int(...) tem sucesso e return sai
do loop e da função em um único movimento. Três unidades combinam aqui — o loop (1.3),
o contrato da função (1.4) e a exceção (1.6) — que é o ponto da
escalação.
Erros comuns.open(path, "w") no caminho errado: o arquivo é criado ou
esvaziado antes que você perceba. Capturar Exception amplamente e continuar com
estado. Esquecendo .rstrip(), assim a saída mostra linhas com espaçamento duplo.
Resumo: with open(...) para leitura e escrita; "w" esvazia primeiro;
exceções são valores — capture o tipo nomeado que você espera, recorra com sensatez, deixe
o restante falhar.
Dois equipes escrevem o mesmo aplicativo; uma entrega em uma semana, outra afoga por um mês. A
diferença não é velocidade de digitação — é que o código da equipe rápida é construído a partir de pequenos
pedaços que cada um pode provar corretos enquanto a equipe lenta depura um aplicativo inteiro de uma vez.
Unit Test 1.7 é essa disciplina.
Decomposição divide um problema em funções de uma tarefa cada — cada pequena
suficiente para testar isoladamente. Teste compara a saída real de uma função com a
esperada, nas fronteiras: zero, um, vazio, negativo, enorme.
O ciclo guiado por testes: vermelho, verde, refatoração
Um ⟨assert⟩ que nunca dispara é um teste aprovado. Escreva os testes antes de escrever o
(vermelho → verde → refatoração) é esse hábito rodando continuamente.
(vermelho → verde → refatoração) é esse hábito rodando continuamente.
desconto de 1% sobre £order_total — o teste é escrito primeiro, na nova fronteira:
Verifique o teste central manualmente: subtotal 10, dez por cento de desconto resulta em 10, e
Verifique o teste intermediário manualmente: subtotal 1003, dez por cento de desconto resulta em 902.7, e
int(...) arredonda para baixo 902. Note a primeira afirmação: em 200 pence e quatro
xícaras, o total permanece 803 — os testes de fronteira provam que o desconto é aplicado
e que não é aplicado prematuramente.
Exemplo resolvido 2 (bissecionar um erro com impressões, escalonado). A contagem de 1.5
imprime {'tea': 3}, mas a linha do best-seller imprime a bebida errada. O método:
(1) reproduzir com a menor entrada falha; (2) imprimir os valores na fronteira
entre a parte confiável e a suspeita — aqui, a contagem antes da max;
(3) corrigir uma coisa; (4) executar os testes novamente. Mover a impressão pela metade do código
em cada execução reduz pela metade a busca: quatro impressões encontram um erro em um programa de dezesseis linhas, embora
bem ele se esconda.
Depuração é encontrar a diferença entre o que você acredita que o código faz
e o que ele realmente faz — a impressão na fronteira de confiança/suspeita é a ferramenta.
Erros comuns. Testar apenas o caminho feliz: as fronteiras (zero, um, vazio,
negativo) são onde os programas quebram. Escrever uma semana de código antes do primeiro teste,
depois depurar uma montanha. Corrigir duas coisas de uma vez, para que nenhum resultado signifique
nada.
Resumo: decompor até que cada peça teste isoladamente; afirmar as fronteiras assim que uma
função existe; escrever o teste primeiro quando possível; depurar por bisseção com impressões
nas fronteiras de confiança — o hábito no qual todo o grau se baseia.
Aulas interativas sobre este tópico
Passe por ele passo a passo, com exercícios de verificação instantânea.