2 pontos por GN⁺ 2025-10-04 | 1 comentários | Compartilhar no WhatsApp
  • Em Python, a convenção mais comum é declarar todos os imports no nível do módulo
  • Porém, ao executar o programa, até módulos de dependências desnecessárias acabam sendo carregados imediatamente, causando problemas de velocidade de inicialização e uso de memória
  • Até agora, o uso de imports manuais com atraso, como imports dentro de funções, era comum, mas tinha a desvantagem de dificultar a manutenção e o gerenciamento de dependências
  • Este PEP 810 introduz uma sintaxe de import explícito com carregamento tardio com a nova palavra-chave lazy, que é local, explícita, controlada e granular
  • Com esse recurso, os módulos são carregados somente quando realmente necessários, melhorando latência de inicialização e desperdício de memória ao mesmo tempo em que mantém a transparência da estrutura do código

Situação atual dos imports em Python e seus problemas

  • Em Python, é amplamente adotada a prática de escrever os imports no topo do módulo
  • Essa abordagem reduz duplicação, permite entender de relance a estrutura de dependências dos imports e minimiza overhead em tempo de execução ao importar apenas uma vez
  • No entanto, quando o programa é executado e o primeiro módulo (main) é carregado, é fácil ocorrer uma cadeia de imports em que até muitos módulos de dependência que não serão usados acabam sendo lidos imediatamente
  • Isso é especialmente problemático em ferramentas CLI, nas quais até chamar apenas a ajuda completa pode pré-carregar dezenas de módulos, gerando overhead desnecessário em todos os subcomandos

Alternativas existentes e seus problemas

  • É comum usar abordagens manuais para adiar o momento do import, como mover imports para dentro de funções
  • Porém, essa abordagem tem grandes desvantagens, como perda de consistência e manutenção, além de aumentar a dificuldade de entender todas as dependências
  • Segundo a análise da biblioteca padrão, em códigos sensíveis a desempenho, cerca de 17% de todos os imports já são usados dentro de funções ou métodos com o objetivo de adiar imports
  • Existem ferramentas relacionadas a import tardio, como importlib.util.LazyLoader e o pacote third-party lazy_loader, mas elas não cobrem todos os casos ou carecem de um padrão único

PEP 810: introdução de imports explícitos com carregamento tardio

  • Introdução da nova palavra-chave suave lazy (ela só tem significado em contextos específicos e também pode ser usada como nome de variável)

  • lazy só pode ser usado antes da instrução import e não pode ser usado em escopos como função/classe/with/try nem em star import

  • O comportamento é aplicado claramente por instrução de import, adiando o carregamento do módulo até o momento de uso

    lazy import 모듈명
    lazy from 모듈명 import 이름
    

Forma de implementação dos imports explícitos com carregamento tardio e regras sintáticas

  • Casos de erro de sintaxe:

    • Não é permitido dentro de funções, classes, try/with nem em star import (*)
  • Exemplo de uso:

    import sys
    lazy import json
    print('json' in sys.modules)  # False (ainda não carregado)
    result = json.dumps({"hello": "world"})  # carrega no primeiro uso
    print('json' in sys.modules)  # True (carregamento do módulo tardio concluído)
    
  • Também é possível declarar os alvos lazy no nível do módulo por meio do atributo __lazy_modules__, como uma lista de strings

    __lazy_modules__ = ["json"]
    import json  # tratado como lazy
    

Controle de comportamento com flag global e filtro

  • É possível controlar se o lazy será aplicado por módulo ou globalmente usando uma flag global ou uma função de filtro

  • Também é possível aplicar exceções de eager import apenas a módulos específicos com uma função de filtro

    def my_filter(importer, name, fromlist):
        if name in {'problematic_module'}:
            return False  # eager import
        return True  # lazy import
    sys.set_lazy_imports_filter(my_filter)
    

Comportamento em runtime e tratamento de erros

  • Ao usar lazy import, o import real acontece não no ponto da instrução, mas no primeiro acesso ao nome

  • Se o import falhar, o encadeamento de exceções (traceback chaining) mostra com clareza tanto o local da definição quanto o local em que ocorreu o acesso

    lazy from json import dumsp  # erro de digitação
    result = dumsp({"key": "value"})  # ImportError ocorre no momento do acesso real
    

