← Kursa Dön
📄 Text · 15 min

__slots__, weakref ve Bellek Optimizasyonu

Giriş: Bellek Bilinci

Python çok rahat bir dil. Nesnelere istediğin zaman yeni attribute ekleyebilirsin, bellek yönetimi otomatik, garbage collector her şeyi halleder. Ama bu rahatlığın bir bedeli var: bellek kullanımı.

Çoğu projede bu önemli değil. Ama binlerce, milyonlarca nesne oluşturuyorsan — mesela bir oyun, bir veritabanı ORM'i veya bir veri pipeline'ı — her byte önemli hale gelir.

Bu derste iki güçlü araç öğreneceksin:

  1. `__slots__`: Nesnelerin bellek kullanımını dramatik şekilde azaltır

  2. `weakref`: Zayıf referanslarla bellek sızıntılarını önler


__slots__: Sabit Çekmece Sistemi

Analoji: Dolap vs Çekmece

Normal bir Python nesnesi sonsuz genişleyebilen bir dolap gibi. İstediğin rafı eklersin, istediğin şeyi koyarsın. Çok esnek ama yer kaplıyor çünkü her rafa bir etiket (anahtar), bir kapak mekanizması (hash tablosu girdisi) lazım.

`__slots__` ile nesne bir sabit çekmeceli dolap oluyor. "Bu nesnede sadece şu 3 çekmece olacak" diyorsun. Ekstra çekmece eklenemez ama var olanlar çok daha az yer kaplıyor.

Normal Class: __dict__ ile

class NormalPoint:
    def __init__(self, x, y):
        self.x = x
        self.y = y

p = NormalPoint(3, 4)

# Her instance'ın bir __dict__'i var
print(p.__dict__)  # {'x': 3, 'y': 4}

# İstediğin attribute'ı ekleyebilirsin
p.z = 5
p.color = "red"
print(p.__dict__)  # {'x': 3, 'y': 4, 'z': 5, 'color': 'red'}

__dict__ bir Python dictionary'si — esnek ama bellek yoğun. Her instance için ayrı bir dict oluşturulur.

__slots__ ile

class SlottedPoint:
    __slots__ = ("x", "y")
    
    def __init__(self, x, y):
        self.x = x
        self.y = y

p = SlottedPoint(3, 4)

# __dict__ YOK!
# print(p.__dict__)  # AttributeError!

# Sadece tanımlı attribute'lar kullanılabilir
print(p.x, p.y)  # 3 4

# Yeni attribute eklenemez
# p.z = 5  # AttributeError: 'SlottedPoint' object has no attribute 'z'

Bellek Farkı

import sys

class Normal:
    def __init__(self, x, y, z):
        self.x = x
        self.y = y
        self.z = z

class Slotted:
    __slots__ = ("x", "y", "z")
    
    def __init__(self, x, y, z):
        self.x = x
        self.y = y
        self.z = z

n = Normal(1, 2, 3)
s = Slotted(1, 2, 3)

# Instance boyutu
print(f"Normal:  {sys.getsizeof(n)} bytes")     # ~48 bytes
print(f"Slotted: {sys.getsizeof(s)} bytes")      # ~64 bytes

# AMA! __dict__'in boyutunu da ekle
print(f"Normal __dict__: {sys.getsizeof(n.__dict__)} bytes")  # ~104 bytes
print(f"Normal toplam:   ~{sys.getsizeof(n) + sys.getsizeof(n.__dict__)} bytes")

# Slotted'da __dict__ yok!
# Gerçek karşılaştırma: Normal ~152 bytes vs Slotted ~64 bytes

Büyük Ölçekte Fark

import sys

class NormalUser:
    def __init__(self, name, age, email):
        self.name = name
        self.age = age
        self.email = email

class SlottedUser:
    __slots__ = ("name", "age", "email")
    
    def __init__(self, name, age, email):
        self.name = name
        self.age = age
        self.email = email

# 1 milyon nesne oluştur
normal_users = [NormalUser(f"User_{i}", i, f"user{i}@test.com") for i in range(1_000_000)]
slotted_users = [SlottedUser(f"User_{i}", i, f"user{i}@test.com") for i in range(1_000_000)]

# Toplam bellek kullanımını hesapla (yaklaşık)
normal_size = sum(sys.getsizeof(u) + sys.getsizeof(u.__dict__) for u in normal_users[:1000]) / 1000
slotted_size = sum(sys.getsizeof(u) for u in slotted_users[:1000]) / 1000

