graphlib no Python: ordenação topológica

Publicado em: 27/07/2026
Tempo de leitura: 6 minutos
Grafo de dependências e fluxo de tarefas em Python

Em muitos projetos, algumas tarefas só podem começar depois que outras terminam. Um pacote depende de bibliotecas, uma migração depende da anterior, uma etapa de ETL precisa aguardar a extração e um curso pode exigir disciplinas prévias. Esse tipo de relação forma um grafo direcionado de dependências. O módulo graphlib no Python, disponível na biblioteca padrão, oferece a classe TopologicalSorter para organizar esses itens em uma ordem válida e detectar ciclos que tornam a execução impossível.

Neste guia, você aprenderá a modelar dependências, gerar uma ordenação topológica, processar tarefas em paralelo, tratar ciclos e testar o resultado. O assunto se conecta a estruturas e técnicas já usadas em contextvars no Python, filas de prioridade com heapq, cached_property e singledispatch.

O que é ordenação topológica?

Uma ordenação topológica é uma sequência linear de vértices de um grafo direcionado na qual cada dependência aparece antes do item que depende dela. Se a tarefa publicar depende de testar, então testar deve surgir primeiro. Essa ordem só existe quando o grafo não possui ciclos direcionados.

Um ciclo ocorre quando A depende de B, B depende de C e C depende novamente de A. Nesse cenário, nenhuma tarefa pode ser a primeira, porque todas aguardam outra etapa do mesmo ciclo. O TopologicalSorter detecta essa condição e lança CycleError.

Como representar dependências

A classe recebe um dicionário em que cada chave é um nó e o valor é um conjunto ou iterável com seus predecessores. Em outras palavras, você descreve de quais itens cada tarefa depende:

from graphlib import TopologicalSorter

dependencias = {
    "publicar": {"testar"},
    "testar": {"compilar"},
    "compilar": {"instalar_dependencias"},
    "instalar_dependencias": set(),
}

ordem = list(TopologicalSorter(dependencias).static_order())
print(ordem)

O resultado respeita todas as relações. A ordem exata pode variar quando existem tarefas independentes, pois mais de uma sequência pode ser válida.

Usando static_order

Para fluxos simples e sequenciais, static_order() é a API mais direta. Ela prepara o grafo, verifica ciclos e devolve um iterador com os nós em ordem topológica:

from graphlib import TopologicalSorter

etapas = {
    "enviar_email": {"gerar_relatorio"},
    "gerar_relatorio": {"consultar_banco", "carregar_config"},
    "consultar_banco": {"carregar_config"},
    "carregar_config": set(),
}

for etapa in TopologicalSorter(etapas).static_order():
    print(f"Executando: {etapa}")

Essa abordagem é adequada quando cada etapa deve terminar antes da próxima, quando o custo é baixo ou quando você apenas precisa validar a estrutura de dependências.

Adicionando nós gradualmente

Também é possível criar o objeto vazio e chamar add(). O primeiro argumento é o nó; os seguintes são predecessores:

from graphlib import TopologicalSorter

ts = TopologicalSorter()
ts.add("build", "lint", "test")
ts.add("deploy", "build")
ts.add("lint")
ts.add("test", "install")
ts.add("install")

print(list(ts.static_order()))

Chamadas repetidas para o mesmo nó acumulam dependências. Isso é útil quando diferentes partes da aplicação registram etapas de um pipeline. No entanto, depois que o grafo é preparado, ele não deve mais ser modificado.

Processamento paralelo

A maior vantagem do TopologicalSorter aparece quando várias tarefas independentes podem executar ao mesmo tempo. Para isso, use prepare(), get_ready(), done() e is_active().

from graphlib import TopologicalSorter

etapas = {
    "deploy": {"testes", "build_frontend"},
    "testes": {"instalar"},
    "build_frontend": {"instalar"},
    "instalar": set(),
}

ts = TopologicalSorter(etapas)
ts.prepare()

while ts.is_active():
    prontas = ts.get_ready()
    for tarefa in prontas:
        print("Executando", tarefa)
        # execute a tarefa e aguarde a conclusão
        ts.done(tarefa)

No exemplo, depois de instalar, as tarefas testes e build_frontend ficam prontas simultaneamente. Em um sistema real, elas podem ser enviadas para threads, processos ou trabalhadores distribuídos. O método done() informa que uma tarefa terminou e libera seus sucessores.

Exemplo com ThreadPoolExecutor

Quando as etapas envolvem operações de entrada e saída, um pool de threads pode aproveitar o paralelismo disponível:

