← Kursa Dön
📄 Text · 15 min

dataclass ve NamedTuple

Python'da sınıf yazarken en sıkıcı kısım ne biliyor musun? __init__ yazıp her attribute'u tek tek self.x = x diye atamak. Sonra __repr__ yaz, __eq__ yaz, __hash__ yaz… Aynı tür boilerplate kodları tekrar tekrar.

Python 3.7 ile gelen `@dataclass` decorator'ı bu sorunu çözüyor. Sınıfın attribute'larını tanımla, gerisini Python halleder.


Dataclass Nedir?

Dataclass, ağırlıklı olarak veri taşımak için kullanılan sınıfları hızlıca oluşturmayı sağlayan bir decorator'dır. __init__, __repr__ ve __eq__ metodlarını otomatik oluşturur.

Analoji: Form Şablonu 📋

Bir form düşün — ad, soyad, email, telefon alanları var. Her müşteri için yeni bir boş form basmak yerine, alanları bir kere tanımlarsın ve form otomatik basılır. Dataclass da böyle: attribute'ları bir kere tanımla, Python boilerplate'i otomatik üretsin.

# Klasik yol — tekrar tekrar aynı şeyler
class ProductOld:
    def __init__(self, name, price, stock=0):
        self.name = name
        self.price = price
        self.stock = stock

    def __repr__(self):
        return f"ProductOld(name='{self.name}', price={self.price}, stock={self.stock})"

    def __eq__(self, other):
        if not isinstance(other, ProductOld):
            return NotImplemented
        return (self.name, self.price, self.stock) == (other.name, other.price, other.stock)
# Dataclass yolu — aynı işlevsellik, çok daha az kod
from dataclasses import dataclass

@dataclass
class Product:
    name: str
    price: float
    stock: int = 0

# __init__, __repr__, __eq__ otomatik oluşturuldu!
p1 = Product("Laptop", 15000, 5)
p2 = Product("Laptop", 15000, 5)
p3 = Product("Mouse", 500)

print(p1)           # Product(name='Laptop', price=15000, stock=5)
print(p1 == p2)     # True — otomatik __eq__
print(p1 == p3)     # False
print(p3.stock)     # 0 — default değer

3 satır vs 15 satır. Ve üretilen kod aynı işi yapıyor. İşte bu yüzden dataclass'a "boilerplate killer" denir.


@dataclass Decorator Parametreleri

@dataclass decorator'ı birçok parametre alır ve davranışını özelleştirmene olanak tanır:

@dataclass(
    init=True,       # __init__ oluştur (default: True)
    repr=True,       # __repr__ oluştur (default: True)
    eq=True,         # __eq__ ve __ne__ oluştur (default: True)
    order=False,     # __lt__, __le__, __gt__, __ge__ oluştur (default: False)
    frozen=False,    # Immutable yap (default: False)
    unsafe_hash=False,  # __hash__ zorla oluştur
    slots=False,     # __slots__ kullan — Python 3.10+ (default: False)
)
class MyClass:
    ...

order=True: Otomatik Karşılaştırma

@dataclass(order=True)
class Student:
    grade: float     # Önce grade'e göre sıralar
    name: str        # Sonra name'e göre

s1 = Student(85, "Ali")
s2 = Student(92, "Veli")
s3 = Student(85, "Ayşe")

print(s1 < s2)          # True (85 < 92)
print(s1 < s3)          # False (85 == 85, "Ali" < "Ayşe"? False)
print(s3 < s1)          # True (85 == 85, "Ayşe" < "Ali"? True)

# Sıralama!
students = [s2, s1, s3]
print(sorted(students))
# [Student(grade=85, name='Ali'), Student(grade=85, name='Ayşe'), Student(grade=92, name='Veli')]

Karşılaştırma, field'ların tanım sırasına göre tuple karşılaştırması gibi çalışır. Bu yüzden sıralama için önemli olan field'ı önce yaz.


Field Options: default, default_factory, field()

Basit Default Değerler

from dataclasses import dataclass

@dataclass
class Config:
    host: str = "localhost"
    port: int = 8080
    debug: bool = False
    workers: int = 4

