Guia de estudo

Revisão Prática — 2ª VA (Aulas 14 a 24)
2ª VA · Revisão Prática

Revisão Prática — 2ª VA

Quatro programas completos que exercitam as aulas 14 a 24.

Material de estudo em casa
Copie, rode e mexa de propósito

POO + coleções + arquivos SQLite (CRUD + Repositório) Observer e Strategy Tkinter que fala com o banco
Revisão Prática — 2ª VA (Aulas 14 a 24)

como aproveitar

Como usar este material

São quatro programas pequenos, completos e propositalmente didáticos. A ideia é rodar e mexer, não só ler. Cada um roda sozinho.

01

Leia o slide

Cada bloco de código tem observações curtas apontando o conceito e a aula. Foque no que está sublinhado.

02

Rode no seu computador

Os códigos completos estão nos guias de estudo (botão 📚). Copie e cole no VS Code. O programa 4 usa Tkinter — rode com o Python do python.org.

03

Compare com a saída

Cada demo traz a saída exata do terminal como comentário. Se a sua diverge, é porque mexeu em algo — ótimo, está aprendendo.

04

Mexa de propósito

Tire o encoding="utf-8", troque o ? por f-string, remova o super().__init__(): veja o que quebra. Quebrar de propósito ensina.

Revisão Prática — 2ª VA (Aulas 14 a 24)

programa 1 · cenário

Cadastro de pizzaria — POO + coleções + arquivos

Um Pedido encapsula o valor, um Cadastro compõe uma lista de pedidos e grava/lê tudo de um .txt. Cobre a base POO (02–05) mais arquivos (17) e coleções (18).

  • 🔒 Aulas 02–04: @property/@setter validam o valor (levanta ValueError), e __str__ dá a representação amigável (aula 03).
  • 🧱 Aulas 05 e 17: Cadastro compõe a lista _pedidos e persiste em arquivo com salvar/carregar_todos — sempre encoding="utf-8", caminho ancorado com Path(__file__).parent.
  • 🔎 Aula 18: busca com list comprehension e ordenação com sorted(key=lambda ...) — a mesma key= que é Strategy (aula 21).

O guia de estudo tem o arquivo completo

Clique no botão 📚 deste slide para copiar o pizzaria.py inteiro e rodar.

Revisão Prática — 2ª VA (Aulas 14 a 24)

programa 1 · parte 1

Pedido — encapsulamento + __str__

O par @property/@setter com o mesmo nome do atributo: o getter lê self._valor, o setter valida e guarda. Valor inválido é barrado na origem com raise ValueError.

🔒
Encapsulamento (aula 04): self.valor = valor no __init__ já passa pelo setter — a validação vale até na criação do objeto.
🎭
Representação (aula 03): __str__ devolve o texto que aparece no print(pedido) e na f-string. Nunca use print dentro do __str__ — ele deve retornar.
pizzaria.py
"""Cadastro de pizzaria — POO + coleções + arquivos (aulas 02–05, 17, 18)."""

from pathlib import Path

ARQUIVO = Path(__file__).parent / "pedidos.txt"  # ancorado ao lado do script (aula 17)


class Pedido:
    """Um pedido: cliente, sabor e valor. Encapsula o valor (aulas 02–04)."""

    def __init__(self, cliente, sabor, valor):
        self.cliente = cliente          # atributos simples (aula 02)
        self.sabor = sabor
        self.valor = valor              # passa pelo setter -> valida

    @property
    def valor(self):                    # getter (aula 04)
        return self._valor

    @valor.setter
    def valor(self, novo):              # setter valida e guarda (aula 04)
        if novo <= 0:
            raise ValueError("valor deve ser positivo")
        self._valor = novo

    def __str__(self):                  # representação amigável (aula 03)
        return f"{self.cliente}{self.sabor} (R$ {self._valor:.2f})"
Revisão Prática — 2ª VA (Aulas 14 a 24)

programa 1 · parte 2

Cadastro — composição + persistência

A coleção vira uma classe: Cadastro cria e possui a lista _pedidos (composição). salvar reescreve o arquivo em "w"; carregar_todos é um factory (@classmethod) que reconstrói os objetos do texto.

