- Con el congelamiento de funciones de Python 3.15.0b1, además de las importaciones diferidas y el perfilador Tachyon, también quedaron confirmadas varias mejoras prácticas
- TaskGroup.cancel() de
asyncio permite cancelar con elegancia un grupo de tareas sin excepciones personalizadas ni contextlib.suppress
- ContextDecorator cambió para envolver todo el ciclo de vida de funciones asíncronas, generadores e iteradores asíncronos
- Las nuevas utilidades de threading permiten serializar o duplicar el consumo de iteradores entre hilos sin perder la abstracción ni depender de Queue
- A
Counter se le agregó la operación xor, y json.loads ahora admite parsing inmutable de JSON con array_hook y frozendict
Cambios menos conocidos de Python 3.15
- Con el congelamiento de funciones de Python 3.15.0b1, ya quedaron definidas las funciones que entrarán este año en Python, y entre los cambios grandes están las importaciones diferidas y el perfilador Tachyon
- Python 3.15 también incluye pequeños cambios de funciones que no llaman tanto la atención como los grandes PEP, pero sí resultan prácticos, con mejoras en
asyncio, administradores de contexto, iteradores seguros para hilos, Counter y parsing de JSON
Cancelación de TaskGroup en asyncio
- Como cambio central en
asyncio, se agregó la capacidad de cancelar con elegancia TaskGroup
TaskGroup es una forma de concurrencia estructurada que permite crear varias tareas concurrentes de forma ordenada y esperar a que todas terminen
async with asyncio.TaskGroup() as tg:
tg.create_task(run())
tg.create_task(run())
# Waits for all the tasks to complete
- Antes de Python 3.15, para detener la ejecución de un
TaskGroup mientras se esperaba una señal en segundo plano, había que lanzar una excepción personalizada y filtrarla con 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()
- Este enfoque funciona porque, si ocurre una excepción dentro del grupo de tareas, las demás tareas se cancelan, y la excepción personalizada
Interrupt aparece como parte de ExceptionGroup antes de ser filtrada por contextlib.suppress
- El comportamiento de
suppress con ExceptionGroup se agregó en Python 3.12, pero no recibió mucha atención
- TaskGroup.cancel de Python 3.15 hace exactamente lo mismo de forma mucho más simple
async with asyncio.TaskGroup() as tg:
tg.create_task(run())
tg.create_task(run())
if await wait_for_signal():
tg.cancel()
TaskGroup.cancel() cancela el grupo sin lanzar una excepción, así que ya no hace falta combinar una excepción separada con suppress
Mejora en los administradores de contexto
- Desde Python 3.3, los administradores de contexto también podían usarse directamente 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)(...)
- Un administrador de contexto como
duration(), que imprime el tiempo de ejecución de un bloque, es cómodo porque puede usarse como decorador de función, pero había casos en los que no funcionaba bien con funciones asíncronas, generadores e iteradores asíncronos
@duration('async workload')
async def async_workload():
...
@duration('generator workload')
def workload():
while True:
yield ...
- Los iteradores, las funciones asíncronas y los iteradores asíncronos tienen una semántica distinta a la de las funciones normales, ya que al llamarlos devuelven de inmediato un objeto generador, un objeto corrutina o un objeto generador asíncrono
- El decorador anterior no alcanzaba a cubrir todo el ciclo de vida del objetivo envuelto y terminaba enseguida, por lo que no abarcaba todo el tiempo real de ejecución
- En Python 3.15,
ContextDecorator ahora verifica el tipo de función que envuelve, para que el decorador cubra su ciclo de vida completo
- Esto permite evitar trampas comunes al usar administradores de contexto como decoradores y usar una sintaxis más limpia
Iteradores seguros para hilos
- Los iteradores son una de las abstracciones centrales de Python y permiten separar la fuente de datos del consumidor de datos para lograr una estructura más limpia
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)
- Esta abstracción puede romperse en entornos con hilos o free-threading, y los iteradores base no son seguros para hilos, por lo que pueden saltarse valores o corromper su estado interno
- threading.serialize_iterator de Python 3.15 envuelve un iterador existente para serializar su consumo entre hilos
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)
- Hasta ahora, para sincronizar el consumo entre hilos normalmente se dependía de Queue, pero con estas nuevas utilidades se puede mantener la abstracción de iteradores existente sin cambiarla, incluso en código multihilo
Funciones adicionales
-
Operación xor en Counter
- collections.Counter es una clase que permite contar con facilidad frecuencias discretas de aparición, y se comporta de manera similar a
dict[KeyType, int] mientras ofrece varias operaciones útiles
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 también tiene las operaciones & y |, equivalentes a intersección y unión
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 puede verse como un conjunto de objetos discretos, y los ejemplos pueden interpretarse así
{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}
- En Python 3.15 se suma a esto la operación 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}
- Si no usabas mucho las operaciones de conjuntos de
Counter, quizá cueste imaginar un caso concreto para xor, pero la incorporación mejora la completitud del conjunto de operaciones
-
Objetos JSON inmutables
- Con la incorporación de frozendict en Python 3.15, ahora es posible representar todos los tipos JSON —arreglos, booleanos, números reales, null, cadenas y objetos— en formas inmutables y hashables
- A json.load y json.loads se les agregó el parámetro
array_hook, que complementa al ya existente object_hook
- Si se usan juntos
array_hook=tuple y object_hook=frozendict, los objetos JSON pueden parsearse directamente en estructuras inmutables
json.loads('{"a": [1, 2, 3, 4]}', array_hook=tuple, object_hook=frozendict) == frozendict({'a': (1, 2, 3, 4)})
1 comentarios
Comentarios de Hacker News
En los ejemplos aparece algo como
lazy from typing import Iterator, así que me pregunté si por fin Python tiene imports diferidosSiento que me perdí ese cambio; me pregunto si esto también es de Python 3.15 o si ya existía en versiones anteriores
Para eso haría falta evaluación diferida de anotaciones, y según entiendo no está activada por defecto
def __getattr__(name: str) -> object:a nivel de móduloEn lo personal la espero mucho. Justo esta semana vi que un proceso de Python se pasó del límite de memoria y dio falta de memoria solo por agregar imports de módulos que la aplicación ni siquiera usaba realmente
importdentro de una función. La biblioteca no se importa hasta que esa función se llamaCon la incorporación de
frozendicten 3.15, ahora ya se pueden representar todos los tipos de JSON —arreglos, booleanos, flotantes, null, cadenas y objetos— en una forma inmutable y hasheableEsa última capacidad sí me encanta
Me gusta que Python 3.15 agregue primitivas de sincronización para iteradores: https://docs.python.org/3.15/library/threading.html#iterator...
Mi paquete
threaded-generatorhace justo eso con hilos/procesos + generadores + colas, así que parece un buen complemento: https://pypi.org/project/threaded-generator/Decían que cuesta imaginar un caso de uso para las operaciones de conjuntos en
Counter, en especial xor, pero basta con ver la diferencia simétricahttps://en.wikipedia.org/wiki/Symmetric_difference
Counterse convierte en la diferencia simétrica de multisets, y eso no tiene una definición naturalSi entendí bien la propuesta, parece definirse como el valor absoluto de la diferencia entre las cantidades de cada elemento, pero eso ni siquiera cumple la asociatividad. Si solo miraras la paridad, podría interpretarse como suma en
F_2, lo cual sería más natural, pero aun así no se ve muy claro para qué serviría en la prácticaUno de los ejemplos de
Counterestá mal. Lo verifiqué tanto en 3.13 como en 3.15.0aEl resultado de
Counter(a=3, b=1) - Counter(a=1, b=2)esCounter({'a': 2})Countery formar multisets; la suma y la resta agregan o restan las cantidades de los elementos correspondientes, y la intersección y la unión devuelven respectivamente la cantidad mínima o máximaCada operación acepta entradas con cantidades negativas, pero en la salida se excluyen los resultados cuya cantidad sea 0 o menor. En cualquier caso, es un gran Counter-example ;-)
Estuve realmente metido en Python durante 10 años y disfruté mucho trabajar con él, pero en el mundo posterior a los codebots de IA este año ya eliminé más de 100 mil líneas y las migré a lenguajes más rápidos. Últimamente sobre todo las estoy pasando a Go
Una opción podría ser prototipar en Python y luego convertirlo
Si intentas escribir código de procesamiento de señales con filtros, windowing, overlap y cosas así, con las bibliotecas actuales casi no hay una manera fácil de hacerlo
Hay una buena entrevista sobre la estructura interna y el funcionamiento de Python, especialmente en relación con free-threading: https://alexalejandre.com/programming/interview-with-ngoldba...
Ah, mi amado Python. Lo usé durante casi 15 años. Lo extraño, pero ya no lo uso. No es tu culpa; la vida cambió
Los iteradores, las funciones async y los iteradores async tenían una semántica distinta de la de las funciones normales, así que no encajaban bien con los decoradores. Al llamarlos, devolvían de inmediato un objeto generador, una función corrutina o un objeto generador async, así que el decorador terminaba enseguida en vez de cubrir todo el ciclo de vida que supuestamente envolvía
En 3.15,
ContextDecoratorcambia para revisar el tipo de función que envuelve y hacer que el decorador cubra todo el ciclo de vida; la idea me gusta mucho, pero sí parece bastante riesgoso que cambie sutilmente el comportamiento de usos existentes sin un mecanismo de adopción opcional. Tendría que darse una situación tipo “calefacción con la barra espaciadora”, donde alguien hubiera usado intencionalmente el decorador de la forma rota anterior, para que hubiera problemas; aun así, si alguien realmente lo hizo, podría romperse de manera inesperadaMuchas veces estas funciones pequeñas terminan siendo las más útiles. En particular, me gustaría probar las nuevas adiciones a la biblioteca estándar en mi proyecto actual