Riverfount

Um espaço para meus devaneios, sejam em TI ou em Filosofia

Generators em Python são funções especiais que usam yield para gerar valores sob demanda, economizando memória em vez de criar listas completas na RAM. Pense neles como “listas preguiçosas” que produzem um item por vez, ideais para processar arquivos grandes ou sequências infinitas sem travar o sistema.

Yield vs Return: A Diferença Fundamental

return encerra a função imediatamente após retornar um único valor, enquanto yield pausa a execução, retorna um valor e preserva o estado interno para continuar de onde parou na próxima chamada. Isso permite que uma única função gere múltiplos valores sequencialmente, como um loop “congelado” e retomado.

def com_return(n):
    for i in range(n):
        return i  # Para após o primeiro valor: sempre retorna 0

def com_yield(n):
    for i in range(n):
        yield i  # Gera 0, 1, 2... até n, pausando entre cada yield

print(next(com_return(5)))  # 0 (e função termina)
for i in com_yield(5):      # 0, 1, 2, 3, 4 (estado preservado)
    print(i)

return é para resultados finais únicos; yield constrói iteradores reutilizáveis.

Como Funcionam os Generators Básicos

Uma função generator parece normal, mas substitui return por yield, que pausa a execução e retorna um valor, preservando o estado para continuar depois. Isso cria um objeto iterável consumido com for ou next(), perfeito para loops sem alocar memória extra.

def contar_ate(n):
    for i in range(n):
        yield i

for numero in contar_ate(5):
    print(numero)  # Saída: 0 1 2 3 4

Vantagens em Memória e Performance

Generators brilham com dados grandes: uma lista de 1 milhão de números usa ~80MB, mas o generator equivalente consome apenas 128 bytes, gerando itens sob demanda. São nativos para for, list(), sum() e economizam tempo em cenários reais como leitura de logs ou CSV gigantes.

Expressões geradoras simplificam ainda mais: (x**2 for x in range(1000000)) cria um generator conciso sem parênteses extras, consumido iterativamente.

Tratamento de Exceções Simples

Exceções funcionam naturalmente dentro do generator. Use try/except para capturar erros durante a geração, como valores inválidos em um parser de JSON, mantendo o fluxo seguro.

def numeros_validos(arquivo):
    with open(arquivo) as f:
        f.readlines()
        for linha in f:
            try:
                yield int(linha.strip())
            except ValueError:
                print(f"Ignorando linha inválida: {linha}")
                continue

Isso previne crashes em dados reais “sujos”, comuns em automações e ETLs.

Fechamento Correto de Resources

Generators com arquivos ou conexões precisam de fechamento para evitar vazamentos. Use try/finally internamente ou generator.close() externamente, garantindo liberação automática.

# Exemplo 1: try/finally interno + close() externo
def ler_arquivo(arquivo):
    f = open(arquivo)
    f.reade.lines()
    try:
        for linha in f:
            yield linha.strip()
    finally:
        f.close()

gen = ler_arquivo('dados.csv')
try:
    for dado in gen:
        processar(dado)
finally:
    gen.close()

Context managers (with) integram perfeitamente para automação, eliminando necessidade de close() manual.

# Exemplo 2: Context manager with (mais limpo e automático)
def ler_arquivo_with(arquivo):
    with open(arquivo) as f:
        f.readlines()
        for linha in f:
            yield linha.strip()

# Uso simples e seguro - fecha automaticamente
for dado in ler_arquivo_with('dados.csv'):
    processar(dado)

Aplicações Práticas Iniciais

  • Arquivos gigantes: Leia linha por linha sem carregar tudo.
  • Sequências infinitas: Fibonacci ou contadores sem fim.
  • Pipelines simples: Filtre e transforme dados em cadeia.
  • Testes unitários: Mock de iteradores sem dados reais.​

Esses padrões otimizam ERPs, scripts de automação e APIs desde o primeiro projeto.

Conclusão

Generators transformam código Python em soluções eficientes e elegantes. Comece substituindo listas por generators em seus loops e veja a diferença em performance imediatamente.

Teste os exemplos acima no seu próximo projeto Python, meça o uso de memória com sys.getsizeof() e compartilhe seus resultados em @riverfount@bolha.us para discutirmos otimizações reais juntos!



Riverfount
Vicente Eduardo Ribeiro Marçal

Você já parou para pensar por que seu código Python consome cada vez mais memória em aplicações de longa duração, mesmo sem vazamentos óbvios? Palavras-chave como “garbage collector Python”, “contagem de referências Python”, “ciclos de referência Python” e “otimização de memória CPython” dominam buscas de desenvolvedores que enfrentam pausas inesperadas, inchaço de heap ou serviços que “incham” ao longo do tempo. Neste guia técnico expandido e atualizado, um engenheiro especialista em Python mergulha nos mecanismos internos do GC do CPython – com exemplos práticos de código, benchmarks reais e dicas avançadas de tuning – para você dominar a gestão de memória, detectar vazamentos sutis, configurar gerações otimizadas e escalar aplicações de produção sem surpresas.

Visão geral da memória no CPython

Python (implementação CPython) representa praticamente tudo como objetos alocados no heap: inteiros pequenos, strings, listas, funções, frames de pilha, módulos etc. Cada objeto PyObject carrega metadados essenciais, incluindo um contador de referências (ob_refcnt), tipo (ob_type) e, para contêineres rastreados pelo GC, ponteiros para listas duplamente ligadas das gerações (gc_next, gc_prev).

O ciclo de vida completo é: alocação via PyObject_New → incremento de refcount em referências → possível promoção geracional → detecção de ciclos ou refcount=0 → tp_dealloc (chama del se aplicável) → PyObject_Free. Objetos imutáveis como tuples pequenos ou strings interned podem ser otimizados pelo PGO (Python Object Generalizer), mas o GC foca em contêineres mutáveis.

CPython usa duas camadas complementares: contagem de referências (imediata, determinística) + GC geracional (para ciclos raros, probabilístico).

Contagem de referências: O coração do gerenciamento

O mecanismo primário é contagem de referências: Py_INCREF() em toda atribuição/nova referência, Py_DECREF() em remoções/saídas de escopo. Ao zerar, tp_dealloc é imediato, sem pausas.

  • Cenários de INCREF: atribuição (a = obj), inserção em lista/dict (lst.append(obj)), passagem por parâmetro, cópia forte.
  • Cenários de DECREF: del var, variável sai de escopo, lst.pop(), dict del, ciclo de vida de frames locais termina.
  • Overhead baixo: Operações atômicas em 64-bit, mas visíveis em workloads intensivos (ex.: loops com listas mutáveis).