📁
Arquivos (aula 17): with open(..., encoding="utf-8") fecha sozinho (o finally automático). Ao carregar, tudo vem como texto — por isso float(valor).
🔎
Coleções (aula 18): [p for p in ... if ...] filtra, e sorted(key=lambda p: p.valor) devolve uma lista nova ordenada — sem alterar a original.
pizzaria.py
class Cadastro:
    """Compõe uma lista de Pedidos e persiste em arquivo (aulas 05, 17, 18)."""

    def __init__(self):
        self._pedidos = []              # composição (aula 05)

    def adicionar(self, pedido):
        self._pedidos.append(pedido)

    def salvar(self, caminho=ARQUIVO):
        # reescreve o arquivo inteiro em "w" (aula 17)
        with open(caminho, "w", encoding="utf-8") as f:
            for p in self._pedidos:
                f.write(f"{p.cliente};{p.sabor};{p.valor}\n")

    @classmethod
    def carregar_todos(cls, caminho=ARQUIVO):
        # factory que reconstrói os objetos a partir do texto (aula 17)
        cadastro = cls()
        try:
            with open(caminho, "r", encoding="utf-8") as f:
                for linha in f:
                    cliente, sabor, valor = linha.strip().split(";")
                    cadastro.adicionar(Pedido(cliente, sabor, float(valor)))
        except FileNotFoundError:
            pass                        # 1ª execução: arquivo ainda não existe
        return cadastro

    def buscar_por_cliente(self, alvo):
        # list comprehension filtrando (aula 18)
        return [p for p in self._pedidos if p.cliente == alvo]

    def por_valor(self):
        # ordena com sorted(key=lambda) — devolve lista nova (aula 18)
        return sorted(self._pedidos, key=lambda p: p.valor)
Revisão Prática — 2ª VA (Aulas 14 a 24)

programa 1 · rodando

Demonstração + saída

Monta três pedidos, tenta um valor negativo (recusado), salva, recarrega do arquivo e mostra a busca e a ordenação. A saída real está comentada no fim.

💾
Persistência de verdade: os pedidos são recarregados do arquivo antes de buscar/ordenar — prova que os dados sobreviveram ao salvar().
🛑
Validação na origem: Pedido("Carla", "Frango", -5) nem chega a ser criado — o setter levanta ValueError no __init__.
pizzaria.py
# ── Demonstração ──────────────────────────────────────────────────────
cadastro = Cadastro()
cadastro.adicionar(Pedido("Ana", "Calabresa", 39.90))
cadastro.adicionar(Pedido("Bruno", "Marguerita", 42.00))
cadastro.adicionar(Pedido("Ana", "Portuguesa", 45.50))

# valor inválido é barrado na origem (aula 04/14)
try:
    Pedido("Carla", "Frango", -5)
except ValueError as erro:
    print(f"[recusado] {erro}")

cadastro.salvar()                       # grava em pedidos.txt

# recarrega do zero, provando a persistência (aula 17)
recarregado = Cadastro.carregar_todos()

print("Pedidos da Ana:")
for p in recarregado.buscar_por_cliente("Ana"):
    print(f"  - {p}")

print("Todos por valor (crescente):")
for p in recarregado.por_valor():
    print(f"  - {p}")

# Saída no terminal:
# [recusado] valor deve ser positivo
# Pedidos da Ana:
#   - Ana — Calabresa (R$ 39.90)
#   - Ana — Portuguesa (R$ 45.50)
# Todos por valor (crescente):
#   - Ana — Calabresa (R$ 39.90)
#   - Bruno — Marguerita (R$ 42.00)
#   - Ana — Portuguesa (R$ 45.50)
Revisão Prática — 2ª VA (Aulas 14 a 24)

programa 2 · cenário

Repositório SQLite — o .txt vira banco

A persistência sobe um degrau: sai do arquivo de texto e entra no sqlite3. Cada objeto é uma linha, cada atributo uma coluna. O RepositorioProduto esconde todo o SQL atrás de métodos do domínio.

  • 🗄️ Aula 24 — SQL com ?: CREATE, INSERT, SELECT, UPDATE, DELETE, sempre com placeholder ? — nunca f-string (SQL injection). .db ancorado com Path(__file__).parent.
  • 🔁 Aula 24 — ORM manual: from_row (@classmethod, tupla → objeto) e to_params (objeto → tupla dos ?) — parentes do from_dict/to_dict da aula 17.
  • 🚨 Aulas 04 e 14: Padrão Repositório = encapsulamento (quem chama nunca vê SQL). O salvar traduz o sqlite3.IntegrityError (uma regra da tabela, ex.: NOT NULL) numa exceção de domínio: raise ProdutoInvalidoError.

