İçeriğe geç
Educora
İleri22 dk18 / 42

Tip ipuçları ve dataclass'lar

Tipleri koda yazmak: `list[int]`, `int | None`, `Callable`, `Literal`, generic'ler ve `Protocol`, mypy ile denetim; ayrıca `@dataclass`, `field`, `frozen`, `order` ve `slots`.

Kendini test et
Bu derste öğreneceklerin
  • Fonksiyonlara ve değişkenlere tip ipucu yazmak ve bunların çalışma zamanında denetlenmediğini açıklamak
  • Callable, Literal, generic fonksiyonlar ve Protocol ile karmaşık tipleri tanımlamak
  • @dataclass, field(default_factory=...) ve __post_init__ ile bir veri sınıfı yazmak
  • frozen, order ve slots seçeneklerini yerinde kullanmak

def price(total, discount) fonksiyonuna bak: discount 0,1 mi yoksa 10 mu? total None olabilir mi? Küçük bir betikte bunu hatırlarsın; beş geliştiricinin çalıştığı 50 000 satırlık bir projede ise hatırlamazsın. Tip ipuçları (type hints) cevabı kodun kendisine yazar: def price(total: float, discount: float) -> float. Düzenleyiciler bunları otomatik tamamlama için kullanır, mypy gibi denetleyiciler de hataları program daha çalışmadan bulur. Dataclass'lar aynı ipuçları üzerine kuruludur ve bir sınıfın sıkıcı kısımlarını senin yerine yazar.

Fonksiyonlara ve değişkenlere ipucu yazmak

İpucu iki noktadan sonra, dönüş tipi ise -> okundan sonra yazılır. Python 3.9'dan beri yerleşik koleksiyonlar doğrudan parametrelendirilebilir: list[int], dict[str, float], tuple[int, int], set[str]. Python 3.10'dan beri birleşim | ile yazılır: int | None, bir tam sayı ya da None demektir. Değişkenlere de ipucu yazılabilir: count: int = 0.

Python
def average(scores: list[float], precision: int = 1) -> float:
    return round(sum(scores) / len(scores), precision)

print(average([4.5, 5, 3.5]))
print(average.__annotations__)
print(average((1, 2)))
▸ Beklenen çıktı
4.3
{'scores': list[float], 'precision': <class 'int'>, 'return': <class 'float'>}
1.5

Son satır önemli: ipucunda list[float] yazmasına rağmen (1, 2) demeti kabul edildi. Python, tip ipuçlarını çalışma zamanında denetlemez. İpuçları __annotations__ içinde saklanır ve araçlar tarafından okunur. Hataları gerçekten bulmak için statik bir tip denetleyicisi çalıştır:

Terminal
$ mypy shop.py
shop.py:12: error: Argument 1 to "average" has incompatible type "str"; expected "list[float]"  [arg-type]
Found 1 error in 1 file (checked 1 source file)
mypy, pip install mypy ile kurulur ve dosyayı çalıştırmadan inceler.

typing modülünün araçları

  • Callable[[int], int] — bir int alıp bir int döndüren fonksiyon.
  • Literal['r', 'w'] — yalnızca tam olarak bu değerlere izin verilir.
  • type Vector = list[float] (Python 3.12+) — tip takma adı: uzun bir tip için kısa bir ad.
  • def first[T](items: list[T]) -> T (Python 3.12+) — generic (genel) fonksiyon: T bir tip değişkenidir; bu yüzden bir list[str] için sonuç str olur. Eski sürümlerde bu, T = TypeVar('T') ile yazılır.
  • Any — denetimi kapatır; olabildiğince az kullan.
Python
from typing import Callable, Literal

type Vector = list[float]
Mode = Literal['r', 'w']

def scale(v: Vector, k: float) -> Vector:
    return [k * x for x in v]

def first[T](items: list[T], default: T | None = None) -> T | None:
    return items[0] if items else default

def apply(func: Callable[[int], int], value: int) -> int:
    return func(value)

def describe(mode: Mode) -> str:
    return 'read' if mode == 'r' else 'write'

