Skip to content

Değişkenler: Python'da 'Etiket vs Kutu' Felsefesi ve Assignment'ın İçsel Gerçeği

Diğer dillerde değişken bir 'kutu'dur — değer kutuya konur. Python'da değişken bir 'etiket'tir — nesneye yapıştırılır. Bu fark gibi görünmeyebilir ama Python'un %30'unun davranışını açıklıyor: mutable default tuzağı, list aliasing, fonksiyon parametre semantics, garbage collection. Bu derste 'a = 5 yazınca aslında ne olur' sorusunun derinlemesine cevabını alıyoruz.

Şükrü Yusuf KAYA
20 min read
Beginner
Değişkenler: Python'da 'Etiket vs Kutu' Felsefesi ve Assignment'ın İçsel Gerçeği
🧠 Bu ders 'çok temel' gibi görünür ama yanılma
Python'a başlayan herkes 'a = 5' yazmayı 5 dakikada öğrenir. Ama Python'un kalbindeki 'etiket sistemi' hatalardan büyük kısmının kaynağı. Mutable default trap'ı (def f(x=[]): ...) yıllarca production'da bug yarattı. List aliasing yüzünden veriler beklenmedik şekilde değişti. Bu dersi düzgün okursan — bu kategori bug'lar senin başına asla gelmeyecek. Ders 'kolay' değil; sadece 'erken'.

Önce diğer dillerde nasıl?#

Bu konuyu anlamak için başka bir dilde nasıl çalıştığını hatırlayalım.
C dilinde bir değişken bellek konumudur. Sen
int x = 5;
yazınca:
  1. RAM'de
    int
    boyutunda (4 byte) bir slot ayrılır.
  2. O slot'a
    5
    yazılır.
  3. x
    o slot'un adresine bağlanır.
Sonra
x = 10;
yapınca: o aynı slot'a
10
yazılır. Slot değişmez, içerik değişir.
[Bellek] x: [0x7ffe0]: 5 → x: [0x7ffe0]: 10
Bu modele "variable as box" — kutu — diyoruz. Değişken bir kutudur, değeri kutunun içine koyuyorsun.
Şimdi Python'a bak. Aynı kod:
x = 5 x = 10
Görünüşte aynı şey ama içsel olarak çok farklı. Python:
  1. Bellekte
    5
    integer nesnesi yaratıyor (zaten varsa kullanıyor — small int caching var).
  2. x
    adlı bir etiket o nesneye yapıştırıyor.
  3. Sonra
    5
    nesnesinden etiketi söküyor,
    10
    nesnesine yapıştırıyor.
[Bellek] [int 5] ← x → [int 5] [int 10] ← x
İlk integer nesnesi (
5
) olduğu gibi duruyor, sadece etiket başka yere geçiyor. Eğer hiçbir etiket bağlı değilse Python garbage collector onu siler.
Bu modele "variable as label" diyoruz. Türkçede en güzel benzetmesi: post-it not. Bir nesnenin üzerine post-it yapıştırıyorsun. Aynı nesnenin üzerinde birden fazla post-it olabilir; aynı post-it farklı zamanda farklı nesnelerde olabilir.
Bu farkı içselleştirdiğinde, Python'un bütün davranışı netleşir.

Pratik kanıt —
id()
fonksiyonu#

Python sana her nesnenin "kimlik numarasını" veren bir fonksiyon sunuyor:
id()
. Bu sayı nesnenin bellekteki adresi (CPython implementasyonunda).
>>> x = 5 >>> id(x) 4368089096 >>> x = 10 >>> id(x) 4368089256 # FARKLI! Aynı slot'a 10 yazılmadı; başka nesneye işaret ediyor
Eğer
x = 10
C tarzı bir atama olsaydı,
id(x)
aynı kalmalıydı. Ama değişti — çünkü
x
artık başka bir nesneye bağlı.
>>> a = 5 >>> b = 5 >>> id(a) 4368089096 >>> id(b) 4368089096 # AYNI! İkisi de aynı 5 nesnesine bağlı
İki etiket aynı nesneye bağlı. Bu mantıklı —
5
immutable bir nesne; iki kopyasını bellekte tutmaya gerek yok.
>>> a = [1, 2, 3] >>> b = [1, 2, 3] >>> id(a) 4374893376 >>> id(b) 4374893568 # FARKLI! Liste mutable; iki ayrı nesne
Aha — list'lerde durum farklı. İki ayrı liste, iki ayrı id. Bu çok önemli — biraz sonra göreceğiz.
python
# Etiket-nesne ilişkisini canlı görmek için
x = 100
y = x # y de aynı nesneye etiket yapıştırdı
 