O guia de estudo tem os arquivos completos

O botão 📚 deste slide traz o repositorio_sqlite.py e o app_estoque.py (programa 4) inteiros.

Revisão Prática — 2ª VA (Aulas 14 a 24)

programa 2 · parte 1

Produto — o ORM feito na mão

A classe de domínio sabe ir e voltar do banco. from_row transforma a tupla lida em objeto; to_params devolve a tupla que preenche os ? do INSERT — sem o id, que o banco gera sozinho.

🔁
from_row (aula 24): é um construtor alternativo (@classmethod) — o mesmo padrão do carregar_todos do programa 1 e do from_dict da aula 17.
🆔
to_params sem o id: a chave primária é AUTOINCREMENT; quem gera é o banco. O objeto só recebe o id depois do INSERT, via lastrowid.
repositorio_sqlite.py
"""Repositório SQLite — CRUD de produtos (aulas 24, 04, 14)."""

import sqlite3
from pathlib import Path

BANCO = Path(__file__).parent / "loja.db"  # ancorado ao lado do script (aula 24)


class ProdutoInvalidoError(Exception):
    """Erro de domínio: o produto fere uma regra da tabela (aula 14)."""

    def __init__(self, motivo):
        self.motivo = motivo            # guarda o dado do erro
        super().__init__(f"Produto inválido: {motivo}")


class Produto:
    """Objeto de domínio; sabe virar/voltar de uma linha (ORM manual, aula 24)."""

    def __init__(self, nome, preco, id=None):
        self.id = id
        self.nome = nome
        self.preco = preco

    @classmethod
    def from_row(cls, linha):           # tupla do banco -> objeto (aula 24)
        id, nome, preco = linha
        return cls(nome, preco, id)

    def to_params(self):                # objeto -> tupla p/ os ? (SEM o id)
        return (self.nome, self.preco)

    def __str__(self):
        return f"#{self.id} {self.nome} — R$ {self.preco:.2f}"
Revisão Prática — 2ª VA (Aulas 14 a 24)

programa 2 · parte 2

RepositorioProduto — o CRUD encapsulado

Cada método abre a conexão com with, roda o SQL com ? e devolve objetos (não tuplas). salvar captura IntegrityError quando um campo obrigatório fica vazio (NOT NULL); buscar_por_nome usa LIKE.

🔒
Repositório (aulas 04/24): quem chama diz repo.salvar(produto) — nunca vê INSERT. O SQL fica trancado aqui dentro (encapsulamento).
🚨
Exceções (aula 14): o except sqlite3.IntegrityError (específico, antes do geral sqlite3.Error) traduz o erro técnico do banco numa exceção de domínio: raise ProdutoInvalidoError(...).
repositorio_sqlite.py
class RepositorioProduto:
    """Esconde todo o SQL atrás de métodos do domínio (Repositório, aulas 04/24)."""

    def __init__(self, caminho=BANCO):
        self.caminho = caminho
        self.criar_tabela()

    def criar_tabela(self):
        with sqlite3.connect(self.caminho) as con:
            con.execute(
                "CREATE TABLE IF NOT EXISTS produtos ("
                "id INTEGER PRIMARY KEY AUTOINCREMENT, "
                "nome TEXT NOT NULL, "
                "preco REAL NOT NULL)"
            )

    def salvar(self, produto):
        # INSERT com placeholder ? (nunca f-string!) — aula 24
        try:
            with sqlite3.connect(self.caminho) as con:
                cursor = con.execute(
                    "INSERT INTO produtos (nome, preco) VALUES (?, ?)",
                    produto.to_params(),
                )
                produto.id = cursor.lastrowid   # id gerado pelo banco
                return produto
        except sqlite3.IntegrityError:          # feriu uma regra da tabela (ex.: NOT NULL) — aula 14
            raise ProdutoInvalidoError("nome e preço são obrigatórios")
        except sqlite3.Error as erro:           # rede de segurança geral
            print(f"[erro no banco] {erro}")
            return None

    def listar(self):
        with sqlite3.connect(self.caminho) as con:
            cursor = con.execute("SELECT id, nome, preco FROM produtos ORDER BY id")
            return [Produto.from_row(l) for l in cursor.fetchall()]

    def buscar_por_nome(self, termo):
        with sqlite3.connect(self.caminho) as con:
            cursor = con.execute(
                "SELECT id, nome, preco FROM produtos WHERE nome LIKE ?",
                (f"%{termo}%",),        # busca parcial com LIKE + % (aula 24)
            )
            return [Produto.from_row(l) for l in cursor.fetchall()]

    def atualizar(self, produto):
        with sqlite3.connect(self.caminho) as con:
            con.execute(
                "UPDATE produtos SET nome = ?, preco = ? WHERE id = ?",
                (produto.nome, produto.preco, produto.id),
            )

    def remover(self, id):
        with sqlite3.connect(self.caminho) as con:
            con.execute("DELETE FROM produtos WHERE id = ?", (id,))