from concurrent.futures import ThreadPoolExecutor, wait, FIRST_COMPLETED
from graphlib import TopologicalSorter
import time

etapas = {
    "publicar": {"testar", "documentar"},
    "testar": {"compilar"},
    "documentar": {"compilar"},
    "compilar": {"baixar"},
    "baixar": set(),
}

def executar(nome):
    print("início", nome)
    time.sleep(0.2)
    print("fim", nome)
    return nome

ts = TopologicalSorter(etapas)
ts.prepare()

com_futuro = {}
with ThreadPoolExecutor(max_workers=3) as executor:
    while ts.is_active():
        for tarefa in ts.get_ready():
            futuro = executor.submit(executar, tarefa)
            com_futuro[futuro] = tarefa

        concluidos, _ = wait(com_futuro, return_when=FIRST_COMPLETED)
        for futuro in concluidos:
            tarefa = com_futuro.pop(futuro)
            futuro.result()
            ts.done(tarefa)

Em produção, trate exceções antes de chamar done(). Uma tarefa com erro não deve liberar automaticamente etapas que dependem dela. Você pode cancelar o fluxo, registrar a falha ou aplicar uma política de repetição.

Detectando ciclos

O método prepare() e o iterador de static_order() detectam ciclos. A exceção CycleError inclui informações sobre um ciclo encontrado:

from graphlib import CycleError, TopologicalSorter

grafo = {
    "a": {"c"},
    "b": {"a"},
    "c": {"b"},
}

try:
    ordem = list(TopologicalSorter(grafo).static_order())
except CycleError as erro:
    print("Dependência circular:", erro.args)

Mesmo quando existe um ciclo, o objeto pode liberar nós que não participam dele antes de ficar bloqueado. Para ferramentas de diagnóstico, isso permite processar partes válidas e depois informar o conjunto problemático.

Dependências ausentes

Se um predecessor é mencionado, mas não aparece como chave, ele é adicionado automaticamente como nó sem predecessores. Esse comportamento é conveniente, porém pode esconder erros de digitação. Se você escrever configuracao em um lugar e configuração em outro, o grafo terá dois nós distintos.

Uma validação prévia pode comparar todos os nomes com um catálogo autorizado:

catalogo = {"config", "extrair", "transformar", "carregar"}

dependencias = {
    "transformar": {"extrair"},
    "carregar": {"transformar", "config"},
}

referenciados = set(dependencias)
for predecessores in dependencias.values():
    referenciados.update(predecessores)

desconhecidos = referenciados - catalogo
if desconhecidos:
    raise ValueError(f"Tarefas desconhecidas: {desconhecidos}")

Usando objetos como nós

Os nós precisam ser hashable, mas não precisam ser strings. Enums, tuplas e objetos imutáveis funcionam bem. Uma dataclass congelada ajuda a carregar metadados:

from dataclasses import dataclass
from graphlib import TopologicalSorter

@dataclass(frozen=True)
class Tarefa:
    nome: str
    equipe: str

extrair = Tarefa("extrair", "dados")
validar = Tarefa("validar", "qualidade")
publicar = Tarefa("publicar", "plataforma")

grafo = {
    validar: {extrair},
    publicar: {validar},
}

for tarefa in TopologicalSorter(grafo).static_order():
    print(tarefa.nome, tarefa.equipe)

Evite objetos mutáveis cujo hash possa mudar. Alterar um campo usado em igualdade ou hash depois que ele entrou no grafo torna o comportamento imprevisível.

Aplicações práticas

A ordenação topológica aparece em vários contextos:

  • resolução da ordem de instalação de pacotes;
  • pipelines de dados e machine learning;
  • migrações de banco de dados;
  • compilação de módulos;
  • planejamento de tarefas com pré-requisitos;
  • geração de planilhas e relatórios dependentes;
  • ordenação de plugins e serviços;
  • validação de currículos acadêmicos.

O módulo resolve a ordem, mas não oferece persistência, filas distribuídas, repetição automática nem controle de recursos. Para fluxos grandes, ele pode atuar como núcleo de planejamento combinado com um executor.

Como testar

Não teste apenas uma sequência fixa quando várias ordens são possíveis. Teste a propriedade: todo predecessor deve aparecer antes do nó dependente.

from graphlib import TopologicalSorter

def validar_ordem(grafo):
    ordem = list(TopologicalSorter(grafo).static_order())
    posicao = {no: indice for indice, no in enumerate(ordem)}

    for no, predecessores in grafo.items():
        for anterior in predecessores:
            assert posicao[anterior] < posicao[no]