config1 = Config()
print(config1)  # Config(host='localhost', port=8080, debug=False, workers=4)

config2 = Config(host="0.0.0.0", debug=True)
print(config2)  # Config(host='0.0.0.0', port=8080, debug=True, workers=4)

Mutable Default: default_factory

Mutable değerler (list, dict, set) için default_factory kullanmalısın:

from dataclasses import dataclass, field

@dataclass
class ShoppingCart:
    owner: str
    items: list = field(default_factory=list)
    discounts: dict = field(default_factory=dict)
    tags: set = field(default_factory=set)

cart1 = ShoppingCart("Ali")
cart2 = ShoppingCart("Veli")

cart1.items.append("Laptop")
print(cart1.items)  # ['Laptop']
print(cart2.items)  # [] — bağımsız! Tuzağa düşmedik.

⚠️ Dikkat: Mutable default değerleri doğrudan yazmak hata verir: items: list = [] dersen ValueError: mutable default <class 'list'> is not allowed alırsın. Bu bilinçli bir tasarım — seni klasik class attribute tuzağından korur. Her zaman field(default_factory=list) kullan.

field() ile İleri Seçenekler

from dataclasses import dataclass, field

@dataclass
class User:
    username: str
    email: str
    password: str = field(repr=False)           # repr'da gösterme!
    is_active: bool = field(default=True)
    login_count: int = field(default=0, repr=False)
    _internal_id: str = field(default="", init=False, repr=False)

    def __post_init__(self):
        self._internal_id = f"USR-{hash(self.username) % 10000:04d}"

u = User("ali", "ali@mail.com", "secret123")
print(u)
# User(username='ali', email='ali@mail.com', is_active=True)
# password ve login_count repr'da YOK!

print(u.password)      # secret123 — erişilebilir ama repr'da gizli
print(u._internal_id)  # USR-XXXX — init'e dahil değil

field() parametreleri:

ParametreDefaultAçıklama
defaultMISSINGVarsayılan değer
default_factoryMISSINGMutable varsayılan için callable
reprTrue__repr__'a dahil et
initTrue__init__'e parametre olarak ekle
compareTrue__eq__ ve order karşılaştırmasına dahil et
hashNoneHash hesaplamasına dahil et
metadataNoneEk bilgi (dict)

frozen=True: Immutable Dataclass

frozen=True ile dataclass'ı değiştirilemez (immutable) yapabilirsin:

@dataclass(frozen=True)
class Point:
    x: float
    y: float

p = Point(3, 4)
print(p)  # Point(x=3, y=4)

# p.x = 10  # FrozenInstanceError: cannot assign to field 'x'

# Frozen dataclass otomatik olarak hashable'dır!
points = {Point(0, 0), Point(1, 1), Point(0, 0)}
print(points)  # {Point(x=0, y=0), Point(x=1, y=1)} — duplicate temizlendi

# Dict key olarak
distances = {
    Point(0, 0): "Başlangıç",
    Point(3, 4): "Hedef",
}
print(distances[Point(3, 4)])  # Hedef

Ne Zaman frozen=True Kullan?

  • Değerin değişmemesi gereken konfigürasyonlarda

  • Set/dict key olarak kullanılacak nesnelerde

  • Fonksiyonel programlama tarzında

  • Thread-safe olması gereken veri yapılarında

@dataclass(frozen=True)
class Color:
    r: int
    g: int
    b: int

    @property
    def hex(self):
        return f"#{self.r:02x}{self.g:02x}{self.b:02x}"

RED = Color(255, 0, 0)
GREEN = Color(0, 255, 0)
BLUE = Color(0, 0, 255)

print(RED.hex)    # #ff0000
# RED.r = 128     # FrozenInstanceError!

Post-Init: __post_init__

__post_init__ metodu, __init__ tamamlandıktan hemen sonra çağrılır. Hesaplanan field'lar, doğrulama veya dönüşüm için idealdir:

from dataclasses import dataclass, field
import math