print(f"x = {x}, id = {id(x)}")
print(f"y = {y}, id = {id(y)}")
print(f"x is y: {x is y}") # True — aynı nesne
 
x = 200 # x BAŞKA bir nesneye bağlandı
print(f"x = {x}, id = {id(x)}")
print(f"y = {y}, id = {id(y)}")
print(f"x is y: {x is y}") # False — artık farklı nesneler
 
# Çıktı (id'ler senin makinende farklı olacak):
# x = 100, id = 4368091296
# y = 100, id = 4368091296
# x is y: True
# x = 200, id = 4368094496
# y = 100, id = 4368091296
# x is y: False
is operatörü iki etiketin aynı nesneye işaret edip etmediğini kontrol eder. == ise değer eşitliğini.

Immutable vs Mutable — kritik ayrım#

Python'daki nesneleri iki büyük kategoriye böleriz:
Immutable (değiştirilemez): Yaratıldıktan sonra içeriği değişmez. Yeni değer istersen yeni nesne yaratırsın.
  • int
    ,
    float
    ,
    complex
  • str
    (string)
  • tuple
  • bool
    ,
    None
  • frozenset
  • bytes
Mutable (değiştirilebilir): Aynı nesneyi yerinde değiştirebilirsin.
  • list
  • dict
  • set
  • bytearray
  • Custom class (genelde)
Neden bu ayrım önemli? Çünkü ikisi farklı davranır:
# Immutable örnek (str) s1 = "merhaba" s2 = s1 s1 = s1 + " dünya" # s1 yeni nesneye bağlandı print(s1) # 'merhaba dünya' print(s2) # 'merhaba' ← değişmedi! # Mutable örnek (list) l1 = [1, 2, 3] l2 = l1 # l2 aynı listeye bağlı l1.append(4) # listeyi yerinde değiştirdik print(l1) # [1, 2, 3, 4] print(l2) # [1, 2, 3, 4] ← değişti! (aynı nesne)
İkinci örnekteki davranış aslında aliasing
l1
ve
l2
aynı nesneye iki etiket. Birinden değiştirince ikisi de değişiyor (çünkü tek bir nesne var).
Bu davranış başlarken kafa karıştırır ama Python öğrendiğin sürece her gün karşına çıkacak. Etiket modelini anlarsan, çelişki yok.
⚠️ List aliasing tuzağı
Bir listeyi 'kopyalamak' isteyenlerin yaptığı klasik hata:
b = a
. Bu kopya değil, etiket eklemek! Gerçek kopya için:
b = a.copy()
veya
b = a[:]
veya
import copy; b = copy.deepcopy(a)
. Bu son seçenek 'derin kopya' — iç içe listelerde önemli. Bu tuzağa düşmemek seni saatlerce debug'tan kurtarır.
python
# Liste kopyalama yöntemleri ve farkları
import copy
 
original = [1, 2, [3, 4]] # iç içe liste
 
# 1. Etiket atama (KOPYA DEĞİL!)
shallow_label = original
shallow_label.append(99)
print(original) # [1, 2, [3, 4], 99] ← orijinal de değişti!
 
# Reset
original = [1, 2, [3, 4]]
 
# 2. Shallow copy (yüzeysel kopya)
shallow_copy = original.copy()
shallow_copy.append(99)
print(original) # [1, 2, [3, 4]] ← değişmedi
print(shallow_copy) # [1, 2, [3, 4], 99]
 