Exemplo simples de contagem de referências

import sys
import tracemalloc

tracemalloc.start()  # Para medir alocações reais

a = []          # PyList_New → refcount=1
print(f"Refcount inicial: {sys.getrefcount(a)}")  # >=2 (a + getrefcount)

b = a           # Py_INCREF → refcount=2
print(f"Após b=a: {sys.getrefcount(a)}")

del b           # Py_DECREF → refcount=1
print(f"Após del b: {sys.getrefcount(a)}")

del a           # Py_DECREF → 0 → tp_dealloc imediato
print(tracemalloc.get_traced_memory())  # Memória liberada
tracemalloc.stop()

sys.getrefcount infla +1 pela referência temporária da função. Use ctypes para refcount “puro” se necessário.

O problema: ciclos de referência e por que falham

Contagem falha em ciclos: A→B→A mantém refcounts >0 mutuamente, mesmo se o “root” externo foi deletado. Comum em árvores bidirecionais, caches LRU com backrefs, grafos.

Exemplo expandido de ciclo com diagnóstico

import gc
import weakref

class Node:
    def __init__(self, value):
        self.value = value
        self.next = None
        self._weak_next = None  # Weakref para evitar ciclo artificial

a = Node(1)
b = Node(2)

a.next = b
b.next = a  # Ciclo forte

print(f"Antes del - refcount a: {sys.getrefcount(a)}, b: {sys.getrefcount(b)}")
del a, b
print(f"Após del - ainda vivos! refcount a: {sys.getrefcount(a)}, b: {sys.getrefcount(b)}")

Weakrefs (unilateral) quebram ciclos sem custo de GC: weakref.ref(other)().

Coletor geracional: Detecção probabilística de ciclos

CPython adiciona GC apenas para contêineres (list, dict, instances, sets, etc. com tp_traverse/tp_clear). Não rastreia ints, floats, strings. Hipótese geracional: 90%+ objetos morrem jovens; sobreviventes são longevos.

4 Gerações reais (não documentadas publicamente): gen0 (jovem), gen1, gen2 (velhas), permanent (estáticas como módulos). Novos contêineres vão para gen0 via PyList_Append etc.

Limiares e disparo expandido

Thresholds padrão: (700, 10, 10) – gen0 dispara a cada ~700 alocs líquidas; a cada 10 coletas gen0, coleta gen1; a cada 10 gen1, full GC. Incremental GC (Python 3.13+?) limpa frações da gen2 por ciclo.

import gc

print("Thresholds:", gc.get_threshold())  # (700, 10, 10)
print("Contagens atuais:", gc.get_count())  # (gen0, gen1, gen2)

gc.set_threshold(1000, 20, 20)  # Menos pausas para throughput
print("Novos thresholds:", gc.get_threshold())

Sobreviventes sobem geração; gen2 é “quase permanente”.

Algoritmo de detecção de ciclos: Mark & Sweep otimizado

Fases (gc_collect_region):

  1. Roots → Reachability: De raízes (globals, stack, registers), traverse contêineres marcando alcançáveis (BFS via tp_traverse).
  2. Tentative Unreachable: Move suspeitos para lista separada; re-traverse para reviver falsos positivos.
  3. Finalizers & Clear: Chama tp_finalize/tp_clear em ciclos confirmados; DEALLOC se possível.

Ciclos puramente internos são liberados mesmo com refcount>0. gc.garbage guarda “uncollectable” com del pendente.

Usando o módulo gc na prática avançada

Monitoramento e debugging em produção

import gc
import objgraph  # pip install objgraph (opcional, para histograms)

print("Stats por geração:", gc.get_stats())
print("Objetos rastreados:", len(gc.get_objects()))

# Simular ciclo e coletar
def create_cycles(n=100):
    for _ in range(n):
        a = []; b = []; a.append(b); b.append(a)

create_cycles()
print(f"Antes GC: {len(gc.get_objects())}")
collected = gc.collect(2)  # Full GC
print(f"Coletados: {collected}, Garbage: {len(gc.garbage)}")
# objgraph.show_most_common_types()  # Se instalado

gc.callbacks para hooks em coletas; gc.is_tracked(obj) para checar.

Exemplo: Hunting vazamentos em loop

import gc
import time

def leaky_loop():
    cache = {}
    while True:
        obj = {'data': list(range(10000))}  # Simula dados grandes
        cache[id(obj)] = obj  # "Vazamento" intencional
        if len(gc.get_objects('dict')) > 10000:
            gc.collect()
            print("GC forçado, objetos:", len(gc.get_objects()))

# Em produção: rode com gc.set_debug(gc.DEBUG_LEAK)

Ajustando o GC para workloads específicos

Alto throughput (FastAPI/Flask): Thresholds altos + disable em endpoints rápidos. Data/ML (Pandas/NumPy): Desabilite GC (NumPy usa refcount puro). Long-running (Celery/services): Monitore gc.get_count(); tune baseado em RSS.

import gc
import signal

def tune_gc():
    gc.disable()  # Para bursts
    gc.set_threshold(0, 0, 0)  # Desabilita thresholds

# Context manager custom
class GCOff:
    def __enter__(self):
        gc.disable()
    def __exit__(self, *args):
        gc.enable()
        gc.collect()

Benchmarks reais: GC pausas <1ms em gen0; full GC pode ser 10-100ms em heaps grandes – meça com gc.get_stats().

Pontos práticos para engenheiros Python

  • **Evite del em ciclos**: Use contextlib ou weakrefs; del bloqueia auto-liberação.
  • Context managers > GC: Sempre with open(), with conn: para files/sockets – GC não garante ordem/timing.
  • **Slots e slots**: Reduzem dict interno (economia ~20-30% memória em classes).
  • Prod monitoring: Integre memory_profiler, fil, gc.get_objects() em healthchecks.
  • PyPy/Jython: Diferentes GCs (tracing); migração requer re-tune.

Conclusão e Próximos Passos

Dominar o GC do CPython transforma você de “esperando pausas” para “engenheiro proativo”: thresholds tunados cortam latência 20-50%, weakrefs eliminam 90% ciclos, e monitoramento previne OOMs em prod. Teste esses exemplos no seu código agora – rode gc.set_debug(gc.DEBUG_STATS) e veja o impacto real.