@dataclass
class Circle:
    radius: float
    area: float = field(init=False)        # init'e dahil değil
    circumference: float = field(init=False)

    def __post_init__(self):
        if self.radius <= 0:
            raise ValueError("Yarıçap pozitif olmalı!")
        self.area = math.pi * self.radius ** 2
        self.circumference = 2 * math.pi * self.radius

c = Circle(5)
print(c)
# Circle(radius=5, area=78.53981633974483, circumference=31.41592653589793)

try:
    bad = Circle(-1)
except ValueError as e:
    print(e)  # Yarıçap pozitif olmalı!

Validation ve Normalizasyon

@dataclass
class Email:
    address: str
    username: str = field(init=False)
    domain: str = field(init=False)

    def __post_init__(self):
        # Normalizasyon
        self.address = self.address.lower().strip()

        # Doğrulama
        if "@" not in self.address:
            raise ValueError(f"Geçersiz email: {self.address}")

        # Hesaplama
        self.username, self.domain = self.address.split("@")

e = Email("  Ali@Gmail.COM  ")
print(e.address)   # ali@gmail.com
print(e.username)  # ali
print(e.domain)    # gmail.com

InitVar: Sadece Init'te Kullanılan Parametre

Bazen bir parametreyi sadece __post_init__'te kullanmak istersin ama field olarak saklamak istemezsin:

from dataclasses import dataclass, field, InitVar

@dataclass
class DatabaseConnection:
    host: str
    port: int
    password: InitVar[str]  # Sadece init'te — field olarak saklanmaz!
    connection_string: str = field(init=False)

    def __post_init__(self, password):
        self.connection_string = f"postgresql://{self.host}:{self.port}?password={password}"

conn = DatabaseConnection("localhost", 5432, "secret123")
print(conn)
# DatabaseConnection(host='localhost', port=5432, connection_string='...')
# password SAKLANMADI — güvenli!

print(conn.connection_string)
# postgresql://localhost:5432?password=secret123

# hasattr(conn, 'password')  # False — nesne üzerinde yok

Dataclass vs Regular Class vs NamedTuple

ÖzellikRegular Class@dataclassNamedTuple
__init__ otomatik
__repr__ otomatik
__eq__ otomatik
Mutable✅ (default)
Immutable❌ (convention)✅ (frozen=True)✅ (her zaman)
Method ekleyebilir✅ (ama alışılmadık)
Inheritance⚠️ (sınırlı)
Index erişimi✅ (p[0], p[1])
Unpack✅ (x, y = point)
Dict key__hash__ gerekirfrozen=True✅ (her zaman)
BellekNormalNormal (slots=True ile az)Az

Ne Zaman Hangisi?

  • Regular class: Karmaşık davranışlar, çok metod, özel __init__ gerekiyorsa.

  • Dataclass: Ağırlıklı veri taşıyan, orta karmaşıklıkta sınıflar. En yaygın seçim.

  • NamedTuple: Basit, immutable, tuple-like veri yapıları. Unpack gerekiyorsa.


typing.NamedTuple: Typed Version

collections.namedtuple'ın modern, type-annotated versiyonu:

from typing import NamedTuple

class Point(NamedTuple):
    x: float
    y: float
    z: float = 0.0  # Default değer

p = Point(3, 4)
print(p)        # Point(x=3, y=4, z=0.0)
print(p.x)      # 3
print(p[0])     # 3 — index erişimi!

x, y, z = p     # Unpack!
print(x, y, z)  # 3 4 0.0

# Immutable
# p.x = 10  # AttributeError

# Hashable — set/dict key olabilir
print(hash(p))  # -3550238948...

NamedTuple vs Frozen Dataclass

from typing import NamedTuple
from dataclasses import dataclass

class PointNT(NamedTuple):
    x: float
    y: float

@dataclass(frozen=True)
class PointDC:
    x: float
    y: float

# NamedTuple: tuple gibi davranır
pnt = PointNT(3, 4)
print(pnt[0])           # 3 — index erişimi
print(list(pnt))        # [3, 4] — iterable
x, y = pnt              # unpack
print(isinstance(pnt, tuple))  # True

# Frozen Dataclass: sınıf gibi davranır
pdc = PointDC(3, 4)
# print(pdc[0])         # TypeError — index yok
# x, y = pdc            # TypeError — unpack yok
print(isinstance(pdc, tuple))  # False

