Neden önemli?
Metaprogramlama (metaprogramming), kodu yöneten kod yazmaktır: sınıfları çalışma anında üretmek, bir sınıf tanımlandığı anda onu bir kayda eklemek, öznitelik atamasını araya girip doğrulamak, bir fonksiyonun imzasını okuyup ona göre davranmak ya da bir Python dosyasını çalıştırmadan çözümlemek. Kulağa egzotik gelir ama her gün kullandığın kütüphanelerin hepsi bunun üzerine kuruludur: @dataclass sınıfına __init__ yazar, Django ve SQLAlchemy modelleri sınıf gövdesindeki alanları toplayıp tabloya çevirir, pytest test fonksiyonlarının parametre adlarına bakıp fixture enjekte eder, FastAPI fonksiyon imzasından API şeması çıkarır.
Bu dersin amacı sana "her şeyi metaclass ile yaz" demek değil; tam tersi. Hangi ihtiyaca en basit aracın yettiğini bilmeni istiyoruz. İleri OOP dersinde descriptor ve metaclass'a kısaca baktın, Dekoratörler (decorator) dersinde fonksiyon saran fonksiyonları gördün. Burada hepsini bir araya getirip Anadolu Market ve Ilgaz Bank için gerçekçi araçlar kuracağız.
Her şey nesnedir
Python'da sayılar, metinler, fonksiyonlar, sınıflar ve modüller birer nesnedir (object). Fonksiyonu bir değişkene atayabilir, listeye koyabilir, ona öznitelik ekleyebilirsin. Sınıf da bir nesnedir ve onu üreten sınıfa metaclass denir; varsayılan metaclass type'tır.
def kdv_ekle(tutar):
return round(tutar * 1.20, 2)
islem = kdv_ekle # fonksiyon bir değerdir
print(islem(100), islem.__name__)
kdv_ekle.surum = "2.1" # fonksiyona öznitelik eklenebilir
print(kdv_ekle.surum)
class Urun:
pass
print(type(42), type(kdv_ekle).__name__)
print(type(Urun)) # sınıfın türü: type
print(type(type)) # type kendisinin örneği
print(isinstance(Urun, object), isinstance(kdv_ekle, object))
Zincir şöyledir: u nesnesi Urun'un örneğidir, Urun de type'ın örneğidir. Bu, "sınıfı da bir fonksiyon çağrısıyla üretebilirim" demektir.
type() ile çalışma anında sınıf üretmek
type tek argümanla türü söyler; üç argümanla yeni bir sınıf üretir: type(ad, tabanlar, sozluk). class ifadesi aslında arka planda tam olarak bunu yapar.
def fiyatla(self, adet):
return self.birim_fiyat * adet
# class Kampanya: birim_fiyat = 50; fiyatla = fiyatla ile aynı
Kampanya = type("Kampanya", (object,), {"birim_fiyat": 50, "fiyatla": fiyatla})
k = Kampanya()
print(Kampanya.__name__, k.fiyatla(3))
# Bir yapılandırma tablosundan rapor sınıfları üretmek
tanimlar = {"GunlukRapor": "gün", "HaftalikRapor": "hafta"}
siniflar = {ad: type(ad, (), {"donem": d}) for ad, d in tanimlar.items()}
for ad, sinif in siniflar.items():
print(ad, "->", sinif.donem)
Ne zaman işe yarar? Bir JSON şemasından, veritabanı tablosundan ya da yapılandırma dosyasından gelen tanımlara göre sınıf üretmen gerektiğinde. Elle yazacağın onlarca neredeyse aynı sınıfı tek döngüyle üretirsin. Ama okunabilirlik düşer: IDE'n bu sınıfları göremez, otomatik tamamlama çalışmaz. Az sayıda sınıf için class ile yazmak hâlâ en iyisidir.
__init_subclass__: alt sınıfı otomatik kaydet
Eklenti (plugin) sistemlerinde klasik ihtiyaç şudur: "Bu temel sınıftan türeyen her sınıf kendiliğinden bir kayda (registry) girsin; kimse elle listeye eklemeyi unutmasın." Python 3.6'dan beri bunun için metaclass'a gerek yok: temel sınıfa __init_subclass__ yazarsın. Bir alt sınıf tanımlandığı anda Python bu metodu o alt sınıfla çağırır.
class Disari:
kayit = {}
def __init_subclass__(cls, bicim, **kwargs):
super().__init_subclass__(**kwargs)
if bicim in Disari.kayit:
raise ValueError(f"{bicim} zaten kayıtlı")
cls.bicim = bicim
Disari.kayit[bicim] = cls
class CsvDisari(Disari, bicim="csv"):
def yaz(self, satirlar):
return "\n".join(",".join(map(str, s)) for s in satirlar)
class JsonDisari(Disari, bicim="json"):
def yaz(self, satirlar):
import json
return json.dumps(satirlar)
print(sorted(Disari.kayit))
veri = [["AM-1", 120], ["AM-2", 80]]
for b in ["csv", "json"]:
print(b, "=>", Disari.kayit[b]().yaz(veri))
Sınıf satırındaki bicim="csv" gibi anahtar kelime argümanları doğrudan __init_subclass__'a gider. super().__init_subclass__(**kwargs) çağrısı, çoklu kalıtımda (inheritance) diğer tabanların da kendi kancalarını çalıştırabilmesi için önemlidir. Yeni bir dışa aktarım biçimi eklemek artık tek bir sınıf yazmaktan ibarettir; hiçbir if/elif zincirine dokunmazsın. Tasarım Kalıpları dersindeki kayıt tabanlı fabrikanın en temiz Python hâli budur.
Sınıf dekoratörleri
Dekoratör yalnızca fonksiyonlar için değildir. Sınıf dekoratörü sınıfı alır, değiştirir (ya da kaydeder) ve geri döndürür. @dataclass, @functools.total_ordering ve @enum.unique birer sınıf dekoratörüdür. Kalıtım gerektirmediği için mevcut sınıflara "yapıştırması" kolaydır.
def alanlari_say(cls):
alanlar = [ad for ad in cls.__annotations__]
cls.alan_sayisi = len(alanlar)
def __repr__(self):
icerik = ", ".join(f"{a}={getattr(self, a)!r}" for a in alanlar)
return f"{cls.__name__}({icerik})"
cls.__repr__ = __repr__
return cls # sınıfı geri döndürmeyi unutma!
@alanlari_say
class Musteri:
ad: str
sehir: str
def __init__(self, ad, sehir):
self.ad, self.sehir = ad, sehir
print(Musteri("Ayşe", "İzmir"), Musteri.alan_sayisi)
| İhtiyaç | En basit araç | Neden |
|---|---|---|
| Bir fonksiyonun davranışını sarmak | Fonksiyon dekoratörü | Tek fonksiyonu etkiler, okunur |
| Bir sınıfa metot/öznitelik eklemek | Sınıf dekoratörü | Kalıtım gerekmez, açıkça görünür |
| Alt sınıfları kaydetmek ya da denetlemek | __init_subclass__ | Kalıtımla otomatik, metaclass çatışması yok |
| Tek bir alanın okuma/yazmasını denetlemek | property ya da descriptor | Alan düzeyinde kural |
| Sınıf gövdesinin oluşumunu değiştirmek | Metaclass | Son çare; nadiren gerekir |
Descriptor protokolü: doğrulanan alanlar
property tek bir alan için harikadır ama on alanın hepsine "pozitif sayı olmalı" kuralı koyacaksan aynı kodu on kez yazarsın. Descriptor, öznitelik erişimini yöneten yeniden kullanılabilir bir nesnedir. Sınıf düzeyinde tanımlanır ve şu metotlardan birini taşır:
__set_name__(self, sahip, ad): sınıf oluşturulurken bir kez çağrılır; descriptor hangi ada atandığını öğrenir.__get__(self, nesne, sahip):u.fiyatokunurken çağrılır. Sınıf üzerinden okunursanesneNonegelir.__set__(self, nesne, deger):u.fiyat = 10yazılırken çağrılır; doğrulamanın yeri burasıdır.
class Pozitif:
def __set_name__(self, sahip, ad):
self.ad = ad
def __get__(self, nesne, sahip=None):
if nesne is None:
return self # Urun.fiyat -> descriptor'ın kendisi
return nesne.__dict__[self.ad]
def __set__(self, nesne, deger):
if not isinstance(deger, (int, float)) or deger <= 0:
raise ValueError(f"{self.ad} pozitif sayı olmalı, gelen: {deger!r}")
nesne.__dict__[self.ad] = deger # değeri NESNEDE sakla
class Urun:
fiyat = Pozitif()
stok = Pozitif()
def __init__(self, ad, fiyat, stok):
self.ad, self.fiyat, self.stok = ad, fiyat, stok
cay = Urun("Çay", 85.5, 40)
kahve = Urun("Kahve", 240, 12)
print(cay.fiyat, kahve.fiyat)
try:
cay.stok = -3
except ValueError as h:
print("Hata:", h)
Değeri descriptor'ın kendisinde (self.deger) saklamak en sık yapılan hatadır: descriptor sınıfa aittir, yani tüm örnekler aynı descriptor'ı paylaşır ve bir ürünün fiyatı diğerini ezer. Değer her zaman nesne.__dict__ içinde, __set_name__ ile öğrenilen adla tutulur.
u.x okunurken Python önce sınıfta __set__ taşıyan bir veri descriptor'ı (data descriptor) arar; varsa o kazanır. Yoksa nesnenin __dict__'ine bakar, sonra sınıftaki sıradan öznitelik ya da yalnız __get__ taşıyan descriptor'a (fonksiyonlar, functools.cached_property) gelir. Bu yüzden veri descriptor'ı değeri __dict__'te aynı adla saklasa da kontrolü kaybetmez.__getattr__, __getattribute__ ve __setattr__
Bu üç kanca nesnenin öznitelik erişiminin tamamına araya girer:
| Kanca | Ne zaman çağrılır | Tipik kullanım | Risk |
|---|---|---|---|
__getattr__ | Yalnızca normal arama başarısız olunca | Vekil (proxy), varsayılan değer, tembel yükleme | Düşük |
__getattribute__ | HER öznitelik okumasında | Erişim günlüğü, çok özel durumlar | Yüksek: sonsuz özyineleme, yavaşlık |
__setattr__ | HER atamada (__init__ içindekiler dahil) | Salt okunur nesne, değişiklik izleme | Orta: içeride self.x = ... yazarsan özyineleme |
class Ayarlar:
"""Eksik ayarlarda varsayılana düşen, değiştirilemeyen ayar nesnesi."""
VARSAYILAN = {"para_birimi": "TRY", "sayfa_boyu": 20}
def __init__(self, **degerler):
object.__setattr__(self, "_degerler", degerler) # __setattr__'ı atla
def __getattr__(self, ad): # yalnız bulunamayan adlarda çalışır
if ad in self._degerler:
return self._degerler[ad]
if ad in self.VARSAYILAN:
return self.VARSAYILAN[ad]
raise AttributeError(f"Bilinmeyen ayar: {ad}")
def __setattr__(self, ad, deger):
raise AttributeError("Ayarlar salt okunurdur")
a = Ayarlar(sayfa_boyu=50)
print(a.sayfa_boyu, a.para_birimi)
for islem in (lambda: a.tema, lambda: setattr(a, "sayfa_boyu", 5)):
try:
islem()
except AttributeError as h:
print("AttributeError:", h)
__setattr__ içinde self.ad = deger ya da setattr(self, ad, deger) yazarsan Python yine __setattr__'ı çağırır ve RecursionError alırsın. Gerçek atamayı object.__setattr__(self, ad, deger) ya da self.__dict__[ad] = deger ile yap. Aynı kural __getattribute__ için de geçerlidir: içeride object.__getattribute__(self, ad) kullan.Metaclass: ne zaman gerçekten gerekir?
Metaclass, sınıfın sınıfıdır; type'tan türetilir ve sınıf oluşturulurken araya girer. Python çekirdek geliştiricisi Tim Peters'ın sözü meşhurdur: "Metaclass'a ihtiyacın olup olmadığını merak ediyorsan, ihtiyacın yoktur." Yukarıdaki tablodaki araçlar vakaların çok büyük bölümünü çözer. Metaclass'ın gerçekten gerektiği yerler: sınıf gövdesinin sözlüğünü değiştirmek (__prepare__), sınıfın kendisine operatör kazandırmak (Enum'un len(Renk) ve Renk["KIRMIZI"] desteği gibi), ABC'nin isinstance davranışı gibi tür sistemi oyunları.
class ZorunluMetotMeta(type):
"""Alt sınıflarda 'hesapla' metodunu zorunlu kılar (gösterim)."""
def __new__(mcs, ad, tabanlar, sozluk):
cls = super().__new__(mcs, ad, tabanlar, sozluk)
if tabanlar and "hesapla" not in sozluk:
raise TypeError(f"{ad} sınıfı hesapla() tanımlamalı")
return cls
def __len__(cls): # sınıfın kendisine len() kazandırır
return len([a for a in vars(cls) if not a.startswith("_")])
class Komisyon(metaclass=ZorunluMetotMeta):
pass
class EftKomisyonu(Komisyon):
oran = 0.002
def hesapla(self, tutar):
return round(tutar * self.oran, 2)
print(EftKomisyonu().hesapla(15000), len(EftKomisyonu))
try:
class HataliKomisyon(Komisyon):
oran = 0.01
except TypeError as h:
print("TypeError:", h)
Aynı zorunluluğu abc.ABC ve @abstractmethod ile ya da __init_subclass__ içinde hasattr kontrolüyle de kurabilirsin; ikisi de daha okunurdur. len(EftKomisyonu) gibi sınıfın kendisine davranış eklemek ise yalnız metaclass ile mümkündür. Ayrıca iki farklı metaclass'a sahip tabanlardan türetmek "metaclass conflict" hatası verir; kütüphane kodunda metaclass kullanmak, kullanıcılarının elini bağlar.
inspect: kodu çalışırken incelemek
inspect modülü fonksiyonların imzasını, sınıfların üyelerini ve çağrı yığınını okumanı sağlar. pytest'in fixture enjeksiyonu ve FastAPI'nin parametre çözümlemesi tam olarak inspect.signature üzerine kuruludur.
import inspect
def kredi_teklifi(musteri_no: int, tutar: float, vade: int = 12, *, sigorta: bool = False):
"""Kredi teklifi üretir."""
return {"musteri_no": musteri_no, "tutar": tutar, "vade": vade}
imza = inspect.signature(kredi_teklifi)
print(imza)
for p in imza.parameters.values():
varsayilan = "zorunlu" if p.default is inspect.Parameter.empty else p.default
print(f"{p.name:12} {p.kind.name:22} {varsayilan}")
# Sözlükten yalnız fonksiyonun kabul ettiği alanları seçip çağır
istek = {"musteri_no": 7, "tutar": 50000, "kanal": "mobil"}
kabul = {k: v for k, v in istek.items() if k in imza.parameters}
print(kredi_teklifi(**kabul))
class Hesap:
def yatir(self, t): ...
def cek(self, t): ...
def _ic(self): ...
print([ad for ad, _ in inspect.getmembers(Hesap, inspect.isfunction) if not ad.startswith("_")])
inspect.getsource fonksiyonun kaynak dosyasını okur; tarayıcıda ve etkileşimli ortamlarda çoğu zaman çalışmaz. Kod çözümlemek istiyorsan kaynağı bir metin olarak al ve ast ile işle.ast: kodu çalıştırmadan çözümlemek
ast.parse Python kaynağını soyut sözdizimi ağacına (abstract syntax tree) çevirir. ruff, black, mypy ve bandit gibi araçlar böyle çalışır: kodu çalıştırmadan okur, kural ihlallerini bulur. ast.NodeVisitor'dan türetip visit_DugumAdi metotları yazarak ilgilendiğin düğümleri yakalarsın.
import ast
kaynak = """
import os
def rapor(siparisler, esik=100):
print("başladı")
return [s for s in siparisler if s > esik]
def temizle(metin):
return eval(metin)
"""
agac = ast.parse(kaynak)
class Denetci(ast.NodeVisitor):
def __init__(self):
self.fonksiyonlar, self.uyarilar = {}, []
def visit_FunctionDef(self, dugum):
self.fonksiyonlar[dugum.name] = len(dugum.args.args)
self.generic_visit(dugum) # iç düğümleri de gez
def visit_Call(self, dugum):
if isinstance(dugum.func, ast.Name) and dugum.func.id in {"eval", "exec", "print"}:
self.uyarilar.append(f"satır {dugum.lineno}: {dugum.func.id}() kullanımı")
self.generic_visit(dugum)
d = Denetci()
d.visit(agac)
print(d.fonksiyonlar)
print(*d.uyarilar, sep="\n")
print(ast.dump(ast.parse("tutar * 1.2", mode="eval").body))
Dinamik import ve eklenti yükleme
Modül adını çalışma anında bir metinden alıp yüklemek için importlib.import_module kullanılır; içinden nesneyi getattr ile çekersin. Yapılandırma dosyasında "rapor.bicim": "json" yazıyorsa ilgili modülü bu yolla yüklersin.
import importlib
def nesne_yukle(yol):
"""'paket.modul:nesne' biçimindeki yolu yükler."""
modul_adi, _, nesne_adi = yol.partition(":")
modul = importlib.import_module(modul_adi)
return getattr(modul, nesne_adi) if nesne_adi else modul
dumps = nesne_yukle("json:dumps")
karekok = nesne_yukle("math:sqrt")
print(dumps({"sehir": "İzmir"}, ensure_ascii=False), karekok(144))
try:
nesne_yukle("olmayan_modul:x")
except ModuleNotFoundError as h:
print("Yüklenemedi:", h.name)
Bu kalıp gunicorn'un app:create_app sözdiziminde, Django'nun INSTALLED_APPS listesinde ve setuptools'un entry_points tanımlarında karşına çıkar. Yalnızca güvendiğin yollardan yükle; kullanıcının yazdığı bir modül adını doğrudan import etmek, keyfi kod çalıştırmaya kapı açar.
exec ve eval: neden tehlikeli?
eval bir ifadeyi, exec bir kod bloğunu çalıştırır. Kullanıcıdan gelen metni bunlara vermek, kullanıcıya sunucunda Python çalıştırma yetkisi vermektir. eval(girdi, {"__builtins__": {}}) gibi "kısıtlama" denemeleri de bilinen yöntemlerle aşılır; güvenli kum havuzu (sandbox) değildir.
import ast
girdi = "[('AM-1', 120), ('AM-2', 80)]"
veri = ast.literal_eval(girdi) # yalnız sabit değerler: sayı, metin, liste, sözlük...
print(veri, type(veri).__name__)
for kotu in ["__import__('os').getcwd()", "open('x.txt', 'w')"]:
try:
ast.literal_eval(kotu)
except ValueError:
print("Reddedildi:", kotu)
eval/exec'e verme. Sabit veri için json.loads ya da ast.literal_eval, formül için ast ile yalnız izin verilen düğümleri değerlendiren küçük bir yorumlayıcı (bu dersin meydan okuma görevi) kullan.bakiye * 0.01 if segment == "premium" else 0. İlk sürümde bu formüller eval ile çalıştırılıyordu; güvenlik denetiminde paneli kullanan biri sunucu dosyalarını okuyabildiği ortaya çıktı. Çözüm, ast ile yalnız sayı, izinli değişken adları ve aritmetik/karşılaştırma düğümlerini kabul eden bir değerlendiriciydi. Aynı ekip yeni kampanya türlerini __init_subclass__ ile kaydedilen sınıflarla ekliyor; yeni tür eklemek merkezi koda dokunmayı gerektirmiyor.Sınıf oluşturma sırası
Bir class ifadesi çalıştığında sıra şudur: metaclass'ın __prepare__'i gövde sözlüğünü hazırlar, gövde çalışır, type.__new__ sınıfı oluşturur, bu sırada her descriptor için __set_name__ çağrılır, ardından taban sınıfın __init_subclass__'ı çalışır, en son sınıf dekoratörleri aşağıdan yukarıya uygulanır. Bir kütüphane hata ayıklarken bu sırayı bilmek, "alan neden henüz kayıtlı değil?" sorusunu çözer.
Tip ipuçlarını okumak
cls.__annotations__ ham ipuçlarını verir; from __future__ import annotations kullanılan modüllerde bunlar metindir. Gerçek türler için typing.get_type_hints(cls) kullan. dataclass, pydantic ve SQLAlchemy'nin Mapped[...] sözdizimi bu bilgiyle alan üretir.
Maliyet
Her sihir bir bedel öder: hata izleri (traceback) uzar, IDE ve tip denetleyicisi dinamik üretilen öznitelikleri göremez, yeni gelen ekip arkadaşı kodun ne yaptığını anlamakta zorlanır. Kurumsal kodda kural: önce sade fonksiyon ve sınıf, sonra dekoratör, sonra __init_subclass__/descriptor, en son metaclass. Metaprogramlamayı uygulama kodunda değil, çerçeve ve altyapı katmanında, iyi test edilmiş ve belgelenmiş olarak kullan.