Riverfount
Vicente Eduardo Ribeiro Marçal

Baseado na Live de Python #150 do canal Eduardo Mendes no YouTube, este artigo explora de maneira prática e direta a “primitive obsession” — code smell onde tipos primitivos (strings, dicts, lists) substituem abstrações de domínio ricas — e como dataclasses (Python 3.7+, PEP 557) oferecem solução definitiva para criar Value Objects tipados, imutáveis e comportamentalmente ricos. Para sêniores buscando elevar modelagem DDD e reduzir technical debt em escala.

Primitive Obsession: Raiz do Problema

Primitive obsession ocorre quando entidades de domínio são reduzidas a “bags of primitives”, i. e., em que objetos de domínio são representados apenas como coleções simples de tipos primitivos (como strings, números, listas e dicionários) sem encapsulamento ou comportamento. Ou seja, em vez de ter classes ou estruturas que representem conceitos ricos com regras, validações e métodos, o código manipula “sacos” ou “pacotes” de dados primitivos soltos, o que aumenta a complexidade, propensão a erros e dispersa a lógica de negócio. Aplicando princípios de POO fundamentais: encapsulamento, polimorfismo e responsabilidade única. Consequências incluem:

  • Lógica de domínio espalhada: Funções utilitárias fazem parsing manual de strings/dicts
  • Falta de invariants: Sem validação, DDDs inválidos ou CPFs malformados passam despercebidos
  • Sem comportamentos: nome_completo() vira função externa poluente
  • Type unsafety: KeyError em runtime, mypy cego para estruturas aninhadas
  • Testabilidade pobre: Mockar dicts vs mockar objetos com dependências claras
# ANTES: Primitive Obsession Clássica
dados = [
    {"nome": "João", "sobrenome": "Silva", "telefone": {"residencial": "1234-5678"}, "ddd": 11},
    {"nome": "Maria", "sobrenome": "Santos", "telefone": {"residencial": "9876-5432"}, "ddd": 99}  # DDD inválido!
]

# Lógica espalhada + runtime errors
def nome_completo(dado: dict) -> str:
    return f"{dado['nome']} {dado['sobrenome']}"

def telefone_residencial(dado: dict) -> str:
    return dado['telefone']['residencial']  # KeyError se faltar!

nomes = [nome_completo(d) for d in dados]
telefone = telefone_residencial(dados[0])  # Fragile!

Custo real: Cyclomatic complexity explode, coverage cai, onboarding demora 3x mais.[9][10][1]

Dataclasses: Antídoto Completo à Primitive Obsession

Dataclasses não são “namedtuples mutáveis”. São geradores de Value Objects que restauram domínio perdido via automação OO inteligente:

1. Encapsulamento Automático + Comportamento

from dataclasses import dataclass, field
from typing import ClassVar
from datetime import date

@dataclass(frozen=True)
class Telefone:
    residencial: str
    
    def formatado(self, ddd: int) -> str:
        return f"({ddd}) {self.residencial}"

@dataclass(frozen=True)
class Pessoa:
    nome: str
    sobrenome: str
    telefone: Telefone
    ddd: int
    data_nascimento: date
    
    # Invariant via post_init
    def __post_init__(self):
        if self.ddd < 11 or self.ddd > 99:
            raise ValueError(f"DDD inválido: {self.ddd}")
        if self.data_nascimento > date.today():
            raise ValueError("Data futura inválida")
    
    @property
    def nome_completo(self) -> str:
        return f"{self.nome} {self.sobrenome}"
    
    @property
    def telefone_formatado(self) -> str:
        return self.telefone.formatado(self.ddd)
    
    @property
    def idade(self) -> int:
        return (date.today() - self.data_nascimento).days // 365
    
    # Value Object equality por valor, não referência
    def __eq__(self, other):
        if not isinstance(other, Pessoa):
            return NotImplemented
        return (self.nome_completo, self.ddd) == (other.nome_completo, other.ddd)

2. Uso: Domínio Restaurado, Zero Boilerplate

# Value Objects puros, type-safe
pessoas = [
    Pessoa("João", "Silva", Telefone("1234-5678"), 11, date(1990, 5, 15)),
    Pessoa("Maria", "Santos", Telefone("9876-5432"), 21, date(1985, 8, 22))
]

# Comportamento encapsulado, sem loops manuais
print([p.nome_completo for p in pessoas])
# ['João Silva', 'Maria Santos']

print(pessoas[0].telefone_formatado)  # "(11) 1234-5678"
print(pessoas[0].idade)  # ~35

# Validação em tempo de construção
try:
    Pessoa("Inválido", "X", Telefone("0000"), 5, date(2026, 1, 1))
except ValueError as e:
    print(e)  # "DDD inválido: 5"

3. Integrações Avançadas

from dataclasses import asdict, astuple
import json
from typing import Any

# Serialização controlada (não expõe internals)
def pessoa_to_json(p: Pessoa) -> str:
    return json.dumps({
        "nome_completo": p.nome_completo,
        "telefone": p.telefone_formatado,
        "idade": p.idade
    })

# mypy + Pydantic validation
# pip install pydantic[email-validator]
from pydantic import BaseModel, validator

class PessoaPydantic(BaseModel):
    nome_completo: str
    telefone: str
    idade: int
    
    @validator('telefone')
    def validate_telefone(cls, v):
        if not v.startswith('('):
            raise ValueError('Formato inválido')
        return v

Benefícios Arquiteturais Profundos

Aspecto Primitive Obsession Dataclasses Value Objects
Encapsulamento 0% (dicts públicos) 100% (métodos + invariants)
Type Safety Runtime KeyError Compile-time + runtime
Testabilidade Mock dicts complexos Mock objetos com dependências claras
Performance ~1x (dict access) ~1.2x (dataclass overhead mínimo)
Serialização json.dumps(dict) asdict() + versionamento
Extensibilidade Refatorar todos consumers[¹] Herança + composição

[1] Dificuldade que ocorre quando a estrutura de dados primitivos é modificada ou evoluída.

Métricas reais: Times reportam 40% menos bugs de dados, 25% mais velocidade em reviews.

Primitive Obsession em Escala: O Verdadeiro Custo

