Neden önemli?
Anadolu Market'in kodu tek bir dosyada başladı: 200 satır. İki yıl sonra fiyatlama, kargo, raporlama ve kampanya kodu 15 000 satıra çıktı ve tek dosya kimsenin açmak istemediği bir canavara dönüştü. Çözüm, kodu anlamlı parçalara bölmektir: her dosya bir modül (module), ilgili modülleri toplayan klasör bir paket (package). Kapsam ve modüller dersinde import math yazmayı ve if __name__ == "__main__" kalıbını gördün. Bu derste import sisteminin içine giriyoruz: Python modülü nerede arar, neden bir kez yükler, paketi nasıl tanır, göreli import neden bazen patlar. Bu konular PCAP sınavının ilk bölümüdür ve gerçek projede en sık karşılaşılan "ModuleNotFoundError" sorunlarının kaynağıdır.
open(...).write(...)) ya da modülü doğrudan bellekte oluşturacağız (types.ModuleType). Arkada çalışan mekanizma kendi bilgisayarındakinin aynısıdır.Dört import biçimi
| Biçim | Ad alanına ne girer? | Kullanım |
|---|---|---|
import math | Yalnızca math | math.sqrt(16) |
import statistics as st | Yalnızca st (takma ad) | st.mean(x) |
from math import ceil, floor | ceil ve floor; math adı GİRMEZ | ceil(2.1) |
from math import sqrt as kok | Yalnızca kok | kok(9) |
from math import * | __all__ varsa onun listesi, yoksa _ ile başlamayan tüm adlar | Etkileşimli denemede; iş kodunda kaçın |
import math
import statistics as st
from math import ceil, floor
from math import sqrt as kok
print(math.pi, st.mean([10, 20, 30]))
print(ceil(2.1), floor(2.9), kok(81))
print("math" in dir(), "sqrt" in dir(), "kok" in dir())
dir() parametresiz çağrılınca o anki ad alanındaki adları, dir(modul) ise modülün içindeki adları listeler. Bir modülün ne sunduğunu keşfetmenin en hızlı yoludur; help(math.hypot) ise belge metnini (docstring) gösterir. Aynı metin math.hypot.__doc__ içinde de durur.
e = 1.4 (fiyat esnekliği) tanımlayıp sonra from math import * yazarsan senin e'n Euler sayısıyla (2.718...) değiştirilir ve hiçbir uyarı almazsın. Ekip kodunda import math ya da açık adlı from math import ceil kullan.Python modülü nerede arar?
import kargo satırında Python şu sırayı izler:
- sys.modules: Daha önce yüklendiyse hazır nesneyi verir, dosyaya hiç bakmaz.
- Yerleşik modüller: Yorumlayıcıya gömülü modüller (
sys.builtin_module_names, ör.sys). - sys.path listesi baştan sona: çalıştırılan betiğin klasörü,
PYTHONPATHortam değişkenindeki klasörler, standart kütüphane,site-packages(pip ile kurulanlar). - Hiçbirinde yoksa ModuleNotFoundError.
import sys
print(type(sys.path), len(sys.path) > 0)
for yol in sys.path[:4]:
print(" -", yol)
print("sys" in sys.builtin_module_names)
print("json" in sys.modules) # henüz yüklenmedi mi?
import json
print("json" in sys.modules) # artık önbellekte
random.py ya da json.py koyarsan, betiğin klasörü sys.path'te önce geldiği için Python seninkini yükler ve "module has no attribute" hatası alırsın. Bu, yeni başlayanların en sık yaşadığı gizemli hatadır.Kendi modülünü yazmak
Her .py dosyası bir modüldür; adı dosya adıdır. Tarayıcıda dosyayı kodla yazıp mevcut klasörü sys.path'e ekleyelim:
import sys, importlib
kaynak = '''"""Anadolu Market fiyat araçları."""
KDV = 0.20
_IC_SAYAC = 0 # alt çizgi: iç kullanım
def kdv_ekle(tutar):
"""Tutara KDV ekler, 2 ondalığa yuvarlar."""
return round(tutar * (1 + KDV), 2)
print("am_fiyat yüklendi, __name__ =", __name__)
'''
with open("am_fiyat.py", "w", encoding="utf-8") as f:
f.write(kaynak)
if "." not in sys.path:
sys.path.insert(0, ".")
importlib.invalidate_caches() # yeni dosyayı bulucuların görmesi için
sys.modules.pop("am_fiyat", None) # kutuyu yeniden çalıştırırsan diye
import am_fiyat
import am_fiyat # ikinci import: hiçbir şey yazdırmaz
print(am_fiyat.kdv_ekle(100), am_fiyat.__doc__)
print(am_fiyat.__file__.endswith("am_fiyat.py"))
Dikkat: modülün içindeki print yalnızca bir kez çalıştı. Modül ilk importta yukarıdan aşağı çalıştırılır, oluşan nesne sys.modules'e konur; sonraki importlar aynı nesneyi döndürür. Modül içinde __name__ modülün adıdır ("am_fiyat"); dosya doğrudan çalıştırıldığında ise "__main__" olur.
Dosyasız modül: types.ModuleType
Modül aslında sıradan bir nesnedir. Dosya olmadan da oluşturup sys.modules'e kaydedebilirsin; testlerde sahte modül hazırlamak için bu yöntem kullanılır.
import sys, types
ayar = types.ModuleType("am_ayar", "Kampanya ayarları")
ayar.ESIK = 500
ayar.ORAN = 0.10
sys.modules["am_ayar"] = ayar
from am_ayar import ESIK, ORAN
print(ESIK, ORAN, ayar.__name__, ayar.__doc__)
__pycache__, .pyc ve reload
Python bir modülü ilk yüklediğinde kaynağı bayt koduna (bytecode) derler ve __pycache__/ad.cpython-312.pyc dosyasına yazar. Sonraki çalıştırmada kaynak değişmemişse (değiştirme zamanı ve boyutu aynıysa) derlemeyi atlar. .pyc dosyaları depoya konmaz; .gitignore'a __pycache__/ eklenir.
Modül dosyasını değiştirdin ve yeni hâlini aynı oturumda görmek istiyorsun: import işe yaramaz, çünkü önbellekteki nesneyi döndürür. importlib.reload(modul) modül kodunu aynı nesnenin içinde yeniden çalıştırır.
import sys, os, importlib, importlib.util
def yaz(metin):
with open("am_kur.py", "w", encoding="utf-8") as f:
f.write(metin)
if "." not in sys.path:
sys.path.insert(0, ".")
yaz("USD = 32.5\n")
importlib.invalidate_caches()
sys.modules.pop("am_kur", None)
import am_kur
print("ilk:", am_kur.USD)
yaz("USD = 33.1\n") # aynı boyut, aynı saniye!
pyc = importlib.util.cache_from_source("am_kur.py")
print("önbellek dosyası:", pyc)
if os.path.exists(pyc):
os.remove(pyc) # bayat .pyc'yi sil
importlib.reload(am_kur)
print("reload sonrası:", am_kur.USD)
from am_kur import USD ile aldığın ad eski değeri göstermeye devam eder; reload yalnızca modül nesnesini günceller. (2) Dosyayı aynı saniyede ve aynı boyutla yeniden yazarsan Python .pyc'yi hâlâ geçerli sanabilir; yukarıdaki gibi önbellek dosyasını sil ya da sys.dont_write_bytecode = True kullan.Paketler: modül klasörü
Paket, içinde __init__.py bulunan bir klasördür. __init__.py paket ilk import edildiğinde çalışır; boş olabilir, paket düzeyi ayarları ve dışa açılan adları içerebilir. Paketin içinde başka paketler (alt paketler) olabilir:
anadolu/
__init__.py # SURUM, __all__
fiyat.py # kdv_ekle
rapor/
__init__.py
ozet.py # from ..fiyat import kdv_ekle
import os, sys, importlib, shutil
dosyalar = {
"am_anadolu/__init__.py": 'SURUM = "1.2.0"\n__all__ = ["fiyat"]\n',
"am_anadolu/fiyat.py": "def kdv_ekle(t):\n return round(t * 1.2, 2)\n",
"am_anadolu/rapor/__init__.py": "",
"am_anadolu/rapor/ozet.py": (
"from ..fiyat import kdv_ekle\n"
"from . import __name__ as paket_adi\n"
"def satir(urun, tutar):\n"
" return f'{urun}: {kdv_ekle(tutar)} TL'\n"
),
}
for yol, icerik in dosyalar.items():
os.makedirs(os.path.dirname(yol), exist_ok=True)
with open(yol, "w", encoding="utf-8") as f:
f.write(icerik)
if "." not in sys.path:
sys.path.insert(0, ".")
importlib.invalidate_caches()
import am_anadolu
from am_anadolu.rapor import ozet
print(am_anadolu.SURUM, am_anadolu.__path__ is not None)
print(ozet.satir("Çay", 50), "|", ozet.paket_adi, "|", ozet.__name__)
shutil.rmtree("am_anadolu")
| Yazım | Anlamı |
|---|---|
from . import fiyat | Aynı paketteki fiyat modülü |
from .fiyat import kdv_ekle | Aynı paketin fiyat modülünden bir ad |
from ..fiyat import kdv_ekle | Bir üst paketin fiyat modülünden |
import am_anadolu.rapor.ozet | Mutlak import; üç modül de yüklenir, ad alanına am_anadolu girer |
Göreli import (relative import) yalnızca paketin içindeki modüllerde çalışır. Bir dosyayı doğrudan python ozet.py ile çalıştırırsan __name__ "__main__" olur, paket bilgisi kaybolur ve "attempted relative import with no known parent package" hatası alırsın. Çözüm: kök klasörden python -m am_anadolu.rapor.ozet.
__all__ ve _ad: dışa ne açılır?
Python'da gerçek "private" yoktur, anlaşma vardır. Alt çizgiyle başlayan ad (_IC_SAYAC) "iç kullanım, dokunma" demektir ve yıldızlı importa girmez. __all__ listesi tanımlıysa yıldızlı import yalnızca o adları getirir; listede alt modül adı varsa o alt modül de yüklenir. Açık importla (from m import _IC_SAYAC) iki kural da aşılabilir; yani koruma değil, belgelemedir.
PCAP'ın sevdiği üç modül
platform: kod nerede çalışıyor?
import platform
print(platform.system()) # Linux, Windows, Darwin; tarayıcıda Emscripten
print(platform.python_version()) # '3.12.x' gibi metin
surum = platform.python_version_tuple() # METİN demeti: ('3', '12', '1')
print(surum, type(surum[1]))
print(tuple(int(p) for p in surum[:2]) >= (3, 10))
print(platform.python_implementation(), platform.machine())
python_version_tuple() elemanları metin döndürür; "9" > "10" True olduğundan karşılaştırmadan önce int'e çevir. platform.platform() tek satırlık özet verir.
math: yuvarlama ailesi ve geometri
| Fonksiyon | 2.7 | -2.7 | Not |
|---|---|---|---|
math.ceil | 3 | -2 | Yukarı (artı sonsuza) |
math.floor | 2 | -3 | Aşağı (eksi sonsuza) |
math.trunc | 2 | -2 | Ondalığı at (sıfıra doğru), int() gibi |
round | 3 | -3 | Yerleşik; .5'te çifte yuvarlar: round(2.5) == 2 |
import math print(math.ceil(-2.7), math.floor(-2.7), math.trunc(-2.7), round(-2.7)) print(math.factorial(5)) # 5 teslimat noktası: 120 farklı sıra print(math.hypot(3, 4), math.sqrt(25)) # depo-müşteri kuş uçuşu mesafe print(math.ceil(1250 / 400), "koli") # 1250 ürün, koli başına 400
random: tekrarlanabilir rastgelelik
import random musteriler = ["Ayşe", "Mehmet", "Zeynep", "Can", "Elif"] random.seed(2024) # aynı tohum = aynı sonuç print(random.random() < 1, random.randint(1, 6), random.randrange(0, 10, 2)) print(random.choice(musteriler)) # tek seçim print(random.sample(musteriler, 2)) # tekrarsız k seçim, yeni liste kopya = musteriler[:] print(random.shuffle(kopya), kopya) # yerinde karıştırır, None döndürür print(musteriler) # orijinal aynı
sample tekrarsız seçer ve yeni liste döndürür; choices(liste, k=3) ise tekrar edebilir. shuffle listeyi yerinde değiştirir ve None döndürür; demet ya da metin gibi değişmez dizilerde çalışmaz.
src/sirket_adi/ altında bir pakettir; testler ayrı tests/ klasöründedir ve paketi mutlak importla kullanır. Kod incelemesinde (code review) en sık yorumlar: "yıldızlı import kaldır", "döngüsel import var, ortak kodu ayrı modüle taşı", "modül import edilince yan etki (veritabanı bağlantısı, print) yapmasın, main()'e al". Jupyter'de modülü düzenleyip değişikliği göremiyorsan sebep sys.modules önbelleğidir; importlib.reload ya da %autoreload kullanılır.Döngüsel import
siparis.py musteri.py'yi, o da siparis.py'yi import ederse ikinci import yarım yüklenmiş modülü alır ve "cannot import name" ya da "partially initialized module" hatası çıkar. Çözümler: ortak kodu üçüncü bir modüle taşımak, importu fonksiyonun içine almak ya da tasarımı sadeleştirmek. Alıştırmalarda bağımlılık grafiğinde döngü bulan bir fonksiyon yazacaksın.
Ad alanı paketleri ve importlib
Python 3.3'ten beri __init__.py olmayan klasörler de ad alanı paketi (namespace package) olarak import edilebilir; büyük kuruluşlar tek bir ad altında ayrı ayrı dağıtılan paketler için kullanır. Yine de normal projede __init__.py koy: niyeti belli eder ve araçların (pytest, mypy) kafası karışmaz. Adı çalışma anında belli olan modüller için importlib.import_module("am_eklenti." + ad), var olup olmadığını yüklemeden öğrenmek için importlib.util.find_spec(ad) kullanılır; eklenti sistemleri böyle kurulur.
Modül bir tekil nesnedir
Modül yalnızca bir kez yüklendiği için modül düzeyindeki bir sözlük tüm uygulamada paylaşılır. Bu, yapılandırma için kullanışlıdır ama testlerde gizli bağımlılık yaratır; ayarları fonksiyon parametresiyle geçirmek daha test edilebilirdir.