Ders 14 / 16
Dosya İşlemleri
Aynı 41 bayt ikili kipte 41 uzunluğunda bir bayt dizisi, metin kipinde utf-8 ile 34 karakterlik bir dizgi verir; yanlış kodlama açma değil okuma anında kırılır ve latin-1 hiç kırılmayıp sessizce 41 yanlış karakter üretir.
İçindekiler
Önceki dersin on iki kaydı bellekte hazır duruyordu. deger alanı zaten bir dizgiydi; tek iş
onu sayıya çevirmek ve alanın sınırlarına vurmaktı. Alan kökü işe yaradı çünkü hatanın
kaynağı alanın kendisiydi.
Kayıtlar bir dosyadan geldiğinde araya bir katman daha girer. Dosyada duran şey ne dizgidir ne sayı — bayttır. Baytın dizgiye çevrilmesi bir kodlama kararıdır ve o karar yanlışsa hata alan katmanına hiç ulaşmaz; daha okuma sırasında ortaya çıkar. Dosya, kodlama ve bayt kavramları Bilgisayarlar Nasıl Çalışır kursunda kuruldu; orada kavram kuruldu, burada Python’un iki dosya kipinin aynı baytlarda ne verdiği ölçülüyor.
İki Kip, İki Tür
open iki kipte çalışır ve aralarındaki fark bir seçenek değil, dönen nesnenin türüdür.
İkili kip ("rb", "wb") bayt dizisi verir. Dosyada ne varsa odur; hiçbir çeviri yapılmaz.
Uzunluk bayt sayısıdır.
Metin kipi ("r", "w") dizgi verir. Araya bir çözücü girer: baytlar bir kodlamaya
göre karakterlere çevrilir ve satır sonları çevrilir. Uzunluk karakter sayısıdır.
Metin kipinde encoding verilmezse yorumlayıcı bir öntanımlı seçer ve o öntanımlı çalıştığı
ortama bağlıdır. Aynı dosya, aynı program, başka bir ortam — başka sonuç. Bu dersin bütün
ölçümleri kodlamayı açıkça yazar; ölçülebilir olmasının koşulu budur.
Aynı Baytlar, Ayrı Kodlamalar
Ölçüm tek bir metinle başlar: Türkçe harfler, bir derece işareti ve bir uzun çizgi içeren bir ölçüm satırı. Bu metin utf-8 ile kodlanır ve elde edilen baytlar beş ayrı kodlamayla çözülmeye çalışılır.
Ölçümün varsayımları:
- HD19 — Bütün ölçüm tek bir bayt dizisi üzerinde yapılır; dizi metinden kodlanarak üretilir, elle yazılmaz.
- HD20 — Kâhin kaynak metnin kendisidir: çözülen dizgi kaynakla özdeş mi diye sorulabilir, çünkü kaynağı biz ürettik.
- HD21 — “Kırılan bayt konumu” istisna nesnesinin kendi alanından okunur; hata iletisi ayrıştırılmaz.
- HD22 — Kodlama adları dilin kendi çözücü kataloğundan gelir; hiçbiri dış bir araca ait değildir.
- HD23 — Parça boyu taraması aynı baytları farklı boylarda böler ve her parçayı ayrı ayrı çözer; parçalar arasında durum taşınmaz.
"""Ayni baytlar, ayri kodlamalar: cozulur mu, ozdes mi, nerede kirilir.""" METIN = "kuzey yamaç: 12,5 °C — ölçüm tamam" HAM = METIN.encode("utf-8") print(f"karakter {len(METIN)} | utf-8 bayt {len(HAM)} | " f"çok baytlı karakter {sum(len(k.encode('utf-8')) > 1 for k in METIN)}") KODLAMALAR = ("utf-8", "utf-16", "latin-1", "ascii", "cp1254") print() print(f"{'kodlama':<12s}{'çözüldü':<10s}{'karakter':>9s}{'özdeş':>8s}" f" kırılan sınıf / bayt konumu") for ad in KODLAMALAR: try: cozulen = HAM.decode(ad) except UnicodeDecodeError as e: print(f" {ad:<10s}{'hayır':<10s}{'-':>9s}{'-':>8s}" f" {type(e).__name__} / {e.start}") else: print(f" {ad:<10s}{'evet':<10s}{len(cozulen):>9d}" f"{('evet' if cozulen == METIN else 'hayır'):>8s} -") print() print(f"{'hata kipi':<20s}{'karakter':>9s}{'özdeş':>8s} ilk yedi karakter") for kip in ("ignore", "replace", "backslashreplace"): c = HAM.decode("ascii", errors=kip) print(f" {kip:<18s}{len(c):>9d}{('evet' if c == METIN else 'hayır'):>8s}" f" {c[:7]!r}") print() print(f"{'parça boyu':<12s}{'parça':>7s}{'kırılan parça':>15s}") for boy in range(4, 25, 2): parcalar = [HAM[i:i + boy] for i in range(0, len(HAM), boy)] kirilan = 0 for p in parcalar: try: p.decode("utf-8") except UnicodeDecodeError: kirilan += 1 print(f" {boy:<10d}{len(parcalar):>7d}{kirilan:>15d}") butun = 0 try: HAM.decode("utf-8") except UnicodeDecodeError: butun = 1 print(f"aynı baytlar bütün olarak çözüldüğünde kırılan {butun}")
karakter 34 | utf-8 bayt 41 | çok baytlı karakter 6 kodlama çözüldü karakter özdeş kırılan sınıf / bayt konumu utf-8 evet 34 evet - utf-16 hayır - - UnicodeDecodeError / 40 latin-1 evet 41 hayır - ascii hayır - - UnicodeDecodeError / 10 cp1254 evet 41 hayır - hata kipi karakter özdeş ilk yedi karakter ignore 28 hayır 'kuzey y' replace 41 hayır 'kuzey y' backslashreplace 80 hayır 'kuzey y' parça boyu parça kırılan parça 4 11 4 6 7 2 8 6 2 10 5 2 12 4 2 14 3 2 16 3 0 18 3 0 20 3 2 22 2 0 24 2 2 aynı baytlar bütün olarak çözüldüğünde kırılan 0
Sessiz Yanlış
İlk satır dersin çekirdeğini veriyor: 34 karakterlik metin 41 bayt tutuyor. Aradaki 7 baytlık fark, çok baytlı 6 karakterden geliyor. Karakter sayısı ile bayt sayısı birbirinden bağımsızdır ve hangisini saydığınız hangi kipte okuduğunuza bağlıdır.
Kodlama tablosu üç ayrı davranış gösteriyor. utf-8 çözüyor, 34 karakter veriyor ve kaynakla özdeş. Doğru kodlamanın tanımı budur.
ascii ve utf-16 çözemiyor ve UnicodeDecodeError fırlatıyor. Konumlar ayrı:
ascii 10’uncu baytta kırılıyor — ilk Türkçe harfin ilk baytında. utf-16 ise 40’ıncı
baytta kırılıyor, yani neredeyse sonda; çünkü utf-16 baytları ikişerli okur ve 41 tek
sayıdır, son bayt eşsiz kalır. İkisi de kırılır ama nerede kırıldıkları kodlamanın
kendi kuralından gelir.
Asıl bulgu latin-1 ve cp1254 satırlarıdır. İkisi de çözüyor — hiç istisna fırlatmıyor — ve 41 karakter veriyor. Yani her baytı bir karaktere çeviriyorlar. Ama özdeş değil. Tek baytlı bir kodlama, önüne gelen her baytı geçerli sayar; kırılacak bir şey yoktur. Bu, ölçümün en tehlikeli sonucudur: yanlış kodlama her zaman istisna üretmez. İstisna fırlatan kodlama sizi uyarır; fırlatmayan kodlama sessizce yanlış metin verir ve yanlışlık ancak metni okuyan insan tarafından fark edilir.
Hata kipleri bunun bilinçli hâlidir. ignore çözemediği baytları atıyor ve 28 karakter
kalıyor — 6 karakterin baytları düşmüş. replace her kusurlu baytın yerine bir karakter
koyuyor ve 41 çıkıyor; sayı bayt sayısına eşitleniyor çünkü değiştirme bayt başına
yapılıyor. backslashreplace her kusurlu baytı dört karakterlik bir yazımla anlatıyor ve
uzunluk 80’e çıkıyor. Üçünün de özdeş sütunu hayır: hata kipi vermek, veriyi
kurtarmaz, yalnız kırılmayı engeller.
Parçanın Sınırı
Son tablo ikili kipte parça parça okumanın bedelini veriyor. Aynı 41 bayt, farklı boylarda bölünüp her parça ayrı ayrı çözülüyor.
Sonuç düzensiz. 4 baytlık parçalarda 11 parçanın 4’ü kırılıyor; 16 ve 18 boyunda hiçbiri kırılmıyor; 20’de yine 2 parça kırılıyor, 22’de 0, 24’te 2. Kırılma sayısı parça boyunun büyüklüğüne bağlı değil, parça sınırının çok baytlı bir karakterin ortasına düşüp düşmediğine bağlı. Aynı baytlar bütün olarak çözüldüğünde kırılan 0.
Bunun doğrudan sonucu şudur: ikili kipte okuyup baytları kendiniz çözüyorsanız, parçalar arasında durum taşımak sizin işinizdir. Metin kipi bu işi zaten yapar — çözücü parça sınırında yarım kalan karakteri bekletir. İkili kip hızlı ve tarafsızdır; çeviriyi üstlenmediği için çevirinin sorunlarını da üstlenmez ve onları çağırana bırakır.
Kip, Tür ve Kırılma Noktası
İkinci ölçüm gerçek bir dosyayla çalışır. Dosya geçici bir dizinde kurulur ve ölçüm bitince dizin silinir; ölçüm hiçbir kalıcı iz bırakmaz.
- HD24 — Dosya geçici bir dizinde üretilir ve baytları önceki ölçümün metninden gelir; hiçbir yol adı basılmaz, çünkü yol ortama bağlıdır.
- HD25 — “Uzunluk” ikili kipte bayt, metin kipinde karakter sayısıdır; ikisi aynı adla ölçülür ama aynı şey değildir.
- HD26 — Kırılma noktası ölçümü açma ile okumayı ayrı adım olarak dener; hangisinin istisna ürettiği koşumdan okunur.
- HD27 — Satır sonu ölçümünde dosya, biri iki karakterlik biri tek karakterlik olmak üzere iki ayrı satır sonu biçimi taşır; çeviri farkı bundan doğar.
"""Metin kipi ile ikili kip: ayni dosya, ayri tur, ayri uzunluk, ayri kirilma noktasi.""" import tempfile from pathlib import Path METIN = "kuzey yamaç: 12,5 °C — ölçüm tamam" SATIRLI = b"kuzey\r\nyama\xc3\xa7\nolcum\r\n" with tempfile.TemporaryDirectory() as gecici: olcum = Path(gecici) / "olcum.dat" olcum.write_bytes(METIN.encode("utf-8")) satir = Path(gecici) / "satir.dat" satir.write_bytes(SATIRLI) print(f"{'okuma kipi':<22s}{'tür':<8s}{'uzunluk':>8s}{'özdeş':>8s}") with open(olcum, "rb") as d: ikili = d.read() print(f" {'ikili':<20s}{type(ikili).__name__:<8s}{len(ikili):>8d}{'-':>8s}") for ad, kodlama, kip in (("metin utf-8", "utf-8", "strict"), ("metin latin-1", "latin-1", "strict"), ("metin ascii replace", "ascii", "replace")): with open(olcum, "r", encoding=kodlama, errors=kip) as d: metin = d.read() print(f" {ad:<20s}{type(metin).__name__:<8s}{len(metin):>8d}" f"{('evet' if metin == METIN else 'hayır'):>8s}") print() print("adım sonuç") try: d = open(olcum, "r", encoding="ascii") except UnicodeDecodeError as e: print(f" {'open':<18s}{type(e).__name__} / bayt {e.start}") else: print(f" {'open':<18s}başarılı, dosya açıldı") try: d.read() except UnicodeDecodeError as e: print(f" {'read':<18s}{type(e).__name__} / bayt {e.start}") finally: d.close() print() print(f"{'okuma biçimi':<22s}{'uzunluk':>8s}{'CR sayısı':>11s}{'satır':>7s}") with open(satir, "rb") as d: ham = d.read() print(f" {'ikili':<20s}{len(ham):>8d}{ham.count(13):>11d}" f"{len(ham.splitlines()):>7d}") for ad, yenisatir in (("metin (öntanımlı)", None), ('metin newline=""', "")): with open(satir, "r", encoding="utf-8", newline=yenisatir) as d: m = d.read() with open(satir, "r", encoding="utf-8", newline=yenisatir) as d: satirlar = d.readlines() print(f" {ad:<20s}{len(m):>8d}{m.count(chr(13)):>11d}" f"{len(satirlar):>7d}") print("geçici dizin silindi:", not Path(gecici).exists())
okuma kipi tür uzunluk özdeş ikili bytes 41 - metin utf-8 str 34 evet metin latin-1 str 41 hayır metin ascii replace str 41 hayır adım sonuç open başarılı, dosya açıldı read UnicodeDecodeError / bayt 10 okuma biçimi uzunluk CR sayısı satır ikili 21 2 3 metin (öntanımlı) 18 0 3 metin newline="" 20 2 3 geçici dizin silindi: True
Sayıların Okunması
Birinci tablo iki kipin farkını türle veriyor. İkili kip bytes döndürüyor ve uzunluk
41; metin kipi str döndürüyor ve doğru kodlamayla uzunluk 34. Aynı dosya, aynı
içerik, iki ayrı sayı — ve ikisi de doğru. latin-1 yine 41 veriyor ve özdeş sütunu
hayır: bellekteki ölçümde görülen sessiz yanlış, dosyadan okurken de aynen tekrarlanıyor.
İkinci tablo dersin en kesin bulgusudur. open çağrısı ascii kodlamasıyla başarılı
oluyor — dosya açılıyor, hiçbir istisna çıkmıyor. İstisna read çağrısında, 10’uncu
baytta çıkıyor. open yalnız dosyayı hazırlar ve kodlama adını saklar; çözme işi ilk okumada
yapılır. Kodlama hatası bir açma hatası değil, okuma hatasıdır ve bunun pratik sonucu
şudur: open çağrısını saran bir try bloğu bu hatayı yakalayamaz. Yakalayacak blok okumayı
sarmalıdır.
Üçüncü tablo satır sonu çevirisini sayıyor. Dosya 21 bayt ve içinde 2 tane satır başı
karakteri var, çünkü iki satır iki karakterlik satır sonuyla, biri tek karakterlik satır
sonuyla bitiyor. Metin kipi öntanımlı ayarıyla okuduğunda uzunluk 18’e iniyor ve satır
başı sayısı 0 oluyor: çözücü her satır sonu biçimini tek bir karaktere çevirdi.
newline="" verildiğinde çeviri kapanıyor ve uzunluk 20, satır başı sayısı 2 oluyor.
Satır sayısı üç okumada da 3. Çeviri satırların nerede bittiğini değiştirmez, yalnız satır sonunun kaç karakterle yazıldığını değiştirir. Metin kipinde okunan bir dosyanın uzunluğunu dosyanın boyutuyla karşılaştırmak bu yüzden yanlış sonuç verir: hem kodlama hem satır sonu çevirisi araya girer.
Yazma Tarafı ve Açma Kipleri
Okuma tarafı kodlamayı varsayar; yazma tarafı onu kurar. Üçüncü ölçüm aynı metni beş kodlamayla yazmayı dener ve her birinde gidiş–dönüş sınaması yapar: kodlanan baytlar aynı kodlamayla geri çözüldüğünde kaynak metin geri gelir mi?
- HD28 — Gidiş–dönüş sınaması her kodlamada kendi kodlamasıyla yapılır; ölçülen şey kodlamanın metni taşıyıp taşıyamadığıdır.
- HD29 — Açma kipleri aynı dosya üzerinde sırayla denenir; her adımda dosyanın bayt sayısı dosya sisteminden okunur, hesaplanmaz.
- HD30 — Dosya yine geçici bir dizinde kurulur; hiçbir yol basılmaz ve ölçüm bitince dizin silinir.
"""Yazma tarafi: kodlanamayan karakter ve dosya acma kipleri.""" import tempfile from pathlib import Path METIN = "kuzey yamaç: 12,5 °C — ölçüm tamam" print(f"{'kodlama':<12s}{'kodlandı':<10s}{'bayt':>6s}" f"{'gidiş-dönüş özdeş':>19s} kırılan sınıf / konum") for ad in ("utf-8", "utf-16", "latin-1", "ascii", "cp1254"): try: ham = METIN.encode(ad) except UnicodeEncodeError as e: print(f" {ad:<10s}{'hayır':<10s}{'-':>6s}{'-':>19s}" f" {type(e).__name__} / {e.start}") else: ozdes = "evet" if ham.decode(ad) == METIN else "hayır" print(f" {ad:<10s}{'evet':<10s}{len(ham):>6d}{ozdes:>19s} -") print() with tempfile.TemporaryDirectory() as gecici: gunluk = Path(gecici) / "olcum.log" print(f"{'kip':<6s}{'çağrı':<8s}{'dosya baytı':>12s} sonuç") for sira, kip in (("1.", "w"), ("2.", "w"), ("3.", "a"), ("4.", "x")): try: with open(gunluk, kip, encoding="utf-8") as d: d.write(METIN + "\n") except FileExistsError as e: print(f" {kip:<4s}{sira:<8s}{gunluk.stat().st_size:>12d}" f" {type(e).__name__}") else: print(f" {kip:<4s}{sira:<8s}{gunluk.stat().st_size:>12d} yazıldı") with open(gunluk, "r", encoding="utf-8") as d: satirlar = d.readlines() print(f"son durumda satır {len(satirlar)}, " f"karakter {sum(len(s) for s in satirlar)}, " f"bayt {gunluk.stat().st_size}")
kodlama kodlandı bayt gidiş-dönüş özdeş kırılan sınıf / konum utf-8 evet 41 evet - utf-16 evet 70 evet - latin-1 hayır - - UnicodeEncodeError / 21 ascii hayır - - UnicodeEncodeError / 10 cp1254 evet 34 evet - kip çağrı dosya baytı sonuç w 1. 42 yazıldı w 2. 42 yazıldı a 3. 84 yazıldı x 4. 84 FileExistsError son durumda satır 2, karakter 70, bayt 84
cp1254 satırı okuma tablosunun karşıtıdır ve dersin en öğretici çiftidir. Orada cp1254 utf-8
baytlarını sessizce yanlış çözüyordu; burada aynı kodlama metni 34 bayta yazıyor ve
gidiş–dönüş özdeş çıkıyor. Kodlama bir dosyanın özelliği değildir; yazanla okuyan
arasındaki anlaşmadır ve dosyanın kendisi o anlaşmayı taşımaz. Yanlış olan cp1254 değildi,
utf-8 ile yazılmış baytları cp1254 sanmaktı.
Yazma tarafı okuma tarafından bir noktada ayrılır: latin-1 ve ascii burada kırılıyor.
latin-1 21’inci karakterde, uzun çizgide; ascii 10‘uncu karakterde, ilk Türkçe
harfte. Yazarken kodlanamayan karakter sessizce geçemez, çünkü karakterin karşılığı
kodlamada yoktur. Okurken sessiz yanlış mümkündü, çünkü her baytın bir karşılığı vardı.
utf-16 metni 70 bayta yazıyor — utf-8’in 41’ine karşı — ve o da özdeş dönüyor;
kodlama seçimi doğruluk kadar boyut kararıdır.
Açma kipleri tablosu üç ayrı davranışı ayırıyor. "w" ilk çağrıda 42 bayt yazıyor; ikinci
"w" çağrısından sonra dosya yine 42 — kip dosyayı budayıp baştan yazdı, ikinci kayıt
birinciyi sildi. "a" çağrısı 84’e çıkarıyor: ekleme kipi budamaz. "x" çağrısı hiç
yazmadan FileExistsError fırlatıyor ve bayt sayısı 84’te kalıyor. Son satır dosyanın
2 satır, 70 karakter ve 84 bayt tuttuğunu söylüyor; karakter ile bayt farkı burada
da duruyor.
Özet
- İkili kip
bytesverir ve uzunluk bayt sayısıdır; metin kipistrverir, uzunluk karakter sayısıdır — aynı dosya 41 ve 34 okunur. - Yanlış kodlama her zaman kırılmaz:
ascii10’uncu baytta,utf-1640’ıncı baytta kırılırkenlatin-1vecp1254hiç kırılmaz ve sessizce 41 yanlış karakter üretir. - Hata kipleri kırılmayı engeller, veriyi kurtarmaz:
ignore28,replace41,backslashreplace80 karakter verir ve hiçbiri kaynakla özdeş değildir. - Baytları parça parça çözmek parça sınırına bağlıdır: aynı 41 bayt boya göre 0, 2 ya da 4 parçada kırılır, bütün olarak çözüldüğünde 0.
- Kodlama hatası
opençağrısında değilreadçağrısında çıkar; satır sonu çevirisi uzunluğu 21’den 18’e indirir ama satır sayısını 3 olarak korur. - Kodlama dosyanın değil, yazanla okuyanın özelliğidir:
cp1254yazarken 34 baytla özdeş gidiş–dönüş verirken utf-8 baytlarını okurken sessizce bozar; yazarken kodlanamayan karakterlatin-1’de 21’inci,ascii’de 10’uncu karakterde kırılır.
Sonraki Adım
Bu dersin bütün dosya örnekleri with bloğu içinde açıldı ve blok bitince dosya kapandı —
ölçüm sonunda geçici dizin bile silinebildi, çünkü açık kalan bir tutamak kalmamıştı. with
şimdiye kadar bir alışkanlık olarak kullanıldı; oysa o da bir sözdizimdir ve altında bir
protokol vardır. Sonraki ders o protokolü ölçer: with girerken hangi özel yöntemi çağırır,
çıkarken hangisini, ve blok içinde bir istisna oluştuğunda ne değişir. Ortak tanımın
with n + istisna satırının üç çağrısı orada ödenir.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.