# Microsserviço com 50+ endpoints primitivos
@app.get("/clientes")
def listar_clientes():
    return [
        {"id": 1, "nome": "João", "email": "joao@email.com", "ativo": True},
        # 100+ campos espalhados, validação em middlewares...
    ]

Virada com dataclasses:

@dataclass(frozen=True)
class Cliente:
    id: int
    nome_completo: str
    email: EmailStr  # pydantic
    status: ClienteStatus  # Enum
    
    @classmethod
    def from_db(cls, row: Row) -> "Cliente":
        return cls(
            id=row.id,
            nome_completo=...,
            email=row.email,
            status=ClienteStatus.from_str(row.status)
        )

Conclusão

Dataclasses eliminam primitive obsession restaurando domínio rico sem sacrificar performance ou a experiência do desenvolvedor. São o “sweet spot” entre namedtuples (imutáveis, sem comportamento) e classes manuais (boilerplate pesado).



Riverfount
Vicente Eduardo Ribeiro Marçal

A arquitetura hexagonal, ou Ports and Adapters, coloca a lógica de negócio no centro de um hexágono simbólico, cercada por portas (interfaces abstratas) que conectam adaptadores externos como bancos de dados, APIs web, filas ou serviços de terceiros. Proposta por Alistair Cockburn em 2005, ela inverte as dependências tradicionais: o domínio não conhece frameworks ou persistência, mas estes dependem dele via injeção de dependências, promovendo código limpo e adaptável em Python. Essa abordagem alinha-se perfeitamente à filosofia “simples é melhor” do Python, mas com rigor para domínios complexos.

Componentes e Fluxo de Dados

O domínio abriga entidades imutáveis, agregados e regras puras, livres de anomalias arquiteturais como dependências de frameworks ou bancos de dados. Portas de entrada definem casos de uso (ex.: CreateUserPort), enquanto portas de saída expõem repositórios abstratos (ex.: UserRepository). Adaptadores concretos — como FastAPI para HTTP ou SQLAlchemy para DB — implementam essas portas, garantindo que o core permaneça intocado por mudanças externas. O fluxo entra pelas bordas, atinge o domínio via portas e sai por adaptadores, criando uma barreira unidirecional contra acoplamento.

Vantagens em Aplicações Python

  • Testes isolados e rápidos: Mockar portas permite TDD sem infraestrutura real, reduzindo flakiness em CI/CD com pytest.
  • Flexibilidade tecnológica: Troque Flask por FastAPI ou SQLite por DynamoDB sem refatorar o core, ideal para microsserviços.
  • Escalabilidade e manutenção: Suporta DDD em equipes grandes.
  • Longevidade: Evolução sem rewrites totais, perfeita para ERPs ou APIs corporativas em Python.

Desvantagens e Limitações

  • Curva de aprendizado e boilerplate: Abstrações iniciais sobrecarregam protótipos ou CRUD simples, violando “menos é mais” em Python.
  • Over-engineering em projetos triviais: Para apps sem domínio rico, aumenta complexidade sem ROI; prefira MVC tradicional.
  • Manutenção de portas: Muitas interfaces podem virar “abstrações vazias” se não gerenciadas, confundindo desenvolvedores menos experientes.

Casos de Uso Práticos em Python

Adote em microsserviços serverless. Útil em sistemas DDD como bibliotecas de gestão ou ERPs, isolando regras fiscais de persistência multi-DB. Evite em scripts ou MVPs rápidos; combine com Clean Architecture para monolitos legados. Exemplos reais incluem APIs de usuários com FastAPI, em que trocar mocks por Redis em produção é trivial.

Refatorando Projetos Existentes para Hexagonal

Refatorar monolitos Python para hexagonal exige abordagem incremental, priorizando estabilidade e testes. Siga estes passos práticos:

  1. Mapeie o Domínio Atual: Identifique entidades principais (ex.: User, Order) e regras de negócio misturadas em controllers/services. Extraia para domain/entities/ e domain/services/, criando dataclasses imutáveis.
  2. Defina Portas Mínimas: Para cada operação externa (DB, email, HTTP), crie interfaces ABC em domain/ports/ (ex.: UserRepository, EmailPort). Comece com 2-3 portas críticas.
  3. Crie Casos de Uso: Migre lógica de controllers para application/use_cases/ injetando portas. Exemplo: CreateUserUseCase(repo: UserRepository) orquestra validação e domínio.
  4. Implemente Adaptadores Gradualmente: Refatore controllers existentes para usar casos de uso (adaptadores de entrada). Crie infrastructure/repositories/ com implementações atuais (SQLAlchemy → PostgresUserRepository).
  5. Reestruture Pastas: Adote src/domain/, src/application/, src/infrastructure/. Use dependency-injector para wiring automático em main.py.
  6. Testes como Rede de Segurança: Escreva testes para portas/mock antes de refatorar, garantindo 100% de cobertura no domínio. Rode pytest em paralelo durante a migração.
  7. Migre por Feature: Refatore uma entidade por sprint (ex.: só User primeiro), mantendo código legado rodando via adaptadores híbridos.youtube​

Use a ferramenta radon para monitorar a complexidade ciclomática do código (ex.: radon cc src/). Se a média subir acima de 10 ou o ROI (retorno sobre investimento) cair — medido por testes mais lentos ou refatorações frequentes —, pause a migração. Projetos grandes (>10k linhas de código) recuperam o investimento em 3-6 meses com testes mais rápidos e manutenção simplificada; para projetos pequenos (<5k linhas), calcule o custo-benefício antes de prosseguir.

Conclusão: Adote Hexagonal e Eleve Seu Código

A arquitetura hexagonal transforma aplicações Python em sistemas resilientes, testáveis e evolutivos, isolando o domínio de frameworks voláteis e promovendo baixa manutenção a longo prazo. Ideal para domínios complexos como ERPs ou microsserviços, ela equilibra a simplicidade zen do Python com escalabilidade enterprise.​

Pronto para refatorar? Comece hoje: pegue um módulo CRUD do seu projeto, extraia as portas em 1 hora e teste com pytest. Baixe repositórios de exemplo no GitHub, experimente em um branch e veja a diferença em testes isolados. Compartilhe conosco no Mastodon em @riverfount@bolha.us sua primeira vitória hexagonal — ou dúvidas para discutirmos.



Riverfount
Vicente Eduardo Ribeiro Marçal

Você já precisou compartilhar um script Python com colegas e teve que explicar: “Instala o Python 3.12, cria um venv, instala requests e rich, depois roda”? Com o gerenciador UV, isso acabou.