print(scale([1.0, 2.5], 2))
print(first(['Aysel', 'Murad']), first([], default='nobody'))
print(apply(lambda n: n * n, 7))
print(describe('w'))
▸ Beklenen çıktı
[2.0, 5.0]
Aysel nobody
49
write

Python'un felsefesi ördek tiplemesidir (duck typing): bir nesnede ihtiyacın olan metot varsa o nesne işini görür. typing.Protocol bu fikri tip sistemine taşır. Bir protokol, nesnenin neler yapabildiğini tanımlar; doğru metotlara sahip her sınıf, ondan kalıtım almadan otomatik olarak ona uyar. Buna yapısal tipleme denir:

Python
from typing import Protocol

class HasArea(Protocol):
    def area(self) -> float: ...

class Square:
    def __init__(self, side: float) -> None:
        self.side = side

    def area(self) -> float:
        return self.side ** 2

class Circle:
    def __init__(self, r: float) -> None:
        self.r = r

    def area(self) -> float:
        return 3.14159 * self.r ** 2

def total_area(shapes: list[HasArea]) -> float:
    return sum(s.area() for s in shapes)

print(round(total_area([Square(2), Circle(1)]), 2))
▸ Beklenen çıktı
7.14

Square ve Circle, HasArea hakkında hiçbir şey bilmez; yine de tip denetleyicisi ikisini de bir list[HasArea] içinde kabul eder, çünkü her birinin area() -> float metodu vardır. area metodunu unutan bir sınıf ise program çalışmadan önce hata olarak bildirilir.

dataclasses: basmakalıp kod olmadan sınıflar

Esas olarak veri saklayan bir sınıfın, argümanları niteliklere kopyalayan bir __init__'e, okunaklı bir __repr__'a ve alan alan karşılaştıran bir __eq__'e ihtiyacı vardır. Bunları elle yazmak uzun sürer ve hataya açıktır. **@dataclass dekoratörü, ipucu yazılmış sınıf niteliklerini (alanları**) okur ve bu metotları senin için üretir:

Python
from dataclasses import dataclass, field

@dataclass
class Student:
    name: str
    grade: int
    scores: list[int] = field(default_factory=list)

    def average(self) -> float:
        return sum(self.scores) / len(self.scores) if self.scores else 0.0

a = Student('Aysel', 9, [5, 4, 5])
b = Student('Aysel', 9, [5, 4, 5])
print(a)
print(a == b, a is b)
print(round(a.average(), 2))
print(Student('Murad', 8))
▸ Beklenen çıktı
Student(name='Aysel', grade=9, scores=[5, 4, 5])
True False
4.67
Student(name='Murad', grade=8, scores=[])

a == b True'dur, çünkü üretilen __eq__ alanları karşılaştırır; a is b ise False'tur, çünkü bunlar iki ayrı nesnedir. scores alanı, field(default_factory=list) sayesinde Murad için yeni ve boş bir liste aldı: fabrika her yeni nesne için ayrıca çağrılır.

Python
from dataclasses import dataclass

try:
    @dataclass
    class Basket:
        items: list[str] = []
except ValueError as e:
    print(e)
▸ Beklenen çıktı
mutable default <class 'list'> for field items is not allowed: use default_factory

frozen, order, slots ve __post_init__

