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 dakikalık okuma
Başlangıç🧠 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 yazınca:
int x = 5;- RAM'de boyutunda (4 byte) bir slot ayrılır.
int - O slot'a yazılır.
5 - o slot'un adresine bağlanır.
x
Sonra yapınca: o aynı slot'a yazılır. Slot değişmez, içerik değişir.
x = 10;10[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:
- Bellekte integer nesnesi yaratıyor (zaten varsa kullanıyor — small int caching var).
5 - adlı bir etiket o nesneye yapıştırıyor.
x - Sonra nesnesinden etiketi söküyor,
5nesnesine yapıştırıyor.10
[Bellek] [int 5] ← x → [int 5] [int 10] ← x
İlk integer nesnesi () 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.
5Bu 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#
id()Python sana her nesnenin "kimlik numarasını" veren bir fonksiyon sunuyor: . Bu sayı nesnenin bellekteki adresi (CPython implementasyonunda).
id()>>> 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 C tarzı bir atama olsaydı, aynı kalmalıydı. Ama değişti — çünkü artık başka bir nesneye bağlı.
x = 10id(x)x>>> 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ı — immutable bir nesne; iki kopyasını bellekte tutmaya gerek yok.
5>>> 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çinx = 100y = 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: Falseis 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,floatcomplex - (string)
str tuple- ,
boolNone frozensetbytes
Mutable (değiştirilebilir): Aynı nesneyi yerinde değiştirebilirsin.
listdictsetbytearray- 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 — ve aynı nesneye iki etiket. Birinden değiştirince ikisi de değişiyor (çünkü tek bir nesne var).
l1l2Bu 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: . Bu kopya değil, etiket eklemek! Gerçek kopya için: veya veya . Bu son seçenek 'derin kopya' — iç içe listelerde önemli. Bu tuzağa düşmemek seni saatlerce debug'tan kurtarır.
b = ab = a.copy()b = a[:]import copy; b = copy.deepcopy(a)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 = originalshallow_label.append(99)print(original) # [1, 2, [3, 4], 99] ← orijinal de değişti! # Resetoriginal = [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şmediprint(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] # Resetoriginal = [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şmediprint(deep_copy) # [1, 2, [3, 4, 100]] # Kural:# - Düz liste: .copy() yeter# - İç içe liste/dict: copy.deepcopy() gerekliShallow 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: . Sağ taraftaki tuple oluşur (), sonra unpack edilir. Bellek-verimli değil ama okunması net.
a, b = b, a(b, a)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 gibi mutable default tuzağına çok benziyor — sonraki konu.
def f(x=[])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 boş başlamamış.
itemsSebep: Python fonksiyon tanımlandığında default argümanlar bir kez değerlendirilir ve fonksiyon nesnesinde saklanır. Yani tek bir liste yaratıyor; her çağrı aynı listeyi kullanıyor.
items=[]>>> add_item.__defaults__ ([],) # Tek bir liste, fonksiyona bağlı
Doğrusu: sentinel kullan, içinde yeni liste yarat.
Nonedef 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 — ya da aynı problemi yaşar.
def f(x={})def f(x=set())Linterler (ruff, pyflakes) bu tuzağı yakalar ve uyarır. Yıllar içinde iyiler — ama refleks olarak yazma alışkanlığı edin.
def f(x=None): if x is None: x = ...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 aynı şey. Ama liste için çok önemli bir fark:
+=x = x + 1a = [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
+=__iadd__x = x + ...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())
:=:=🎯 Pratik tavsiye: Walrus'a "ihtiyacın varsa kullan", zorla kullanma. Kodu daha kısa ama daha az okunabilir yapabilir.
del ve garbage collection#
delBir etiketi silmek istersen keyword'ü kullanırsın:
delx = [1, 2, 3] del x print(x) # NameError: name 'x' is not defined
delimport 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 = 5b = 5print(a is b) # True — küçük int'ler cache'lenir a = 256b = 256print(a is b) # True — 256 hâlâ cache'te a = 257b = 257print(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) # Trueprint(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 FalsePerformans 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:
- Fonksiyon adı net olsun: mutate ettiğini belli ediyor.
add_item(items, item) - Mutate etmek istemiyorsan kopya al: .
def f(items): items = items.copy(); ... - Type hint kullan: — None dönüyorsa side-effect yapacağı belli.
def f(items: list[int]) -> None
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: ile
xfarklı.X - Reserved keyword olamaz: ,
if,for,def,class, ... (toplam ~35 keyword).return - 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_caseHands-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ı.
✓ ile nesne kimliği inceleme.
id()✓ 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 — , swap idiomu.
a, b = 1, 2✓ Mutable default tuzağı — Python'un en ünlü gotcha'sı, sentinel ile çözüm.
None✓ ve mutable nesnelerde dramatik fark.
+=x = x +✓ Walrus operator ne zaman kullanılır.
:=✓ ve garbage collection — reference counting + cyclic GC.
del✓ 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.
___Sık Sorulan Sorular
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...
İlgili İçerikler
Modül 1: Giriş ve Kurulum
Python Nedir, Neden Bu Kadar Popüler?
Öğrenmeye BaşlaModül 1: Giriş ve Kurulum
Python Sürümlerinin Tarihi: 2'den 3.14'e, AI Winter'lardan 'No-GIL' Devrimine
Öğrenmeye BaşlaModül 1: Giriş ve Kurulum