Agora é possível escrever um único arquivo .py que já traz suas dependências dentro dele, como requests<3 e rich, e rodar tudo com apenas uv run script.py. Neste guia, você vai aprender como usar o bloco # /// script do UV para transformar scripts comuns em artefatos autocontidos, reprodutíveis e portáteis — perfeitos para automações, ferramentas internas e protótipos.

1. O que é o bloco # /// script

O UV entende um cabeçalho especial em arquivos Python, baseado no PEP 723, que permite declarar dependências diretamente no script. Ele tem esse formato:

# /// script
# dependencies = [
#   "nome-do-pacote>=versão",
#   "outro-pacote<versão",
# ]
# ///

Esse bloco é um TOML comentado, então o Python ignora, mas o UV o lê como metadados do script. Ele define:

  • Quais pacotes são necessários (dependencies).
  • A versão mínima de Python (requires-python).
  • O índice de pacotes, se for diferente do PyPI (index-url).

Com isso, o script vira um “mini-projeto” que carrega suas próprias dependências.

2. Instalando o UV (pré-requisito)

Antes de tudo, instale o UV no seu sistema:

# Linux/macOS
curl -LsSf https://astral.sh/uv/install.sh | sh

# Windows (PowerShell)
irm https://astral.sh/uv/install.ps1 | iex

Depois, verifique se está tudo certo:

bash
uv --version

Se aparecer a versão, o UV está pronto para usar.

3. Criando um script com dependências embutidas

Vamos criar um exemplo prático: um script que faz uma requisição HTTP com requests e mostra a saída formatada com rich.

Crie um arquivo http_client.py com o seguinte conteúdo:

#!/usr/bin/env -S uv run --script

# /// script
# requires-python = ">=3.12"
# dependencies = [
#   "requests>=2.31.0,<3",
#   "rich>=13.0.0",
# ]
# ///

import requests
from rich.console import Console
from rich.table import Table

console = Console()

def main():
    url = "https://httpbin.org/json"
    console.print(f"[bold blue]Fazendo GET em {url}...[/bold blue]")

    try:
        response = requests.get(url, timeout=10)
        response.raise_for_status()
        data = response.json()

        table = Table(title="Dados retornados")
        table.add_column("Chave")
        table.add_column("Valor")

        for key, value in data.items():
            table.add_row(str(key), str(value))

        console.print(table)
    except requests.RequestException as e:
        console.print(f"[bold red]Erro na requisição: {e}[/bold red]")

if __name__ == "__main__":
    main()

Esse script já traz:

  • Um shebang que usa uv run --script para executar diretamente.
  • Um bloco # /// script com requests e rich como dependências.
  • Um código real que usa essas bibliotecas para mostrar dados em uma tabela bonita.

4. Rodando o script pela primeira vez

Na pasta onde está http_client.py, execute:

uv run http_client.py

Na primeira execução, o UV vai:

  1. Ler o bloco # /// script.
  2. Criar um ambiente isolado para esse script.
  3. Instalar requests e rich (e suas dependências) nesse ambiente.
  4. Executar o script dentro desse ambiente.

Nas execuções seguintes, ele reutiliza o ambiente e roda direto, sem instalar nada de novo.

5. Adicionando dependências com uv add --script

Se precisar adicionar mais uma biblioteca (por exemplo, typer para CLI), use:

uv add --script http_client.py typer

Esse comando atualiza automaticamente o bloco # /// script no topo do arquivo, adicionando "typer" à lista de dependências. Depois disso, basta importar e usar typer no código.

6. Tornando o script executável (opcional)

Para rodar o script diretamente como um comando, torne-o executável:

chmod +x http_client.py

Agora é possível executá-lo sem chamar uv run explicitamente:

./http_client.py

O shebang #!/usr/bin/env -S uv run --script garante que o UV será usado para rodar o script com o ambiente correto.

7. Quando usar esse padrão

Esse modelo é ideal para:

  • Scripts de automação interna (CI, deploy, backup, etc.).
  • Ferramentas de linha de comando que circulam entre times.
  • Protótipos e PoCs que precisam de bibliotecas externas.
  • Scripts que você quer rodar em servidores sem configurar projeto completo.

Evite usar isso para:

  • Aplicações grandes com múltiplos módulos e estrutura de projeto.
  • Projetos que já usam pyproject.toml e uv init.

8. Dicas para produção

  • Sempre declare versões mínimas e máximas (requests>=2.31.0,<3) para evitar quebras.
  • Use requires-python para garantir que o script não rode em versões incompatíveis.
  • Em CI/CD, prefira usar uv sync em projetos completos, mas mantenha scripts autocontidos para tarefas pontuais.
  • Se quiser lock exato, o UV pode gerar um script.py.lock para fixar versões exatas.

Conclusão

Com o bloco # /// script do UV, scripts Python deixam de ser “códigos soltos” e viram artefatos autocontidos: trazem suas dependências, versão de Python e ambiente embutidos.

Basta instalar o UV, escrever o cabeçalho # /// script e rodar com uv run. O resultado é mais produtividade, menos setup manual e scripts que “simplesmente funcionam” em qualquer máquina com UV instalado.



Riverfount
Vicente Eduardo Ribeiro Marçal

O UV é um gerenciador de pacotes e projetos Python extremamente rápido, escrito em Rust, que substitui ferramentas como pip, venv e pipenv por comandos simples e automação de ambientes virtuais. Ele conecta gerenciamento de versões do Python, instalação de dependências e execução de scripts em um único comando, proporcionando agilidade no desenvolvimento.​

Instalação Rápida

Para começar, instale o UV facilmente via terminal:

  • Linux/macOS: curl -LsSf https://astral.sh/uv/install.sh | sh
  • Windows PowerShell: irm https://astral.sh/uv/install.ps1 | iex

Confirme a instalação com uv --version para garantir que está pronto para uso.

Criando e Executando Projetos

Com uv init nome_do_projeto, você cria um projeto Python completo com estrutura padrão, pyproject.toml, README.md, .gitignore e ambiente virtual configurado automaticamente. Navegue para a pasta (cd nome_do_projeto) e rode scripts diretamente usando:

uv run nome_do_script.py

Esse comando executa o script dentro do ambiente virtual gerenciado pelo UV, sem necessidade de ativação manual, mantendo o isolamento e limpeza do ambiente, ideal para aplicações Flask ou qualquer projeto Python.​