# Ama iç içe listeyi değiştirirsen?
shallow_copy[2].append(100)
print(original) # [1, 2, [3, 4, 100]] ← İÇ liste de değişti!
print(shallow_copy) # [1, 2, [3, 4, 100], 99]
 
# Reset
original = [1, 2, [3, 4]]
 
# 3. Deep copy (derin kopya)
deep_copy = copy.deepcopy(original)
deep_copy[2].append(100)
print(original) # [1, 2, [3, 4]] ← değişmedi
print(deep_copy) # [1, 2, [3, 4, 100]]
 
# Kural:
# - Düz liste: .copy() yeter
# - İç içe liste/dict: copy.deepcopy() gerekli
Shallow vs deep copy farkı. İç içe yapılarda deep copy kritik.

Multiple assignment — birden fazla değişkeni tek satırda#

Python'da çok zarif bir özellik: tek satırda birden fazla değişkene atama.
# Tuple unpacking a, b = 1, 2 print(a, b) # 1 2 # Üç değişken x, y, z = "merhaba", 3.14, True print(x, y, z) # merhaba 3.14 True # Klasik swap (Python'un sevimli özelliği) a, b = b, a print(a, b) # 2 1
C/Java'da swap için temp değişken gerekir:
// C int temp = a; a = b; b = temp;
Python'da tek satır:
a, b = b, a
. Sağ taraftaki tuple oluşur (
(b, a)
), sonra unpack edilir. Bellek-verimli değil ama okunması net.

Aynı değer çoklu atama#

x = y = z = 0 print(x, y, z) # 0 0 0
Bu üçü de aynı nesneye etiket atıyor:
>>> x is y is z True
Immutable nesnelerde sorun değil, ama mutable'da dikkat:
# TUZAK! a = b = c = [] # ÜÇÜ DE AYNI listeye bağlı a.append(1) print(a, b, c) # [1] [1] [1] ← üçü de değişti! # Doğrusu a, b, c = [], [], [] # üç ayrı liste a.append(1) print(a, b, c) # [1] [] []
Bu pattern
def f(x=[])
gibi mutable default tuzağına çok benziyor — sonraki konu.

Klasik tuzak: Mutable default arguments#

Python'un en ünlü tuzağı. Hadi gör:
def add_item(item, items=[]): items.append(item) return items print(add_item("apple")) # ['apple'] ← Beklenen print(add_item("banana")) # ['apple', 'banana'] ← ?!?! print(add_item("cherry")) # ['apple', 'banana', 'cherry'] ← FAKE!
Beklenmeyen davranış! Her çağrıda
items
boş başlamamış.
Sebep: Python fonksiyon tanımlandığında default argümanlar bir kez değerlendirilir ve fonksiyon nesnesinde saklanır. Yani
items=[]
tek bir liste yaratıyor; her çağrı aynı listeyi kullanıyor.
>>> add_item.__defaults__ ([],) # Tek bir liste, fonksiyona bağlı
Doğrusu:
None
sentinel kullan, içinde yeni liste yarat.
def add_item(item, items=None): if items is None: items = [] items.append(item) return items print(add_item("apple")) # ['apple'] print(add_item("banana")) # ['banana'] ← Doğru! print(add_item("cherry")) # ['cherry']
Bu pattern Python'da o kadar yaygın ki "Python idiom" sayılır. Her gördüğün üretim kodunda göreceksin.
🚨 Aynı tuzak dict ve set için de geçerli —
def f(x={})
ya da
def f(x=set())
aynı problemi yaşar.
Linterler (ruff, pyflakes) bu tuzağı yakalar ve uyarır. Yıllar içinde iyiler — ama refleks olarak
def f(x=None): if x is None: x = ...
yazma alışkanlığı edin.

Augmented assignment —
+=
,
-=
,
*=
, ...#