print(f"Normal (1M):  ~{normal_size * 1_000_000 / 1024 / 1024:.0f} MB")
print(f"Slotted (1M): ~{slotted_size * 1_000_000 / 1024 / 1024:.0f} MB")
print(f"Tasarruf:     ~{(normal_size - slotted_size) / normal_size * 100:.0f}%")

1 milyon nesnede yüzlerce MB tasarruf edebilirsin. Bu ciddi bir fark.


__slots__ Kullanım Kuralları

1. Tuple Olarak Tanımla

class Good:
    __slots__ = ("x", "y", "z")  # Tuple (en yaygın)

class AlsoGood:
    __slots__ = ["x", "y", "z"]  # Liste de olur

class AlsoOk:
    __slots__ = "x"  # Tek attribute için string

2. __dict__ İstiyorsan Ekle

class Hybrid:
    __slots__ = ("x", "y", "__dict__")
    
    def __init__(self, x, y):
        self.x = x
        self.y = y

h = Hybrid(1, 2)
h.z = 3  # Artık yeni attribute eklenebilir!
print(h.z)  # 3

Bu hibrit yaklaşım, bazı attribute'ları hızlı/hafif tutarken esnekliği korur.

3. __weakref__ Slot'u

Varsayılan olarak __slots__ kullanan sınıflar weakref ile referans olunamaz:

import weakref

class NoWeakRef:
    __slots__ = ("x",)

class WithWeakRef:
    __slots__ = ("x", "__weakref__")

# obj = NoWeakRef()
# weakref.ref(obj)  # TypeError!

obj = WithWeakRef()
ref = weakref.ref(obj)  # Çalışır!

4. Default Değerler

class Config:
    __slots__ = ("host", "port", "debug")
    
    # ❌ Slot'lara class-level default değer atanamaz
    # host = "localhost"  # Hata!
    
    def __init__(self, host="localhost", port=8080, debug=False):
        self.host = host
        self.port = port
        self.debug = debug