Gerenciando Dependências

Adicione pacotes facilmente:

uv add requests flask numpy

Esses comandos instalam as dependências dentro do ambiente virtual do projeto, gerenciam o arquivo pyproject.toml e criam um arquivo de bloqueio uv.lock para garantir ambientes reproduzíveis. Para remover pacotes, use uv remove nome_pacote, e para atualizar use uv update.​

Ambientes Virtuais e Comandos Úteis

O UV cria e gerencia ambientes virtuais automaticamente, mas você também pode criar manualmente com:

uv venv --python 3.12

Além disso, você pode listar dependências (uv list), sincronizar ambiente (uv sync) e executar ferramentas CLI do Python com uvx nome_da_ferramenta sem instalar globalmente.

O destaque é o comando uv run, que, além de executar scripts simples, pode ser usado para executar projetos Flask, testes ou qualquer outro comando Python garantindo que tudo rode no ambiente adequado e isolado do sistema.​

Para Aprofundar

Esta é uma visão introdutória do UV, focada nos conceitos básicos para iniciantes. Para explorar mais recursos avançados, personalização e exemplos detalhados, recomenda-se a leitura da documentação oficial, que está bem completa e constantemente atualizada em https://docs.astral.sh/uv/. Assim, você poderá aproveitar todo o potencial dessa poderosa ferramenta para gerenciar seus projetos Python com eficiência e facilidade.



Riverfount
Vicente Eduardo Ribeiro Marçal

Desde sua introdução oficial na versão 3.10 do Python, o pattern matching trouxe uma maneira estruturada e expressiva de lidar com fluxos condicionais complexos, especialmente ao trabalhar com dados estruturados como tuplas, listas, dicionários ou objetos. Para desenvolvedores experientes, explorar seus recursos avançados—como guards, padrões combinados OR/AND—e compreender suas limitações é fundamental para escrever códigos claros, eficientes e robustos.

Guardas: Condições Dinâmicas para Refinar Combinações

Guards são cláusulas condicionais if adicionadas depois do padrão básico, que só são avaliadas caso o pattern inicial case com o dado. Eles permitem filtrar ainda mais a correspondência com expressões arbitrárias.

Exemplo avançado classificando números com guards para regras detalhadas:

def classificar_numero(x):
    match x:
        case int() if x < 0:
            return "Número inteiro negativo"
        case int() if x == 0:
            return "Zero"
        case int() if x % 2 == 0:
            return "Número inteiro par"
        case int():
            return "Número inteiro ímpar"
        case float() if x.is_integer():
            return "Float que representa um inteiro"
        case float():
            return "Número float"
        case _:
            return "Não é um número"

Guards melhoram a legibilidade evitando ifs aninhados espalhados e permitem separar lógica granular enquanto mantêm a clareza do fluxo de decisão.

Combinações com Padrões OR e AND

Python permite combinar múltiplos padrões num mesmo case usando o operador | (OR). Isso evita duplicação de código quando múltiplos padrões devem resultar na mesma ação.

Exemplo com Enum usando OR para agrupar casos:

from enum import Enum

class Status(Enum):
    OK = 1
    WARNING = 2
    ERROR = 3

def tratar_status(status):
    match status:
        case Status.OK | Status.WARNING:
            print("Status aceitável")
        case Status.ERROR:
            print("Erro detectado")

Não existe operador direto para AND nos padrões; o comportamento AND deve ser implementado via guards, combinando várias condições:

def avaliar_evento(event):
    match event:
        case {"type": "click", "button": btn} if btn in ("left", "right"):
            print(f"Clique {btn} detectado")
        case _:
            print("Outro evento")

Limitações e Armadilhas Técnicas

  • Ordem dos casos é crucial: O match vai do topo para baixo, executando o primeiro case que casar. Padrões genéricos deveriam vir após os específicos para evitar capturas indesejadas.

  • Guards são avaliados após binding: Variáveis do pattern são ligadas antes do guard rodar. Mesmo que o guard retorne falso, as variáveis já foram criadas, podendo ter efeitos colaterais se seus valores forem usados inadvertidamente.

  • Sem fall-through: Cada case executa exclusivamente, diferente de switch/case em outras linguagens que permitem cair no próximo case.

  • Um único * por desempacotamento: A sintaxe não permite múltiplos nomes com * para capturar “resto” em padrões de sequência, limitando certas formas de combinar ou expandir sequências.

  • Complexidade prejudica legibilidade: O uso intensivo de múltiplos guards, ORs e outras combinações pode gerar padrões difíceis de entender e manter. Muitas vezes, dividir a lógica em funções auxiliares é preferível.

  • Guards com múltiplos padrões OR: Um guard não pode aplicar uma condição que dependa de múltiplos padrões OR simultaneamente, pois o binding de variáveis pode ocorrer em somente um dos padrões, causando inconsistência ou erros. Exemplo problemático:

def processar_tupla(t):
    match t:
        case (x, y) | (y, x) if x == y:
            print("Tupla simétrica")  # NÃO FUNCIONA como esperado
        case _:
            print("Outro caso")

Foi necessário replicar a lógica para casos diferentes ou desestruturar a lógica para manter clareza e correção.

Conclusão

Dominar pattern matching avançado com guards e padrões OR/AND eleva o nível do seu código Python, dando expressividade e eliminando estruturas condicionalmente complexas. Conhecer suas nuances e limitações ajuda a evitar armadilhas comuns que podem levar a bugs difíceis ou código difícil de manter, tornando seu código mais elegante, legível e eficiente diante de fluxos de dados complexos.



Riverfount
Vicente Eduardo Ribeiro Marçal

Em desenvolvimento de APIs REST, status codes HTTP são tão importantes quanto o payload da resposta. Eles comunicam, de forma padronizada, o resultado de cada requisição e são consumidos por clientes, gateways, observabilidade e ferramentas de monitoração. Apesar disso, ainda é comum encontrar código repleto de “números mágicos”, como 200, 404 ou 500 espalhados pela base.

Uma abordagem mais robusta é substituir esses valores literais por constantes descritivas, como HTTP_200_OK ou HTTP_404_NOT_FOUND. Essa prática aproxima o código das boas práticas de engenharia de software e melhora diretamente a legibilidade, a manutenção e a confiabilidade da API.

Constantes de status HTTP em Python