Benefícios de memória e desempenho

  • Módulos adiados aparecem apenas no conjunto sys.lazy_modules e não são registrados em sys.modules antes do uso real
  • Depois do uso, eles são substituídos por objetos de módulo normais e podem ser usados sem penalidade adicional de desempenho
  • Em cargas de trabalho reais, observou-se redução de 50% a 70% na latência de inicialização e economia de 30% a 40% de memória

Resumo do funcionamento

  • No primeiro acesso ao objeto lazy ocorre a reification (import real e substituição)
  • Quando um código externo acessa o __dict__ do módulo, todos os objetos lazy são carregados à força (reification)
  • Ao extrair o dicionário com globals(), o proxy lazy é mantido, exigindo acesso direto

Otimização para type annotations e TYPE_CHECKING

  • Com lazy from módulo import nome, imports usados apenas para tipos têm custo ZERO em runtime garantido
  • Isso substitui o padrão existente com from typing import TYPE_CHECKING, deixando o código mais conciso e claro

Diferenças em relação ao PEP 690 e características da implementação

  • O PEP 810 segue uma estrutura opt-in explícita, por import individual e baseada em objeto proxy simples
  • Já o PEP 690 tinha uma estrutura global e implícita de lazy import

Cuidados e interação entre módulos

  • Star import (*) não é suportado com lazy (é sempre eager)
  • Hooks e loaders de import personalizados continuam funcionando normalmente no momento da reification
  • Mesmo em ambiente multithread, há garantia de import thread-safe, feito apenas uma vez e com binding seguro
  • Se o mesmo módulo for usado ao mesmo tempo como lazy e eager, o eager sempre tem prioridade

Guia de aplicação no código e migração

  • Ao aplicar isso em código existente, recomenda-se converter para lazy apenas os imports necessários com base em profiling, em uma adoção gradual
  • Ao usar __lazy_modules__, há compatibilidade também com versões anteriores ao Python 3.15

Outros pontos importantes de perguntas e respostas

  • Efeitos colaterais em tempo de import (por exemplo, padrões de registro) também são adiados até o primeiro acesso. Se o side effect for essencial, recomenda-se um padrão com função de inicialização explícita
  • O problema de circular import não é resolvido completamente com lazy import (só pode ser amenizado se o acesso também for adiado)
  • O desempenho em hot paths elimina completamente a checagem lazy após o primeiro uso, com otimização automática (adaptive specialization de bytecode)
  • O módulo real só é registrado em sys.modules após a reification (primeiro uso)
  • Diferentemente de importlib.util.LazyLoader, não exige configuração separada, mantém o desempenho e oferece clareza com uma sintaxe padrão

Conclusão

  • O PEP 810 adiciona a palavra-chave lazy às instruções de import do Python, permitindo otimizar de forma simples e previsível problemas de desempenho causados por carregamento desnecessário de módulos em várias áreas, como CLIs com subcomandos, grandes aplicações e type annotations
  • A nova palavra-chave permite especificar com precisão o momento de adoção e os alvos, sendo adequada para adoção gradual e tuning de performance em serviços reais
  • Trata-se de uma evolução prática do sistema de imports do Python, atendendo ao mesmo tempo às três exigências de visibilidade, manutenibilidade e desempenho