Dekoratörün parametreleri ek özellikler açar. frozen=True nesneleri değiştirilemez yapar (dolayısıyla hash'lenebilir olurlar; küme elemanı ve sözlük anahtarı olabilirler), order=True alanları demetler gibi sırayla karşılaştıran <, <=, >, >= işlemlerini üretir, slots=True (Python 3.10+) ise nitelikleri bir sözlük yerine __slots__ içinde saklar; bu da bellek kazandırır ve erişimi hızlandırır. __post_init__ metodu, üretilen __init__'ten hemen sonra çalışır; doğrulamanın yeri burasıdır:

Python
from dataclasses import dataclass, replace, asdict

@dataclass(frozen=True, order=True, slots=True)
class Version:
    major: int
    minor: int = 0
    patch: int = 0

    def __post_init__(self):
        if self.major < 0:
            raise ValueError('major must be >= 0')

v1 = Version(0, 1)
v2 = replace(v1, minor=2)
print(v1 < v2)
print(sorted([Version(1), v2, v1]))
print(asdict(v2))
print({v1, Version(0, 1, 0)})
try:
    v1.major = 1
except AttributeError as e:
    print(type(e).__name__)
▸ Beklenen çıktı
True
[Version(major=0, minor=1, patch=0), Version(major=0, minor=2, patch=0), Version(major=1, minor=0, patch=0)]
{'major': 0, 'minor': 2, 'patch': 0}
{Version(major=0, minor=1, patch=0)}
FrozenInstanceError

Çıktıyı inceleyelim. sorted, order=True sayesinde çalışır: sürümler (major, minor, patch) demetleri gibi karşılaştırılır. Kümede tek eleman kaldı, çünkü eşit ve değiştirilemez nesnelerin hash değerleri de eşittir. Atama denemesi ise AttributeError'ın bir alt sınıfı olan FrozenInstanceError verdi; nesne oluşturulduktan sonra değiştirilemez.

SeçenekNe sağlar
frozen=Truedeğiştirilemez nesneler; atama FrozenInstanceError verir; hash'lenebilirlik
order=Truealan alan karşılaştırma < <= > >=; nesneler sıralanabilir
slots=True__slots__: daha az bellek, hızlı erişim, yeni nitelik eklenemez
kw_only=Truetüm alanlar yalnızca adıyla verilir
field(default_factory=...)her nesne için yeni bir varsayılan değer
field(repr=False, compare=False)alanı repr'dan ya da karşılaştırmalardan çıkarır
Alıştırma

Cart dataclass'ını tamamla: varsayılan olarak boş bir sözlük olan items alanını ekle (her sepetin kendi sözlüğü olsun), add ürünü fiyatıyla kaydetsin, total ise fiyatların 2 ondalığa yuvarlanmış toplamını döndürsün.

Alıştırma · Python
from dataclasses import dataclass, field

@dataclass
class Cart:
    owner: str
    # add a field `items`: dict[str, float], an empty dict by default

    def add(self, name: str, price: float) -> None:
        ...

    def total(self) -> float:
        ...

a = Cart('Aysel')
b = Cart('Murad')
a.add('tea', 3.5)
a.add('bread', 0.8)
print(a)
print(b)
print(a.total())
▸ Beklenen çıktı
Cart(owner='Aysel', items={'tea': 3.5, 'bread': 0.8})
Cart(owner='Murad', items={})
4.3
Alıştırma

Koşucular süreye göre (en hızlısı önce), süreler eşitse ada göre sıralanmalıdır. key argümanı kullanmadan sınıfı, sorted() tam olarak böyle sıralayacak şekilde değiştir.

Alıştırma · Python
from dataclasses import dataclass

@dataclass(order=True, frozen=True)
class Runner:
    name: str
    time: float

results = [Runner('Murad', 12.4), Runner('Leyla', 11.9), Runner('Aysel', 12.4)]
for r in sorted(results):
    print(r.name, r.time)
▸ Beklenen çıktı
Leyla 11.9
Aysel 12.4
Murad 12.4

Önemli noktalar

  • İpuçları (x: int, -> str, list[int], int | None) tipleri belgeler; Python bunları çalışma zamanında zorlamaz.
  • mypy ya da pyright ipuçlarını statik olarak, yani program çalışmadan önce denetler.
  • Generic'ler (def first[T]), Callable, Literal ve Protocol daha zengin sözleşmeler tanımlar; Protocol yapısal tiplemedir.
  • @dataclass, ipucu yazılmış alanlardan __init__, __repr__ ve __eq__ üretir; değiştirilebilir varsayılanlar için field(default_factory=...) gerekir.
  • frozen, order, slots ve __post_init__; değiştirilemezlik, sıralama, bellek tasarrufu ve doğrulama ekler.

Kendini test et

10 soru. Her doğru cevap XP kazandırır.

1 / 10
Parametresi x: int olarak işaretlenmiş bir fonksiyona çalışma zamanında bir karakter dizisi verirsen ne olur?