Revisão Prática — 2ª VA (Aulas 14 a 24)

programa 2 · rodando

Demonstração + saída

Grava três produtos, tenta salvar um sem nome (recusado pelo NOT NULL), lista, atualiza o preço do café, remove o leite e faz uma busca parcial. A saída real está comentada no fim.

💾
O with do sqlite3 commita ao sair (rollback se der erro). Cuidado: diferente do with de arquivo, ele NÃO fecha a conexão — aqui abrimos uma por operação.
🔎
Busca por 'a' com LIKE: traz Cafe e Pao. O % nas pontas (%a%) casa o termo em qualquer posição do nome.
repositorio_sqlite.py
# ── Demonstração (rode num loja.db novo pra bater a saída) ─────────────
repo = RepositorioProduto()
repo.salvar(Produto("Cafe", 18.0))
repo.salvar(Produto("Leite", 6.5))
repo.salvar(Produto("Pao", 8.0))

# produto sem nome: fere o NOT NULL; o Repositório traduz o IntegrityError (aula 14)
try:
    repo.salvar(Produto(None, 20.0))
except ProdutoInvalidoError as erro:
    print(f"[recusado] {erro}")        # print(erro) usa a mensagem do super().__init__

print("Todos os produtos:")
for p in repo.listar():
    print(f"  {p}")

cafe = repo.buscar_por_nome("Cafe")[0]  # atualiza o preço do café
cafe.preco = 21.0
repo.atualizar(cafe)

leite = repo.buscar_por_nome("Leite")[0]  # remove o leite
repo.remover(leite.id)

print("Depois de atualizar Cafe e remover Leite:")
for p in repo.listar():
    print(f"  {p}")

print("Busca por 'a':")
for p in repo.buscar_por_nome("a"):
    print(f"  {p}")

# Saída no terminal:
# [recusado] Produto inválido: nome e preço são obrigatórios
# Todos os produtos:
#   #1 Cafe — R$ 18.00
#   #2 Leite — R$ 6.50
#   #3 Pao — R$ 8.00
# Depois de atualizar Cafe e remover Leite:
#   #1 Cafe — R$ 21.00
#   #3 Pao — R$ 8.00
# Busca por 'a':
#   #1 Cafe — R$ 21.00
#   #3 Pao — R$ 8.00
Revisão Prática — 2ª VA (Aulas 14 a 24)

programa 3 · cenário

Padrões aplicados — Observer e Strategy

Os dois padrões comportamentais da aula 21, lado a lado. Observer: um Subject avisa vários inscritos. Strategy: um Contexto delega a uma estratégia trocável em runtime. Ambos vivem de ABC + polimorfismo + composição.

  • 📣 Observer (aula 21): Canal é o Subject — mantém _inscritos, e notificar() percorre a lista chamando atualizar() em cada um (um-para-muitos).
  • 🔀 Strategy (aula 21): Carrinho é o Contexto — guarda uma estratégia de desconto e delega a ela. Trocar carrinho.desconto muda o cálculo sem tocar em quem chama.
  • 🌉 Você já usou os dois: o command= de um botão Tkinter (aula 22) é Observer; o sorted(key=...) da aula 18 é Strategy. Aqui eles aparecem explícitos.
Revisão Prática — 2ª VA (Aulas 14 a 24)

programa 3 · parte 1

Observer — o Canal que avisa todos

Inscrito é o contrato (ABC). Canal é o Subject: guarda os inscritos e, ao publicar, chama o mesmo atualizar() em cada um — e cada um responde do seu jeito (polimorfismo).

🎭
ABC + polimorfismo (aulas 10/09): o laço em notificar não usa isinstance — chama atualizar() e confia que cada observer implementa o seu.
🌉
Ponte Tkinter (aula 22): é exatamente o que botao.config(command=funcao) faz — inscrever um observer que roda quando o evento acontece.
padroes.py
"""Padrões aplicados — Observer e Strategy (aula 21)."""

from abc import ABC, abstractmethod


# ── Observer: um Canal avisa vários inscritos (aula 21) ────────────────
class Inscrito(ABC):
    @abstractmethod
    def atualizar(self, video):         # contrato do observer
        pass


class Pessoa(Inscrito):
    def __init__(self, nome):
        self.nome = nome

    def atualizar(self, video):         # cada observer reage do seu jeito
        print(f"  {self.nome} recebeu: '{video}'")


class Canal:
    """Subject: mantém a lista e notifica todos (um-para-muitos)."""

    def __init__(self, nome):
        self.nome = nome
        self._inscritos = []            # composição (aula 05)

    def inscrever(self, inscrito):
        self._inscritos.append(inscrito)

    def notificar(self, video):
        print(f"[{self.nome}] publicou '{video}' — avisando {len(self._inscritos)}:")
        for inscrito in self._inscritos:  # polimorfismo: mesma chamada (aula 09)
            inscrito.atualizar(video)
Revisão Prática — 2ª VA (Aulas 14 a 24)

programa 3 · parte 2 + rodando

Strategy — trocar o desconto em runtime

Cada regra de desconto é uma classe com o mesmo método calcular. O Carrinho guarda a estratégia ativa e delega. Trocar a estratégia (carrinho.desconto = ...) muda o resultado sem alterar o Carrinho.

🔀
Delegar ≠ notificar: o Strategy tem UMA estratégia ativa e delega a ela; o Observer tem VÁRIOS inscritos e avisa todos. Mesmo tripé (ABC + composição), intenções opostas.
Trocável em runtime: SemDesconto devolve 200,00; ao trocar para DescontoBlackFriday, a mesma compra vira 140,00 — 30% off.
padroes.py
# ── Strategy: o Carrinho troca a regra de desconto em runtime (aula 21) ─
class Desconto(ABC):
    @abstractmethod
    def calcular(self, valor):          # contrato da estratégia
        pass


class SemDesconto(Desconto):
    def calcular(self, valor):
        return valor


class DescontoBlackFriday(Desconto):
    def calcular(self, valor):
        return valor * 0.70             # 30% off


class Carrinho:
    """Contexto: guarda a estratégia ativa e DELEGA a ela."""

    def __init__(self, total, desconto):
        self.total = total
        self.desconto = desconto        # composição (aula 05)

    def total_final(self):
        return self.desconto.calcular(self.total)


# ── Demonstração ──────────────────────────────────────────────────────
canal = Canal("SENAI Play")
canal.inscrever(Pessoa("Ana"))
canal.inscrever(Pessoa("Bruno"))
canal.notificar("POO com Python - Aula 24")

carrinho = Carrinho(200.0, SemDesconto())       # mesma compra, estratégias diferentes
print(f"Sem desconto: R$ {carrinho.total_final():.2f}")

carrinho.desconto = DescontoBlackFriday()       # troca em runtime (aula 21)
print(f"Black Friday: R$ {carrinho.total_final():.2f}")

# Saída no terminal:
# [SENAI Play] publicou 'POO com Python - Aula 24' — avisando 2:
#   Ana recebeu: 'POO com Python - Aula 24'
#   Bruno recebeu: 'POO com Python - Aula 24'
# Sem desconto: R$ 200.00
# Black Friday: R$ 140.00
Revisão Prática — 2ª VA (Aulas 14 a 24)