1 comentários

 
GN⁺ 2025-10-04
Comentários do Hacker News
  • Minha ferramenta de CLI llm.datasette.io oferece suporte a plugins, mas houve muitas reclamações sobre o tempo de inicialização muito lento até em comandos como llm --help; ao investigar, descobri que plugins populares importavam por padrão pacotes pesados como pytorch, bloqueando toda a inicialização, então passei a orientar na documentação para autores de plugins que importem dependências apenas dentro das funções, quando necessário (link da documentação), mas seria muito melhor se esse tipo de problema fosse suportado pela própria linguagem Python

    • Dá para implementar isso na ferramenta hoje mesmo (link com explicação), mas essa abordagem se aplica globalmente ao processo inteiro, então se você tornar o import do numpy lento, todos os imports de submódulos dele também ficarão lentos; no fim, se você não precisar do numpy inteiro, talvez ele nunca seja importado, mas o fenômeno de importar módulos parcialmente só no momento em que forem necessários pode acabar espalhado de forma imprevisível durante o runtime; em experimentos adicionais, vi que ao fazer import foo.bar.baz, foo e foo.bar ainda são carregados imediatamente, e só foo.bar.baz fica lazy, o que provavelmente explica em parte o uso de "mostly" no PEP; se eu melhorar mais minha implementação, talvez consiga resolver isso

    • Recomendo primeiro fazer o parsing da linha de comando, para tratar opções como --help sem importar nada; execute imports só quando for realmente necessário, ou, dito de outra forma, projete a ferramenta para só importar depois de processar as opções fáceis da linha de comando e ainda restar trabalho a fazer

  • Propostas de lazy import já existiram no passado e a mais recente foi rejeitada em 2022 (link para a discussão); se bem me lembro, lazy import já existe no Cinder, a variante de CPython da Meta, e este PEP também está sendo conduzido por pessoas que trabalharam no Cinder; o foco do debate era "opt-in ou opt-out?", "até onde se aplica?", "deve entrar como flag de build do CPython?" e afins; no fim, disseram que o Steering Council rejeitou por causa da complexidade de dividir o comportamento de import em dois; espero muito que esta proposta passe, porque eu realmente quero usar esse recurso

    • Gosto especialmente do fato de ser opt-in, incluir aplicação em nível granular e ainda ter uma chave global para desligar tudo; é uma especificação muito bem montada dentro de várias restrições

    • Eu também espero que essa proposta passe, mas não estou otimista; isso vai quebrar uma enorme quantidade de código e despejar muitos problemas inesperados; instruções import têm efeitos colaterais por natureza, e mudar o momento em que eles se aplicam vai fazer muita gente sofrer por muito tempo com bugs de causa obscura; isso não é alarmismo, é uma preocupação real e fundamentada; há um motivo para lazy import ter ficado só na Meta — é o tipo de coisa que talvez só dê para administrar com os recursos que a Meta tem; muita gente está olhando apenas para "pandas, numpy ou meu weird module complicado é lento demais, seria ótimo se ficasse mais rápido", mas acho que pouca gente sequer entende como o sistema de import do Python realmente funciona; há até muita gente apoiando sem nem saber como lazy import é implementado; no PEP 690 há várias desvantagens — por exemplo, quebra código que usa decorators para adicionar funções a um registry central; a biblioteca Dash é um caso representativo, porque conecta interfaces baseadas em JavaScript e callbacks Python via decorators no momento do import, e se o import virar lazy esse frontend simplesmente morre; serviços com muitos usuários podem quebrar de imediato; dizem "mas é opt-in, se não servir é só desligar o lazy import" — e se o import for transitivo? E se for preciso iniciar um processo importante só depois que o frontend estiver completamente inicializado? Quem sabe que efeitos isso terá num ecossistema em que código e bibliotecas de várias pessoas se misturam? Diferente de type hints, isso afeta de fato o comportamento em runtime; a instrução import aparece em praticamente todo código Python relevante, então ao introduzir lazy import você muda fundamentalmente a forma de execução; além disso, há outros casos estranhos mencionados no PEP; é um problema muito mais difícil do que parece

    • Seria ótimo poder fazer imports com versão explícita, como import torch==2.6.0+cu124 e import numpy>=1.2.6, e instalar/importar múltiplas versões do mesmo pacote ao mesmo tempo no mesmo ambiente Python; já passou da hora de acabar com o inferno de conda/virtualenv/docker/bazel

  • Não odeio a ideia, mas também não a recebo com grande entusiasmo; desse jeito, parece que quase todo import vai acabar recebendo lazy na frente, exceto alguns poucos casos que realmente precisem ser eager, então o código fica poluído; e como não há plano de tornar isso o comportamento padrão, essa verbosidade vai ficar para sempre; eu preferiria um sistema em que o módulo declarasse opt-in para lazy loading, sem mudar a sintaxe de import; assim, só as bibliotecas grandes precisariam se preocupar com laziness; claro, isso exigiria que o interpretador vasculhasse o sistema de arquivos no momento do import, além de trazer outras desvantagens

    • Se todo mundo usar bastante lazy import sem grandes problemas, então lazy deveria ter sido o padrão, e <i>eager</i> é que deveria ser a palavra-chave opcional; esse tipo de mudança de paradigma não seria inédito em Python: na v2, várias construções criavam listas de forma eager, e na v3 viraram generators, sem maiores problemas

    • Se existisse uma flag de linha de comando para tornar lazy o import de módulos em todo o Python, eu usaria sem pensar duas vezes; na prática, fora scripts ou código realmente simples, gerar side effects no carregamento do módulo é um padrão que deveria ser evitado a todo custo

    • Não acho certo deixar que o módulo decida se deve ou não usar lazy loading; só o chamador sabe se precisa de lazy load, então faz sentido que a opção fique no código que faz o import; qualquer módulo pode ser carregado de forma lazy, e mesmo que tenha side effects, o chamador pode querer adiar isso também

    • Eu queria poder declarar opções de lazy loading por regex no pyproject.toml

    • No passado, surgiram preocupações parecidas sempre que apareceram recursos novos como type hint, walrus, asyncio, dataclasses etc., mas na prática não houve adoção em massa nem substituição total dos padrões existentes; muitos usuários ainda trabalham basicamente com um Python 2.4 modernizado, e mesmo assim continuam produtivos; já funcionou bem por 20 anos, então não acho que haverá grandes problemas

  • Se tiver interesse, apresento o lazyimp, que implementa lazy import de forma muito conveniente com context manager; normalmente basta envolver a instrução import com um bloco with, então funciona bem com ferramentas existentes, e se precisar depurar, dá para voltar facilmente ao eager import; ele altera f_builtins do frame via cext, ficando mais poderoso que hooks do importlib; não é perfeito, mas também existem versões thread-safe e com handler global; no começo eu estava receoso, mas agora migrei quase toda a codebase para isso e, na prática, não tive problema nenhum (tirando não cuidar do processamento de registro por módulo), e o ganho de velocidade percebido foi enorme, então estou satisfeito

  • É realmente incômodo que linters Python forcem imports no topo do arquivo; sempre que tento usar a forma óbvia de implementar lazy import, recebo erro de lint; isso vai além de uma simples questão de performance; por exemplo, quando uma biblioteca específica de plataforma é necessária, eu queria importá-la só naquela plataforma, mas se o import no topo for obrigatório, a importação pode falhar de vez

    • Nesse caso, acho que não tem jeito a não ser corrigir o linter

    • A maioria dos linters permite ignorar isso com comentários como #noqa E402

  • Dizem que já dá para fazer lazy import automático até certo ponto com a classe LazyLoader, mas como isso depende de internals do import do Python que não são nada claros, até a explicação no Stack Overflow não fica muito bonita (Q&A relacionada); por isso, implementei eu mesmo uma prova de conceito que torna todos os imports lazy sem sintaxe explícita