💡 İpucu: NamedTuple seç: basit veri, tuple uyumluluğu, unpack gerekli. Frozen dataclass seç: daha zengin davranış, method ekleme, kalıtım.


Inheritance ile Dataclass

Dataclass'lar normal sınıflar gibi kalıtımı destekler:

@dataclass
class Person:
    name: str
    age: int

@dataclass
class Employee(Person):
    company: str
    salary: float

@dataclass
class Manager(Employee):
    department: str
    team_size: int = 0

# Tüm field'lar zincirlenir
m = Manager("Ali", 35, "TechCorp", 50000, "Engineering", 12)
print(m)
# Manager(name='Ali', age=35, company='TechCorp', salary=50000, department='Engineering', team_size=12)

# isinstance çalışır
print(isinstance(m, Person))    # True
print(isinstance(m, Employee))  # True

Kalıtımda Default Değer Kuralı

Önemli bir kural: Default'suz field'lar, default'lu field'lardan sonra gelemez:

@dataclass
class Base:
    x: int
    y: int = 0  # Default var

# ❌ Bu çalışmaz!
# @dataclass
# class Child(Base):
#     z: int  # Default yok ama y'nin default'u var → TypeError!

# ✅ Bu çalışır
@dataclass
class Child(Base):
    z: int = 0  # Default ekle

# Veya field sırasını düşünerek tasarla

Frozen Dataclass Kalıtımı

@dataclass(frozen=True)
class Coordinate:
    x: float
    y: float

@dataclass(frozen=True)
class GeoPoint(Coordinate):
    name: str = ""

gp = GeoPoint(41.0082, 28.9784, "İstanbul")
print(gp)  # GeoPoint(x=41.0082, y=28.9784, name='İstanbul')

# Hâlâ immutable
# gp.x = 0  # FrozenInstanceError

slots=True: Bellek Optimizasyonu (Python 3.10+)

Normal Python sınıfları attribute'ları __dict__ sözlüğünde saklar. slots=True ile attribute'lar sabit slotlarda saklanır — daha az bellek, daha hızlı erişim:

@dataclass
class RegularPoint:
    x: float
    y: float

@dataclass(slots=True)
class SlottedPoint:
    x: float
    y: float

import sys

rp = RegularPoint(3, 4)
sp = SlottedPoint(3, 4)

print(sys.getsizeof(rp.__dict__))  # ~104 bytes (dict overhead)
# sp.__dict__  # AttributeError — __dict__ yok!

# Slotted nesne daha küçük
print(hasattr(rp, '__dict__'))  # True
print(hasattr(sp, '__dict__'))  # False
print(hasattr(sp, '__slots__'))  # True

Slots'ın Kısıtlamaları

@dataclass(slots=True)
class Strict:
    x: int
    y: int

s = Strict(1, 2)
# s.z = 3  # AttributeError! Slots dinamik attribute eklemeye izin vermez

# __dict__ olmadığı için vars() çalışmaz
# print(vars(s))  # TypeError

Ne Zaman slots=True Kullan?

  • Binlerce/milyonlarca nesne oluşturacaksan (bellek tasarrufu).

  • Performans kritikse (attribute erişimi ~%10-20 daha hızlı).

  • Dinamik attribute ekleme gerekmiyorsa.


Pratik: Product, Order, User Dataclass'ları

Gerçek bir e-ticaret senaryosu:

from dataclasses import dataclass, field
from datetime import datetime
from typing import Optional

@dataclass
class Product:
    """Ürün bilgisi."""
    name: str
    price: float
    category: str
    stock: int = 0
    sku: str = ""
    description: str = ""

    def __post_init__(self):
        if self.price < 0:
            raise ValueError("Fiyat negatif olamaz!")
        if not self.sku:
            # Otomatik SKU oluştur
            clean = self.name.upper().replace(" ", "-")[:10]
            self.sku = f"PRD-{clean}"

    @property
    def is_in_stock(self):
        return self.stock > 0

    @property
    def price_with_tax(self):
        return round(self.price * 1.20, 2)  # %20 KDV

    def sell(self, quantity=1):
        if quantity > self.stock:
            raise ValueError(f"Yetersiz stok! Mevcut: {self.stock}")
        self.stock -= quantity
        return quantity * self.price


