1 puntos por GN⁺ 2 시간 전 | 1 comentarios | Compartir por WhatsApp
  • 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

 
GN⁺ 2 시간 전
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 diferidos
    Siento que me perdí ese cambio; me pregunto si esto también es de Python 3.15 o si ya existía en versiones anteriores

    • Es una función de 3.15: https://docs.python.org/3.15/whatsnew/3.15.html#whatsnew315-...
    • No me queda claro cuál es la ventaja de los imports diferidos aquí. De todos modos, si usas ese valor en una pista de tipo a nivel de módulo, ¿no necesitas hacer el import?
      Para eso haría falta evaluación diferida de anotaciones, y según entiendo no está activada por defecto
    • En versiones anteriores de Python también se podía rodear esto implementando def __getattr__(name: str) -> object: a nivel de módulo
    • Esta parece ser una de las funciones principales de Python 3.15, así que da la impresión de que este artículo la omitió. En el documento de What's New también se menciona primero, así que claramente cuenta como una función destacada
      En 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
    • Python ha permitido hacer imports diferidos prácticamente desde el primer día, metiendo la sentencia import dentro de una función. La biblioteca no se importa hasta que esa función se llama
  • Con la incorporación de frozendict en 3.15, ahora ya se pueden representar todos los tipos de JSON —arreglos, booleanos, flotantes, null, cadenas y objetos— en una forma inmutable y hasheable
    Esa ú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-generator hace 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étrica
    https://en.wikipedia.org/wiki/Symmetric_difference

    • Sí, pero al aplicarlo a Counter se convierte en la diferencia simétrica de multisets, y eso no tiene una definición natural
      Si 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áctica
  • Uno de los ejemplos de Counter está mal. Lo verifiqué tanto en 3.13 como en 3.15.0a
    El resultado de Counter(a=3, b=1) - Counter(a=1, b=2) es Counter({'a': 2})

    • Yo también vi eso. Según la documentación, se ofrecen varias operaciones matemáticas para combinar objetos Counter y 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áxima
      Cada 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

    • Al principio quizá sea sencillo, pero me pregunto cómo piensas manejar en el futuro el mantenimiento de esos proyectos, sobre todo al agregar funciones más complejas
      Una opción podría ser prototipar en Python y luego convertirlo
    • Go es realmente flojo para cómputo científico o trabajo de machine learning. No tiene un ecosistema de bibliotecas suficiente, y hasta con ayuda de LLM se queda corto al envolver APIs en C
      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
    • Sigo buscando para Go un framework web integral al estilo Django. Si apareciera algo así, creo que me engancharía enseguida
    • Para empezar, me da curiosidad por qué terminaste usando Python. ¿Qué le recomendarías a alguien que no sabe absolutamente nada de programación?
    • Interesante. Si no te molesta decirlo, me da curiosidad si eso era un proyecto de trabajo o un proyecto personal
  • 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ó

    • El Python moderno de hoy me resulta realmente agradable tanto en el trabajo como en proyectos personales
    • ¿Alguien está creando un lenguaje tipo Python que se integre bien con Python, pero con menos carga y más potencia?
  • 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, ContextDecorator cambia 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 inesperada

    • El equipo central de Python parece considerar poco probable que haya gente dependiendo del comportamiento anterior: https://github.com/python/cpython/pull/136212#issuecomment-4...
    • ¿Qué sería lo peor que podría pasar? ¿Que por un cambio incompatible los desarrolladores sigan usando versiones viejas de Python? Eso jamás pasa, claro
  • Muchas 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