1 pontos por GN⁺ 2 시간 전 | 1 comentários | Compartilhar no WhatsApp
  • Com o congelamento de recursos do Python 3.15.0b1, além de imports preguiçosos e do profiler Tachyon, também foram confirmadas melhorias práticas
  • O TaskGroup.cancel() do asyncio permite cancelar grupos de tarefas de forma elegante, sem exceções personalizadas nem contextlib.suppress
  • O ContextDecorator passou a envolver todo o ciclo de vida de funções assíncronas, geradores e iteradores assíncronos
  • Novos utilitários de threading permitem serializar ou duplicar o consumo de iteradores entre threads, mantendo a abstração sem usar Queue
  • O Counter ganhou a operação xor, e json.loads passou a oferecer suporte a parsing imutável de JSON com array_hook e frozendict

Mudanças menos conhecidas do Python 3.15

  • Com o congelamento de recursos do Python 3.15.0b1, ficaram definidos os recursos que entrarão no Python este ano; entre as grandes mudanças estão os imports preguiçosos e o profiler Tachyon
  • O Python 3.15 também inclui pequenas mudanças de recursos menos chamativas do que os grandes PEPs, mas bastante práticas, com melhorias em asyncio, gerenciadores de contexto, iteradores thread-safe, Counter e parsing de JSON

Cancelamento de TaskGroup no asyncio

  • Uma mudança central no asyncio é a adição de uma forma de cancelar TaskGroup de maneira elegante
  • TaskGroup é uma forma de concorrência estruturada, que permite criar várias tarefas concorrentes de forma organizada e esperar até que todas terminem
async with asyncio.TaskGroup() as tg:
    tg.create_task(run())
    tg.create_task(run())




# Waits for all the tasks to complete
  • Antes do Python 3.15, para interromper a execução de um TaskGroup ao aguardar um sinal em segundo plano, era preciso lançar uma exceção personalizada e filtrá-la com contextlib.suppress
class Interrupt(Exception):
    ...

with suppress(Interrupt):
    async with asyncio.TaskGroup() as tg:
        tg.create_task(run())
        tg.create_task(run())

        if await wait_for_signal():
            raise Interrupt()
  • Isso funciona porque, quando ocorre uma exceção dentro do grupo de tarefas, as outras tarefas são canceladas, e a exceção personalizada Interrupt é gerada como parte de um ExceptionGroup, sendo depois filtrada por contextlib.suppress
  • O funcionamento de suppress com ExceptionGroup foi adicionado no Python 3.12, mas não recebeu muita atenção
  • O TaskGroup.cancel do Python 3.15 torna a mesma tarefa muito mais simples
async with asyncio.TaskGroup() as tg:
    tg.create_task(run())
    tg.create_task(run())

    if await wait_for_signal():
        tg.cancel()
  • TaskGroup.cancel() cancela o grupo sem lançar exceção, eliminando a necessidade da combinação de exceção separada com suppress

Melhorias em gerenciadores de contexto

  • Desde o Python 3.3, gerenciadores de contexto também podiam ser usados diretamente como decoradores
@contextmanager
def duration(message: str) -> Iterator[None]:
    start = time.perf_counter()
    try:
        yield
    finally:
        print(f"{message} elapsed {time.perf_counter() - start:.2f} seconds")
@duration('workload')
def workload():
    ...





# Or simple as a wrapper
duration('stuff')(other_workload)(...)
  • Gerenciadores de contexto como duration(), que imprimem o tempo de execução de um bloco, são práticos por poderem ser usados como decoradores de função, mas havia casos em que funções assíncronas, geradores e iteradores assíncronos não funcionavam corretamente
@duration('async workload')
async def async_workload():
    ...

@duration('generator workload')
def workload():
    while True:
        yield ...
  • Iteradores, funções assíncronas e iteradores assíncronos têm semântica diferente das funções normais: no momento da chamada, retornam imediatamente um objeto gerador, um objeto coroutine ou um objeto gerador assíncrono, respectivamente
  • O decorador anterior não conseguia abranger todo o ciclo de vida do alvo envolvido e era concluído imediatamente, sem cobrir todo o tempo real de execução
  • No Python 3.15, ContextDecorator passa a verificar o tipo da função que envolve, para que o decorador cubra o ciclo de vida completo daquele alvo
  • Isso evita armadilhas comuns ao usar gerenciadores de contexto como decoradores e permite uma sintaxe mais limpa

Iteradores thread-safe

  • Iteradores são uma das abstrações centrais do Python, separando a fonte de dados do consumidor dos dados para criar uma estrutura mais limpa
lazy from typing import Iterator

def stream_events(...) -> Iterator[str]:
    while True:
        yield blocking_get_event(...)

events = stream_events(...)

for event in events:
    consume(event)
  • Essa abstração pode se quebrar em ambientes com threading ou free-threading, e os iteradores padrão não são thread-safe, o que pode fazer valores serem pulados ou corromper o estado interno do iterador
  • O threading.serialize_iterator do Python 3.15 envolve um iterador existente para serializar seu consumo entre threads
import threading

events = threading.serialize_iterator(stream_events(...))

with ThreadPoolExecutor() as executor:
    fut1 = executor.submit(consume, events)
    fut2 = executor.submit(consume, events)
source1, source2 = threading.concurrent_tee(squares(10), n=2)

with ThreadPoolExecutor() as executor:
    fut1 = executor.submit(consume, source1)
    fut2 = executor.submit(consume, source2)
  • Até agora, a sincronização do consumo entre threads normalmente dependia de Queue, mas com os novos utilitários é possível manter a abstração existente de iteradores sem alterá-la, mesmo em código multithread

Recursos adicionais

  • Operação xor no Counter

    • collections.Counter é uma classe que facilita contar frequências discretas, funcionando de forma parecida com dict[KeyType, int] e oferecendo várias operações úteis
c = Counter(a=3, b=1)
d = Counter(a=1, b=2)

print(f"{c + d = }") # add two counters together: c[x] + d[x]
print(f"{c - d = }") # subtract (keeping only positive counts)
Counter(a=4, b=3)
Counter(a=1, b=0)
  • Counter também já tinha as operações & e |, correspondentes à interseção e união
print(f"{c & d = }") # intersection: min(c[x], d[x])
print(f"{c | d = }") # union: max(c[x], d[x])
Counter(a=1, b=1)
Counter(a=3, b=2)
  • Counter pode ser visto como um conjunto de objetos discretos, e os exemplos acima podem ser interpretados assim
{a_0, a_1, a_2, b_0} & {a_0, b_0, b_1} == {a_0, b_0}
{a_0, a_1, a_2, b_0} | {a_0, b_0, b_1} == {a_0, a_1, a_2, b_0, b_1}
  • No Python 3.15, foi adicionada a operação xor
c = Counter(a=3, b=1)
d = Counter(a=1, b=2)

c ^ d == c | d - c & d == Counter(a=3, b=2) - Counter(a=1, b=1) == Counter(a=2, b=1)
{a_0, a_1, a_2, b_0} ^ {a_0, b_0, b_1} == {a_1, a_2, b_1}
  • Se você nunca usou com frequência as operações de conjunto de Counter, pode ser difícil imaginar um caso de uso concreto para xor, mas a adição completa o conjunto de operações
  • Objetos JSON imutáveis

    • Com a adição de frozendict no Python 3.15, passa a ser possível representar todos os tipos JSON — arrays, booleanos, números de ponto flutuante, null, strings e objetos — em formas imutáveis e hashable
    • json.load e json.loads ganharam o parâmetro array_hook, complementando o já existente object_hook
    • Usando array_hook=tuple junto com object_hook=frozendict, é possível fazer o parsing de objetos JSON diretamente para estruturas imutáveis
json.loads('{"a": [1, 2, 3, 4]}', array_hook=tuple, object_hook=frozendict) == frozendict({'a': (1, 2, 3, 4)})