@dataclass
class OrderItem:
    """Sipariş kalemi."""
    product: Product
    quantity: int
    unit_price: float = field(init=False)

    def __post_init__(self):
        self.unit_price = self.product.price

    @property
    def total(self):
        return self.unit_price * self.quantity


@dataclass
class Order:
    """Sipariş."""
    customer_name: str
    items: list[OrderItem] = field(default_factory=list)
    order_id: str = field(default="", init=False)
    created_at: datetime = field(default_factory=datetime.now, repr=False)
    status: str = "pending"
    _order_counter: int = field(default=0, init=False, repr=False)

    _counter = 0  # Class variable (dataclass field değil)

    def __post_init__(self):
        Order._counter += 1
        self.order_id = f"ORD-{Order._counter:04d}"

    def add_item(self, product, quantity=1):
        item = OrderItem(product, quantity)
        self.items.append(item)
        return self

    @property
    def total(self):
        return sum(item.total for item in self.items)

    @property
    def total_with_tax(self):
        return round(self.total * 1.20, 2)

    @property
    def item_count(self):
        return sum(item.quantity for item in self.items)

    def confirm(self):
        self.status = "confirmed"
        for item in self.items:
            item.product.sell(item.quantity)
        return f"Sipariş {self.order_id} onaylandı. Toplam: {self.total_with_tax} TL"

    def summary(self):
        lines = [f"📦 Sipariş: {self.order_id} ({self.status})"]
        lines.append(f"👤 Müşteri: {self.customer_name}")
        lines.append(f"📅 Tarih: {self.created_at.strftime('%d/%m/%Y %H:%M')}")
        lines.append("─" * 40)
        for item in self.items:
            lines.append(f"  {item.product.name} x{item.quantity} = {item.total:.2f} TL")
        lines.append("─" * 40)
        lines.append(f"  Ara Toplam: {self.total:.2f} TL")
        lines.append(f"  KDV (%20):  {self.total * 0.20:.2f} TL")
        lines.append(f"  TOPLAM:     {self.total_with_tax:.2f} TL")
        return "\n".join(lines)


@dataclass
class User:
    """Kullanıcı profili."""
    username: str
    email: str
    full_name: str = ""
    is_active: bool = True
    orders: list[Order] = field(default_factory=list, repr=False)
    created_at: datetime = field(default_factory=datetime.now, repr=False)

    def __post_init__(self):
        self.email = self.email.lower().strip()
        if not self.full_name:
            self.full_name = self.username.title()

    def place_order(self, order: Order):
        self.orders.append(order)

    @property
    def total_spent(self):
        return sum(
            o.total_with_tax for o in self.orders
            if o.status == "confirmed"
        )

    @property
    def order_count(self):
        return len(self.orders)
# Kullanım
laptop = Product("MacBook Pro", 45000, "Elektronik", stock=10)
mouse = Product("Magic Mouse", 2500, "Elektronik", stock=50)
keyboard = Product("Mechanical KB", 3500, "Elektronik", stock=30)

print(laptop)
# Product(name='MacBook Pro', price=45000, category='Elektronik', stock=10, sku='PRD-MACBOOK-PR', ...)

user = User("ali.yilmaz", "Ali@Gmail.COM", "Ali Yılmaz")
print(user)
# User(username='ali.yilmaz', email='ali@gmail.com', full_name='Ali Yılmaz', is_active=True)

order = Order("Ali Yılmaz")
order.add_item(laptop, 1).add_item(mouse, 2).add_item(keyboard, 1)

print(order.summary())

result = order.confirm()
print(result)

user.place_order(order)
print(f"\nToplam harcama: {user.total_spent} TL")
print(f"Laptop stok: {laptop.stock}")  # 9 — 1 satıldı

İleri Teknikler

asdict() ve astuple()

from dataclasses import dataclass, asdict, astuple

@dataclass
class Address:
    street: str
    city: str
    zip_code: str

