← Kursa Dön
📄 Text · 15 min

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:

  1. Veri ve davranışı bir arada tutar (bundling).

  2. İç 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" diyor

Python'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ımAnlamıErişim
namePublic — herkes erişebilirSınırsız
_nameProtected — "dokunma" sinyaliTeknik olarak erişilebilir
__nameName 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())      # Veli

Bu ç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 attribute

Read-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 attribute

Computed 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.com

Fiyat 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.0

Neden 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:

  1. Başla basit: Doğrudan attribute kullan (self.name = name).

  2. İhtiyaç olursa: Property ekle — dışarıdaki kod kırılmaz.

  3. 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_name dışarıdan değiştirilemez.

  • İç durum: _login_count ve _last_login sadece login() 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 = v

Bu 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öster

3. 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, __name ise 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.