Frameworks modernos como FastAPI, Django REST Framework e outros já fornecem coleções de constantes ou enums para representar status codes HTTP. Isso cria um vocabulário padrão no código, evita ambiguidade e reduz dependência de “decorar” números.

Quando o framework ou stack utilizado não oferece esse mapeamento, é altamente recomendável criar o seu próprio módulo de constantes. Um exemplo simples é a criação do arquivo helper/status_code.py, que centraliza os códigos mais utilizados pela sua API. Esse arquivo funciona como um contrato semântico para a aplicação e pode ser adotado como padrão de time.

Exemplo de uso em código:

from helper import status_code

if response.status_code == status_code.HTTP_200_OK:
    print("Requisição bem-sucedida!")

Esse padrão é facilmente reconhecível por qualquer desenvolvedor que já tenha trabalhado com APIs, independentemente do backend ou da linguagem.

Legibilidade e intenção clara do código

Legibilidade é um dos pilares de um código de qualidade. Quando a base utiliza HTTP_201_CREATED em vez de 201, a intenção da resposta fica explícita.

Em uma revisão de código (code review), não é necessário parar para lembrar o que cada número significa. A constante descreve o comportamento desejado e reduz o esforço cognitivo de quem lê. Em times grandes ou em projetos que passam por muitas mãos, essa economia de contexto se traduz em menos dúvidas, menos ruído em revisões e onboarding mais rápido de novos membros.

Manutenção, consistência e redução de erros

Espalhar números mágicos na base aumenta o risco de inconsistência. É fácil um endpoint retornar 200 em um caso, 201 em outro cenário similar, ou ainda alguém digitar 2040 em vez de 204.

Com um módulo de constantes, você:

  • Centraliza a definição dos status codes.
  • Garante consistência sem depender da memória individual.
  • Facilita refinos pontuais de semântica (por exemplo, trocar um 200 por 204 quando a API deixa de retornar corpo em uma deleção).

Embora os valores dos status codes HTTP sejam padronizados e raramente mudem, o mapeamento semântico dentro da sua aplicação pode evoluir. Ter isso encapsulado em constantes torna essa evolução muito menos dolorosa.

Documentação viva e apoio das ferramentas

Constantes descritivas funcionam como documentação viva embutida no código. Em vez de manter uma tabela à parte em uma wiki, o próprio módulo de status codes evidencia quais valores são usados e com qual propósito.

Além disso, IDEs e ferramentas de desenvolvimento conseguem:

  • Sugerir autocompletar para HTTP_4xx ou HTTP_5xx.
  • Ajudar na navegação (go to definition) para entender onde e como os códigos são definidos.
  • Aumentar a segurança estática, evitando valores inválidos.

Tudo isso contribui para um fluxo de desenvolvimento mais seguro e produtivo.

Alinhamento com padrões de API e design de contratos

Do ponto de vista de design de APIs, status codes são parte do contrato público da sua interface. Tratá-los como constantes nomeadas reforça essa visão de contrato.

Alguns benefícios práticos:

  • Facilita a padronização de respostas entre diferentes microserviços.
  • Ajuda a manter alinhamento com guias internos de API (API Guidelines).
  • Simplifica a documentação em ferramentas como OpenAPI/Swagger, onde você pode mapear diretamente as constantes utilizadas no código para os status documentados.

Quando cada serviço expõe respostas coerentes – por exemplo, sempre usando HTTP_404_NOT_FOUND para recursos inexistentes e HTTP_422_UNPROCESSABLE_ENTITY para erros de validação –, consumidores conseguem tratar erros de forma genérica e previsível.

Integração com frameworks e bibliotecas

Em frameworks como FastAPI, é comum configurar o status code diretamente nos endpoints, muitas vezes usando constantes ou enums oriundos do próprio framework ou da biblioteca padrão de HTTP. Essa prática reduz acoplamento a valores “mágicos” e torna o código mais idiomático.

Mesmo que a stack atual não ofereça esse suporte nativamente, nada impede que você:

  • Crie um módulo helper/status_code.py com constantes alinhadas à especificação HTTP.
  • Use essas constantes tanto no backend quanto em testes automatizados, evitando duplicações.
  • Compartilhe o padrão com bibliotecas internas ou SDKs que consomem a API.

Assim, helper/status_code.py se torna um exemplo de padrão arquitetural que pode ser replicado em diferentes serviços e projetos.

Observabilidade, logs e debugging

Em ambientes de produção, a qualidade dos logs é fundamental para análise de incidentes. Status codes aparecem em traces, métricas e dashboards. Quando constantes descritivas são usadas na aplicação, é natural que o mesmo vocabulário apareça nas mensagens de log e no contexto de exceções.

Ver algo como “Falha ao chamar serviço externo: HTTP503SERVICE_UNAVAILABLE” é mais autoexplicativo do que apenas registrar “503”. Isso acelera o diagnóstico, reduz ambiguidade e diminui o tempo médio de resolução de incidentes (MTTR).

Conclusão prática para projetos Python

Para projetos Python que expõem APIs REST, adotar constantes de status code não é apenas um detalhe estético: é uma decisão de design que impacta legibilidade, manutenção, padronização e observabilidade.

  • Se o framework já fornece constantes, use-as.
  • Se não fornece, crie um módulo dedicado – como o helper/status_code.py – e estabeleça-o como padrão de equipe.
  • Garanta que todas as camadas que lidam com HTTP (handlers, services, middlewares, testes) utilizem as mesmas constantes.

Esse pequeno investimento inicial gera um retorno significativo ao longo do ciclo de vida da aplicação, tornando a base mais previsível, profissional e preparada para crescer com segurança.



Riverfount
Vicente Eduardo Ribeiro Marçal

Este artigo mostra como aplicar Abstract Base Classes (ABC) em um projeto real robusto, focado no desenvolvimento de microserviços. O objetivo é garantir clareza, contratos explícitos e extensibilidade, aliando os conceitos a práticas modernas.

Contexto do Projeto

Imagine um sistema de microserviços para gerenciamento de pedidos, em que diferentes serviços precisam manipular objetos que representam entidades diversas, como Pedido e Cliente. Queremos garantir que todas as entidades sigam um contrato explícito para operações comuns (ex.: obter ID, validação). Além disso, há um repositório genérico para armazenar dados dessas entidades com verificação de tipo.

Definição da ABC para Entidades

Criamos uma Abstract Base Class chamada Entity com métodos abstratos para garantir que toda entidade implemente os comportamentos necessários:

