Python 3.15'in az bilinen özellikleri: Asyncio TaskGroup iptali, geliştirilmiş bağlam yöneticileri ve iş parçacığı güvenli yineleyiciler.

Python 3.15’in Geliştirici Dünyasını Değiştiren Az Bilinen Özellikleri

Python 3.15’in Geliştirici Dünyasını Değiştiren Az Bilinen Özellikleri

Her yıl olduğu gibi, Python’ın yeni sürümü ufukta beliriyor. Python 3.15.0b1‘in özellik dondurması ile bu yıl Python’a nelerin geleceğini biliyoruz. Tembel içe aktarmalar (lazy imports) ve Tachyon profiler gibi birçok büyük özellik geliyor. Bu makale, geçen yıl Python 3.14’ün küçük özelliklerini incelemekten keyif alan bir yaklaşımla, Python 3.15’in manşetlere çıkmayan, ancak geliştiriciler için oldukça ilgi çekici olan detaylarına odaklanıyor.

Asyncio TaskGroup İptali

Bu sürümde çok fazla Asyncio değişikliği bulunmamaktadır. Buradaki ana özellik, bir `TaskGroup`’u sorunsuz bir şekilde iptal edebilme yeteneğidir.

TaskGroup, yapılandırılmış eşzamanlılığın bir biçimidir ve geliştiricilerin birden fazla eşzamanlı görevi temiz bir şekilde oluşturmasını sağlar.

async with asyncio.TaskGroup() as tg:\n    tg.create_task(run())\n    tg.create_task(run())\n    # Waits for all the tasks to complete

Arka planda, TaskGroup’un yürütülmesini kesintiye uğratmak için bir tür sinyal beklemek istediğimizi varsayalım. Asyncio’da basit gibi görünse de, gerçekte bunu yapmak biraz karmaşıktır.

class Interrupt(Exception):\n    ...\n\nwith suppress(Interrupt):\n    async with asyncio.TaskGroup() as tg:\n        tg.create_task(run())\n        tg.create_task(run())\n        if await wait_for_signal():\n            raise Interrupt()

Bu yöntem işe yarar çünkü bir görev grubu içinde yükseltilen istisnalar diğer görevlerin iptal edilmesine neden olur. Özel `Interrupt` istisnası, bir `ExceptionGroup`’un parçası olarak yükseltilir ve bu, daha sonra contextlib.suppress tarafından filtrelenir ve sorunsuz bir çıkış sağlar.

Yeni TaskGroup.cancel yöntemi bu süreci çok daha kolay hale getiriyor:

async with asyncio.TaskGroup() as tg:\n    tg.create_task(run())\n    tg.create_task(run())\n    if await wait_for_signal():\n        tg.cancel()

Daha önce olduğundan çok daha basit; neredeyse açıklamaya gerek yok. Sadece herhangi bir istisna yükseltmeden grubu iptal eder.

Bağlam Yöneticisi Geliştirmeleri

Decorator’lar şaşırtıcı derecede yazması zor kodlardır, öyle ki artık bir mülakat sorusu haline gelmiştir. Ancak bağlam yöneticilerinin (context managers) aynı zamanda decorator olarak da kullanılabileceğini biliyor muydunuz?

@contextmanager\ndef duration(message: str) -> Iterator[None]:\n    start = time.perf_counter()\n    try:\n        yield\n    finally:\n        print(f'{message} elapsed {time.perf_counter() - start:.2f} seconds')

Burada, bir blokta harcanan süreyi yazdırmak için çok yaygın kullanılan bir bağlam yöneticisi bulunmaktadır. Python 3.3’ten beri doğrudan bir decorator olarak da kullanabiliyoruz:

@duration('workload')\ndef workload():\n    ...\n\n# Or simple as a wrapper\n\nduration('stuff')(other_workload)(...)

Ancak bu uygun olsa da, hiç işe yaramadığı durumlar da vardır:

@duration('async workload')\nasync def async_workload():\n    ...\n\n@duration('generator workload')\ndef workload():\n    while True:\n        yield ...

Yineleyiciler, asenkron fonksiyonlar ve asenkron yineleyiciler burada iyi çalışmazlar, çünkü standart fonksiyonlardan farklı semantiklere sahiptirler. Onları çağırdığınızda sırasıyla bir jeneratör nesnesi, korutin fonksiyonu ve asenkron jeneratör nesnesi ile hemen dönerler. Böylece decorator, sardığı tüm yaşam döngüsü yerine hemen tamamlanır.

Bu, birçok kez karşılaştığım talihsiz bir sorundur ve genellikle normal decorator’lar için de bir problem teşkil eder. Ancak bu durum 3.15’te değişti; artık `ContextDecorator`, sardığı fonksiyonun türünü kontrol edecek ve decorator’ın tüm yaşam süresini kapsamasını sağlayacaktır. Bu sayede bağlam yöneticileri, decorator oluşturmanın en iyi yolu haline gelmiştir. Ortak tuzaklardan kaçınır ve daha temiz bir sözdizimi sağlar.