1 comentários

 
GN⁺ 2 시간 전
Comentários no Hacker News
  • Pelos exemplos, usam algo como lazy from typing import Iterator, então fiquei pensando se o Python finalmente ganhou importação preguiçosa
    Acho que deixei essa mudança passar, então queria saber se isso também é do Python 3.15 ou se já existia em versões anteriores

    • É recurso do 3.15: https://docs.python.org/3.15/whatsnew/3.15.html#whatsnew315-...
    • Não entendo bem qual é a vantagem da importação preguiçosa aqui. No fim, se você usar esse valor em type hints no escopo do módulo, ainda vai precisar fazer o import, não?
      Para isso seria preciso avaliação preguiçosa de anotações, e pelo que sei isso não vem ativado por padrão
    • Também dá para contornar em versões anteriores do Python implementando def __getattr__(name: str) -> object: no nível do módulo
    • Isso parece ser um dos recursos de destaque do Python 3.15, então imagino que tenha ficado de fora do artigo por isso. Também é a primeira coisa citada no documento What's New, então dá para considerar com segurança uma funcionalidade principal
      Pessoalmente estou bem animado. Só nesta semana vi um processo Python estourar o limite de memória e dar falta de memória só porque foi adicionado o import de um módulo que o aplicativo nem usava de fato
    • O Python já permitia importação preguiçosa praticamente desde o primeiro dia, colocando a instrução import dentro de uma função. A biblioteca não é importada até que essa função seja chamada
  • Com a adição de frozendict no 3.15, agora dá para representar todos os tipos do JSON — arrays, booleanos, ponto flutuante, null, strings e objetos — em uma forma imutável e hashable
    Esse último recurso me agradou bastante

  • Gostei da adição de primitivas de sincronização de iteradores no Python 3.15: https://docs.python.org/3.15/library/threading.html#iterator...
    Meu pacote threaded-generator faz exatamente isso com threads/processos + generators + filas, então parece que vai complementar bem: https://pypi.org/project/threaded-generator/

  • Disseram que é difícil imaginar um uso para as operações de conjunto do Counter, especialmente xor, mas basta olhar para a diferença simétrica
    https://en.wikipedia.org/wiki/Symmetric_difference

    • Sim, mas ao aplicar isso a Counter vira a diferença simétrica de multiconjuntos, e isso não tem uma definição natural
      Se entendi a proposta corretamente, parece que seria definido pelo valor absoluto da diferença da contagem de cada elemento, mas isso nem satisfaz a associatividade. Se você olhar só para a paridade, dá para interpretar como soma em F_2, o que é mais natural, mas ainda assim não vejo muito onde isso seria usado na prática
  • Um dos exemplos de Counter está errado. Confirmei tanto no 3.13 quanto no 3.15.0a
    O resultado de Counter(a=3, b=1) - Counter(a=1, b=2) é Counter({'a': 2})

    • Eu também vi isso. Segundo a documentação, vários operadores matemáticos são fornecidos para combinar objetos Counter e produzir multiconjuntos; soma e subtração adicionam ou subtraem as contagens dos elementos correspondentes, enquanto interseção e união retornam as contagens mínima e máxima, respectivamente
      Cada operação pode aceitar entradas com contagens negativas, mas na saída os resultados com contagem menor ou igual a 0 são excluídos. De qualquer forma, é um belo Counter-example ;-)
  • Fiquei realmente obcecado por Python por 10 anos e adorava trabalhar com ele, mas no mundo pós-codebot de IA já apaguei mais de 100 mil linhas só neste ano e migrei para linguagens mais rápidas. Hoje em dia estou migrando principalmente para Go

    • No começo deve ser simples, mas fico curioso sobre como você pretende fazer a manutenção desses projetos no futuro, especialmente para adicionar recursos mais complexos
      Uma possibilidade seria prototipar em Python e depois converter
    • Go é realmente ruim para computação científica ou machine learning. O ecossistema de bibliotecas não está lá, e até com ajuda de LLM ele é fraco quando o assunto é encapsular APIs em C
      Se você tentar escrever código de processamento de sinais com filtros, windowing, overlap e coisas do tipo, hoje praticamente não existe um caminho fácil só com as bibliotecas atuais
    • Continuo procurando um framework web completo estilo Django para Go. Se surgir algo assim, acho que eu embarco na hora
    • Fico curioso sobre por que você acabou usando Python em primeiro lugar. O que você recomendaria para alguém que não sabe absolutamente nada de programação?
    • Interessante. Se não se importar em dizer, fiquei curioso se isso era um projeto de trabalho ou um projeto pessoal
  • Há uma boa entrevista sobre a estrutura interna e a operação do Python, especialmente em relação a free-threading: https://alexalejandre.com/programming/interview-with-ngoldba...

  • Ah, meu amor Python. Usei você por quase 15 anos. Sinto saudades, mas não uso mais. A culpa não é sua; a vida mudou

    • O Python moderno de hoje em dia é realmente muito prazeroso de usar, tanto no trabalho quanto em projetos pessoais
    • Será que alguém está criando uma linguagem parecida com Python que se integre bem com ele, mas venha com menos bagagem e mais poder?
  • Iteradores, funções assíncronas e iteradores assíncronos tinham semântica diferente da de funções comuns, então não combinavam bem com decoradores. Ao serem chamados, eles retornam imediatamente um objeto gerador, uma coroutine function ou um objeto gerador assíncrono, então o decorador terminava na hora, em vez de envolver todo o ciclo de vida
    No 3.15, ContextDecorator passa a verificar o tipo da função envolvida para que o decorador cubra todo o ciclo de vida, e eu gosto muito da ideia, mas parece bastante arriscado mudar sutilmente o comportamento de usos existentes sem um mecanismo de adesão opcional. É uma situação meio “aquecimento com barra de espaço”, em que só vira problema se alguém tiver usado o decorador intencionalmente da forma antiga e quebrada, mas se isso aconteceu pode quebrar de forma inesperada

    • A equipe principal do Python parece considerar baixa a chance de haver gente dependendo do comportamento antigo: https://github.com/python/cpython/pull/136212#issuecomment-4...
    • Qual seria o pior cenário? Desenvolvedores continuarem usando versões antigas do Python por causa de mudanças incompatíveis? Como se isso fosse acontecer
  • Muitas vezes são esses recursos pequenos que acabam sendo os mais úteis. Em especial, quero testar as novas adições à biblioteca padrão no meu projeto atual