@dataclass
class Contact:
    name: str
    email: str
    address: Address

addr = Address("Atatürk Cad. 123", "İstanbul", "34000")
contact = Contact("Ali", "ali@mail.com", addr)

# Sözlüğe çevir (nested dahil!)
d = asdict(contact)
print(d)
# {'name': 'Ali', 'email': 'ali@mail.com',
#  'address': {'street': 'Atatürk Cad. 123', 'city': 'İstanbul', 'zip_code': '34000'}}

# JSON'a çevirme
import json
print(json.dumps(d, ensure_ascii=False, indent=2))

# Tuple'a çevir
t = astuple(contact)
print(t)  # ('Ali', 'ali@mail.com', ('Atatürk Cad. 123', 'İstanbul', '34000'))

fields(): Field Bilgilerini Alma

from dataclasses import dataclass, fields, field

@dataclass
class Config:
    host: str = "localhost"
    port: int = 8080
    debug: bool = field(default=False, metadata={"env": "APP_DEBUG"})

for f in fields(Config):
    print(f"  {f.name}: {f.type.__name__} = {f.default}")
    if f.metadata:
        print(f"    metadata: {f.metadata}")

# host: str = localhost
# port: int = 8080
# debug: bool = False
#   metadata: {'env': 'APP_DEBUG'}

replace(): Kopyala ve Değiştir

from dataclasses import dataclass, replace

@dataclass(frozen=True)
class Config:
    host: str
    port: int
    debug: bool = False

config = Config("localhost", 8080)

# Frozen olduğu için değiştiremiyoruz ama kopyalayıp değiştirebiliriz
dev_config = replace(config, debug=True)
prod_config = replace(config, host="0.0.0.0", port=80)

print(config)      # Config(host='localhost', port=8080, debug=False)
print(dev_config)   # Config(host='localhost', port=8080, debug=True)
print(prod_config)  # Config(host='0.0.0.0', port=80, debug=False)

Yaygın Hatalar

1. Mutable Default Tuzağı

# ❌ HATA!
# @dataclass
# class Bad:
#     items: list = []  # ValueError!

# ✅ DOĞRU
@dataclass
class Good:
    items: list = field(default_factory=list)

2. Default Sıra Hatası

# ❌ HATA! Default'suz field, default'lu field'dan sonra
# @dataclass
# class Bad:
#     x: int = 0
#     y: int      # TypeError!

# ✅ DOĞRU
@dataclass
class Good:
    y: int        # Default'suz önce
    x: int = 0    # Default'lu sonra

3. __post_init__'te Self Kullanmayı Unutmak

@dataclass
class Example:
    name: str

    def __post_init__(self):
        # self.name zaten set edilmiş, kullanabilirsin
        self.name = self.name.strip().title()

💡 İpucu: Dataclass'lar çok güçlü ama her sınıf dataclass olmak zorunda değil. Eğer sınıfın ağırlıklı olarak veri taşıyorsa ve çok metodu yoksa → dataclass. Eğer karmaşık davranışlar, özel init mantığı veya performans optimizasyonu gerekiyorsa → normal class. İkisini de bilmek ve doğru yerde doğru olanı kullanmak önemli.


Özet

  • `@dataclass` decorator'ı __init__, __repr__, __eq__ metodlarını otomatik oluşturur — boilerplate killer.

  • `field()` ile default değer, repr/init/compare dahil etme, metadata gibi seçenekler ayarlanır. Mutable default'lar için default_factory kullan.

  • `frozen=True` ile immutable dataclass oluşturulur. Otomatik hashable — set/dict key olarak kullanılabilir.

  • `__post_init__` metodu init sonrası hesaplama, doğrulama ve normalizasyon için kullanılır.

  • Dataclass vs regular class vs NamedTuple: Veri ağırlıklıysa dataclass, karmaşık davranış varsa regular class, basit/immutable/tuple-like ise NamedTuple.

  • `slots=True` (Python 3.10+) bellek optimizasyonu sağlar — binlerce nesne oluştururken faydalı.

  • asdict(), astuple(), fields(), replace() yardımcı fonksiyonlar güçlü araçlar sunar.