Kısayollar:
x = 5 x += 3 # x = x + 3 → 8 x -= 1 # x = x - 1 → 7 x *= 2 # x = x * 2 → 14 x /= 4 # x = x / 4 → 3.5 x //= 1 # x = x // 1 → 3.0 (floor) x %= 2 # x = x % 2 → 1.0 x **= 2 # x = x ** 2 → 1.0
Sayılar için
+=
ile
x = x + 1
aynı şey. Ama liste için çok önemli bir fark:
a = [1, 2, 3] b = a a += [4] # a'yı yerinde değiştirdi print(a) # [1, 2, 3, 4] print(b) # [1, 2, 3, 4] ← b de değişti! (aynı nesne) # Karşılaştır: a = [1, 2, 3] b = a a = a + [4] # YENİ liste yarattı, a yeni liste'ye bağlı print(a) # [1, 2, 3, 4] print(b) # [1, 2, 3] ← b değişmedi
+=
mutable nesnede yerinde değiştirme (
__iadd__
çağırır),
x = x + ...
ise yeni nesne yaratma. Sayılarda fark görünmez (immutable), listelerde dramatik.
Bu detayı bilmek senin "ben bu listeyi neden değiştirmedim" hatalarından kurtulman demek.

Walrus operator (
:=
) — Python 3.8 yenilikçisi#