⚠️ Dikkat: __slots__ ile class-level attribute tanımlayamazsın (constant'lar hariç). Default değerleri __init__ içinde ata.


Kalıtımda __slots__

Base Class ile Slots

class Base:
    __slots__ = ("x", "y")
    
    def __init__(self, x, y):
        self.x = x
        self.y = y

class Child(Base):
    __slots__ = ("z",)  # Sadece EK slot'ları tanımla
    
    def __init__(self, x, y, z):
        super().__init__(x, y)
        self.z = z

c = Child(1, 2, 3)
print(c.x, c.y, c.z)  # 1 2 3
# c.w = 4  # AttributeError — hala kısıtlı

Dikkat: Slots'sız Base

class NormalBase:
    pass  # __dict__ var

class SlottedChild(NormalBase):
    __slots__ = ("x", "y")

# Child'ın __slots__'ı var ama Base'in __dict__'i var
c = SlottedChild()
c.x = 1    # Slot'tan
c.z = 3    # __dict__'ten! Kısıtlama çalışmaz

# __slots__'ın tam etkisi için TÜM hiyerarşide slots olmalı

Slot Tekrarı Yapmamak

class Base:
    __slots__ = ("x", "y")

class Child(Base):
    # ❌ x ve y'yi tekrar tanımlama!
    # __slots__ = ("x", "y", "z")  # x ve y zaten var
    
    # ✅ Sadece yeni slot'ları ekle
    __slots__ = ("z",)

weakref: Zayıf Referans

Normal Referans vs Weak Referans

Normal bir referans, nesnenin yaşamasını garanti eder — referans var olduğu sürece garbage collector nesneyi silmez.

Weak referans ise nesneyi "gözlemler" ama yaşamasını garanti etmez. Nesnenin başka güçlü referansı kalmadığında garbage collector nesneyi siler, weak referans da otomatik geçersiz olur.

Analoji: İsim Kartı vs Tanıdık

Normal referans = birinin telefon numarasını rehberine kaydetmek. Numara rehberinde olduğu sürece o kişiye ulaşabilirsin.

Weak referans = birinin yüzünü tanımak ama numarasını kaydetmemek. O kişi şehirden taşınırsa (garbage collected), artık ulaşamazsın.

import weakref

class BigObject:
    def __init__(self, name):
        self.name = name
    
    def __repr__(self):
        return f"BigObject({self.name!r})"
    
    def __del__(self):
        print(f"🗑️ {self.name} silindi (garbage collected)")

# Strong reference
obj = BigObject("Data")

# Weak reference
weak = weakref.ref(obj)

print(weak())       # BigObject('Data') — nesne hala yaşıyor
print(weak() is obj)  # True — aynı nesne

# Strong reference'ı kaldır
del obj
# Çıktı: 🗑️ Data silindi (garbage collected)

print(weak())  # None — nesne artık yok!

weakref.ref() ve weakref.proxy()

weakref.ref()

import weakref

class Node:
    def __init__(self, value):
        self.value = value

node = Node(42)

# Weak reference oluştur
ref = weakref.ref(node)

# Nesneye erişmek için çağır
print(ref())        # <__main__.Node object ...>
print(ref().value)  # 42

# Nesne silininse
del node
print(ref())  # None

weakref.proxy()

proxy(), ref()'ten farklı olarak doğrudan nesne gibi kullanılabilir:

import weakref

class DataStore:
    def __init__(self, data):
        self.data = data
    
    def get_data(self):
        return self.data

store = DataStore([1, 2, 3])

# ref ile: her seferinde () ile çağırmak lazım
ref = weakref.ref(store)
print(ref().get_data())  # [1, 2, 3]

# proxy ile: doğrudan kullan
proxy = weakref.proxy(store)
print(proxy.get_data())  # [1, 2, 3]
print(proxy.data)        # [1, 2, 3]

# Ama nesne silinirse:
del store
# print(proxy.data)  # ReferenceError: weakly-referenced object no longer exists

Callback: Nesne Silindiğinde Bildirim

import weakref

def on_deleted(ref):
    print(f"⚠️ Nesne silindi! ref={ref}")

class Resource:
    def __init__(self, name):
        self.name = name

res = Resource("dosya.txt")
ref = weakref.ref(res, on_deleted)

print(ref())  # Resource nesnesi

del res
# Çıktı: ⚠️ Nesne silindi! ref=<weakref at 0x...; dead>

Neden weakref?

1. Circular Reference (Döngüsel Referans)

import weakref

class Parent:
    def __init__(self, name):
        self.name = name
        self.children = []
    
    def add_child(self, child):
        self.children.append(child)
        child.parent = weakref.ref(self)  # Zayıf referans!

class Child:
    def __init__(self, name):
        self.name = name
        self.parent = None
    
    def get_parent_name(self):
        parent = self.parent()  # Dereference
        return parent.name if parent else "Yok"

# Kullanım
dad = Parent("Ahmet")
kid = Child("Ali")
dad.add_child(kid)

print(kid.get_parent_name())  # Ahmet

# Parent silinirse
del dad
print(kid.get_parent_name())  # Yok — parent garbage collected

Eğer weakref yerine normal referans kullansaydık, Parent ve Child birbirini referans ettiği için ikisi de garbage collected olmazdı.

2. Cache Sistemi

import weakref

class ImageCache:
    """Zayıf referanslı cache — bellek dolunca otomatik temizlenir."""
    
    def __init__(self):
        self._cache = weakref.WeakValueDictionary()
    
    def get(self, key):
        return self._cache.get(key)
    
    def put(self, key, image):
        self._cache[key] = image
    
    def size(self):
        return len(self._cache)

class Image:
    def __init__(self, path):
        self.path = path
        self.data = b"x" * 1000  # Büyük veri simülasyonu

# Cache kullan
cache = ImageCache()

# Resim yükle ve cache'e koy
img1 = Image("photo1.jpg")
img2 = Image("photo2.jpg")
cache.put("photo1", img1)
cache.put("photo2", img2)

print(f"Cache boyutu: {cache.size()}")  # 2

# img1 referansı silinirse, cache'ten de otomatik çıkar
del img1
import gc; gc.collect()
print(f"Cache boyutu: {cache.size()}")  # 1 — photo1 otomatik silindi!

3. Observer Pattern

import weakref

class EventEmitter:
    """Zayıf referanslı event sistemi."""
    
    def __init__(self):
        self._listeners = weakref.WeakSet()
    
    def add_listener(self, listener):
        self._listeners.add(listener)
    
    def emit(self, event):
        # Dead referanslar otomatik temizlenir
        for listener in self._listeners:
            listener.on_event(event)

class Logger:
    def on_event(self, event):
        print(f"📝 Log: {event}")

class Monitor:
    def on_event(self, event):
        print(f"📊 Monitor: {event}")

emitter = EventEmitter()

logger = Logger()
monitor = Monitor()

emitter.add_listener(logger)
emitter.add_listener(monitor)

emitter.emit("Kullanıcı giriş yaptı")
# 📝 Log: Kullanıcı giriş yaptı
# 📊 Monitor: Kullanıcı giriş yaptı

# Monitor silinirse, listener listesinden otomatik çıkar
del monitor
import gc; gc.collect()

emitter.emit("Dosya yüklendi")
# 📝 Log: Dosya yüklendi
# (Monitor artık yok — otomatik temizlendi)

WeakValueDictionary ve WeakSet

WeakValueDictionary

Dict'in value'ları weak referans:

import weakref

class User:
    def __init__(self, name):
        self.name = name
    def __repr__(self):
        return f"User({self.name})"

# Normal dict: nesneleri canlı tutar
normal_cache = {}

# WeakValueDictionary: nesneleri canlı TUTMAZ
weak_cache = weakref.WeakValueDictionary()

user = User("Ali")
normal_cache["ali"] = user
weak_cache["ali"] = user

del user
import gc; gc.collect()

print("ali" in normal_cache)  # True — normal dict canlı tutar
print("ali" in weak_cache)    # False — weak dict otomatik temizledi

WeakKeyDictionary

Dict'in key'leri weak referans:

import weakref

class Connection:
    def __init__(self, host):
        self.host = host

# Bağlantılara metadata ekle
metadata = weakref.WeakKeyDictionary()

conn = Connection("db.example.com")
metadata[conn] = {"created": "2024-01-15", "queries": 42}

print(metadata[conn])  # {'created': '2024-01-15', 'queries': 42}

del conn
import gc; gc.collect()
print(len(metadata))  # 0 — connection silinince metadata da gitti

WeakSet

Elemanları weak referans olan set:

import weakref

class Task:
    def __init__(self, name):
        self.name = name
    def __repr__(self):
        return f"Task({self.name})"

active_tasks = weakref.WeakSet()

t1 = Task("indirme")
t2 = Task("işleme")
t3 = Task("yükleme")

active_tasks.add(t1)
active_tasks.add(t2)
active_tasks.add(t3)

print(f"Aktif görevler: {len(active_tasks)}")  # 3

del t2  # İşleme görevi bitti
import gc; gc.collect()

print(f"Aktif görevler: {len(active_tasks)}")  # 2
for task in active_tasks:
    print(f"  - {task}")

sys.getsizeof() ile Bellek Ölçümü

import sys

# Temel tiplerin boyutları
print(f"int (0):       {sys.getsizeof(0)} bytes")
print(f"int (42):      {sys.getsizeof(42)} bytes")
print(f"float:         {sys.getsizeof(3.14)} bytes")
print(f"str (''):      {sys.getsizeof('')} bytes")
print(f"str ('hello'): {sys.getsizeof('hello')} bytes")
print(f"list ([]):     {sys.getsizeof([])} bytes")
print(f"dict ({{}}):     {sys.getsizeof({})} bytes")
print(f"tuple (()):    {sys.getsizeof(())} bytes")
print(f"set (set()):   {sys.getsizeof(set())} bytes")

Slots Karşılaştırma Testi

import sys

class Normal:
    def __init__(self, a, b, c, d, e):
        self.a = a
        self.b = b
        self.c = c
        self.d = d
        self.e = e

class Slotted:
    __slots__ = ("a", "b", "c", "d", "e")
    
    def __init__(self, a, b, c, d, e):
        self.a = a
        self.b = b
        self.c = c
        self.d = d
        self.e = e

n = Normal(1, 2, 3, 4, 5)
s = Slotted(1, 2, 3, 4, 5)

n_size = sys.getsizeof(n) + sys.getsizeof(n.__dict__)
s_size = sys.getsizeof(s)

print(f"Normal:  {n_size} bytes (instance + __dict__)")
print(f"Slotted: {s_size} bytes")
print(f"Tasarruf: {n_size - s_size} bytes ({(n_size - s_size)/n_size*100:.0f}%)")

Derin Boyut Ölçümü

sys.getsizeof() sadece nesnenin kendisini ölçer, içerdiği nesneleri değil. Derin ölçüm için:

import sys

def deep_getsizeof(obj, seen=None):
    """Nesnenin ve içerdiği tüm nesnelerin toplam boyutu."""
    if seen is None:
        seen = set()
    
    obj_id = id(obj)
    if obj_id in seen:
        return 0
    seen.add(obj_id)
    
    size = sys.getsizeof(obj)
    
    if isinstance(obj, dict):
        size += sum(deep_getsizeof(k, seen) + deep_getsizeof(v, seen) 
                    for k, v in obj.items())
    elif isinstance(obj, (list, tuple, set, frozenset)):
        size += sum(deep_getsizeof(item, seen) for item in obj)
    elif hasattr(obj, "__dict__"):
        size += deep_getsizeof(obj.__dict__, seen)
    elif hasattr(obj, "__slots__"):
        size += sum(
            deep_getsizeof(getattr(obj, slot, None), seen)
            for slot in obj.__slots__
            if hasattr(obj, slot)
        )
    
    return size

# Karşılaştırma
n = Normal(1, 2, 3, 4, 5)
s = Slotted(1, 2, 3, 4, 5)

print(f"Normal (derin):  {deep_getsizeof(n)} bytes")
print(f"Slotted (derin): {deep_getsizeof(s)} bytes")

Ne Zaman __slots__ Kullanmalı?

✅ Kullan

  • Çok sayıda nesne oluşturuyorsan (10,000+)

  • Performans kritik uygulamalar (oyun, simülasyon)

  • Veri sınıfları (sadece veri tutan basit nesneler)

  • ORM entity'leri (milyonlarca veritabanı kaydı)

  • Attribute erişim hızının önemli olduğu durumlar

❌ Kullanma

  • Prototipleme/deneme aşamasında

  • Az sayıda nesne oluşturuyorsan

  • Dinamik attribute'lara ihtiyacın varsa

  • Mixin sınıflarında (karmaşıklık artırır)

  • __dict__'e bağımlı kütüphanelerle çalışıyorsan

💡 İpucu: dataclass ile __slots__ kullanmak çok kolay (Python 3.10+): ``python from dataclasses import dataclass @dataclass(slots=True) class Point: x: float y: float z: float `` Tek parametre ile slots aktif!


Yaygın Hatalar

1. __slots__ ile Default Attribute

class Bad:
    __slots__ = ("x",)
    # x = 10  # ❌ ValueError! Slot ve class attribute çakışır

class Good:
    __slots__ = ("x",)
    def __init__(self):
        self.x = 10  # ✅ __init__ içinde ata

2. weakref ile Immutable Tipler

import weakref

# ❌ Bazı built-in tipler weakref desteklemez
# weakref.ref(42)        # TypeError
# weakref.ref("hello")   # TypeError
# weakref.ref((1, 2))    # TypeError

# ✅ Kendi sınıfların destekler
class MyClass:
    pass

weakref.ref(MyClass())  # Çalışır

3. WeakValueDictionary'de Geçici Nesne

import weakref

cache = weakref.WeakValueDictionary()

# ❌ Geçici nesne hemen silinir!
cache["key"] = MyClass()
print("key" in cache)  # False! Nesne hemen garbage collected

# ✅ Önce referans tut
obj = MyClass()
cache["key"] = obj
print("key" in cache)  # True — obj değişkeni canlı tutuyor

Özet

  • `__slots__`, sınıfın attribute'larını sabitleyerek __dict__ oluşturulmasını engeller. Bu, nesne başına %30-50 bellek tasarrufu sağlar.

  • __slots__ kullanan sınıflarda yeni attribute eklenemez. Esneklik azalır ama performans ve bellek verimliliği artar.

  • Kalıtımda __slots__'ın tam etkisi için tüm hiyerarşide slots tanımlanmalıdır. Child sınıfta sadece ek slot'ları tanımla.

  • `weakref` modülü, nesneyi garbage collection'dan korumayan zayıf referanslar oluşturur. Cache, observer pattern ve circular reference sorunlarını çözer.

  • `WeakValueDictionary`, `WeakKeyDictionary` ve `WeakSet`, elemanları weak referans tutan koleksiyon yapılarıdır.

  • __slots__ çok sayıda nesne oluşturulan durumlarda, weakref ise bellek sızıntısı riski olan referans ilişkilerinde kullanılmalıdır.