import sys
import threading  # 파이썬 3.13에선 필요, REPL에서만이라도
from importlib.util import LazyLoader  # 이건 반드시 즉시 import해야 함!
class LazyPathFinder(sys.meta_path[-1]):  # _frozen_importlib_external.PathFinder 상속
  @classmethod
  def find_spec(cls, fullname, path=None, target=None):
    base = super().find_spec(fullname, path, target)
    base.loader = LazyLoader(base.loader)
    return base
sys.meta_path[-1] = LazyPathFinder

Assim, substituí o meta path finder por um wrapper, trocando o loader por LazyLoader; quando o import é executado, o nome do módulo é inicialmente vinculado a <class 'importlib.util._LazyModule'>, e o módulo real só é carregado quando um atributo é acessado; código de teste:

import this  # 아무런 결과도 안 나옴
print(type(this))  # <class 'importlib.util._LazyModule'>
rot13 = this.s  # Zen이 출력됨, 이 시점에 모듈 로딩
print(type(this))  # <class 'module'>

Mas eu não sei exatamente o que significa o "mostly" no PEP

  • Acho que os riscos de thread safety em lazy import estão sendo subestimados; não dá para prever quando, em qual thread e sob quais locks o import será executado, e fora o lock do importer não há muita garantia; antes, mesmo que código perigoso rodasse no momento do import do módulo, isso quase sempre acontecia só durante a inicialização single-threaded, então raramente virava um grande problema; se isso mudar para lazy, os erros podem aparecer de forma realmente imprevisível, no estilo Heisenbug; imports em nível de função também têm esse risco, mas ao menos existe a previsibilidade de que eles serão executados logo no começo de um trecho explícito de código

  • Parece um bom recurso; a explicação é simples, os casos de uso reais e o escopo (global, palavra-chave simples) parecem adequados, gostei

    • Entre os PEPs recentes, este me parece um dos mais limpos do ponto de vista do usuário; depois de passar por esse tradicional processo de bikeshedding sintático, estou curioso pelo resultado final

    • Acho que é um PEP preparado com muito cuidado: validação em casos reais de trabalho e edge cases, compromissos apropriados, abordagem sem exageros, várias rodadas de refinamento; especialmente porque mexer no sistema ósseo central de uma linguagem grande com comunidades tão diferentes no mundo todo pode ser muito arriscado, então isso me impressiona ainda mais

    • Espero que tenham aprendido bem por que o PEP-690 foi rejeitado; na nossa codebase também tentamos implementar algo assim por conta própria, mas nunca conseguimos fazer funcionar bem o suficiente para valer a pena

  • O perigo do lazy import é que ele facilita a criação de erros inesperados em runtime em serviços de longa duração; parece uma grande vantagem para inicialização rápida, mas o tradeoff é aceitar a possibilidade de a execução parar no meio por falha de import; além disso, podem surgir edge cases em que já não seja possível garantir no início do programa quais coisas ainda serão importadas depois

    • Ainda assim, esse é um problema real que precisa ser resolvido; não é só uma questão de velocidade de startup, é que a inicialização do Python fica absurdamente lenta quando entram dependências grandes; projetos maiores não podem simplesmente embutir todas as bibliotecas pesadas que nem todos os usuários usam, então os desenvolvedores já recorrem a gambiarras ainda mais bizarras, que por sua vez trazem outros problemas absurdos; só de eliminar a necessidade de esconder ou duplicar imports em nível de função já seria um grande avanço; e isso está sendo proposto apenas como um recurso opcional da linguagem

    • Testes automatizados já conseguem mitigar bastante o risco, e isso vale a pena em troca de uma inicialização rápida; tempo de startup nunca é só uma questão de "aparência"; num monólito Django, eu já vivi situações em que, por causa de apenas algumas bibliotecas pesadas, era preciso esperar 10 a 15 segundos em todo management command, teste e recarregamento de contêiner; adiar esses imports com lazy fez uma diferença enorme

  • Nós tendemos a preferir imports explícitos no topo do arquivo, justamente para expor problemas de dependência logo no início do programa; com lazy import, você ganha a inconveniência de só descobrir o problema quando um caminho específico de código for executado (talvez horas ou dias depois)

    • Por outro lado, se todos os imports fossem automaticamente adiados, a velocidade de execução do pip em tarefas curtas melhoraria imediatamente
$ time pip install --disable-pip-version-check
ERROR: You must give at least one requirement to install (see "pip help install")

real  0m0.399s
user  0m0.360s
sys   0m0.041s

A maior parte desse tempo vai para importar e descarregar módulos vendorizados que nem chegam a ser usados de fato (por exemplo, só os módulos relacionados a Requests já somam quase 100), e ao investigar vi que mais de 500 módulos estão sendo importados desnecessariamente

  • Também não entendo por que geradores de código estão produzindo tanto código com local import dentro de funções em vez de imports no topo; eu não gostaria de incentivar esse padrão, porque fica mais difícil entender as dependências do módulo e aumenta o risco de dependências cíclicas depois

  • Ainda não li o PEP inteiro, mas acho que seria bom se houvesse algum tipo de validação de dependências por flag de linha de comando ou ferramenta externa, como existem ferramentas para type hints

  • Fiquei curioso sobre quem exatamente está incluído nesse "nós"

  • Não seria esse um problema que deveria ser coberto por testes?