- 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
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çosaAcho 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
Para isso seria preciso avaliação preguiçosa de anotações, e pelo que sei isso não vem ativado por padrão
def __getattr__(name: str) -> object:no nível do móduloPessoalmente 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
importdentro de uma função. A biblioteca não é importada até que essa função seja chamadaCom a adição de
frozendictno 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 hashableEsse ú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-generatorfaz 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étricahttps://en.wikipedia.org/wiki/Symmetric_difference
Countervira a diferença simétrica de multiconjuntos, e isso não tem uma definição naturalSe 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áticaUm dos exemplos de
Counterestá errado. Confirmei tanto no 3.13 quanto no 3.15.0aO resultado de
Counter(a=3, b=1) - Counter(a=1, b=2)éCounter({'a': 2})Countere 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, respectivamenteCada 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
Uma possibilidade seria prototipar em Python e depois converter
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
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
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,
ContextDecoratorpassa 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 inesperadaMuitas 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