programa 4 · cenário

App Tkinter que consome o repositório

O fecho da 2ª VA: uma janela CRUD que grava e lê do SQLite. App herda de tk.Tk, tem Entry + Button + Listbox, confirma exclusão com messagebox.askyesno e conversa com o banco pelo RepositorioProduto do programa 2.

  • 🪟 Aulas 22 e 23: App(tk.Tk) — a janela É um objeto (herança + super().__init__() primeiro). command= nos botões e o Listbox como espelho da lista.
  • 🗄️ Aula 24 — ponte GUI↔banco: a tela não sabe SQL; ela chama repo.salvar, repo.listar e repo.remover. O banco é a fonte da verdade; o Listbox só reflete.
  • 📐 Um gerente de geometria por container (aula 22): tudo aqui usa só .pack. Misturar pack e grid no mesmo pai levanta TclError.
Revisão Prática — 2ª VA (Aulas 14 a 24)

programa 4 · parte 1

RepositorioProduto — a mesma ideia do programa 2

Versão enxuta do repositório (só id e nome) para a GUI consumir. Mesmo desenho: cada método abre a conexão com with, roda SQL com ? e devolve dados prontos. A tela nunca vê SQL.

🔒
Repositório (aula 24): salvar, listar e remover são a interface do domínio. Trocar .txt por SQLite não muda quem chama — só esta classe.
Placeholder ?: o nome digitado pelo usuário entra por (nome,) — nunca colado na string. É a defesa contra SQL injection da aula 24.
app_estoque.py
"""App Tkinter que consome o repositório SQLite (aulas 22, 23, 24)."""

import sqlite3
import tkinter as tk
from tkinter import messagebox
from pathlib import Path

BANCO = Path(__file__).parent / "loja_gui.db"  # ancorado ao lado do script (aula 24)


class RepositorioProduto:
    """CRUD mínimo em SQLite (aula 24). A GUI usa isto sem ver SQL."""

    def __init__(self, caminho=BANCO):
        self.caminho = caminho
        with sqlite3.connect(self.caminho) as con:
            con.execute(
                "CREATE TABLE IF NOT EXISTS produtos ("
                "id INTEGER PRIMARY KEY AUTOINCREMENT, nome TEXT NOT NULL)"
            )

    def salvar(self, nome):
        with sqlite3.connect(self.caminho) as con:
            con.execute("INSERT INTO produtos (nome) VALUES (?)", (nome,))

    def listar(self):
        with sqlite3.connect(self.caminho) as con:
            cursor = con.execute("SELECT id, nome FROM produtos ORDER BY id")
            return cursor.fetchall()    # lista de tuplas (id, nome)

    def remover(self, id):
        with sqlite3.connect(self.caminho) as con:
            con.execute("DELETE FROM produtos WHERE id = ?", (id,))
Revisão Prática — 2ª VA (Aulas 14 a 24)

programa 4 · parte 2

App(tk.Tk) — a janela CRUD

A app inteira herda de tk.Tk: self é a janela, o estado vira atributo, os callbacks viram métodos. preencher_lista limpa e reflete o banco; excluir confirma com askyesno antes de remover.