from abc import ABC, abstractmethod

class Entity(ABC):
    @abstractmethod
    def id(self) -> int:
        """Retorna o identificador único da entidade."""
        pass

    @abstractmethod
    def validate(self) -> bool:
        """Valida as regras de negócio da entidade."""
        pass

Implementação Concreta das Entidades

Exemplo de uma entidade Order (Pedido) que implementa a ABC e regras específicas:

class Order(Entity):
    def __init__(self, order_id: int, total: float) -> None:
        self._order_id = order_id
        self.total = total

    def id(self) -> int:
        return self._order_id

    def validate(self) -> bool:
        # Validação simples: total não pode ser negativo
        return self.total >= 0

Outro exemplo com Customer (Cliente):

class Customer(Entity):
    def __init__(self, customer_id: int, email: str) -> None:
        self._customer_id = customer_id
        self.email = email

    def id(self) -> int:
        return self._customer_id

    def validate(self) -> bool:
        # Validação simples: e-mail deve conter '@'
        return '@' in self.email

Repositório Genérico para Armazenar Entidades Validando Antes

A seguir, um repositório que aceita apenas entidades válidas, usando o tipo genérico limitado para Entity:

from typing import TypeVar, Generic, List

T = TypeVar('T', bound=Entity)

class Repository(Generic[T]):
    def __init__(self) -> None:
        self._items: List[T] = []

    def add(self, item: T) -> None:
        if not item.validate():
            raise ValueError(f"Invalid entity: {item}")
        self._items.append(item)

    def get_by_id(self, entity_id: int) -> T | None:
        for item in self._items:
            if item.id() == entity_id:
                return item
        return None

    def get_all(self) -> List[T]:
        return self._items

Uso Prático no Microserviço

def main():
    order_repo = Repository[Order]()
    customer_repo = Repository[Customer]()

    order = Order(1, 150.0)
    invalid_order = Order(2, -10.0)  # Total inválido

    customer = Customer(1, "user@example.com")
    invalid_customer = Customer(2, "invalid_email")  # E-mail inválido

    order_repo.add(order)
    try:
        order_repo.add(invalid_order)
    except ValueError as e:
        print(e)

    customer_repo.add(customer)
    try:
        customer_repo.add(invalid_customer)
    except ValueError as e:
        print(e)

    print("Pedidos:")
    for o in order_repo.get_all():
        print(f"ID: {o.id()}, Total: {o.total}")

    print("Clientes:")
    for c in customer_repo.get_all():
        print(f"ID: {c.id()}, Email: {c.email}")

if __name__ == "__main__":
    main()

Benefícios desse padrão no projeto real

  • Contratos explícitos: A ABC obriga à implementação dos métodos id e validate.

  • Segurança em tempo de execução: Objetos inválidos não serão adicionados ao repositório.

  • Reuso e manutenibilidade: O repositório é genérico e reutilizável com qualquer entidade.

  • Facilidade para testes: É simples isolar e testar entidades e repositórios separadamente.

  • Escalabilidade: Novas entidades podem ser criadas seguindo o contrato, sem mudanças na infraestrutura do repositório.

Conclusão

Esta abordagem demonstra o poder das ABCs combinadas com generics e tipagem avançada, garantindo sistemas Python mais estruturados, robustos e suscetíveis a erros minimizados, essenciais para microserviços confiáveis e manteníveis. Se desejar, posso aprofundar a integração com outros padrões ou frameworks.



Riverfount
Vicente Eduardo Ribeiro Marçal

Este artigo aborda como usar funcionalidades avançadas de tipagem em Python, como Protocols, Generics e técnicas avançadas de typing, para criar aplicações escaláveis, flexíveis e de fácil manutenção.

Protocols: Contratos Flexíveis e Estruturais

Protocols permitem definir contratos de métodos e propriedades sem herança explícita, facilitando a interoperabilidade entre microserviços. Qualquer classe que implemente os métodos definidos no protocolo pode ser usada onde esse protocolo é esperado.

Exemplo prático:

from typing import Protocol

class Serializer(Protocol):
    def serialize(self) -> bytes:
        pass

class JsonSerializer:
    def serialize(self) -> bytes:
        return b'{"user": "alice"}'

class XmlSerializer:
    def serialize(self) -> bytes:
        return b'<user>alice</user>'

def send_data(serializer: Serializer) -> None:
    data = serializer.serialize()
    print(f"Enviando dados: {data}")

send_data(JsonSerializer())
send_data(XmlSerializer())

Neste exemplo, send_data aceita qualquer objeto que implemente o método serialize, garantindo baixo acoplamento e flexibilidade.

Generics: Componentes Reutilizáveis e Tipados

Generics permitem criar classes e funções genéricas que mantêm a segurança de tipos, facilitando a modularidade.

Exemplo de repositório genérico:

from typing import TypeVar, Generic, List

T = TypeVar('T')

class Repository(Generic[T]):
    def __init__(self) -> None:
        self._items: List[T] = []

    def add(self, item: T) -> None:
        self._items.append(item)

    def get_all(self) -> List[T]:
        return self._items

class User:
    def __init__(self, username: str) -> None:
        self.username = username

user_repo = Repository[User]()
user_repo.add(User("alice"))
for user in user_repo.get_all():
    print(user.username)

Este padrão permite criar repositórios ou caches que funcionam com qualquer tipo de objeto, aumentando a reutilização e segurança de tipos.

Tipagem Avançada: Operador | e Literal

Prefira o operador | para tipos alternativos ao invés de Union e use Literal para valores fixos, reforçando contratos claros.

Exemplo:

from typing import Literal

def login(role: Literal['admin', 'user', 'guest']) -> str:
    if role == 'admin':
        return "Acesso total"
    elif role == 'user':
        return "Acesso limitado"
    return "Acesso restrito"

print(login('admin'))  # Acesso total

Isso aumenta a legibilidade e reduz riscos de erro nas chamadas de função.

Conclusão

Combinando Protocols, Generics e tipagem avançada, é possível construir aplicações com contratos claros, flexíveis e robustos, facilitando o trabalho em times desacoplados e a manutenção do código.

Essas práticas elevam a qualidade do código e tornam os sistemas mais escaláveis e confiáveis, sendo indispensáveis para desenvolvedores focados em arquiteturas modernas, principalmente as de microserviços.



Riverfount
Vicente Eduardo Ribeiro Marçal