Ana içeriğe geç
Kesit Python
Orta · Modüller, paketler ve Git

Modüller ve paketler: import sisteminin içi

Tüm import biçimlerini, sys.path ve modül arama sırasını, __name__, __pycache__ ve .pyc dosyalarını, paket ve __init__.py yapısını, göreli import ve __all__ kurallarını, importlib.reload ile platform, math ve random modüllerini PCAP düzeyinde öğrenirsin.

Uygulamada aç, 20 alıştırmayı çözÜcretsiz, hesap gerekmez, kurulum yok.

Bu sayfa dersin okuma sürümüdür. Metin bir kutuyu çalıştırmanı ya da adıma geçmeni söylüyorsa dersi uygulamada aç.

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.

Tarayıcıda çoklu dosyaBu uygulamada tek bir kod kutun var. Modül ve paket alıştırmalarında dosyaları kodla yazacağız (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çimAd alanına ne girer?Kullanım
import mathYalnızca mathmath.sqrt(16)
import statistics as stYalnızca st (takma ad)st.mean(x)
from math import ceil, floorceil ve floor; math adı GİRMEZceil(2.1)
from math import sqrt as kokYalnızca kokkok(9)
from math import *__all__ varsa onun listesi, yoksa _ ile başlamayan tüm adlarEtkileş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.

Yıldızlı import sessizce ezere = 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:

  1. sys.modules: Daha önce yüklendiyse hazır nesneyi verir, dosyaya hiç bakmaz.
  2. Yerleşik modüller: Yorumlayıcıya gömülü modüller (sys.builtin_module_names, ör. sys).
  3. sys.path listesi baştan sona: çalıştırılan betiğin klasörü, PYTHONPATH ortam değişkenindeki klasörler, standart kütüphane, site-packages (pip ile kurulanlar).
  4. 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
Dosyana standart modül adı vermeProje klasörüne 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)
reload'un iki tuzağı(1) 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ımAnlamı
from . import fiyatAynı paketteki fiyat modülü
from .fiyat import kdv_ekleAynı paketin fiyat modülünden bir ad
from ..fiyat import kdv_ekleBir üst paketin fiyat modülünden
import am_anadolu.rapor.ozetMutlak 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

Fonksiyon2.7-2.7Not
math.ceil3-2Yukarı (artı sonsuza)
math.floor2-3Aşağı (eksi sonsuza)
math.trunc2-2Ondalığı at (sıfıra doğru), int() gibi
round3-3Yerleş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.

Gerçek işteKurumsal projelerde kod genelde 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.
Uzmanlaş

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.

Alıştırmalara geç (20)