Modern Python'da güzel bir kısayol: hem değer ata hem o değeri kullan, tek ifadede.
# Eski yol — okumak istediğin şeyi iki kere yazıyorsun data = read_chunk() while data: process(data) data = read_chunk() # Walrus ile while (data := read_chunk()): process(data)
Comprehension'da çok faydalı:
# numbers'ın karelerini al, ama 100'den büyük olanları sayma numbers = [10, 20, 5, 100, 50, 200] # Eski yol — iki kere n*n hesaplıyor result = [n*n for n in numbers if n*n < 10000] # Walrus ile — bir kere hesapla, atayıp kullan result = [sq for n in numbers if (sq := n*n) < 10000] print(result) # [100, 400, 25, 10000... oh wait, 10000 değil 100, 400, 25, 2500]
If içinde:
# Eski match = re.search(pattern, text) if match: print(match.group()) # Walrus ile if match := re.search(pattern, text): print(match.group())
:=
ismini "morsa" (walrus) gibi göründüğü için aldı:
:
gözler,
=
dişler. PEP 572. İlk başta tartışmalıydı (Guido o yüzden BDFL'i bıraktı bile derler), şimdi hayatın parçası.
🎯 Pratik tavsiye: Walrus'a "ihtiyacın varsa kullan", zorla kullanma. Kodu daha kısa ama daha az okunabilir yapabilir.

del
ve garbage collection#

Bir etiketi silmek istersen
del
keyword'ü kullanırsın:
x = [1, 2, 3] del x print(x) # NameError: name 'x' is not defined
del
etiketi siler. Eğer bu nesnede başka etiket kalmadıysa, Python garbage collector onu temizler.
import sys l = [1, 2, 3] sys.getrefcount(l) # 2 (l + sys.getrefcount'un kendi referansı) a = l sys.getrefcount(l) # 3 b = l sys.getrefcount(l) # 4 del a sys.getrefcount(l) # 3 del b del l # l artık 0 referansa düştü, garbage collector temizler
CPython reference counting kullanır:
  • Her nesne kaç etikete sahipse o sayıda "referans" sayar.
  • Sayı 0'a düştüğünde nesneyi otomatik siler.
Cycles (döngüsel referanslar) için ek bir GC var. Örneğin:
a = [] b = [a] a.append(b) # a, b'yi içerir; b, a'yı içerir → reference counting bunları silmez # Ek GC algoritması bu cycles'ı bulur ve temizler
Genelde garbage collection senin sorunun değil — Python halleder. Ama bilmek iyi.
python
# CPython'un small int caching özelliği — sürpriz davranış
 
a = 5
b = 5
print(a is b) # True — küçük int'ler cache'lenir
 
a = 256
b = 256
print(a is b) # True — 256 hâlâ cache'te
 
a = 257
b = 257
print(a is b) # False (genelde) — cache dışı
 
# Sebep: CPython -5..256 arası int'leri pre-allocate ediyor.
# Hız optimizasyonu — bu range'deki int'ler bellekte tek nesne.
 
# DİKKAT: Bu "is" ile int karşılaştırma için GÜVENİLMEZ.
# Her zaman "==" kullan.
 
# True/False de cache'lenmiş
print(True is True) # True
print(None is None) # True
 
# String interning (kısa string'ler ve identifier-benzeri string'ler)
a = "hello"
b = "hello"
print(a is b) # True (genelde)
 
a = "hello world!"
b = "hello world!"
print(a is b) # Implementation-defined; CPython genelde False
Performans optimizasyonu için Python bazı immutable nesneleri cache'liyor. 'is' ile değer karşılaştırma — yapma; '==' kullan.

Fonksiyon parametreleri — pass by value mı, reference mı?#

Eski tartışma: "Python pass by value mı, pass by reference mı?"
Doğru cevap: pass by object reference (veya "call by sharing"). Yani:
  • Fonksiyon parametresi yeni bir etiket.
  • O etiket, dışardan verilen nesneye işaret ediyor.
  • Mutable nesne dışardan içeride mutate edilirse — değişiklik kalıcı.
  • Immutable nesne içeride "değiştirildiğinde" — yeni nesne yaratılır, dışarıdaki değişmez.
def modify_list(lst): lst.append(99) # mutate — dış da etkilenir def reassign_list(lst): lst = [4, 5, 6] # yerel etiket başka nesneye bağlandı; dış değişmez def modify_int(n): n = n + 1 # yerel etiket başka nesneye; dış değişmez # Test my_list = [1, 2, 3] modify_list(my_list) print(my_list) # [1, 2, 3, 99] ← DEĞIŞTI my_list2 = [1, 2, 3] reassign_list(my_list2) print(my_list2) # [1, 2, 3] ← DEĞIŞMEDI my_int = 5 modify_int(my_int) print(my_int) # 5 ← DEĞIŞMEDI (immutable)
Kural: Fonksiyona mutable nesne geçince, fonksiyon onu mutate edebilir. Sürpriz olmasın diye:
  1. Fonksiyon adı net olsun:
    add_item(items, item)
    mutate ettiğini belli ediyor.
  2. Mutate etmek istemiyorsan kopya al:
    def f(items): items = items.copy(); ...
    .
  3. Type hint kullan:
    def f(items: list[int]) -> None
    — None dönüyorsa side-effect yapacağı belli.
Fonksiyon programlama dillerinde bu sorun yok (immutable-by-default). Python esnek ama disiplin ister.

Değişken adı kuralları (kısa giriş)#

Python'da değişken adı:
  • Harf veya
    _
    ile başlar
    , devamında harf/rakam/
    _
    olabilir.
  • Case-sensitive:
    x
    ile
    X
    farklı.
  • Reserved keyword olamaz:
    if
    ,
    for
    ,
    def
    ,
    class
    ,
    return
    , ... (toplam ~35 keyword).
  • Türkçe karakter teknik olarak çalışır ama kullanma — Modül 2/Ders 2'de detayda göreceğiz.
Geçerli örnekler:
x = 1 total = 100 my_var = "hello" _private = True __name__ = "main" counter1 = 0
Geçersiz:
1x = 1 # rakamla başlayamaz my-var = 1 # tire değil, alt çizgi class = "X" # reserved keyword my var = 1 # boşluk olmaz
Reserved keyword'lerin tam listesi:
import keyword print(keyword.kwlist) # ['False', 'None', 'True', 'and', 'as', 'assert', 'async', 'await', # 'break', 'class', 'continue', 'def', 'del', 'elif', 'else', 'except', # 'finally', 'for', 'from', 'global', 'if', 'import', 'in', 'is', # 'lambda', 'nonlocal', 'not', 'or', 'pass', 'raise', 'return', 'try', # 'while', 'with', 'yield', 'match', 'case', '_']
Sıradaki ders bu temel üzerine profesyonel isimlendirme kuralları (PEP 8) inşa edecek. Şimdiden bilmen gereken: Python'da konvansiyon
snake_case
.

Hands-on egzersizi 1: Etiket vs nesne#

Aşağıdaki kodun çıktısını çalıştırmadan önce tahmin et. Sonra çalıştırıp karşılaştır.
a = [1, 2, 3] b = a c = a.copy() a.append(4) b.append(5) c.append(6) print(f"a = {a}") print(f"b = {b}") print(f"c = {c}") print(f"a is b: {a is b}") print(f"a is c: {a is c}")
Cevabını yaz. Sonra burayı oku:
Doğru cevap:
a = [1, 2, 3, 4, 5] # a ve b aynı listede; iki append uygulandı b = [1, 2, 3, 4, 5] # aynı liste — b is a True c = [1, 2, 3, 6] # ayrı bir liste — sadece c.append(6) a is b: True a is c: False
Bunu doğru tahmin ettiysen tebrikler — etiket modelini anlamışsın. Yanılttıysa, dersi tekrar oku.

Hands-on egzersizi 2: Mutable default tuzağı#

Aşağıdaki fonksiyonun problemini bul ve düzelt.
def collect_users(name, users=[]): users.append({"name": name, "added_at": "now"}) return users # Test print(collect_users("Ali")) print(collect_users("Ayşe")) print(collect_users("Mehmet"))
Beklenen çıktı (her çağrı bağımsız olmalı):
[{'name': 'Ali', 'added_at': 'now'}] [{'name': 'Ayşe', 'added_at': 'now'}] [{'name': 'Mehmet', 'added_at': 'now'}]
Gerçek çıktı (tuzağa düşmüş):
[{'name': 'Ali', 'added_at': 'now'}] [{'name': 'Ali', 'added_at': 'now'}, {'name': 'Ayşe', 'added_at': 'now'}] [{'name': 'Ali', 'added_at': 'now'}, {'name': 'Ayşe', 'added_at': 'now'}, {'name': 'Mehmet', 'added_at': 'now'}]
Düzeltme:
def collect_users(name, users=None): if users is None: users = [] users.append({"name": name, "added_at": "now"}) return users
Bunu kendine yaz, çalıştır. Refleks halinde gelene kadar pratiğe devam.
Resmi belge'de "name binding" deniyor. "Variable" terimi kullanılıyor ama Python'un kendine özgü davranışını ifade etmek için topluluk "label" / "name" / "reference" tercih ediyor. Ned Batchelder'in 'Facts and myths about Python names and values' konuşması bu kavramı anlatan klasik kaynak — YouTube'da bul, izle.

Bu derste neler kazandın?#

'Variable as label, not box' felsefesi — Python'un C/Java'dan farkı.
id()
ile nesne kimliği inceleme.
Immutable vs Mutable ayrımı (int/str/tuple → immutable; list/dict/set → mutable) ve davranış farkları.
List aliasing tuzağı ve shallow vs deep copy farkı.
Multiple assignment
a, b = 1, 2
, swap idiomu.
Mutable default tuzağı — Python'un en ünlü gotcha'sı,
None
sentinel ile çözüm.
+=
ve
x = x +
mutable nesnelerde dramatik fark.
Walrus operator
:=
ne zaman kullanılır.
del
ve garbage collection
— reference counting + cyclic GC.
Fonksiyon parametreleri — pass by object reference semantics.
✓ Geçerli değişken adı kuralları + reserved keyword listesi.
✓ İki hands-on egzersiz ile pekiştirme.
Sıradaki ders: PEP 8 isimlendirme kuralları. Python'un kendi resmi stil rehberinde 'değişken nasıl adlandırılır?' sorusunun cevapları — snake_case, PascalCase, UPPER_SNAKE, ne zaman
_
ne zaman
__
. Profesyonel kod yazmanın görsel disiplini.

Frequently Asked Questions

Erken bilmen iyi olan şeyler. **Etiket modeli** olmadan Python'da debug etmek imkansız hale gelir. **Mutable default trap** ilk büyük projende karşına çıkacak. **List aliasing** yıllarca production bug'ı yaratıyor. Bu derste anlatılanlar 'ileri seviye' değil, 'temel ama erken'. 1 saat şimdi öğrenmek, 10 saat sonradan debug yapmaktan kaçınmak demek.

Yorumlar & Soru-Cevap

(0)
Yorum yazmak için giriş yap.
Yorumlar yükleniyor...

Related Content