🪟
Herança de tk.Tk (aula 23): super().__init__() é a PRIMEIRA linha. Sem ela, a janela nem existe quando você tenta criar os widgets.
🔗
Espelho do banco (aula 23): _ids guarda o id do banco na mesma ordem do Listbox — o índice selecionado vira o id certo para remover. Todo change chama preencher_lista().
app_estoque.py
class App(tk.Tk):
    """A janela É um objeto: herda de tk.Tk (aulas 07/23)."""

    def __init__(self, repo):
        super().__init__()              # PRIMEIRA linha do __init__ (aula 23)
        self.repo = repo
        self.title("Estoque")
        self.geometry("320x300")

        # só .pack neste container — um gerente de geometria por container (aula 22)
        self.campo = tk.Entry(self, width=30)
        self.campo.pack(padx=10, pady=(10, 4))

        tk.Button(self, text="Adicionar", command=self.adicionar).pack(pady=2)

        self.lista = tk.Listbox(self, width=40, height=8)
        self.lista.pack(padx=10, pady=4)

        tk.Button(self, text="Excluir", command=self.excluir).pack(pady=2)

        self._ids = []                  # espelha os ids na ordem do Listbox (aula 23)
        self.preencher_lista()

    def preencher_lista(self):
        # o banco é a fonte da verdade; o Listbox é só o espelho (aula 23)
        self.lista.delete(0, tk.END)    # limpa ANTES de reinserir
        self._ids = []
        for id, nome in self.repo.listar():
            self._ids.append(id)
            self.lista.insert(tk.END, nome)

    def adicionar(self):
        nome = self.campo.get().strip()  # Entry.get() sempre string (aula 22)
        if not nome:
            messagebox.showerror("Ops", "Digite um nome.")
            return
        self.repo.salvar(nome)          # grava no banco (aula 24)
        self.campo.delete(0, tk.END)
        self.preencher_lista()
        print(f"[+] adicionado: {nome}")

    def excluir(self):
        selecao = self.lista.curselection()   # tupla, vazia se nada (aula 23)
        if not selecao:
            messagebox.showerror("Ops", "Selecione um item.")
            return
        indice = selecao[0]
        id = self._ids[indice]          # índice do Listbox -> id do banco
        nome = self.lista.get(indice)
        if messagebox.askyesno("Excluir", f"Remover '{nome}'?"):  # confirma (aula 23)
            self.repo.remover(id)
            self.preencher_lista()
            print(f"[-] removido: {nome}")
Revisão Prática — 2ª VA (Aulas 14 a 24)

programa 4 · rodando

Rodando o app + saída dos callbacks

O aluno roda com o Python do python.org (Tk 8.6, igual ao Windows). Ao adicionar dois itens e excluir o primeiro (confirmando no diálogo), os print dos callbacks aparecem no terminal — a saída real está comentada abaixo.

🖱️
command= é Observer (aula 21): o botão é o Subject, o método self.adicionar/self.excluir é o observer. Vai SEM parênteses — command=self.adicionar, não self.adicionar().
🗄️
GUI ↔ banco: cada clique em Adicionar/Excluir grava no .db via repositório. Feche e reabra o app: os itens continuam lá — persistência real (aula 24).
app_estoque.py
if __name__ == "__main__":
    repo = RepositorioProduto()
    app = App(repo)
    app.mainloop()                      # o aluno roda de verdade

# Saída no terminal:
# [+] adicionado: Teclado
# [+] adicionado: Mouse
# [-] removido: Teclado
Revisão Prática — 2ª VA (Aulas 14 a 24)

autoavaliação

Você está pronto para a 2ª VA se consegue...

  • Escrever o par @property/@setter com o mesmo nome, validando no setter e levantando ValueError — e lembrar do __str__. (aulas 04, 03)
  • Compor uma coleção numa classe (self._itens = []) e persistir em .txt com encoding="utf-8" e caminho ancorado. (aulas 05, 17)
  • Filtrar com list comprehension e ordenar com sorted(key=lambda ...). (aula 18)
  • Criar a tabela e o CRUD em SQLite com placeholder ? — CREATE, INSERT, SELECT, UPDATE, DELETE — e explicar por que nunca usar f-string no SQL. (aula 24)
  • Mapear objeto ↔ linha com from_row (@classmethod) e to_params, e devolver objetos do listar(). (aula 24)
  • Encapsular o SQL num Repositório, definir uma exceção de domínio (class ProdutoInvalidoError(Exception)) e traduzir o sqlite3.IntegrityError nela, com super().__init__. (aulas 04, 14, 24)
  • Montar um Observer (Subject com inscrever/notificar, observers com atualizar) e um Strategy (Contexto que delega, estratégia trocável em runtime). (aula 21)
  • Reconhecer que command= é Observer e sorted(key=) é Strategy — os padrões que você já usava sem saber o nome. (aulas 21, 22, 18)
  • Escrever App(tk.Tk) com super().__init__() primeiro, Entry + Button + Listbox, e um só gerente de geometria por container. (aulas 22, 23)
  • Ligar a GUI ao banco: a tela chama o Repositório, o Listbox espelha a lista/tabela, e askyesno confirma a exclusão. (aulas 23, 24)