İş Parçacığı Güvenli Yineleyiciler

Yineleyiciler, modern Python’ın temellerinden biridir. Yineleyici türü, veri kaynaklarını veri tüketicilerinden ayırmamıza olanak tanıyarak daha temiz soyutlamalar sağlar:

lazy from typing import Iterator\n\ndef stream_events(...) -> Iterator[str]:\n    while True:\n        yield blocking_get_event(...)\n\nevents = stream_events(...)\n\nfor event in events:\n    consume(event)

Ancak bu soyutlama, iş parçacığı (threading) veya serbest iş parçacığı (free-threading) kullanıldığında bozulur. Bir yineleyici varsayılan olarak iş parçacığı güvenli değildir, bu nedenle atlanan değerler veya bozuk iç yineleyici durumu görebiliriz.

Bu sorun 3.15’te threading.serialize_iterator ile çözüldü; orijinal yineleyicimizi bununla sararız ve işte:

import threading\n\nevents = threading.serialize_iterator(stream_events(...))\n\nwith ThreadPoolExecutor() as executor:\n    fut1 = executor.submit(consume, events)\n    fut2 = executor.submit(consume, events)

Ayrıca, bir jeneratör fonksiyonunun sonucuna `threading.serialize_iterator` uygulayan threading.synchronized_iterator decorator’ı da bulunmaktadır.

Son olarak, değerleri bölmek yerine birden çok yineleyici arasında çoğaltan threading.concurrent_tee de mevcuttur:

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

Bu araçlar mevcut olmadan önce, iş parçacıkları arasında tüketimi senkronize etmek için öncelikle Queue‘lara güveniyorduk. Bunların eklenmesiyle, çok iş parçacıklı kod için soyutlamalarımızı değiştirmekten kaçınabiliriz.

Bonus Özellikler

Geçen yıl sadece 3 özelliğe dikkat çekmiştim, ancak bu yıl beni daha fazla meraklandıran pek çok güncelleme var. İşte belki de daha az etkili ama yine de çok ilginç olan 2 değişiklik daha.

Counter xor İşlemi

collections.Counter çok kullanışlı bir sınıftır. Ayrık olayların sıklığını kolayca saymamızı sağlar. Bir `dict[KeyType, int]`’e çok benzer davranır ancak tonlarca yararlı işlemi vardır:

c = Counter(a=3, b=1)\nd = Counter(a=1, b=2)\nprint(f'{c + d = }')  # add two counters together:  c[x] + d[x]\nprint(f'{c - d = }')  # subtract (keeping only positive counts)

Yukarıdaki kod aşağıdaki çıktıyı verir:

Counter(a=4, b=3)\nCounter(a=1, b=0)

Ancak bazı garip işlemleri de vardır:

print(f'{c & d = }')  # intersection:  min(c[x], d[x])\nprint(f'{c | d = }')  # union:  max(c[x], d[x])

Yukarıdaki kod aşağıdaki çıktıyı verir:

Counter(a=1, b=1)\nCounter(a=3, b=2)

Bunu düşünmenin yolu, bir `Counter`’ın aynı zamanda ayrık nesneler kümesini de temsil edebilmesidir. Yani örneğimizde aslında şunu yapıyoruz:

{a_0, a_1, a_2, b_0} & {a_0, b_0, b_1} == {a_0, b_0}\n{a_0, a_1, a_2, b_0} | {a_0, b_0, b_1} == {a_0, a_1, a_2, b_0, b_1}

3.15’te XOR işlemini de listeye ekleyebiliriz:

c = Counter(a=3, b=1)\nd = Counter(a=1, b=2)\n\nc ^ d == c | d - c & d == Counter(a=3, b=2) - Counter(a=1, b=1) == Counter(a=2, b=1)

Bu, daha önceki gösterimimizle en iyi şekilde açıklanır:

{a_0, a_1, a_2, b_0} ^ {a_0, b_0, b_1} == {a_1, a_2, b_1}

Bu özelliği bonus bölümüne bıraktım çünkü `Counter`’lar üzerinde küme işlemleri hiç kullanmadım ve özellikle XOR için bir kullanım durumu düşünmekte zorlanıyorum. Ancak geliştiricilerin eksiksizlik için eklemesini takdir ediyorum.

Sabit JSON Nesneleri

frozendict‘in 3.15’e eklenmesiyle, artık tüm JSON türlerini (dizi, boolean, float, null, string, object) değişmez (hashable) biçimlerde temsil edebilme yeteneğine sahibiz.

json.load ve json.loads‘a `object_hook` parametresini tamamlayan `array_hook` parametresinin eklenmesiyle bir değişiklik yapıldı. Bu, artık JSON nesnelerini doğrudan bu forma ayrıştırmamıza olanak tanıyor:

json.loads('{\"a\": [1, 2, 3, 4]}', array_hook=tuple, object_hook=frozendict) == frozendict({'a': (1, 2, 3, 4)})

Comments

No comments yet. Why don’t you start the discussion?

    Bir yanıt yazın

    E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir