Encapsulation ve Property
OOP'nin dört temel prensibinden biri olan Encapsulation (kapsülleme), belki de en pratik ve en sık kullanacağın olan. Diğer üç prensip (kalıtım, polimorfizm, soyutlama) biraz daha "mimari" düzeyde kalırken, encapsulation günlük kodunda her gün karşına çıkar.
Bu derste verini nasıl koruyacağını, Python'un erişim kontrolü felsefesini ve Pythonic getter/setter olan @property decorator'ını öğreneceğiz.
Encapsulation Nedir?
Encapsulation iki şey yapar:
Veri ve davranışı bir arada tutar (bundling).
İç detayları dışarıdan gizler (information hiding).
Uzaktan Kumanda Analojisi 📺
Televizyonun uzaktan kumandasını düşün. Kumandada "ses aç", "ses kıs", "kanal değiştir" düğmeleri var. Bu düğmeler arayüz (interface). Kumandanın içindeki devre kartlarını, kızılötesi LED'i, frekans ayarlarını görmüyorsun — ve görmen de gerekmiyor.
Encapsulation bunu yapar: Kullanıcıya sadece bilmesi gereken arayüzü sunar, karmaşık iç yapıyı gizler. Bu sayede:
Kullanıcı yanlışlıkla iç mekanizmayı bozamaz.
İç yapıyı değiştirirsin ama dışarıdan bakıldığında hiçbir şey değişmemiş gibi görünür.
# Encapsulation olmadan — her şey açık
class NaiveAccount:
def __init__(self, balance):
self.balance = balance
acc = NaiveAccount(1000)
acc.balance = -5000 # Bakiye negatif! Kimse durduramadı!
print(acc.balance) # -5000# Encapsulation ile — kontrollü erişim
class SafeAccount:
def __init__(self, balance):
self._balance = balance # "private"
def deposit(self, amount):
if amount > 0:
self._balance += amount
def withdraw(self, amount):
if 0 < amount <= self._balance:
self._balance -= amount
def get_balance(self):
return self._balance
acc = SafeAccount(1000)
acc.withdraw(500) # Kontrollü
print(acc.get_balance()) # 500
# acc._balance = -5000 # Teknik olarak yapabilirsin ama "yapma" diyorPython'da Erişim Kontrolü: Convention-Based
Java veya C++ gibi dillerde private, protected, public gibi kesin erişim belirleyicileri vardır. Python'da ise kesin bir erişim kontrolü yoktur. Bunun yerine Python convention (uzlaşı) tabanlı bir yaklaşım benimser.
Python felsefesi şunu der: "We're all consenting adults here." (Hepimiz yetişkin insanlarız.) Yani seni zorla durdurmaz ama "buraya dokunma" sinyali verir. Bu sinyaller alt çizgi convention'larıdır.
| Yazım | Anlamı | Erişim |
|---|---|---|
name | Public — herkes erişebilir | Sınırsız |
_name | Protected — "dokunma" sinyali | Teknik olarak erişilebilir |
__name | Name mangling — kasıtlı gizleme | _ClassName__name ile erişilebilir |
__name__ | Magic/dunder — Python'un özel metodları | Sınırsız (ama override etmek farklı) |
_tek_alt_çizgi: "Buna Dokunma" Sinyali
Tek alt çizgi ile başlayan isimler convention olarak private'tır. Python seni durdurmaz ama "bu iç kullanım için, doğrudan erişme" der.
class EmailService:
def __init__(self, smtp_host, smtp_port):
self._smtp_host = smtp_host # İç kullanım
self._smtp_port = smtp_port # İç kullanım
self._connection = None # İç kullanım
def send(self, to, subject, body):
"""Public API — kullanıcı bunu çağırır."""
self._connect()
self._authenticate()
self._send_message(to, subject, body)
self._disconnect()
def _connect(self):
"""Sunucuya bağlan — iç kullanım."""
print(f"Bağlanılıyor: {self._smtp_host}:{self._smtp_port}")
self._connection = True
def _authenticate(self):
"""Kimlik doğrula — iç kullanım."""
print("Kimlik doğrulanıyor...")
def _send_message(self, to, subject, body):
"""Mesajı gönder — iç kullanım."""
print(f"Gönderiliyor: {to} — {subject}")
def _disconnect(self):
"""Bağlantıyı kes — iç kullanım."""
self._connection = None
print("Bağlantı kesildi.")
# Kullanım — sadece send() çağır
service = EmailService("smtp.gmail.com", 587)
service.send("ali@mail.com", "Merhaba", "Nasılsın?")
# Teknik olarak erişebilirsin ama YAPMA
print(service._smtp_host) # Çalışır ama kötü pratik!from module import * ve Tek Alt Çizgi
_ ile başlayan isimler, from module import * ile import edilmez:
# my_module.py
public_var = "herkes görebilir"
_private_var = "import * ile gelmez"
def public_func():
return "public"
def _private_func():
return "private"# main.py
from my_module import *
print(public_var) # Çalışır
# print(_private_var) # NameError — import edilmedi!
# Ama açıkça import edersen çalışır
from my_module import _private_var
print(_private_var) # Çalışır__çift_alt_çizgi: Name Mangling
Çift alt çizgi ile başlayan isimler name mangling (isim karıştırma) uygulanır. Python, __name gibi bir özelliği _ClassName__name olarak yeniden adlandırır.
class Secret:
def __init__(self):
self.public = "herkes görebilir"
self._protected = "dokunma sinyali"
self.__private = "isim karıştırılmış"
s = Secret()
print(s.public) # herkes görebilir
print(s._protected) # dokunma sinyali — çalışır (convention)
# print(s.__private) # AttributeError!
# Ama name mangling ile erişebilirsin
print(s._Secret__private) # isim karıştırılmışName Mangling'i __dict__ ile Görmek
print(s.__dict__)
# {'public': 'herkes görebilir',
# '_protected': 'dokunma sinyali',
# '_Secret__private': 'isim karıştırılmış'}Gördün mü? __private, _Secret__private olarak saklanmış. Python ismi karıştırdı.
Name Mangling Neden Var?
Name mangling'in asıl amacı privacy sağlamak değil, kalıtımda isim çakışmasını önlemektir:
class Parent:
def __init__(self):
self.__secret = "parent secret"
def get_secret(self):
return self.__secret
class Child(Parent):
def __init__(self):
super().__init__()
self.__secret = "child secret" # Bu Parent'ın __secret'ını EZMİYOR!
def get_child_secret(self):
return self.__secret
c = Child()
print(c.get_secret()) # parent secret — Parent'ın __secret'ı
print(c.get_child_secret()) # child secret — Child'ın __secret'ı
# Perde arkasında:
print(c._Parent__secret) # parent secret
print(c._Child__secret) # child secret
# İkisi farklı attribute'lar!💡 İpucu: Çift alt çizgiyi "gizlilik" amacıyla kullanma. Tek alt çizgi convention'u Python dünyasında yeterlidir. Çift alt çizgiyi sadece kalıtımda isim çakışması riski olduğunda kullan. Guido van Rossum (Python'un yaratıcısı) de bunu söylüyor.
@property Decorator: Pythonic Getter/Setter
Java gibi dillerde getter ve setter yazmak çok yaygındır:
# Java tarzı — Python'da yapma!
class JavaStyle:
def __init__(self, name):
self._name = name
def get_name(self):
return self._name
def set_name(self, value):
self._name = value
p = JavaStyle("Ali")
print(p.get_name()) # Ali
p.set_name("Veli")
print(p.get_name()) # VeliBu çalışır ama Pythonic değil. Python'da @property decorator'ı kullanarak aynı şeyi çok daha temiz yapabilirsin:
class Person:
def __init__(self, name):
self._name = name
@property
def name(self):
"""Name getter."""
return self._name
@name.setter
def name(self, value):
"""Name setter."""
if not isinstance(value, str):
raise TypeError("İsim string olmalı!")
if len(value) < 2:
raise ValueError("İsim en az 2 karakter olmalı!")
self._name = value
p = Person("Ali")
print(p.name) # Ali — getter çağrılır (ama attribute gibi görünür!)
p.name = "Veli" # setter çağrılır
print(p.name) # Veli
# p.name = 5 # TypeError: İsim string olmalı!
# p.name = "A" # ValueError: İsim en az 2 karakter olmalı!Gördün mü? p.name yazdığında bir özelliğe erişiyormuş gibi görünüyor ama arka planda getter çağrılıyor. p.name = "Veli" yazdığında ise setter çağrılıyor. Kullanıcı farkı bile anlamıyor — ama sen doğrulama yapabiliyorsun.
Property'nin Güzelliği
Property'nin en büyük avantajı: Başlangıçta sade bir attribute ile başla, sonradan ihtiyaç olursa property'ye çevir — dışarıdaki kod değişmez.
# Versiyon 1: Basit attribute
class Circle:
def __init__(self, radius):
self.radius = radius
c = Circle(5)
print(c.radius) # 5
c.radius = 10
# Versiyon 2: Validation gerekti — property ekle
class Circle:
def __init__(self, radius):
self.radius = radius # setter'ı tetikler!
@property
def radius(self):
return self._radius
@radius.setter
def radius(self, value):
if value <= 0:
raise ValueError("Yarıçap pozitif olmalı!")
self._radius = value
# Dışarıdaki kod DEĞİŞMEDİ!
c = Circle(5)
print(c.radius) # 5
c.radius = 10 # Hâlâ aynı syntax
# c.radius = -1 # ValueError: Yarıçap pozitif olmalı!Bu Java'da mümkün değildir. Java'da obj.field yazdıysan, sonradan getter/setter'a geçmek dışarıdaki tüm kodu kırar. Bu yüzden Java'da baştan getter/setter yazarsın. Python'da buna gerek yok — ihtiyaç olduğunda property eklersin.
@property + @setter + @deleter
Property'nin üç bileşeni vardır:
class Temperature:
def __init__(self, celsius):
self._celsius = celsius
@property
def celsius(self):
"""Getter — değeri okur."""
return self._celsius
@celsius.setter
def celsius(self, value):
"""Setter — değeri yazar (validation ile)."""
if value < -273.15:
raise ValueError("Mutlak sıfırın altına inemezsin!")
self._celsius = value
@celsius.deleter
def celsius(self):
"""Deleter — değeri siler."""
print("Sıcaklık verisi siliniyor...")
del self._celsius
@property
def fahrenheit(self):
"""Hesaplanan (computed) property — sadece getter."""
return self._celsius * 9/5 + 32
t = Temperature(100)
# Getter
print(t.celsius) # 100
print(t.fahrenheit) # 212.0
# Setter
t.celsius = 25
print(t.fahrenheit) # 77.0
# Deleter
del t.celsius # "Sıcaklık verisi siliniyor..."
# Sadece getter olan property'ye yazamazsın
# t.fahrenheit = 100 # AttributeError: can't set attributeRead-Only Property (Computed Properties)
Sadece @property tanımlayıp setter yazmazsan, property read-only (salt okunur) olur:
class Square:
def __init__(self, side):
self._side = side
@property
def side(self):
return self._side
@side.setter
def side(self, value):
if value <= 0:
raise ValueError("Kenar pozitif olmalı!")
self._side = value
@property
def area(self):
"""Hesaplanan, salt okunur."""
return self._side ** 2
@property
def perimeter(self):
"""Hesaplanan, salt okunur."""
return 4 * self._side
sq = Square(5)
print(sq.area) # 25
print(sq.perimeter) # 20
sq.side = 10
print(sq.area) # 100 — otomatik güncellenir!
# sq.area = 50 # AttributeError: can't set attributeComputed property'ler çok güçlü: değer her erişildiğinde yeniden hesaplanır. Cache gerekiyorsa farklı teknikler var (örneğin functools.cached_property) ama çoğu zaman bu yeterli.
Property ile Validation: Pratik Örnekler
Yaş Doğrulaması (0-150 Arası)
class Person:
def __init__(self, name, age):
self.name = name
self.age = age # setter'ı tetikler
@property
def age(self):
return self._age
@age.setter
def age(self, value):
if not isinstance(value, int):
raise TypeError("Yaş tam sayı olmalı!")
if not 0 <= value <= 150:
raise ValueError("Yaş 0-150 arası olmalı!")
self._age = value
@property
def is_adult(self):
return self._age >= 18
@property
def age_group(self):
if self._age < 13:
return "çocuk"
elif self._age < 18:
return "ergen"
elif self._age < 65:
return "yetişkin"
else:
return "yaşlı"
p = Person("Ali", 25)
print(p.age) # 25
print(p.is_adult) # True
print(p.age_group) # yetişkin
p.age = 15
print(p.is_adult) # False
print(p.age_group) # ergen
try:
p.age = -5
except ValueError as e:
print(e) # Yaş 0-150 arası olmalı!
try:
p.age = 200
except ValueError as e:
print(e) # Yaş 0-150 arası olmalı!Email Doğrulaması
class User:
def __init__(self, username, email):
self.username = username
self.email = email # setter'ı tetikler
@property
def email(self):
return self._email
@email.setter
def email(self, value):
if not isinstance(value, str):
raise TypeError("Email string olmalı!")
if "@" not in value or "." not in value.split("@")[-1]:
raise ValueError(f"Geçersiz email: {value}")
self._email = value.lower().strip()
@property
def domain(self):
"""Email domain'ini döner."""
return self._email.split("@")[-1]
user = User("ali", "Ali@Gmail.COM")
print(user.email) # ali@gmail.com — otomatik normalize
print(user.domain) # gmail.comFiyat Doğrulaması ve Dönüşüm
class Product:
def __init__(self, name, price_tl):
self.name = name
self.price_tl = price_tl
@property
def price_tl(self):
return self._price_tl
@price_tl.setter
def price_tl(self, value):
if value < 0:
raise ValueError("Fiyat negatif olamaz!")
self._price_tl = round(value, 2)
@property
def price_usd(self):
"""Dolar karşılığı (yaklaşık)."""
return round(self._price_tl / 32, 2)
@property
def price_with_tax(self):
"""KDV dahil fiyat (%20)."""
return round(self._price_tl * 1.20, 2)
p = Product("Laptop", 45000)
print(f"TL: {p.price_tl}") # TL: 45000
print(f"USD: {p.price_usd}") # USD: 1406.25
print(f"KDV dahil: {p.price_with_tax}") # KDV dahil: 54000.0Neden Getter/Setter Yerine Property?
Python felsefesini anlamak önemli. Java'da getter/setter yazmak bir zorunluluktur çünkü doğrudan field erişimi sonradan değiştirilemez. Python'da ise:
Başla basit: Doğrudan attribute kullan (
self.name = name).İhtiyaç olursa: Property ekle — dışarıdaki kod kırılmaz.
Gereksiz getter/setter yazma: "Ya ileride lazım olursa?" diye Java tarzı
get_x()/set_x()yazma. Lazım olduğunda property eklersin.
# ❌ Bunu yapma (Java tarzı)
class Over_Engineered:
def __init__(self, x, y, z):
self._x = x
self._y = y
self._z = z
def get_x(self): return self._x
def set_x(self, v): self._x = v
def get_y(self): return self._y
def set_y(self, v): self._y = v
def get_z(self): return self._z
def set_z(self, v): self._z = v
# ✅ Bunu yap (Pythonic)
class Simple:
def __init__(self, x, y, z):
self.x = x
self.y = y
self.z = z
# Gerektiğinde property ekle, şimdilik gereksiz!⚠️ Dikkat: "Her attribute için property yaz" tuzağına düşme. Property sadece doğrulama, hesaplama veya yan etki (logging, notification) gerektiğinde kullan. Başka türlü basit attribute yeterli. YAGNI — "You Ain't Gonna Need It."
Property'nin Perde Arkası: Descriptor Protocol
@property aslında Python'un descriptor protocol'ünü kullanan bir sınıf:
# @property dekoratörü aslında şunu yapar:
class Manual:
def __init__(self, x):
self._x = x
def _get_x(self):
return self._x
def _set_x(self, value):
if value < 0:
raise ValueError("Negatif olamaz!")
self._x = value
x = property(_get_x, _set_x)
m = Manual(5)
print(m.x) # 5 — _get_x çağrılır
m.x = 10 # _set_x çağrılır@property dekoratörü bu yapıyı daha temiz hale getirir ama arka planda aynı mekanizma çalışır.
Gerçek Dünya Örneği: Kullanıcı Profili
Tüm kavramları bir araya getirelim:
class UserProfile:
"""Kullanıcı profili — encapsulation örneği."""
_valid_roles = {"admin", "editor", "viewer"}
def __init__(self, username, email, role="viewer"):
self.username = username # property
self.email = email # property
self.role = role # property
self._login_count = 0
self._last_login = None
# ─── Username ──────────────────────
@property
def username(self):
return self._username
@username.setter
def username(self, value):
if not isinstance(value, str) or len(value) < 3:
raise ValueError("Username en az 3 karakter olmalı!")
if not value.isalnum():
raise ValueError("Username sadece harf ve rakam içermeli!")
self._username = value.lower()
# ─── Email ─────────────────────────
@property
def email(self):
return self._email
@email.setter
def email(self, value):
if "@" not in value:
raise ValueError("Geçersiz email!")
self._email = value.lower().strip()
# ─── Role ──────────────────────────
@property
def role(self):
return self._role
@role.setter
def role(self, value):
if value not in self._valid_roles:
raise ValueError(f"Geçersiz rol! Geçerli: {self._valid_roles}")
self._role = value
# ─── Read-only Properties ──────────
@property
def login_count(self):
return self._login_count
@property
def is_admin(self):
return self._role == "admin"
@property
def display_name(self):
return f"@{self._username}"
# ─── Methods ───────────────────────
def login(self):
from datetime import datetime
self._login_count += 1
self._last_login = datetime.now()
return f"{self.display_name} giriş yaptı."
def __str__(self):
return f"{self.display_name} ({self._email}) [{self._role}]"# Kullanım
user = UserProfile("AliYilmaz", "Ali@Gmail.COM", "editor")
print(user) # @aliyilmaz (ali@gmail.com) [editor]
print(user.username) # aliyilmaz — otomatik lowercase
print(user.is_admin) # False
user.role = "admin"
print(user.is_admin) # True
print(user.login()) # @aliyilmaz giriş yaptı.
print(user.login_count) # 1
# Validation çalışıyor
try:
user.role = "superadmin"
except ValueError as e:
print(e) # Geçersiz rol! Geçerli: {'admin', 'editor', 'viewer'}
try:
user.username = "ab" # 3 karakterden kısa
except ValueError as e:
print(e) # Username en az 3 karakter olmalı!Bu örnekte encapsulation'ın tüm yönlerini görüyorsun:
Doğrulama: Username, email ve rol set edilirken kontrol ediliyor.
Normalleştirme: Username ve email otomatik lowercase'e çevriliyor.
Read-only property:
login_count,is_admin,display_namedışarıdan değiştirilemez.İç durum:
_login_countve_last_loginsadecelogin()metodu ile güncelleniyor.
Encapsulation Anti-Pattern'ler
1. Her Şeyi "Private" Yapma
# ❌ Aşırı kapsülleme — gereksiz
class OverProtected:
def __init__(self, x, y):
self.__x = x
self.__y = y
def get_x(self): return self.__x
def set_x(self, v): self.__x = v
def get_y(self): return self.__y
def set_y(self, v): self.__y = vBu sadece boilerplate ekler, hiçbir değer katmaz. Doğrulama yoksa, basit attribute kullan.
2. Convention'ı İhlal Etmek
class Config:
def __init__(self):
self._db_password = "secret123" # "private"
# Başka dosyada
config = Config()
print(config._db_password) # Çalışır ama YAPMA!
# Convention'a saygı göster3. Property İçinde Ağır İşlem
# ❌ Kötü — her erişimde API çağrısı
class BadExample:
@property
def data(self):
return requests.get("https://api.example.com/data").json()
# Her print(obj.data) yeni bir HTTP request yapar!Property'ler hızlı olmalı. Ağır işlemler için normal metod kullan veya caching ekle.
💡 İpucu: Property bir attribute gibi görünür, bu yüzden kullanıcı hızlı çalışmasını bekler. Eğer bir işlem 1 saniyeden fazla sürüyorsa, metod olarak tanımla —
obj.fetch_data(). Bu, kullanıcıya "bu işlem zaman alabilir" sinyali verir.
Özet
Encapsulation, veri ve davranışı bir arada tutup iç detayları gizleme prensibidir — uzaktan kumanda gibi.
Python'da erişim kontrolü convention-based'tir:
_private"dokunma" sinyali,__nameise name mangling uygular.`_tek_alt_çizgi`: İç kullanım için. Dışarıdan erişilebilir ama erişilmemeli.
`__çift_alt_çizgi`: Name mangling — asıl amacı kalıtımda isim çakışmasını önlemek, gizlilik değil.
`@property` decorator'ı Pythonic getter/setter sağlar. Attribute gibi görünür ama arka planda metod çağrılır.
Python felsefesi: Basit attribute ile başla, ihtiyaç olursa property ekle. Gereksiz getter/setter yazma.
AI Asistan
Sorularını yanıtlamaya hazır