validar_ordem({
    "deploy": {"teste", "build"},
    "teste": {"install"},
    "build": {"install"},
    "install": set(),
})

Crie ainda testes para grafo vazio, nó isolado, múltiplas dependências, ciclo simples, ciclo indireto e falha de uma tarefa executada em paralelo.

Erros comuns

  • Inverter o formato e informar sucessores em vez de predecessores.
  • Assumir que a ordem entre tarefas independentes será sempre a mesma.
  • Esquecer de chamar done() no processamento incremental.
  • Chamar done() antes de a tarefa realmente terminar.
  • Modificar o grafo depois de prepare().
  • Ignorar exceções de trabalhadores e liberar dependentes incorretamente.
  • Usar nomes inconsistentes e criar nós fantasmas.
  • Esperar que a classe execute as tarefas por conta própria.

Boas práticas

  • Use identificadores imutáveis e consistentes.
  • Valide nós desconhecidos antes de preparar o grafo.
  • Registre dependências em uma única direção: nó para predecessores.
  • Separe planejamento, execução e persistência.
  • Defina a política para falhas antes de paralelizar.
  • Limite concorrência conforme CPU, memória e serviços externos.
  • Registre início, fim, duração e resultado de cada tarefa.
  • Teste propriedades do grafo, não apenas uma ordem específica.

Conclusão

O graphlib no Python transforma relações de dependência em uma ordem executável sem exigir bibliotecas externas. Com static_order(), você resolve fluxos sequenciais de forma simples. Com prepare(), get_ready(), done() e is_active(), pode liberar tarefas independentes para execução concorrente.

A parte mais importante é modelar corretamente os predecessores e tratar ciclos como erro de configuração. O TopologicalSorter não substitui um orquestrador completo, mas oferece uma base confiável para instaladores, pipelines, compiladores, migrações e qualquer sistema em que a ordem dependa de pré-requisitos.

Para detalhes de API e comportamento entre versões, consulte a documentação oficial de graphlib e a documentação de concurrent.futures.

Compartilhe:

Facebook
WhatsApp
Twitter
LinkedIn

Conteúdo do artigo

    Artigos relacionados

    Código Python para contexto seguro em aplicações assíncronas
    Python Avançado
    Foto de perfil de Leandro Hirt da Academify

    contextvars no Python: contexto seguro

    Aprenda a usar contextvars no Python para isolar contexto em asyncio, logs, threads e testes sem depender de variáveis globais.

    Ler mais

    Tempo de leitura: 7 minutos
    26/07/2026
    Código Python usando cached_property para armazenar cálculos
    Python Avançado
    Foto de perfil de Leandro Hirt da Academify

    cached_property no Python: cache em objetos

    Aprenda cached_property no Python para armazenar cálculos caros, invalidar valores e evitar caches desatualizados em objetos.

    Ler mais

    Tempo de leitura: 7 minutos
    25/07/2026
    Código Python com funções especializadas por tipo
    Python Avançado
    Foto de perfil de Leandro Hirt da Academify

    singledispatch no Python: polimorfismo simples

    Aprenda singledispatch no Python para criar funções por tipo, reduzir isinstance e organizar polimorfismo com exemplos práticos.

    Ler mais

    Tempo de leitura: 6 minutos
    25/07/2026
    Código Python com descriptors e atributos
    Python Avançado
    Foto de perfil de Leandro Hirt da Academify

    Descriptors em Python: guia prático

    Aprenda descriptors em Python com __get__, __set__, validação, property, armazenamento por instância, testes e boas práticas.

    Ler mais

    Tempo de leitura: 6 minutos
    22/07/2026
    Criando instalador EXE com ícone personalizado em Python
    Python Avançado
    Foto de perfil de Leandro Hirt da Academify

    Como criar um instalador .exe com ícone personalizado no Python

    Se você já desenvolveu algum script útil, provavelmente já se perguntou como criar um instalador .exe com ícone personalizado no

    Ler mais

    Tempo de leitura: 11 minutos
    25/04/2026
    Herança múltipla em Python sem causar problemas no código
    Python Avançado
    Foto de perfil de Leandro Hirt da Academify

    Como usar herança múltipla no Python sem bugar seu código

    Entender como usar herança múltipla no Python sem bugar seu código é um dos grandes marcos na jornada de qualquer

    Ler mais

    Tempo de leitura: 9 minutos
    21/04/2026