O Modelo Mental de Arquitetura para Serviços Python
Arquitetura, uma vez que você deixa de lado a escolha do framework e os debates sobre nomenclatura de pastas, é apenas o conjunto de decisões em uma base de código Python que são caras de reverter - qual módulo pode importar qual outro módulo, onde fica um limite de Protocol, se a lógica de domínio conhece um modelo ORM.
As outras páginas desta seção cobrem cada peça disso em profundidade - Layout src e Estrutura de Pacotes, Arquitetura Limpa / Hexagonal, Modelagem de Domínio, Injeção de Dependência e Conexão, Configuração e Ajustes.
Esta página é a camada abaixo de todas elas: o que realmente torna uma base de código Python fácil ou difícil de mudar, expresso em termos que o próprio Python torna literal - o grafo de importação - em vez de diagramas abstratos emprestados da cultura de arquitetura de outra linguagem.
Resumo
- Em Python, o grafo de importação é a arquitetura - cada
importé uma seta de dependência, e a direção dessas setas determina o que pode mudar independentemente do quê. - Por que Importa: O sistema flexível de importação do Python torna limites fracos invisíveis até que eles apareçam como importações circulares, módulos-deuses (god-modules) ou "por que esta mudança de uma linha toca doze arquivos".
- Conceitos Chave: acoplamento (coupling), coesão (cohesion), direção da dependência, inversão de dependência, raiz de composição (composition root), grafo de importação.
- Quando Usar: Decidir onde um limite de pacote pertence, se uma porta
Protocolvale a pena a indireção, como conectar adaptadores concretos a dependências abstratas na inicialização, ou se o código de domínio deve importar um tipo de framework ou ORM. - Limitações / Trade-offs: Cada limite que reduz o acoplamento - um
Protocol, um pacote extra, uma raiz de composição - adiciona um arquivo que você precisa ler para entender o que realmente acontece, e a tipagem dinâmica do Python significa que esses limites são tão fortes quanto suas ferramentas e disciplina os impõem. - Tópicos Relacionados: layout
src, arquitetura hexagonal, injeção de dependência, modelagem de domínio, importações circulares.
Fundamentos
Duas propriedades determinam quase tudo sobre se uma base de código Python permanece fácil de mudar: acoplamento (coupling) - o quanto um módulo depende dos internos de outro módulo em vez de sua interface declarada - e coesão (cohesion) - o quanto as coisas dentro de um módulo realmente pertencem à mesma responsabilidade.
O objetivo é o mesmo que a arquitetura busca em qualquer linguagem: alta coesão dentro de um módulo, baixo acoplamento entre módulos - mas o Python torna o lado do acoplamento incomumente visível, porque cada instrução import é uma seta de dependência literal e inspecionável, não um relacionamento implícito que você tem que inferir de um diagrama.
Uma analogia útil: pense em pacotes como apartamentos em um prédio, cada um com suas próprias utilidades e exatamente uma porta de entrada - a API pública do módulo, o que quer que ele exponha através de __init__.py e __all__. Um pacote bem delimitado permite que você renove seu interior sem que um vizinho perceba, porque nada externo alcança através das paredes; um mal delimitado tem moradores alcançando constantemente os apartamentos uns dos outros para um fusível sobressalente, então ninguém pode mudar nada sem verificar três outras unidades primeiro.
# domain/orders.py - alcançando um fusível sobressalente através da parede
from adapters.sqlite_orders import SqliteOrderRepository # o domínio agora sabe que sqlite existe
def place_order(order_id: str, total_cents: int) -> None:
repo = SqliteOrderRepository() # não é possível testar sem um arquivo de banco de dados real
repo.save(order_id, total_cents)Mecânicas e Interações
Direção da dependência é onde isso se torna uma regra concreta e verificável em vez de uma preferência: se domain/orders.py importa adapters/sqlite_orders.py, a seta aponta do domínio para a infraestrutura, e trocar SQLite por Postgres agora significa tocar na lógica de negócios, não apenas no adaptador.
Esta é exatamente a causa mecânica de um dos bugs estruturais mais comuns do Python. Uma importação circular - ImportError: cannot import name X from partially initialized module Y - não é realmente uma peculiaridade do interpretador para contornar; é o grafo de importação dizendo, em tempo de execução, que dois módulos acham que precisam de algo que o outro possui, o que é acoplamento tornado visível como uma falha em vez de permanecer oculto como atrito em câmera lenta. Cenários de Importação Circular cobre a mecânica de corrigir o sintoma - esta página é sobre por que o sintoma existe em primeiro lugar: dois módulos no lado errado de uma seta de dependência.
Inversão de dependência é a técnica que corrige a direção em vez de apenas o sintoma: em vez do domínio importar um adaptador concreto, o domínio define um typing.Protocol descrevendo o que ele precisa, e o adaptador implementa essa forma.
from typing import Protocol
class OrderRepository(Protocol):
def save(self, order_id: str, total_cents: int) -> None: ...
def place_order(order_id: str, total_cents: int, repo: OrderRepository) -> None:
repo.save(order_id, total_cents) # o domínio nunca importa sqlite, fastapi, ou qualquer coisa concretaA tipagem estrutural do Python torna isso mais barato do que em linguagens que exigem uma hierarquia de classes explícita: qualquer objeto com um método save correspondente satisfaz OrderRepository, então um teste pode passar para place_order um simulador em memória com zero herança e zero framework de mock.
Alguém ainda tem que conectar o adaptador concreto à porta abstrata, e isso acontece em exatamente um lugar: a raiz de composição - um main(), um bloco lifespan do FastAPI, um hook app-ready do Django - onde objetos concretos são construídos e entregues às funções e classes que só conhecem Protocols. Todos os outros lugares na base de código devem receber dependências, não construí-las.
Considerações Avançadas e Aplicações
Layout src e Estrutura de Pacotes é onde a arquitetura deixa de ser uma convenção de revisão de código e se torna algo que a CI realmente impõe: um layout plano permite que a raiz do repositório satisfaça importações durante o desenvolvimento local, mesmo quando os metadados de empacotamento estão quebrados, enquanto src/ força uma instalação (pip install -e . ou uv sync) antes que qualquer coisa possa importar o pacote - então uma dependência ausente ou uma entrada incorreta em pyproject.toml falha na CI, não três semanas depois em uma imagem Docker.
Configuração merece o mesmo tratamento de inversão de dependência que qualquer outra preocupação de infraestrutura: código que chama os.environ.get(...) espalhado pelos módulos de domínio está tão acoplado a "o ambiente" quanto código que importa SQLite diretamente está acoplado a um banco de dados - Configuração e Ajustes cobre a construção de um objeto de configurações tipado na raiz de composição e sua injeção, em vez de permitir que cada módulo alcance sua própria variável de ambiente.
Nem toda base de código Python deve pagar pela separação hexagonal completa, e isso é um trade-off real, não um compromisso. O estilo "modelo gordo" do Django - lógica de negócios vivendo diretamente em modelos ORM - é uma escolha legítima e produtiva para aplicações CRUD pesadas onde o esquema do banco de dados e o modelo de domínio são genuinamente a mesma forma; a separação hexagonal ganha sua indireção especificamente quando as regras de domínio são complexas o suficiente, ou esperadas para sobreviver ao framework ou banco de dados atual por tempo suficiente, que testá-las sem infraestrutura se torna valioso por si só.
A progressão de um script plano, para um aplicativo em camadas por convenção, para limites de módulo impostos, para portas e adaptadores hexagonais completos, mapeia diretamente para o custo de mudança: cada etapa reduz o custo de mudar uma parte isoladamente e aumenta o custo fixo do próprio mecanismo de limite, então uma equipe que paga por portas Protocol em um script interno de 200 linhas está pagando custo puro, e uma equipe sem elas em um serviço com lógica de domínio pesada e três mecanismos de entrega está pagando custo puro da outra forma.
| Abordagem | Força | Fraqueza | Melhor Ajuste |
|---|---|---|---|
| Script plano / módulo único | Mais rápido de escrever, zero indireção | O acoplamento cresce invisivelmente à medida que o arquivo cresce | Protótipos, scripts únicos, notebooks |
Camadas por convenção (pastas domain/, api/, adapters/, sem imposição) | Intenção clara, ainda ferramentas simples | Nada impede um módulo de domínio de importar um adaptador diretamente | Equipes pequenas, serviços em estágio inicial |
Layout src + limites de importação impostos (regras ruff/import-linter) | Empacotamento ruim e importações entre camadas falham na CI, não em um laptop | Requer etapa de instalação e configuração do linter para realmente impor | Qualquer serviço instalável ou biblioteca distribuída via CI/Docker |
Hexagonal (portas Protocol + adaptadores) | Domínio testável sem banco de dados, independente de framework | Uma interface extra para ler mesmo para os casos simples | Regras de domínio complexas, múltiplos mecanismos de entrega (API + CLI + worker) |
Conceitos Equivocados Comuns
- "O layout
srcé apenas uma pasta extra sem motivo." É um mecanismo de imposição - força uma instalação editável antes que qualquer coisa possa importar o pacote, o que é o que expõe metadados de empacotamento quebrados na CI em vez de apenas em uma compilação Docker semanas depois. - "Portas baseadas em
Protocolsão um excesso de engenharia estilo Java para uma linguagem dinâmica." A tipagem estrutural do Python torna umProtocolmais barato que uma interface na maioria das linguagens com tipagem estática - sem herança, sem hierarquia, apenas uma forma que um simulador ou um adaptador real pode satisfazer para testes. - "Importações circulares são uma peculiaridade do Python que você conserta com uma importação local dentro da função." Uma importação local pode silenciar a falha, mas a causa subjacente - dois módulos dependendo cada um da responsabilidade do outro - é um problema de acoplamento que a importação local esconde em vez de resolver.
- "Modelos 'gordos' do Django sempre são má arquitetura." Para aplicações CRUD pesadas onde o esquema e o modelo de domínio têm a mesma forma, modelos gordos são uma escolha razoável e produtiva - a separação hexagonal é um trade-off que compensa especificamente quando a complexidade do domínio ou a rotatividade da infraestrutura justificam a indireção extra.
- "Você precisa de um framework de injeção de dependência para fazer DI em Python." Injeção de construtor mais uma raiz de composição (um
main(), um blocolifespando FastAPI) cobre a maioria dos serviços Python; um container de DI adiciona valor real principalmente quando os grafos de conexão ficam grandes o suficiente para que a construção manual se torne seu próprio fardo de manutenção.
FAQs
O que "arquitetura" significa concretamente para uma base de código Python?
O conjunto de decisões de direção de importação e limites que são caras de reverter - qual módulo pode importar qual, onde fica uma porta Protocol, se a lógica de domínio conhece um framework ou ORM. Todo o resto é código comum que é barato de mudar.
Por que o grafo de importação é descrito como "a arquitetura" em vez de apenas um detalhe dela?
Porque em Python, cada instrução import é uma seta de dependência real e inspecionável - não é necessário um diagrama de arquitetura separado para ver o acoplamento, já que python -c "import ast; ..." ou qualquer ferramenta de grafo de importação pode mostrar a estrutura de dependência real diretamente do código.
Uma importação circular é sempre um sinal de má arquitetura?
Quase sempre, sim - significa que dois módulos acham que precisam de algo que o outro possui, o que é acoplamento tornado visível como um erro de tempo de execução. Uma importação local e adiada pode silenciar a falha, mas o problema subjacente de direção ainda está lá até que um módulo pare de depender dos internos do outro.
Por que usar typing.Protocol em vez de uma classe base abstrata para portas?
Protocol fornece tipagem estrutural - qualquer objeto com métodos correspondentes o satisfaz, sem necessidade de herança - o que é mais barato para testes (um objeto simulado simples funciona) e não força uma hierarquia de classes em adaptadores que já podem herdar de outra coisa, como uma classe base ORM.
O que é uma raiz de composição, concretamente?
O único lugar em um serviço - uma função main(), um bloco lifespan do FastAPI, um hook app-ready do Django - onde objetos concretos (uma conexão de banco de dados, um repositório real) são construídos e entregues a código que só conhece tipos Protocol abstratos. Todos os outros lugares devem receber dependências, não construí-las.
O layout `src` realmente muda como o código roda, ou apenas onde os arquivos ficam?
Ambos, na prática - a localização física muda (src/mypackage/ em vez de um pacote de nível superior), mas a mudança real é comportamental: força pip install -e . ou uv sync antes que as importações sejam resolvidas, o que é o que captura metadados de empacotamento ausentes que um layout plano ocultaria silenciosamente durante o desenvolvimento local.
Quando um "modelo gordo" do Django é uma arquitetura razoável, não um atalho?
Quando a aplicação é genuinamente focada em CRUD e o esquema do banco de dados já corresponde de perto ao modelo de domínio - ferramentas administrativas, dashboards internos - colocar regras de negócios diretamente em modelos ORM evita indireções que não estariam pagando por nada nesse contexto.
Como a configuração se encaixa na inversão de dependência?
Da mesma forma que um banco de dados ou um cliente HTTP - código que chama os.environ.get(...) em módulos de domínio está acoplado ao "ambiente" da mesma forma que código que importa SQLite diretamente está acoplado a um banco de dados. Construir um objeto de configurações tipado na raiz de composição e injetá-lo mantém essa dependência explícita e substituível.
Sempre vale a pena a indireção extra para adicionar uma porta `Protocol`?
Não - para um script pequeno ou uma ferramenta descartável, uma dependência concreta direta é mais simples e a interface "extra" é custo puro. Ela compensa seu custo especificamente quando a lógica de domínio por trás dela é complexa o suficiente, ou esperada para sobreviver à escolha de infraestrutura atual por tempo suficiente, que testá-la isoladamente importa.
Como sei se os limites do meu pacote são impostos ou apenas convencionais?
Pergunte se algo realmente impede uma violação - uma regra de linter bloqueando importações profundas entre pacotes, um layout src exigindo uma instalação, uma verificação de grafo de importação na CI - versus nada além de um entendimento compartilhado de "nós não fazemos isso aqui", que se desgasta sob pressão de prazo.
Preciso de um framework/container de DI em Python?
Geralmente não para tamanhos de serviço típicos - injeção de construtor manual mais uma raiz de composição lida com a maioria dos grafos de conexão. Um container começa a se pagar assim que o número de dependências e seus tempos de vida (singleton vs por requisição) se torna grande o suficiente para que a conexão manual seja difícil de ler.
Por que a direção da dependência importa mais à medida que uma equipe cresce?
Porque o limite imposto de um módulo geralmente também é um limite de propriedade - quando a API pública de um pacote é estreita e imposta, uma equipe pode mudar seus internos sem coordenar com todas as outras equipes que tocam na base de código, enquanto limites fracos forçam a coordenação mesmo quando as responsabilidades subjacentes não se sobrepõem de fato.
Relacionados
- Layout
srce Estrutura de Pacotes - o mecanismo físico de aplicação por trás dos limites de pacotes - Arquitetura Limpa / Hexagonal - portas
Protocole adaptadores em profundidade total - Modelagem de Domínio - o que pertence dentro do limite que esta página descreve
- Injeção de Dependência e Conexão - o padrão de raiz de composição na prática
- Configuração e Ajustes - invertendo o ambiente como uma dependência
- Cenários de Importação Circular - o sintoma em tempo de execução de uma direção de dependência incorreta
Versões da Stack: Esta página é conceitual e não está vinculada a uma versão específica da stack.