4 Sınıflar, Hata Yakalama ve Dosyalar
Şimdiye kadar yazdığımız programların hepsi tek bir dosyada yaşadı, girdileri hep düzgündü ve sonuçlarını yalnız ekrana bastı. Gerçek bir hesap bundan fazlasını ister. Daha önce yazılmış fonksiyonları yeniden kullanmak isteriz; sıfıra bölme, yakınsamayan bir iterasyon ya da bozuk bir veri satırı karşısında programın anlamlı bir şey yapmasını isteriz; verileri bir dosyadan okuyup sonuçları başka bir dosyaya yazmak isteriz. Bir de matematiğin kendi nesneleri vardır: vektörler, kesirler, polinomlar, aralıklar. Bunları işlemleriyle birlikte, u + v ya da p * q yazarak kullanabilmek isteriz.
Bu bölüm bu dört ihtiyaca karşılık gelen dört aracı sırayla tanıtıyor: modüller, istisnalar, dosyalar ve sınıflar. Sonunda hepsini birleştirip bir ölçüm dosyasını okuyan, bozuk satırları ayıklayan ve sonucu yeni bir dosyaya yazan küçük bir program kuracağız. Bir sonraki bölümde başlayacağımız NumPy da bir modüldür ve onun dizileri de bir sınıfın nesneleridir; burada öğrendiklerimiz o kütüphanelerin nasıl çalıştığını anlamanın da anahtarı olacak.
4.1 Modüller ve import
Python’un gücünün büyük kısmı dilin kendisinden değil, hazır yazılmış kod kütüphanelerinden gelir; bu kodlara modüller üzerinden ulaşırız.
Modül kavramını ve math modülünü Python ile İlk Adımlar bölümünde tanıdık: bir modül, içinde Python kodu bulunan bir .py dosyasıdır ve import deyimi onun tanımladığı adları programa açar. Python’la birlikte gelen modüllerin oluşturduğu koleksiyona standart kütüphane (standard library) denir; math, fractions ve statistics bunlardandır. Birden çok modülü bir klasörde toplayan yapıya da paket (package) denir. NumPy, SciPy ve Matplotlib birer pakettir; ayrıca kurulurlar ama aynı import deyimiyle kullanılırlar.
import deyiminin dört biçimi vardır; ilk üçü aşağıdaki kodda görülüyor:
import math
import fractions as fr
from statistics import mean, median
print(math.lcm(4, 6, 10), math.isqrt(50))
print(fr.Fraction(3, 4) + fr.Fraction(1, 6))
data = [2, 3, 3, 5, 7, 10]
print(mean(data), median(data))Çıktı:
60 7
11/12
5 4.0
import math: modülmathadıyla gelir; içindekileremath.lcm,math.isqrtdiye ulaşılır.import fractions as fr: modül daha kısa bir takma adla gelir. NumPy için her yerde göreceğinizimport numpy as npkalıbı budur.from statistics import mean, median: modülden yalnız istenen adlar doğrudan programa gelir;statistics.öneki gerekmez.from math import *: modüldeki bütün adlar programa dökülür. Bu biçimden kaçınılır; nedenini birazdan bir örnekle göreceğiz.
Çıktıda \(\operatorname{lcm}(4, 6, 10) = 60\), \(\lfloor \sqrt{50} \rfloor = 7\) ve \(\frac34 + \frac16 = \frac{11}{12}\) var. Verinin ortalaması tam sayı çıktığı için 5, medyanı ise ortadaki iki değerin, \(3\) ile \(5\)’in ortalaması olduğu için 4.0 olarak basıldı.
Bir modülün içinde ne olduğunu dir fonksiyonu listeler; help(math.log1p) gibi bir çağrı da bir fonksiyonun belgesini gösterir. Aşağıda math modülündeki adlardan log ile başlayanları bir list comprehension ile süzüyoruz:
import math
names = [name for name in dir(math) if name.startswith("log")]
print(names)
x = 1e-12
print(math.log(1 + x))
print(math.log1p(x))Çıktı:
['log', 'log10', 'log1p', 'log2']
1.000088900581841e-12
9.999999999995e-13
log1p(x) fonksiyonu \(\ln(1 + x)\) değerini hesaplar. Neden ayrı bir fonksiyon gerekiyor? \(x = 10^{-12}\) iken \(1 + x\) toplamı kayan noktada yuvarlanır ve \(x\)’in basamaklarının çoğu kaybolur; math.log(1 + x) bu yüzden yaklaşık \(9 \cdot 10^{-5}\) bağıl hata yapar (bkz. Nümerik Analiz). log1p ise toplamı hiç oluşturmadan doğru sonucu verir: \(\ln(1 + x) = x - x^2/2 + \cdots\) olduğundan doğru değer \(10^{-12} - 5 \cdot 10^{-25}\)’tir. Modülleri gezmek, bu tür hazır çözümleri bulmanın en kısa yoludur.
from modül import * modüldeki bütün adları programa döker ve aynı adı taşıyan eski bir tanımın üstüne sessizce yazar; bunun ne kadar kafa karıştırıcı olabileceğini görelim. Karmaşık sayılar için matematik modülü olan cmath ile math modülünün ikisinde de sqrt ve log vardır:
from math import *
from cmath import *
print(sqrt(-4))
print(log(-1))Çıktı:
2j
3.141592653589793j
Son import kazandığı için sqrt artık karmaşık karekök, log(-1) da \(i\pi\) oldu. İki import satırının sırası değişseydi aynı kod ValueError verirdi. Hangi sqrt’nin çalıştığını anlamak için importları tek tek okumak gerekir; math.sqrt ve cmath.sqrt yazmak bu belirsizliği baştan ortadan kaldırır.
Bu derste en sık kullanacağımız standart kütüphane modülleri şunlardır:
| Modül | İçerik |
|---|---|
math |
gerçel sayılar için fonksiyonlar ve sabitler |
cmath |
karmaşık sayılar için aynı fonksiyonlar |
fractions, decimal |
tam kesirler ve ondalık aritmetik |
statistics |
ortalama, medyan, varyans |
itertools |
permütasyon, kombinasyon, kartezyen çarpım |
csv, json |
tablo ve sözlük biçimindeki dosyalar |
pathlib |
dosya yolları |
time |
süre ölçümü |
Kendi modülümüz
Kendi yazdığımız fonksiyonları da bir modüle koyup başka programlardan kullanabiliriz. Aşağıdaki kodu numtools.py adıyla kaydedelim. Öklid algoritması ve deneme bölmesi Kontrol Yapıları ve Fonksiyonlar bölümünden tanıdık; burada yalnız bir dosyada toplanıyorlar:
"""Sayılar teorisi için küçük yardımcı fonksiyonlar."""
def gcd(a, b):
"""Öklid algoritmasıyla en büyük ortak böleni döndürür."""
while b != 0:
a, b = b, a % b
return abs(a)
def is_prime(n):
"""n asal ise True döndürür (deneme bölmesi)."""
if n < 2:
return False
d = 2
while d * d <= n:
if n % d == 0:
return False
d += 1
return True
if __name__ == "__main__":
# Modül doğrudan çalıştırılınca küçük bir sınama yapılır
print("gcd(182, 70) =", gcd(182, 70))
print([p for p in range(30) if is_prime(p)])Dosyanın sonundaki if __name__ == "__main__": bloğu yaygın bir kalıptır. Python her modüle __name__ adında bir değişken verir. Dosya doğrudan çalıştırıldığında (python numtools.py) bu değişkenin değeri "__main__", başka bir dosyadan import edildiğinde ise modülün adı, yani "numtools" olur. Böylece blok yalnız dosya doğrudan çalıştırıldığında işler ve modülü kendi kendini sınayan bir program gibi kullanmamızı sağlar. Dosya doğrudan çalıştırıldığında sınama bloğu şunu basar.
Çıktı:
gcd(182, 70) = 14
[2, 3, 5, 7, 11, 13, 17, 19, 23, 29]
\(\gcd(182, 70) = 14\) sonucunu Sayılar Teorisi notlarındaki elle yapılan hesapla karşılaştırabilirsiniz. Şimdi aynı klasörde ikinci bir dosya açıp modülü kullanalım. Program 80’den küçük ikiz asalları, yani farkı \(2\) olan asal çiftlerini buluyor:
import numtools
from numtools import is_prime
print(numtools.gcd(2**10 * 3**5, 2**4 * 3**8))
print(numtools.__name__, __name__)
twins = [(p, p + 2) for p in range(3, 80)
if is_prime(p) and is_prime(p + 2)]
print(twins)Çıktı:
3888
numtools __main__
[(3, 5), (5, 7), (11, 13), (17, 19), (29, 31), (41, 43), (59, 61), (71, 73)]
\(\gcd(2^{10} \cdot 3^5,\ 2^4 \cdot 3^8) = 2^4 \cdot 3^5 = 3888\) beklendiği gibi çıktı. İkinci satır __name__ değişkeninin iki dosyadaki değerini gösteriyor; modül import edildiği için numtools.py’deki sınama bloğu bu kez çalışmadı. Programın kullandığı adlar ve bağlandıkları nesneler aşağıdaki şekilde özetleniyor:
import numtools programa yalnız numtools adını ekler; gcd fonksiyonuna numtools.gcd diye modülün içinden ulaşılır. from numtools import is_prime ise modüldeki fonksiyonu programın kendi adlarına is_prime adıyla bağlar.Bir modül, programın ömrü boyunca yalnız bir kez yüklenir; ikinci bir import numtools satırı dosyayı yeniden okumaz. Jupyter’de numtools.py’yi düzenledikten sonra değişikliği görmek için ya çekirdeği yeniden başlatın ya da import importlib satırından sonra importlib.reload(numtools) çağırın.
4.2 Hatalar ve istisnalar
Şimdiye kadar yazdığımız kodlar hep uygun girdilerle çalıştı; şimdi bir işlem yapılamadığında ne olduğuna bakalım. Aşağıdaki döngü \(12\)’yi bir listedeki sayılara sırayla bölüyor:
values = [4, 2, 0, 5]
for v in values:
print(12 / v)
print("bu satıra hiç gelinmez")Çıktı:
3.0
6.0
Traceback (most recent call last):
File "<stdin>", line 3, in <module>
ZeroDivisionError: division by zero
İlk iki bölme yapıldı, üçüncüsü yapılamadı ve program orada durdu: listedeki \(5\)’e de, son satıra da hiç gelinmedi. Python’un bastığı bu metne izleme kaydı (traceback) denir ve alttan yukarı okunur. Son satır hatanın türünü (ZeroDivisionError) ve açıklamasını (division by zero, yani sıfıra bölme) verir; üstündeki satırlar hatanın hangi dosyanın hangi satırında çıktığını gösterir. Python ile İlk Adımlar bölümünde math.sqrt(-1) çağrısının da böyle bir kayıt bıraktığını görmüştük. Bu çıktıda kod Python’a standart girdiden verildiği için dosya adı <stdin> görünüyor; kodu bir dosyadan çalıştırdığınızda o dosyanın yolu ve hatalı satırın kendisi görünür.
Tanım 4.1 (İstisna) Çalışma sırasında bir işlem gerçekleştirilemediğinde Python bir istisna (exception) nesnesi oluşturur ve onu fırlatır (raise). Her istisnanın bir türü (ValueError, ZeroDivisionError, …) ve bir mesajı vardır. Fırlatılan istisna program tarafından yakalanmazsa program durur ve izleme kaydı basılır.
Yani istisna, “bu adımı yapamıyorum” diyen bir sinyaldir. Sinyal, onu bekleyen bir kod parçasına ulaşana kadar fonksiyon çağrılarının zincirinde yukarı doğru ilerler; böyle bir kod yoksa en üste kadar çıkar ve programı durdurur. Bir istisnayı bekleyip yakalamanın yolu try deyimidir.
Tanım 4.2 (try Deyimi) Bir istisnayı yakalamak için try deyimi kullanılır. Genel biçimi şudur:
try:
... # istisna fırlatabilecek kod
except ValueError as e:
... # ValueError çıkarsa çalışır; e istisna nesnesidir
except (TypeError, KeyError):
... # bu iki türden biri çıkarsa çalışır
else:
... # try bloğu istisnasız biterse çalışır
finally:
... # her durumda, en son çalışırexcept blokları yukarıdan aşağı sınanır ve istisnanın türüne uyan ilk blok çalışır. else ve finally blokları isteğe bağlıdır; bir try deyiminde en az bir except ya da bir finally bloğu bulunur.
Yani istisna fırlatabilecek kodu try bloğuna koyarız ve her olası hata için ne yapılacağını bir except bloğuna yazarız. else bloğu yalnız her şey yolunda gittiğinde, finally bloğu ise ne olursa olsun çalışır. Aşağıdaki şekil, birazdan inceleyeceğimiz safe_ratio fonksiyonu üzerinde blokların sırasını gösteriyor:
safe_ratio fonksiyonunda blokların çalışma sırası. Önce try bloğu çalışır. İstisna çıkmazsa else, ZeroDivisionError çıkarsa except bloğu çalışır; başka türden bir istisna hiçbir except ile eşleşmez. Üç yolun hepsi finally bloğundan geçer: bu blok return deyiminden sonra bile çalışır.İlk örnek olarak, ondalık ayıracı virgül olan Türkçe yazımları da okuyabilen bir fonksiyon yazalım. float("3,75") bir ValueError fırlatır; bunu yakalayıp virgülü noktaya çevirerek ikinci bir deneme yapıyoruz:
def parse_number(s):
"""'3.75' ya da '3,75' biçimindeki metni float'a çevirir."""
try:
return float(s)
except ValueError:
return float(s.replace(",", "."))
for s in ["2.5", "3,75", "1e-3", " 42 "]:
print(repr(s), "->", parse_number(s))Çıktı:
'2.5' -> 2.5
'3,75' -> 3.75
'1e-3' -> 0.001
' 42 ' -> 42.0
repr(s) metni tırnaklarıyla birlikte, boşlukları ve satır sonu gibi özel karakterleri görünür biçimde yazar; ' 42 ' metnindeki boşluklar çıktıda bu yüzden görünüyor. float baştaki ve sondaki boşlukları kendisi atar. parse_number("abc") ise ikinci denemede de ValueError fırlatır; except bloğunun içinde çıkan bu istisnayı yakalayan başka bir blok olmadığından program durur. Bu doğru davranıştır: bir metni sayıya çeviremiyorsak bunu sessizce geçiştirmek yerine haber vermeliyiz.
Bölüm boyunca karşılaşacağımız istisna türlerini, her birini tetikleyen küçük bir ifadeyle bir arada görelim. Her ifade bir lambda içinde duruyor ve döngü onları sırayla bir try bloğunda çağırıyor. type(e).__name__ istisnanın türünün adını verir:
import math
tests = [
lambda: 1 / 0,
lambda: math.sqrt(-1),
lambda: int("3,5"),
lambda: "2" + 3,
lambda: [1, 2][2],
lambda: {"a": 1}["b"],
lambda: math.exp(1000),
lambda: 10.0 ** 400,
]
for f in tests:
try:
f()
except Exception as e:
print(f"{type(e).__name__}: {e}")Çıktı:
ZeroDivisionError: division by zero
ValueError: math domain error
ValueError: invalid literal for int() with base 10: '3,5'
TypeError: can only concatenate str (not "int") to str
IndexError: list index out of range
KeyError: 'b'
OverflowError: math range error
OverflowError: (34, 'Result too large')
int("3,5") virgüllü metni tam sayı olarak okuyamadı, "2" + 3 ise metinle sayıyı toplamaya çalıştı. Son iki satır taşmadır (bkz. Nümerik Analiz): \(e^{1000} \approx 10^{434}\) ve \(10^{400}\) değerleri en büyük çift duyarlıklı sayıyı, yaklaşık \(1{,}8 \cdot 10^{308}\)’i aşar. Python’un tam sayılarında böyle bir sınır yoktur: 10 ** 400 hatasız hesaplanır, taşan yalnız 10.0 ** 400 biçimindeki kayan noktalı hesaptır.
Yukarıdaki döngü gösteri amacıyla except Exception kullandı; bu satır neredeyse bütün hataları yakalar. Gerçek kodda yalnız beklediğiniz ve ne yapacağınızı bildiğiniz türü yakalayın: geniş bir except, yanlış yazılmış bir değişken adının yol açtığı NameError’ı da yutar ve hatanın gerçek yerini gizler. Hiçbir tür yazılmayan çıplak except: daha da kötüdür: programı Ctrl+C ile durdurma isteğini (KeyboardInterrupt) ve programdan çıkma isteğini (SystemExit) bile yutar.
Örnek 4.1 (Blokların Çalışma Sırası) Aşağıdaki safe_ratio fonksiyonu safe_ratio(7, 2) ve safe_ratio(7, 0) biçiminde çağrıldığında hangi bloklar hangi sırayla çalışır?
def safe_ratio(a, b):
try:
q = a / b
except ZeroDivisionError:
print(" except: sıfıra bölme")
return None
else:
print(" else: bölme başarılı")
return q
finally:
print(" finally: her durumda")
print(safe_ratio(7, 2))
print(safe_ratio(7, 0))Çözüm
Kod çalıştırıldığında çıktı şöyledir.
Çıktı:
else: bölme başarılı
finally: her durumda
3.5
except: sıfıra bölme
finally: her durumda
None
İlk çağrıda bölme başarılıdır: try bloğu istisnasız biter, else bloğu çalışır ve return q deyimine gelinir. Fonksiyon değeri döndürmeden hemen önce finally bloğu çalışır; 3.5 değeri bu yüzden finally mesajından sonra basıldı. İkinci çağrıda 7 / 0 bir ZeroDivisionError fırlatır, try bloğunun geri kalanı atlanır, except bloğu çalışır ve None döndürülmeden önce yine finally çalışır.
Yani finally, try, except ve else bloklarından hangisi return ile biterse bitsin en son çalışan bloktur. Açık bir dosyayı kapatmak gibi ne olursa olsun yapılması gereken işler buraya yazılır. \(\blacksquare\)
İstisna türlerinin soyağacı
İstisna türleri de birbirleriyle akrabadır ve bir except satırının hangi istisnaları yakaladığı bu akrabalığa bağlıdır. Her türün __mro__ niteliği, o türü ve bütün üst türlerini sırayla listeler:
errors = [ZeroDivisionError, OverflowError, IndexError, KeyError,
ValueError, TypeError]
for cls in errors:
print(" -> ".join(c.__name__ for c in cls.__mro__))Çıktı:
ZeroDivisionError -> ArithmeticError -> Exception -> BaseException -> object
OverflowError -> ArithmeticError -> Exception -> BaseException -> object
IndexError -> LookupError -> Exception -> BaseException -> object
KeyError -> LookupError -> Exception -> BaseException -> object
ValueError -> Exception -> BaseException -> object
TypeError -> Exception -> BaseException -> object
Her satır bir türden başlayıp onun üst türlerini sıralıyor. ZeroDivisionError ile OverflowError’ın ortak üst türü ArithmeticError, IndexError ile KeyError’ınki LookupError’dır; hepsi Exception’dan, o da BaseException’dan türer. En sondaki object, Python’daki her sınıfın ortak atasıdır. Bir except T satırı T türündeki istisnaları ve T’nin bütün alt türlerini yakalar:
__mro__ zincirinden okunmuştur. Bir except satırı, yazılan türü ve onun altındaki bütün türleri yakalar: except ArithmeticError hem sıfıra bölmeyi hem taşmayı, except LookupError hem liste indisi hem sözlük anahtarı hatasını yakalar.Örnek 4.2 (Bir Üst Türü Yakalamak) Bir fonksiyonu verilen bir noktada hesaplayan, sıfıra bölme ya da taşma olursa programı durdurmak yerine hatanın türünü bildiren bir metin döndüren try_compute(f, x) fonksiyonunu, iki hata türünü tek bir except satırıyla yakalayarak yazınız.
Çözüm
İki tür de ArithmeticError’ın alt türü olduğundan bu üst türü yakalamak yeterlidir:
import math
def try_compute(f, x):
try:
return f(x)
except ArithmeticError as e:
return f"{type(e).__name__} yakalandı"
print(try_compute(lambda x: 1 / x, 0))
print(try_compute(math.exp, 1000))
print(try_compute(math.exp, 1))Çıktı:
ZeroDivisionError yakalandı
OverflowError yakalandı
2.718281828459045
except ArithmeticError hem \(1/0\)’daki ZeroDivisionError’ı hem \(e^{1000}\)’deki OverflowError’ı yakaladı; üçüncü çağrıda istisna çıkmadı ve \(e\) döndü. Aynı fonksiyon math.sqrt ile \(-1\)’de çağrılsaydı ValueError yakalanmaz ve programı durdururdu, çünkü ValueError, ArithmeticError’ın alt türü değildir. Akraba olmayan türleri birlikte yakalamak için bir demet yazılır: except (ValueError, ArithmeticError). \(\blacksquare\)
İstisna fırlatmak
Kendi fonksiyonlarımız da anlamsız bir girdi aldıklarında istisna fırlatabilir; bu, sessizce yanlış bir sonuç döndürmekten her zaman iyidir.
Tanım 4.3 (raise Deyimi) raise T(mesaj) deyimi T türünden, verilen mesajı taşıyan bir istisna oluşturup fırlatır. Bir except bloğunun içinde tek başına yazılan raise ise o an yakalanmış olan istisnayı olduğu gibi yeniden fırlatır.
Yani raise, Python’un kendi fonksiyonlarının hata bildirdiği mekanizmanın aynısını bizim kodumuza açar. Tür seçerken yerleşik türlerin anlamına uyulur: değer uygunsuzsa ValueError, tür uygunsuzsa TypeError, bir iterasyon sonuç vermiyorsa RuntimeError.
Örnek 4.3 (Girdisini Denetleyen İkiye Bölme) İkiye bölme yönteminin (bkz. Nümerik Analiz) bir kök bulacağı, \(f\) sürekli, \(a < b\) ve \(f(a) f(b) \le 0\) iken güvencededir. Süreklilik kodla denetlenemez; son iki koşul sağlanmadığında ValueError fırlatan bir bisect(f, a, b) fonksiyonu yazıp \(f(x) = x^3 - 2x - 5\) fonksiyonuyla deneyiniz.
Çözüm
Koşulları fonksiyonun başında denetleyip ihlal edildiklerinde raise ile çıkıyoruz; geri kalanı, her adımda kökü içeren aralığı yarılayan bilinen döngüdür:
def bisect(f, a, b, tol=1e-12):
"""f'nin [a, b] aralığındaki bir kökünü ikiye bölmeyle bulur."""
if not a < b:
raise ValueError(f"a < b olmalı: a={a}, b={b}")
fa = f(a)
if fa * f(b) > 0:
raise ValueError("f(a) ile f(b) aynı işaretli")
while b - a > tol:
m = (a + b) / 2
fm = f(m)
if fa * fm <= 0:
b = m
else:
a, fa = m, fm
return (a + b) / 2
def f(x):
return x**3 - 2 * x - 5
print(bisect(f, 2, 3))
for a, b in [(3, 4), (3, 2)]:
try:
bisect(f, a, b)
except ValueError as e:
print("Hata:", e)Çıktı:
2.0945514815425668
Hata: f(a) ile f(b) aynı işaretli
Hata: a < b olmalı: a=3, b=2
\([2, 3]\) aralığında \(f(2) = -1 < 0 < 16 = f(3)\) olduğundan kök bulundu: \(x \approx 2{,}0945514815\). \([3, 4]\) aralığında \(f(3) = 16\) ile \(f(4) = 51\) aynı işaretli olduğu, \((3, 2)\) çiftinde ise \(a < b\) sağlanmadığı için fonksiyon hesaba hiç girmeden istisna fırlattı; çağıran kod da onu yakalayıp mesajı bastı.
Denetim olmasaydı \([3, 4]\) için döngü her adımda sol ucu orta noktaya taşır ve sessizce \(4\)’e çok yakın bir sayı döndürürdü. Oysa \(f(4) = 51\) olduğundan bu sayı bir kök değildir. \(\blacksquare\)
Doğrulama: assert
Bir fonksiyonun girdisini değil, kendi hesabının doğru gittiğini denetlemek için assert deyimi kullanılır.
Tanım 4.4 (assert Deyimi) assert koşul, mesaj deyimi koşulu hesaplar. Koşul doğruysa hiçbir şey yapmaz; yanlışsa verilen mesajla bir AssertionError fırlatır. Mesaj isteğe bağlıdır.
Yani assert, koda yazılmış bir “burada şu kesinlikle doğru olmalı” notudur; not yanlış çıkarsa program yüksek sesle durur. Bir hesabın, doğru olduğunu bildiğimiz bir özelliğini sınamak için idealdir. Örneğin \(\sqrt 2\) için Newton iterasyonundan (bkz. Kontrol Yapıları ve Fonksiyonlar) yalnız üç adım atarsak sonuç henüz yeterince doğru değildir ve assert bunu yakalar:
def newton_sqrt(a, steps):
x = a
for _ in range(steps):
x = (x + a / x) / 2
return x
r = newton_sqrt(2, 3)
print(r)
assert abs(r * r - 2) < 1e-12, f"yakınsama yetersiz: {r * r - 2:.1e}"
print("buraya gelinmez")Çıktı:
1.4142156862745097
Traceback (most recent call last):
File "<stdin>", line 10, in <module>
AssertionError: yakınsama yetersiz: 6.0e-06
Üç adımın sonunda \(r^2 - 2 \approx 6 \cdot 10^{-6}\) kaldı; koşul sağlanmadığı için program son satıra gelmeden durdu.
Örnek 4.4 (Bölme Algoritmasını assert ile Güvenceye Almak) Bölme Algoritması’na göre (bkz. Sayılar Teorisi) her \(a\) ve \(b \ne 0\) tam sayısı için \(a = bq + r\) ve \(0 \le r < |b|\) koşullarını sağlayan tek bir \((q, r)\) çifti vardır. Python’un divmod(a, b) fonksiyonu negatif bölende farklı bir kalan kuralı izler. Bu çifti her işaret için doğru veren ve sonucunu assert ile denetleyen bir division(a, b) fonksiyonu yazınız.
Çözüm
Python ile İlk Adımlar bölümünde gördüğümüz gibi a // b bölümü aşağı yuvarlanır, yani \(\lfloor a/b \rfloor\)’dir, ve a % b kalanı bölenin işaretini taşır. \(b > 0\) iken bu tam istediğimiz \(0 \le r < b\) koşuludur. \(b < 0\) iken ise \(b < r \le 0\) olur; \(r = 0\) ise düzeltme gerekmez. \(b < 0\) ve \(r < 0\) iken bölümü \(1\) artırıp kalandan \(b\) çıkarırsak
\[a = bq + r = b(q + 1) + (r - b)\]
eşitliği korunur ve yeni kalan \(0 < r - b < |b|\) aralığına girer:
def division(a, b):
"""a = b*q + r ve 0 <= r < |b| olacak biçimde (q, r) döndürür."""
if b == 0:
raise ZeroDivisionError("b sıfır olamaz")
q, r = divmod(a, b)
if r < 0: # b < 0 iken Python'un kalanı r <= 0 olur
q, r = q + 1, r - b
assert a == b * q + r and 0 <= r < abs(b), (a, b, q, r)
return q, r
for a, b in [(17, 5), (-17, 5), (17, -5), (-17, -5)]:
print((a, b), "divmod:", divmod(a, b), "division:", division(a, b))Çıktı:
(17, 5) divmod: (3, 2) division: (3, 2)
(-17, 5) divmod: (-4, 3) division: (-4, 3)
(17, -5) divmod: (-4, -3) division: (-3, 2)
(-17, -5) divmod: (3, -2) division: (4, 3)
\(b = 5\) iken iki fonksiyon aynı sonucu veriyor. \(b = -5\) iken divmod kalanları \(-3\) ve \(-2\) olarak, yani negatif veriyor; division ise \(17 = (-5)(-3) + 2\) ve \(-17 = (-5) \cdot 4 + 3\) ayrışımlarını buluyor. assert satırı her çağrıda hem eşitliği hem kalanın aralığını denetledi; dört durumda da koşul sağlandığı için sessiz kaldı. Mesaj olarak (a, b, q, r) demetini verdik: koşul bir gün bozulursa hangi girdide bozulduğunu hemen görürüz. \(\blacksquare\)
Python -O seçeneğiyle (python -O program.py) çalıştırıldığında bütün assert satırları atlanır. Bu yüzden kullanıcının verdiği girdiyi assert ile değil, if ve raise ile denetleyin. bisect’teki işaret denetimi bu yüzden bir raise’dir, division’daki son denetim ise bir assert. Kural basittir: hatalı girdi raise ile bildirilir, kodun kendi yanlışı assert ile yakalanır.
4.3 Dosyalarla çalışmak
Hesapların girdisi çoğu zaman bir dosyadan gelir; sonuçları da kaybolmasınlar diye bir dosyaya yazarız.
Tanım 4.5 (Metin Dosyası) Karakterlerden oluşan ve satır sonu karakteriyle ("\n") satırlara ayrılan dosyaya metin dosyası denir. Diskte karakterler bayt olarak saklanır; karakterden bayta çeviri kuralına kodlama (encoding) denir. Türkçe karakterleri de kapsayan en yaygın kodlama UTF-8’dir.
Yani bir metin dosyası, "\n" karakterleriyle bölünmüş uzun bir karakter dizisidir; sayılar bile dosyada rakam rakam, metin olarak durur. Dosyayı okurken bu metni yeniden sayıya çevirmek bizim işimizdir.
Python’da bir dosya open(yol, kip, encoding="utf-8") ile açılır. Kip (mode), dosyayla ne yapacağımızı söyler:
| Kip | Anlamı |
|---|---|
"r" |
okumak için aç (varsayılan); dosya yoksa FileNotFoundError |
"w" |
yazmak için aç; dosya varsa içeriği silinir |
"a" |
sonuna eklemek için aç |
"x" |
yeni dosya oluştur; dosya varsa FileExistsError |
open bir dosya nesnesi döndürür ve işimiz bitince dosyanın kapatılması gerekir. Bunu with bloğu kendiliğinden yapar: with open(...) as f: satırının altındaki blok bittiğinde, blokta bir istisna çıksa bile dosya kapatılır. Bu, bir try ve finally çifti yazmanın kısa ve güvenli yoludur.
with open(yol, kip, encoding="utf-8") as f:ile dosyayı açın; yazmak için"w", okumak için"r"kipini kullanın.- Yazarken
f.write(metin)kullanın ve satır sonlarını ("\n") kendiniz ekleyin; okurkenfor line in f:ile satır satır dolaşın. - Okuduğunuz her satırı
int,floatya dasplitile ihtiyacınız olan biçime çevirin.
Örnek 4.5 (Fibonacci Sayılarını Dosyaya Yazıp Okumak) İlk \(30\) Fibonacci sayısını, her satıra bir sayı gelecek biçimde fibonacci.txt dosyasına yazınız. Sonra dosyayı okuyup son iki sayının oranını altın oran \(\varphi = \frac{1 + \sqrt 5}{2}\) ile karşılaştırınız.
Çözüm
Yazmak. Listeyi kurup her sayıyı bir f-string ile, satır sonuyla birlikte yazıyoruz:
F = [1, 1]
while len(F) < 30:
F.append(F[-1] + F[-2])
with open("fibonacci.txt", "w", encoding="utf-8") as f:
for n in F:
f.write(f"{n}\n")
print("yazılan satır sayısı:", len(F))Çıktı:
yazılan satır sayısı: 30
Okumak. Dosya nesnesi üzerinde kurulan bir for döngüsü dosyayı satır satır dolaşır; readline() ise tek bir satır okur. Dosyanın nasıl göründüğünü incelemek için ilk satırı bu yolla ayrı okuyalım. Bu kod, önceki kodun yazdığı dosyayı okur:
with open("fibonacci.txt", encoding="utf-8") as f:
first = f.readline()
rest = [int(line) for line in f]
print(repr(first))
print(len(rest), rest[:6], rest[-1])
phi = (1 + 5**0.5) / 2
print(rest[-1] / rest[-2], phi)Çıktı:
'1\n'
29 [1, 2, 3, 5, 8, 13] 832040
1.6180339887482036 1.618033988749895
repr ilk satırın sonunda "\n" karakterinin durduğunu gösteriyor. int baştaki ve sondaki boşlukları ve satır sonunu kendisi attığı için int(line) doğrudan çalıştı. Okuma readline’ın kaldığı yerden devam ettiği için listede \(29\) sayı var. \(F_{30}/F_{29} = 832040/514229\) oranı \(\varphi\) ile virgülden sonra \(11\) basamak örtüşüyor. \(\blacksquare\)
Okunacak dosya yoksa open bir FileNotFoundError fırlatır. Bu, yakalamanın anlamlı olduğu tipik bir durumdur:
from pathlib import Path
path = Path("deney_verisi.txt")
try:
with open(path, encoding="utf-8") as f:
text = f.read()
except FileNotFoundError as e:
print("Dosya bulunamadı:", e.filename)
text = ""
print(path.exists(), len(text))Çıktı:
Dosya bulunamadı: deney_verisi.txt
False 0
pathlib modülündeki Path nesnesi bir dosya yolunu temsil eder. exists() metodu dosyanın var olup olmadığını söyler; read_text ve write_text metotları da küçük dosyaları tek satırda okuyup yazar.
encoding verilmezse Python işletim sisteminin varsayılan kodlamasını kullanır; Türkçe Windows’ta bu genellikle UTF-8 değil, cp1254’tür. UTF-8 ile yazılmış bir dosya başka bir kodlamayla okunursa Türkçe karakterler bozulur:
text = "ağırlık, sıcaklık, ölçüm"
with open("kodlama.txt", "w", encoding="utf-8") as f:
f.write(text)
with open("kodlama.txt", encoding="cp1254") as f:
print(f.read())
with open("kodlama.txt", encoding="utf-8") as f:
print(f.read())Çıktı:
ağırlık, sıcaklık, ölçüm
ağırlık, sıcaklık, ölçüm
Diskteki baytlar iki okumada da aynıdır; değişen yalnız onları karakterlere çeviren kuraldır. Her open çağrısına encoding="utf-8" yazmak bu sorunu baştan önler.
CSV dosyaları
Sayısal veriler çoğu zaman tablo biçiminde saklanır; bunun en yaygın düz metin biçimi CSV’dir.
Tanım 4.6 (CSV Dosyası) Her satırı bir kayıt olan ve bir satırdaki alanların belirli bir ayraçla, çoğunlukla virgülle ayrıldığı metin dosyasına CSV dosyası (comma-separated values) denir. İlk satır genellikle sütun adlarını taşıyan başlık satırıdır.
Yani CSV, bir tablonun düz metin hâlidir: satırlar tablonun satırları, ayraçlar da sütun çizgileridir. Satırı split(",") ile bölmek basit durumlarda işe yarar ama bir alanın içinde virgül geçtiğinde ("Ankara, Türkiye") bozulur. Standart kütüphanedeki csv modülü tırnak içindeki alanlar gibi ayrıntıları bizim yerimize halleder.
Aşağıdaki kod \([0, \pi/2]\) aralığında \(\pi/12\) adımla \(\sin\) ve \(\cos\) değerlerinin tablosunu trig.csv dosyasına yazıyor ve dosyanın içeriğini olduğu gibi basıyor. csv modülüyle kullanılan dosyalar newline="" ile açılır; satır sonlarını modülün kendisi yönetir:
import csv
import math
with open("trig.csv", "w", newline="", encoding="utf-8") as f:
writer = csv.writer(f)
writer.writerow(["x", "sin", "cos"])
for k in range(7):
x = k * math.pi / 12
writer.writerow([f"{x:.6f}", f"{math.sin(x):.6f}",
f"{math.cos(x):.6f}"])
with open("trig.csv", encoding="utf-8") as f:
print(f.read(), end="")Çıktı:
x,sin,cos
0.000000,0.000000,1.000000
0.261799,0.258819,0.965926
0.523599,0.500000,0.866025
0.785398,0.707107,0.707107
1.047198,0.866025,0.500000
1.308997,0.965926,0.258819
1.570796,1.000000,0.000000
Dosyayı geri okumanın en rahat yolu csv.DictReader’dır: başlık satırını okur ve sonraki her satırı, anahtarları sütun adları olan bir sözlüğe çevirir. Aşağıdaki kod önceki kodun yazdığı dosyayı okuyup her satırda \(\sin^2 x + \cos^2 x = 1\) özdeşliğinden sapmayı ölçüyor:
import csv
with open("trig.csv", newline="", encoding="utf-8") as f:
rows = list(csv.DictReader(f))
print(len(rows), rows[2])
err = max(abs(float(r["sin"])**2 + float(r["cos"])**2 - 1)
for r in rows)
print(f"en büyük sapma: {err:.1e}")Çıktı:
7 {'x': '0.523599', 'sin': '0.500000', 'cos': '0.866025'}
en büyük sapma: 7.0e-07
Alanların hâlâ metin ('0.500000') olduğuna dikkat edin; float ile sayıya çevirmek bize kalıyor. Sapma, değerleri altı ondalık basamağa yuvarlamamızdan geliyor: her değerdeki yuvarlama hatası en çok \(5 \cdot 10^{-7}\) olduğundan özdeşlikten sapma da \(10^{-6}\) mertebesinde kalıyor.
Örnek 4.6 (Noktalı Virgül ve Ondalık Virgül) Türkçe ayarlı bir tablolama programı CSV dosyalarını ondalık virgülle yazar ve alanları virgül yerine noktalı virgülle ayırır. İçeriği
x;f(x)
0,5;0,4794
1,0;0,8415
1,5;0,9975
olan tr.csv dosyasını okuyup \((x, f(x))\) sayı çiftlerinin listesini oluşturunuz.
Çözüm
Kod önce dosyayı verilen içerikle oluşturuyor. Sonra csv.reader’a delimiter=";" vererek alanları noktalı virgülden böldürüyoruz. Başlık satırını, okuyucudan bir sonraki satırı çeken next(reader) ile ayrı alıyoruz. Kalan her satırda iki alanın virgülünü noktaya çevirip float uyguluyoruz:
import csv
text = "x;f(x)\n0,5;0,4794\n1,0;0,8415\n1,5;0,9975\n"
with open("tr.csv", "w", encoding="utf-8") as f:
f.write(text)
points = []
with open("tr.csv", newline="", encoding="utf-8") as f:
reader = csv.reader(f, delimiter=";")
header = next(reader)
for row in reader:
x, y = (float(s.replace(",", ".")) for s in row)
points.append((x, y))
print(header)
print(points)Çıktı:
['x', 'f(x)']
[(0.5, 0.4794), (1.0, 0.8415), (1.5, 0.9975)]
Değerler, \(\sin x\)’in dört basamağa yuvarlanmış değerleridir. Ayraç olarak yanlışlıkla varsayılan virgül kullanılsaydı 0,5;0,4794 satırı ['0', '5;0', '4794'] biçiminde üç anlamsız alana bölünürdü. \(\blacksquare\)
Bir satırın dosyadan sayı çiftine dönüşmesi, ara adımlarıyla birlikte aşağıda görülüyor:
tr.csv dosyasının ikinci satırının yolculuğu. csv.reader satırı noktalı virgülden böler ve satır sonunu atar; alanlar hâlâ metindir. Ondalık virgül noktaya çevrildikten sonra float her alanı sayıya dönüştürür.Sözlükleri saklamak: JSON
Tablo olmayan verileri, örneğin bir hesabın ayarlarını ya da iç içe sonuçları saklamak için JSON biçimi daha uygundur. json modülü sözlükleri, listeleri, sayıları ve metinleri insanın da okuyabildiği bir metne çevirip dosyaya yazar (json.dump) ve geri okur (json.load):
import json
params = {"method": "simpson", "a": 0, "b": 3.14159, "n": [10, 20, 40]}
with open("ayarlar.json", "w", encoding="utf-8") as f:
json.dump(params, f, indent=2)
with open("ayarlar.json", encoding="utf-8") as f:
loaded = json.load(f)
print(loaded == params, loaded["n"][-1])Çıktı:
True 40
Çıktının ilk değeri, okunan sözlüğün yazılanla aynı olduğunu söylüyor. JSON’un demet, küme ya da Fraction gibi türleri yoktur; bir demet yazılırsa listeye dönüşerek geri gelir.
4.4 Sınıflar
Python’daki her değer bir nesnedir ve her nesnenin bir sınıfı vardır; şimdi bu kavramı kendi matematik nesnelerimizi tanımlamak için kullanacağız.
from fractions import Fraction
q = Fraction(6, 8)
print(type(7), type(0.75), type(q))
print(q, q.numerator, q.denominator)
print((0.75).as_integer_ratio(), isinstance(q, Fraction))Çıktı:
<class 'int'> <class 'float'> <class 'fractions.Fraction'>
3/4 3 4
(3, 4) True
type bir nesnenin sınıfını söyler. Fraction(6, 8) nesnesi kendi verisini, yani pay ve paydasını, numerator ve denominator niteliklerinde (attribute) taşır. as_integer_ratio gibi, nesneye nokta ile bağlanan fonksiyonlar ise metotlardır. isinstance(q, Fraction) bir nesnenin verilen sınıftan olup olmadığını sınar.
Tanım 4.7 (Sınıf ve Nesne) Bir sınıf (class), nesnelerinin hangi verileri (nitelikler) ve hangi işlemleri (metotlar) taşıyacağını tanımlayan bir kalıptır. Bu kalıptan üretilen her değere o sınıfın bir nesnesi (object, instance) denir. Sınıf class Ad: bloğuyla tanımlanır; Ad(...) çağrısı yeni bir nesne üretir ve onu ilk değerleriyle donatmak için sınıfın __init__ metodunu çalıştırır.
Yani sınıf bir türün tarifidir, nesne ise o tariften yapılmış somut bir örnek. Matematikte “düzlemde bir vektör” demek bir sınıf tanımlamaya, \(u = (3, 4)\) demek de o sınıftan bir nesne üretmeye benzer. İlk sınıfımız da düzlemde bir vektör olsun:
import math
class Vec2:
"""Düzlemde bir vektör."""
def __init__(self, x, y):
self.x = x
self.y = y
def norm(self):
"""Öklid normu."""
return math.hypot(self.x, self.y)
u = Vec2(3, 4)
v = Vec2(1, 1)
v.x = 5
print(u.x, u.y, u.norm())
print(v.x, v.y, v.norm(), Vec2.norm(v))Çıktı:
3 4 5.0
5 1 5.0990195135927845 5.0990195135927845
Kodu satır satır okuyalım:
class Vec2:sınıfı başlatır. Sınıf adları geleneksel olarak büyük harfle başlar; altındaki üç tırnaklı metin sınıfın belgesidir.- Metotlar, sınıfın içinde
defile tanımlanan fonksiyonlardır. İlk parametreleri üzerinde çalışılan nesnedir ve geleneksel adıself’tir. Vec2(3, 4)çağrısı yeni bir nesne oluşturur ve onun için__init__(self, 3, 4)metodunu çalıştırır;self.x = xsatırı nesneyexniteliğini ekler.v.norm()yazdığımızda Python bunuVec2.norm(v)olarak çalıştırır, yaniselfyerinevkonur. Çıktının son iki değeri bu iki yazımın aynı olduğunu gösteriyor.- Nitelikler sonradan değiştirilebilir:
v.x = 5yalnızvnesnesini değiştirdi,u’ya dokunmadı.
__init__, norm) sınıfta bir kez tanımlıdır; her nesne kendi x, y niteliklerini taşır. u.norm() çağrısı Vec2.norm(u) demektir: metot, self yerine u konarak çalışır.Bu sınıfın bir nesnesini print ile basmayı denerseniz <__main__.Vec2 object at 0x...> gibi, sonu bellekteki adrese göre değişen anlamsız bir metin görürsünüz; u + v yazarsanız da TypeError alırsınız. Python iki Vec2 nesnesinin nasıl yazılacağını ve toplanacağını henüz bilmiyor. Bunu ona özel metotlarla öğretiriz.
Tanım 4.8 (Özel Metot) Adı iki alt çizgiyle başlayıp iki alt çizgiyle biten metotlara özel metot (special method) denir. Python bu metotları dilin kendi sözdizimi için çağırır: a + b ifadesi a.__add__(b) çağrısına, a == b ifadesi a.__eq__(b) çağrısına, repr(a) de a.__repr__() çağrısına dönüşür.
Yani bir sınıfa özel metotlar ekleyerek onun nesnelerine +, *, ==, abs gibi tanıdık işlemleri öğretebiliriz. Sık kullanılanlar şunlardır:
| Yazım | Çağrılan metot |
|---|---|
a + b, a - b |
a.__add__(b), a.__sub__(b) |
a * b |
a.__mul__(b); o işe yaramazsa ve b başka türdense b.__rmul__(a) |
-a, abs(a) |
a.__neg__(), a.__abs__() |
a == b |
a.__eq__(b) |
repr(a), print(a) |
a.__repr__() |
a(x) |
a.__call__(x) |
x in a |
a.__contains__(x) |
print aslında __str__ metodunu arar; sınıfta yalnız __repr__ varsa onu kullanır. Vec2’yi bu metotlarla yeniden yazalım:
import math
class Vec2:
"""Düzlemde, işlemleriyle birlikte bir vektör."""
def __init__(self, x, y):
self.x = x
self.y = y
def __repr__(self):
return f"Vec2({self.x}, {self.y})"
def __add__(self, other):
return Vec2(self.x + other.x, self.y + other.y)
def __sub__(self, other):
return Vec2(self.x - other.x, self.y - other.y)
def __neg__(self):
return Vec2(-self.x, -self.y)
def __mul__(self, k):
"""Skalerle çarpım: v * k."""
if not isinstance(k, (int, float)):
return NotImplemented
return Vec2(k * self.x, k * self.y)
__rmul__ = __mul__ # k * v de aynı sonucu versin
def __eq__(self, other):
if not isinstance(other, Vec2):
return NotImplemented
return self.x == other.x and self.y == other.y
def __abs__(self):
return math.hypot(self.x, self.y)
def dot(self, other):
"""İç çarpım."""
return self.x * other.x + self.y * other.yBirkaç ayrıntı:
__repr__bir metin döndürür; Python’un konsolu veprintnesneyi bu metinle gösterir. Metni, nesneyi yeniden üreten koda benzetmek (Vec2(4, 3)) iyi bir alışkanlıktır.- İşlem metotları yeni bir
Vec2döndürür;u + vişlemiu’yu değiştirmez. __mul__yalnız sayılarla çarpımı tanımlar ve çarpan sayı değilseNotImplementeddöndürür. Bu özel değer Python’a “bu işlemi bilmiyorum” der: öbür terim farklı türdense Python o zaman onun metodunu dener, o da bilmiyorsaTypeErrorfırlatır.2 * uyazıldığında önceintsınıfının__mul__metodu denenir. O birVec2ile çarpmayı bilmediği için Pythonu.__rmul__(2)çağrısına geçer. Skalerle çarpma değişmeli olduğundan__rmul__ = __mul__satırı aynı metodu ikinci bir adla bağlıyor.
Sınıfı kullanalım. Bu kod önceki kodun devamıdır:
u, v = Vec2(3, 1), Vec2(1, 2)
print(u + v, u - v, -v)
print(2 * u, u * 0.5)
print(abs(Vec2(3, 4)), u.dot(v))
print(u + v == v + u, u == Vec2(3.0, 1.0))Çıktı:
Vec2(4, 3) Vec2(2, -1) Vec2(-1, -2)
Vec2(6, 2) Vec2(1.5, 0.5)
5.0 5
True True
u + v == v + u toplamanın değişmeliliğini sınadı; Vec2(3.0, 1.0) ise 3 == 3.0 olduğundan u’ya eşit çıktı. Vec2’nin toplama ve skalerle çarpma işlemleri, \(\mathbb{R}^2\)’yi bir vektör uzayı yapan işlemlerin ta kendisidir (bkz. Lineer Cebir). Toplamın ve farkın geometrisi paralelkenar kuralıdır:
Vec2 ile hesaplanan u + v ve u - v. Toplam, u ile v üzerine kurulan paralelkenarın köşegenidir; fark ise v'nin ucundan u'nun ucuna giden vektördür (kesikli ok), başlangıç noktası orijine taşınmış olarak.Tanımlamadığımız işlemler ise hâlâ hata verir, __eq__ metodunun da bir yan etkisi vardır. Bu kod da önceki iki kodun devamıdır:
u, v = Vec2(3, 1), Vec2(1, 2)
try:
u * v
except TypeError as e:
print("TypeError:", e)
try:
{u, v}
except TypeError as e:
print("TypeError:", e)
w = Vec2(0.1, 0.2) + Vec2(0.2, 0.1)
print(w, w == Vec2(0.3, 0.3))Çıktı:
TypeError: unsupported operand type(s) for *: 'Vec2' and 'Vec2'
TypeError: unhashable type: 'Vec2'
Vec2(0.30000000000000004, 0.30000000000000004) False
- İki vektörün
*ile çarpımını tanımlamadık:u.__mul__(v)çağrısıNotImplementeddöndürdü. İki terim aynı türden olduğu için Pythonv.__rmul__(u)çağrısını hiç denemez ve doğrudanTypeErrorfırlatır. İç çarpım için ayrı birdotmetodu yazmamız bilinçli bir seçimdir. __eq__tanımlayan bir sınıfın nesneleri, ayrıca bir__hash__metodu yazılmadıkça hashlenebilir değildir (bkz. Veri Yapıları ve Comprehension), yani kümeye konamaz ve sözlük anahtarı olamaz. Eşit nesnelerin hash değerleri de eşit olmalıdır ve nitelikleri değiştirilebilen bir nesnede bu koşul zamanla bozulabilir. Birazdan göreceğimizdataclass(frozen=True)bu sorunu çözer.- Son satır, kayan nokta aritmetiğinin bilinen tuzağıdır.
\(0{,}1 + 0{,}2\) makinede tam olarak \(0{,}3\) etmediği için (bkz. Python ile İlk Adımlar) Vec2(0.1, 0.2) + Vec2(0.2, 0.1) toplamı Vec2(0.3, 0.3) nesnesine eşit çıkmadı. __eq__ tam eşitlik sorar ve tam sayı bileşenlerde doğru çalışır. Ölçümden ya da hesaptan gelen vektörleri karşılaştırmak için abs(u - v) < tol biçiminde bir tolerans ya da bileşenler üzerinde math.isclose (bkz. Python ile İlk Adımlar) kullanılır.
Örnek 4.7 (Kendi Kesir Sınıfımız) Rasyonel sayıları \(p/q\) biçiminde, hep sadeleşmiş ve paydası pozitif olarak tutan bir Rational sınıfı yazınız. Payda sıfırsa ZeroDivisionError fırlatılsın; toplama, çarpma, eşitlik ve repr tanımlansın. Sınıfı \(H_{10} = 1 + \frac12 + \dots + \frac1{10}\) harmonik sayısıyla deneyip sonucu fractions.Fraction ile karşılaştırınız.
Çözüm
Sadeleştirmeyi __init__ içinde bir kez yapıyoruz: pay ve paydayı \(g = \gcd(p, q)\)’ya böleriz, payda negatifse ikisinin de işaretini değiştirmek için \(-g\)’ye böleriz. Böylece her kesrin tek bir gösterimi olur ve iki kesrin eşitliği, pay ve paydalarının eşitliğine indirgenir. Toplama ve çarpma
\[\frac{p}{q} + \frac{r}{s} = \frac{ps + rq}{qs}, \qquad \frac{p}{q} \cdot \frac{r}{s} = \frac{pr}{qs}\]
formüllerinden gelir; sonuç yeni bir Rational olarak kurulduğu için kendiliğinden sadeleşir.
from fractions import Fraction
from math import gcd
class Rational:
"""p/q kesri; hep sadeleşmiş ve paydası pozitif tutulur."""
def __init__(self, p, q=1):
if q == 0:
raise ZeroDivisionError("payda sıfır olamaz")
g = gcd(p, q)
if q < 0:
g = -g
self.p = p // g
self.q = q // g
def __repr__(self):
return f"{self.p}/{self.q}"
def __add__(self, other):
return Rational(self.p * other.q + other.p * self.q,
self.q * other.q)
def __mul__(self, other):
return Rational(self.p * other.p, self.q * other.q)
def __eq__(self, other):
return self.p == other.p and self.q == other.q
print(Rational(6, -8), Rational(-3, 4) == Rational(6, -8))
H = Rational(0)
for k in range(1, 11):
H = H + Rational(1, k)
print("H_10 =", H)
print("Fraction ile:", sum(Fraction(1, k) for k in range(1, 11)))
try:
Rational(1, 0)
except ZeroDivisionError as e:
print("Hata:", e)Çıktı:
-3/4 True
H_10 = 7381/2520
Fraction ile: 7381/2520
Hata: payda sıfır olamaz
\(6/(-8)\) kesri \(-3/4\) olarak saklandı ve \(-3/4\)’e eşit çıktı. Harmonik sayı \(H_{10} = 7381/2520\) bulundu ve Fraction ile aynı. Sıfır paydalı bir kesir oluşturma denemesi, kendi yazdığımız mesajla durduruldu. Sadeleştirmedeki \(\gcd\), math.gcd’den geliyor ve Öklid algoritmasıyla hesaplanıyor (bkz. Sayılar Teorisi). Gerçek bir hesapta elbette hazır Fraction sınıfı kullanılır; buradaki amaç, bir sınıfın içini kendi elimizle kurmaktı. \(\blacksquare\)
Kalıtım ve kendi istisna türlerimiz
Bir sınıf, var olan bir sınıfı genişleterek de tanımlanabilir; kendi istisna türlerimizi tanımlamanın yolu budur.
Tanım 4.9 (Kalıtım) class B(A): biçiminde tanımlanan B sınıfı, A sınıfından kalıtım (inheritance) yoluyla türer: A’nın bütün metotlarına sahip olur, bunlara yenilerini ekleyebilir ya da aynı adla yeniden tanımlayarak onları değiştirebilir. A’ya üst sınıf, B’ye alt sınıf denir ve her B nesnesi aynı zamanda bir A nesnesidir.
Yani alt sınıf, üst sınıfın özel bir hâlidir; istisna soyağacında ZeroDivisionError’ın bir ArithmeticError olması tam olarak budur. Kendi istisna türümüzü uygun bir yerleşik türden türetirsek onu hem kendi adıyla hem de üst türünün adıyla yakalayabiliriz. Alt sınıfın __init__ metodunda super().__init__(...) çağrısı üst sınıfın __init__ metodunu çalıştırır; böylece istisnanın mesajı gibi standart bilgiler üst sınıfça saklanır.
Örnek 4.8 (Yakınsamayan Newton İterasyonu) \(f(x) = x^3 - 2x + 2\) için Newton yöntemi (bkz. Nümerik Analiz) \(x_0 = 0\)’dan başlatıldığında \(0\) ile \(1\) arasında gidip gelir ve hiç yakınsamaz:
newton fonksiyonu bu yüzden ConvergenceError fırlatır.İzin verilen adım sayısında yakınsamazsa son iterasyonları da taşıyan bir ConvergenceError fırlatan bir newton fonksiyonu yazıp \(x_0 = -2\) ve \(x_0 = 0\) için deneyiniz.
Çözüm
ConvergenceError sınıfını RuntimeError’dan türetiyoruz ve __init__ metoduna iterasyon listesini saklayan bir nitelik ekliyoruz:
class ConvergenceError(RuntimeError):
"""Bir iterasyon izin verilen adım sayısında yakınsamazsa."""
def __init__(self, message, iterates):
super().__init__(message)
self.iterates = iterates
def newton(f, df, x0, tol=1e-12, maxiter=20):
xs = [x0]
for _ in range(maxiter):
x = xs[-1]
x_new = x - f(x) / df(x)
xs.append(x_new)
if abs(x_new - x) < tol:
return x_new
raise ConvergenceError(f"{maxiter} adımda yakınsamadı", xs)
def f(x):
return x**3 - 2 * x + 2
def df(x):
return 3 * x**2 - 2
print(newton(f, df, -2.0))
try:
newton(f, df, 0.0)
except ConvergenceError as e:
print("Hata:", e)
print("son iterasyonlar:", e.iterates[-4:])
print(issubclass(ConvergenceError, RuntimeError))Çıktı:
-1.7692923542386314
Hata: 20 adımda yakınsamadı
son iterasyonlar: [1.0, 0.0, 1.0, 0.0]
True
\(x_0 = -2\)’den başlayan iterasyon, tek gerçel kök olan \(-1{,}7692923542\ldots\) sayısına yakınsadı. \(x_0 = 0\) için
\[x_1 = 0 - \frac{f(0)}{f'(0)} = 0 - \frac{2}{-2} = 1, \qquad x_2 = 1 - \frac{f(1)}{f'(1)} = 1 - \frac{1}{1} = 0\]
olduğundan dizi \(0, 1, 0, 1, \ldots\) döngüsüne girer. \(20\) adımın sonunda fonksiyon istisnayı fırlattı ve çağıran kod, istisna nesnesinin iterates niteliğinden döngüyü okuyabildi. Son satır ConvergenceError’ın bir RuntimeError olduğunu doğruluyor: except RuntimeError yazan bir kod da bu istisnayı yakalardı. \(\blacksquare\)
4.5 Veri sınıfları: dataclass
Vec2’yi yazarken __init__, __repr__ ve __eq__ için hep aynı kalıbı tekrarladık; yalnız veri taşıyan sınıflar için standart kütüphane bu kalıbı bizim yerimize yazar.
Tanım 4.10 (Veri Sınıfı) dataclasses modülündeki @dataclass dekoratörü bir sınıfın önüne yazıldığında, sınıfta ad: tür biçiminde bildirilen alanlardan __init__, __repr__ ve __eq__ metotlarını kendiliğinden üretir. Böyle bir sınıfa veri sınıfı (data class) denir. @dataclass(frozen=True) yazımı alanları salt okunur yapar ve nesneleri hashlenebilir kılar; @dataclass(order=True) ise nesneleri alanlarına göre sözlük sırasıyla karşılaştıran <, <= gibi metotları da ekler.
Yani veri sınıfı, alanlarını saymamızın yettiği bir sınıftır: tekrarlanan metotları Python yazar, biz yalnız matematiğe özgü metotları ekleriz. Dekoratör, @ ile başlayıp bir tanımın üstüne yazılan ve o tanımı alıp değiştiren bir fonksiyondur. Tür bildirimleri (x: float) okur için bir belgedir; Python çalışırken bunları denetlemez.
import math
from dataclasses import dataclass
@dataclass(frozen=True)
class Point:
x: float
y: float
def dist(self, other):
return math.hypot(self.x - other.x, self.y - other.y)
A, B, C = Point(0, 0), Point(4, 0), Point(0, 3)
print(A, B == Point(4.0, 0.0))
print(A.dist(B), B.dist(C), C.dist(A))
print(len({A, B, C, Point(0, 0)}))
try:
A.x = 1
except AttributeError as e:
print(type(e).__name__, "-", e)Çıktı:
Point(x=0, y=0) True
4.0 5.0 3.0
3
FrozenInstanceError - cannot assign to field 'x'
Point(0, 0) yazdırılınca alan adlarıyla birlikte göründü. Üretilen __eq__ alanları sırayla karşılaştırdığı için Point(4, 0) == Point(4.0, 0.0) doğru çıktı ve dik üçgenin kenarları \(4\), \(5\), \(3\) bulundu. frozen=True sayesinde noktalar kümeye konabildi; iki eşit \((0, 0)\) noktası kümede tek eleman sayıldığı için küme \(3\) elemanlı. Buna karşılık bir alanı değiştirme denemesi FrozenInstanceError fırlattı; bu tür AttributeError’ın alt türü olduğu için onu üst türünün adıyla yakaladık.
Bir veri sınıfına alanlarını denetleyen bir metot da eklenebilir: __post_init__ metodu, üretilen __init__ alanları atadıktan hemen sonra çağrılır. Bir koşul bozuksa burada raise ile istisna fırlatmak, geçersiz bir nesnenin hiç oluşmamasını sağlar. Bölümün son örneğinde bunu kullanacağız.
Bir alana varsayılan değer olarak boş bir liste vermek, bütün nesnelerin aynı listeyi paylaşmasına, yani her nesnede o listenin bir takma adının bulunmasına yol açardı (bkz. Veri Yapıları ve Comprehension). dataclass bu hatayı daha sınıf tanımlanırken yakalar. Doğrusu, her nesne için yeni bir liste üreten field(default_factory=list) yazımıdır:
from dataclasses import dataclass, field
try:
@dataclass
class Run:
method: str
history: list = []
except ValueError as e:
print("ValueError:", e)
@dataclass
class Run:
method: str
history: list = field(default_factory=list)
r1, r2 = Run("newton"), Run("bisect")
r1.history.append(1.5)
print(r1, r2)Çıktı:
ValueError: mutable default <class 'list'> for field history is not allowed: use default_factory
Run(method='newton', history=[1.5]) Run(method='bisect', history=[])
İki nesnenin listeleri ayrıdır: birine eklenen değer öbürünü etkilemedi.
4.6 Hepsi bir arada: bir ölçüm dosyasını işlemek
Bölümün araçlarını tek bir gerçekçi görevde birleştirelim: hatalı satırlar da içeren bir ölçüm dosyasından bir fiziksel sabiti tahmin edeceğiz.
- Bir kaydı temsil eden bir veri sınıfı tanımlayın; fiziksel olarak imkânsız değerleri
__post_init__içindeValueErrorile reddedin. - Dosyayı
withveencoding="utf-8"ile açın,csv.readerile satırlara ayırın ve başlık satırınınextile atlayın. - Her satırı bir
trybloğu içinde kayda dönüştürün;except ValueErrorbloğunda satır numarasını ve hatayı bir listeye yazıp sonraki satıra geçin. - Hesabı geçerli kayıtlarla yapın, atlanan satırları raporlayın ve sonucu bir dosyaya yazın.
Örnek 4.9 (Soğuma Ölçümlerinden Bir Sabit Tahmin Etmek) \(90\) °C’deki bir sıvı \(20\) °C’lik bir odada soğuyor. Newton’un soğuma yasasına göre sıcaklık \(T(t) = 20 + 70e^{-kt}\) biçiminde azalır; burada \(t\) dakika cinsinden zaman, \(k > 0\) bir sabittir. İki dakikada bir alınan ölçümler Türkçe biçimde sogutma.csv dosyasına kaydedilmiş, ama bazı satırlar hatalı girilmiş. Dosyayı okuyup hatalı satırları raporlayan, her geçerli ölçümden
\[k = \frac{1}{t} \ln \frac{70}{T - 20}\]
değerini hesaplayıp bunların ortalamasını bir dosyaya yazan bir program yazınız.
Çözüm
Dört adımı sırayla uyguluyoruz. Program önce dosyayı kendisi oluşturuyor; gerçek bir işte bu dosya bir ölçüm cihazından gelirdi.
import csv
import math
from dataclasses import dataclass
DATA = """t;T
0;90,0
2;77,7
4;67,2
6;58,2
8;
10;45,8
12;4l,3
14;37,4
16;34,6
18;3,18
20;29,6
"""
with open("sogutma.csv", "w", encoding="utf-8") as f:
f.write(DATA)
@dataclass
class Measurement:
t: float # dakika
T: float # santigrat derece
def __post_init__(self):
if self.T <= 20:
raise ValueError(f"T = {self.T} oda sıcaklığının altında")
def to_float(s):
return float(s.replace(",", "."))
def read_measurements(path):
good, bad = [], []
with open(path, newline="", encoding="utf-8") as f:
reader = csv.reader(f, delimiter=";")
next(reader) # başlık satırı
for line_no, row in enumerate(reader, start=2):
try:
t, T = row
good.append(Measurement(to_float(t), to_float(T)))
except ValueError as e:
bad.append((line_no, e))
return good, bad
good, bad = read_measurements("sogutma.csv")
for line_no, e in bad:
print(f"satır {line_no} atlandı: {e}")
ks = [math.log(70 / (m.T - 20)) / m.t for m in good if m.t > 0]
k = sum(ks) / len(ks)
print(f"{len(good)} geçerli ölçüm, k ≈ {k:.4f}")
with open("sogutma_ozet.txt", "w", encoding="utf-8") as f:
f.write(f"gecerli={len(good)}\nk={k:.6f}\n")Çıktı:
satır 6 atlandı: could not convert string to float: ''
satır 8 atlandı: could not convert string to float: '4l.3'
satır 11 atlandı: T = 3.18 oda sıcaklığının altında
8 geçerli ölçüm, k ≈ 0.0989
Hatalı satırlar. Üç satır üç farklı nedenle atlandı:
- 6. satırdaki \(T\) alanı boştu ve
float("")birValueErrorfırlattı. - 8. satırda bir rakamın yerine küçük L harfi girilmişti (
4l,3). Mesajdaki'4l.3', virgülün noktaya çevrilmiş hâlidir. - 11. satırdaki
3,18sayıya çevrilebildi ama oda sıcaklığının altında kaldığı için__post_init__onu reddetti; büyük olasılıkla31,8yazılmak istenmişti.
Üç hata da aynı except ValueError satırıyla yakalandı, çünkü ikisini float, birini bizim raise deyimimiz fırlattı. Satırda iki alan olmasaydı t, T = row ataması da bir ValueError fırlatırdı ve o durum da aynı yere düşerdi.
Tahmin. Formül, \(T - 20 = 70e^{-kt}\) eşitliğinin iki tarafının logaritması alınarak bulunur. \(t = 0\) satırı formüle giremeyeceği için hesaba katılmadı; kalan yedi ölçümün ortalaması \(k \approx 0{,}0989\) verdi. Yani sıvı ile oda arasındaki sıcaklık farkı her dakika yaklaşık %9,4 azalıyor, çünkü \(e^{-0{,}0989} \approx 0{,}906\)’dır. Sonuç sogutma_ozet.txt dosyasına yazıldı. \(\blacksquare\)
Programın okuduğu ölçümler, atlanan satırlar ve tahmin edilen model aşağıdaki şekilde bir arada görülüyor:
sogutma.csv dosyasından okunan sekiz geçerli ölçüm (noktalar; t dakika, T santigrat derece) ve ortalama k ≈ 0,0989 ile çizilen T = 20 + 70e−kt modeli. Alttaki çarpılar atlanan üç satırın zamanlarıdır: boş alan, harfli değer ve oda sıcaklığının altında kalan değer.4.7 Alıştırmalar
Aşağıdaki alıştırmalar bölümün araçlarını tek tek ve birlikte kullanıyor; her çözüm, çalıştırılmış kodu ve çıktısını içeriyor.
Alıştırma 4.1 (Hiperbolik Fonksiyonlar ve Kayıp Basamaklar) math modülündeki hiperbolik fonksiyonları dir ile listeleyiniz. Sonra \(\cosh^2 x - \sinh^2 x\) ifadesini \(x = 0, 5, 10, 15, 20\) için hesaplayıp sonuçların \(1\)’den neden saptığını açıklayınız.
Çözüm
Hiperbolik fonksiyonların adları h harfiyle bittiği için dir(math) listesini bu harfe göre süzmek yeterli:
import math
print([name for name in dir(math) if name.endswith("h")])
for x in range(0, 21, 5):
d = math.cosh(x) ** 2 - math.sinh(x) ** 2
print(x, d)Çıktı:
['acosh', 'asinh', 'atanh', 'cosh', 'sinh', 'tanh']
0 1.0
5 1.000000000001819
10 0.9999999850988388
15 1.0
20 0.0
Özdeşliğe göre her satırda \(1\) çıkmalıydı. Büyük \(x\) için \(\cosh^2 x\) ve \(\sinh^2 x\) yaklaşık \(e^{2x}/4\) büyüklüğündedir; \(x = 20\) için bu yaklaşık \(5{,}9 \cdot 10^{16}\)’dır. Bu büyüklükteki iki ardışık kayan noktalı sayının arası \(8\)’dir, yani iki kare yuvarlanırken \(1\)’lik fark tamamen kaybolur ve sonuç \(0\) çıkar. \(x = 10\) için iki ardışık sayının arası yaklaşık \(1{,}5 \cdot 10^{-8}\)’dir ve sapma tam bu büyüklüktedir; \(x = 15\)’te sonucun \(1\) çıkması yuvarlamaların şanslı birleşimidir. Birbirine çok yakın iki büyük sayıyı çıkarmak öncü basamakları yok eder (bkz. Nümerik Analiz). \(\blacksquare\)
Alıştırma 4.2 (Esnek Bir Sayı Okuyucu) '12', '3,5', '7/2' ve '1e3' gibi metinleri float’a çeviren bir to_number fonksiyonu yazınız; kesir biçimindeki yazımlar için fractions.Fraction kullanınız. Fonksiyonu ["12", "3,5", "7/2", "abc", "", "1e3", "1/0"] listesine uygulayıp çevrilebilen değerleri bir listede, çevrilemeyen metinleri de hatanın türüyle birlikte başka bir listede toplayınız.
Çözüm
Önce float deneniyor; o başarısız olursa Fraction deneniyor. Fraction da metni anlamazsa ValueError, payda sıfırsa ZeroDivisionError fırlatır. Çağıran döngü bu iki türü bir demetle birlikte yakalıyor:
from fractions import Fraction
def to_number(s):
"""'12', '3,5', '7/2', '1e3' gibi metinleri float'a çevirir."""
s = s.strip().replace(",", ".")
try:
return float(s)
except ValueError:
return float(Fraction(s))
inputs = ["12", "3,5", "7/2", "abc", "", "1e3", "1/0"]
values, failed = [], []
for s in inputs:
try:
values.append(to_number(s))
except (ValueError, ZeroDivisionError) as e:
failed.append((s, type(e).__name__))
print(values)
print(failed)Çıktı:
[12.0, 3.5, 3.5, 1000.0]
[('abc', 'ValueError'), ('', 'ValueError'), ('1/0', 'ZeroDivisionError')]
"7/2" metni float ile okunamadı ama Fraction("7/2") onu \(7/2\) olarak tanıdı. "abc" ve boş metin iki denemede de ValueError verdi; "1/0" ise metin olarak geçerli bir kesir olduğu hâlde paydası sıfır olduğundan ZeroDivisionError fırlattı. Hiçbir hata programı durdurmadı ve her biri türüyle kaydedildi. \(\blacksquare\)
Alıştırma 4.3 (Girdisini Denetleyen Binom Katsayısı) Pascal üçgeninin satırlarını kurarak \(\binom{n}{k}\) değerini hesaplayan bir binomial(n, k) fonksiyonu yazınız. Fonksiyon tam sayı olmayan girdide TypeError, negatif girdide ValueError fırlatsın ve \(k > n\) için \(0\) döndürsün. Sonuçları \(0 \le n < 30\), \(0 \le k < 32\) için math.comb ile karşılaştırınız.
Çözüm
Pascal üçgeninin bir sonraki satırı, satırın başına ve sonuna \(0\) eklenmiş iki kopyasının terim terim toplamıdır: \([1, 2, 1]\) satırından \([0, 1, 2, 1]\) ile \([1, 2, 1, 0]\) toplanarak \([1, 3, 3, 1]\) elde edilir. Denetimler hesaptan önce yapılıyor:
import math
def binomial(n, k):
"""C(n, k) değerini Pascal üçgeninin n. satırından hesaplar."""
if not isinstance(n, int) or not isinstance(k, int):
raise TypeError("n ve k tam sayı olmalı")
if n < 0 or k < 0:
raise ValueError("n ve k negatif olamaz")
if k > n:
return 0
row = [1]
for _ in range(n):
row = [a + b for a, b in zip([0] + row, row + [0])]
return row[k]
print(all(binomial(n, k) == math.comb(n, k)
for n in range(30) for k in range(32)))
for n, k in [(5.0, 2), (-1, 2), (10, 3)]:
try:
print((n, k), "->", binomial(n, k))
except (TypeError, ValueError) as e:
print((n, k), "->", type(e).__name__, "-", e)Çıktı:
True
(5.0, 2) -> TypeError - n ve k tam sayı olmalı
(-1, 2) -> ValueError - n ve k negatif olamaz
(10, 3) -> 120
\(30 \cdot 32 = 960\) karşılaştırmanın hepsi tuttu. math.comb da aynı kuralları izler: math.comb(5.0, 2) bir TypeError, math.comb(-1, 2) bir ValueError fırlatır ve \(k > n\) için \(0\) döndürür. \(\blacksquare\)
Alıştırma 4.4 (Tam Sayı Karekökü) Newton iterasyonunun tam sayı sürümü \(x \mapsto \lfloor (x + \lfloor n/x \rfloor)/2 \rfloor\) ile, yalnız tam sayı aritmetiği kullanarak \(\lfloor \sqrt n \rfloor\) değerini hesaplayan bir isqrt(n) fonksiyonu yazınız. Sonucun \(r^2 \le n < (r + 1)^2\) koşulunu sağladığını assert ile denetleyiniz ve sonucu \(n < 10^5\) için math.isqrt ile, \(n = (2^{53} + 1)^2\) için ayrıca int(math.sqrt(n)) ile karşılaştırınız.
Çözüm
\(x_0 = n\)’den başlayıp \(y = \lfloor (x + \lfloor n/x \rfloor)/2 \rfloor\) değerini hesaplıyoruz ve \(y\) küçülmeyi bıraktığında duruyoruz. \(s = \lfloor \sqrt n \rfloor\) diyelim. Bu kuralın neden \(s\)’de durduğu iki gözlemden çıkar:
- \(x\) tam sayı olduğundan \(y = \lfloor (x + n/x)/2 \rfloor\)’dir ve aritmetik-geometrik ortalama eşitsizliğinden \((x + n/x)/2 \ge \sqrt n\) olur. Dolayısıyla her zaman \(y \ge s\)’dir.
- \(x > s\) ise \(x > \sqrt n\), yani \(n/x < x\) olur; bu durumda \(y \le (x + n/x)/2 < x\)’tir ve dizi kesin azalır.
Dizi \(s\)’nin altına inemediğinden ve \(s\)’nin üstündeyken kesin azaldığından sonlu adımda \(x = s\) değerine ulaşır; o noktada \(y \ge s = x\) olduğu için döngü durur. assert satırı bu akıl yürütmenin sonucunu her çağrıda yeniden sınıyor:
import math
def isqrt(n):
"""n >= 0 için karekökün tam kısmı; yalnız tam sayı aritmetiği."""
if n < 0:
raise ValueError("negatif sayının karekökü alınamaz")
if n < 2:
return n
x = n
y = (x + n // x) // 2
while y < x:
x = y
y = (x + n // x) // 2
assert x * x <= n < (x + 1) ** 2, n
return x
print(all(isqrt(n) == math.isqrt(n) for n in range(10**5)))
N = (2**53 + 1) ** 2
print(isqrt(N), math.isqrt(N))
print(int(math.sqrt(N)))Çıktı:
True
9007199254740993 9007199254740993
9007199254740992
\(10^5\) değerin hepsinde sonuç math.isqrt ile aynı. \(n = (2^{53} + 1)^2 \approx 8{,}1 \cdot 10^{31}\) için iki tam sayı fonksiyonu doğru kök olan \(2^{53} + 1\)’i verirken int(math.sqrt(n)) bir eksiğini verdi. Asıl neden, doğru kökün kendisinin bir kayan noktalı sayı olmamasıdır: \(2^{53} + 1\) sayısı \(54\) anlamlı bit ister ve \(53\) bitlik mantise sığmaz; float(2**53 + 1) zaten \(2^{53}\)’tür. \(2^{53}\) civarında ardışık iki kayan noktalı sayının arası \(2\) olduğundan math.sqrt, \(n\) kendisine tam olarak verilse bile \(2^{53} + 1\) döndüremez ve sonuç \(2^{53}\)’e yuvarlanır. \(n\)’nin kayan noktaya çevrilirken \(2^{106} + 2^{54}\)’e yuvarlanması ise ikincil bir etkidir: o sayının karekökü yaklaşık \(2^{53} + 1 - 2^{-54}\)’tür ve yine \(2^{53}\)’e yuvarlanır. Büyük tam sayılarla çalışırken kayan noktaya hiç geçmemek bu yüzden önemlidir. \(\blacksquare\)
Alıştırma 4.5 (Blokların İzini Sürmek) Aşağıdaki kodun çıktısını çalıştırmadan tahmin ediniz, sonra çalıştırıp tahmininizi denetleyiniz.
def g(x):
try:
r = 10 // x
except ZeroDivisionError:
print("A")
r = 0
except TypeError:
print("B")
raise
else:
print("C")
finally:
print("D")
return r
print(g(3))
print(g(0))
try:
g("5")
except TypeError:
print("E")Çözüm
Üç çağrıyı tek tek izleyelim.
g(3):10 // 3 = 3hatasız hesaplanır;elsebloğuC,finallybloğuDbasar ve fonksiyon3döndürür.g(0):10 // 0birZeroDivisionErrorfırlatır; ilkexceptbloğuAbasıpr = 0atar,finallybloğuDbasar ve fonksiyon0döndürür.g("5"):10 // "5"birTypeErrorfırlatır; ikinciexceptbloğuBbasar ve tek başına yazılmışraiseaynı istisnayı yeniden fırlatır. Fonksiyondan çıkılmadan öncefinallyyineDbasar.return rsatırına hiç gelinmez; istisna dışarıdakitrytarafından yakalanır veEbasılır.
Kodu çalıştırmak bu tahmini doğruluyor.
Çıktı:
C
D
3
A
D
0
B
D
E
\(\blacksquare\)
Alıştırma 4.6 (Pascal Üçgeni Dosyada) Pascal üçgeninin \(0\)’dan \(10\)’a kadar olan satırlarını, sayılar boşlukla ayrılmış olarak pascal.txt dosyasına yazınız. Dosyayı geri okuyup her satırdaki sayıların toplamının \(2^n\) olduğunu doğrulayınız.
Çözüm
Her satırı " ".join ile tek bir metne çevirip sonuna "\n" ekliyoruz. Okurken argümansız split() satırı boşluklardan böler ve satır sonunu da atar:
from math import comb
with open("pascal.txt", "w", encoding="utf-8") as f:
for n in range(11):
row = [comb(n, k) for k in range(n + 1)]
f.write(" ".join(str(c) for c in row) + "\n")
with open("pascal.txt", encoding="utf-8") as f:
rows = [[int(s) for s in line.split()] for line in f]
print(rows[4], rows[10])
print(all(sum(r) == 2**n for n, r in enumerate(rows)))Çıktı:
[1, 4, 6, 4, 1] [1, 10, 45, 120, 210, 252, 210, 120, 45, 10, 1]
True
Dördüncü ve onuncu satırlar beklenen katsayıları taşıyor ve on bir satırın hepsinde toplam \(2^n\) çıktı. Bu, binom teoreminde \(a = b = 1\) almanın sonucudur: \(\sum_{k=0}^{n} \binom{n}{k} = (1 + 1)^n = 2^n\). \(\blacksquare\)
Alıştırma 4.7 (Faktöriyellerin Basamak Sayıları) \(n = 1, 2, \dots, 100\) için \(n!\) sayısının basamak sayısını n ve basamak sütunlu bir CSV dosyasına yazınız. Dosyayı csv.DictReader ile okuyup basamak sayısı ilk kez \(100\)’e ulaşan ya da onu aşan \(n\) değerini bulunuz.
Çözüm
Python’un tam sayıları sınırsız olduğundan \(n!\) tam olarak hesaplanır ve basamak sayısı len(str(...)) ile okunur. Dosyayı okurken satırları bir sözlük comprehension ile n → basamak sözlüğünde topluyoruz:
import csv
import math
with open("faktoriyel.csv", "w", newline="", encoding="utf-8") as f:
writer = csv.writer(f)
writer.writerow(["n", "basamak"])
for n in range(1, 101):
writer.writerow([n, len(str(math.factorial(n)))])
with open("faktoriyel.csv", newline="", encoding="utf-8") as f:
digits = {int(r["n"]): int(r["basamak"])
for r in csv.DictReader(f)}
first = min(n for n, d in digits.items() if d >= 100)
print(first, digits[first - 1], digits[first])Çıktı:
70 99 101
Aranan değer \(n = 70\)’tir. İlginç olan, \(100\) basamaklı hiçbir faktöriyelin olmamasıdır: \(69!\) sayısı \(99\), \(70!\) sayısı \(101\) basamaklıdır. Basamak sayısı \(\lfloor \log_{10} n! \rfloor + 1\) olduğundan \(70\) ile çarpmak logaritmayı \(\log_{10} 70 \approx 1{,}85\) kadar artırır; \(\log_{10} 69! \approx 98{,}23\) iken \(\log_{10} 70! \approx 100{,}08\) olur ve basamak sayısı bir adımda iki artar. \(\blacksquare\)
Alıştırma 4.8 (Bir Polinom Sınıfı) Katsayıları artan derece sırasıyla bir listede tutan bir Poly sınıfı yazınız. Sınıf, p(x) yazımıyla Horner yöntemine göre değer hesaplasın (__call__), + ve * işlemlerini ve okunaklı bir repr metnini desteklesin. \((1 + x)^3\) polinomunu hesaplayıp \(x = 2\)’deki değerini \(3^3\) ile karşılaştırınız.
Çözüm
Çarpımın \(x^m\) katsayısı, \(i + j = m\) olan bütün \(a_i b_j\) çarpımlarının toplamıdır; iç içe döngü her \(a_i b_j\) çarpımını prod[i + j] hücresine ekliyor. Horner yöntemi \(a_0 + a_1x + a_2x^2 + a_3x^3\) ifadesini \(a_0 + x(a_1 + x(a_2 + x a_3))\) biçiminde, en yüksek dereceden başlayarak hesaplar. reversed(self.c) katsayı listesini sondan başa doğru dolaştığı için döngü en yüksek dereceli katsayıdan başlar:
class Poly:
"""Katsayıları artan derece sırasıyla tutulan polinom."""
def __init__(self, coeffs):
self.c = list(coeffs)
def __repr__(self):
terms = []
for k, a in enumerate(self.c):
if a == 0:
continue
coef = "" if a == 1 and k > 0 else str(a)
power = "" if k == 0 else "x" if k == 1 else f"x^{k}"
terms.append(coef + power)
return " + ".join(terms) or "0"
def __call__(self, x):
result = 0
for a in reversed(self.c): # Horner yöntemi
result = result * x + a
return result
def __add__(self, other):
n = max(len(self.c), len(other.c))
a = self.c + [0] * (n - len(self.c))
b = other.c + [0] * (n - len(other.c))
return Poly([s + t for s, t in zip(a, b)])
def __mul__(self, other):
prod = [0] * (len(self.c) + len(other.c) - 1)
for i, a in enumerate(self.c):
for j, b in enumerate(other.c):
prod[i + j] += a * b
return Poly(prod)
p = Poly([1, 1]) # 1 + x
q = p * p * p
print(q)
print(q(2), 3**3)
print(q + Poly([0, -3, -3]))Çıktı:
1 + 3x + 3x^2 + x^3
27 27
1 + x^3
\((1 + x)^3 = 1 + 3x + 3x^2 + x^3\) bulundu ve \(x = 2\)’deki değeri \(3^3 = 27\) ile aynı. Son satırda \(-3x - 3x^2\) eklenince sıfır olan katsayılar repr tarafından atlandı. NumPy’nin numpy.polynomial.Polynomial sınıfı aynı fikrin hazır ve çok daha kapsamlı hâlidir (bkz. Polinomlar, İnterpolasyon ve Eğri Uydurma). \(\blacksquare\)
Alıştırma 4.9 (Aralık Aritmetiği) Kapalı bir \([a, b]\) aralığını temsil eden, frozen=True ile tanımlanmış bir Interval veri sınıfı yazınız. \(a > b\) ise __post_init__ bir ValueError fırlatsın; işlemler
\[ \begin{aligned} [a, b] + [c, d] &= [a + c,\ b + d], \\[1mm] [a, b] - [c, d] &= [a - d,\ b - c], \\[1mm] [a, b] \cdot [c, d] &= [\min P,\ \max P], \quad P = \{ac, ad, bc, bd\} \end{aligned} \]
kurallarıyla tanımlansın ve x in I yazımı desteklensin. \(X = [0, 1]\) için \(X(1 - X)\) ve \(X - X^2\) ifadelerini hesaplayıp \(x(1 - x)\) fonksiyonunun \([0, 1]\)’deki gerçek değer kümesiyle karşılaştırınız.
Çözüm
__contains__ metodu x in I yazımını, __post_init__ de geçersiz aralık denetimini sağlıyor. Sabit \(1\)’i \([1, 1]\) aralığı olarak yazıyoruz:
from dataclasses import dataclass
@dataclass(frozen=True)
class Interval:
lo: float
hi: float
def __post_init__(self):
if self.lo > self.hi:
raise ValueError(f"boş aralık: [{self.lo}, {self.hi}]")
def __add__(self, other):
return Interval(self.lo + other.lo, self.hi + other.hi)
def __sub__(self, other):
return Interval(self.lo - other.hi, self.hi - other.lo)
def __mul__(self, other):
p = [self.lo * other.lo, self.lo * other.hi,
self.hi * other.lo, self.hi * other.hi]
return Interval(min(p), max(p))
def __contains__(self, x):
return self.lo <= x <= self.hi
X = Interval(0, 1)
one = Interval(1, 1)
print(X * (one - X))
print(X - X * X)
print(0.25 in X * (one - X), 2 in X - X * X)
try:
Interval(2, 1)
except ValueError as e:
print("Hata:", e)Çıktı:
Interval(lo=0, hi=1)
Interval(lo=-1, hi=1)
True False
Hata: boş aralık: [2, 1]
\(x(1 - x)\) fonksiyonunun \([0, 1]\)’deki gerçek değer kümesi \([0, \frac14]\)’tür; en büyük değer \(x = \frac12\)’de alınır. Aralık aritmetiği iki yazım için de bu kümeyi kapsayan bir aralık verdi (\(\frac14 \in X(1 - X)\)), ama kümeyi büyüttü: \(X(1 - X) = [0, 1]\) ve \(X - X^2 = [-1, 1]\). Üstelik aynı fonksiyonun iki yazımı farklı sonuç verdi. Bunun nedeni, aralık aritmetiğinin \(X\)’in her geçişini birbirinden bağımsız bir sayı gibi ele almasıdır: \(X - X^2\) hesabında ilk \(X\) için \(0\), \(X^2\) için \(1\) seçilebilir ve \(-1\) alt sınırı böyle oluşur; oysa ikisi aynı \(x\)’tir. Bu olguya aralık aritmetiğinde bağımlılık sorunu denir. \(\blacksquare\)
Alıştırma 4.10 (Iraksamayı Bildiren Sabit Nokta İterasyonu) \(x_{n+1} = g(x_n)\) sabit nokta iterasyonunu (bkz. Nümerik Analiz) uygulayan bir fixed_point(g, x0) fonksiyonu yazınız. İterasyon \(|x| > 10^8\) değerine ulaşırsa ArithmeticError’dan türeyen bir DivergenceError, izin verilen adım sayısında yakınsamazsa bir RuntimeError fırlatılsın. Fonksiyonu \(g(x) = \cos x\) ve \(x_0 = 1\), \(g(x) = x^2 - 2\) ve \(x_0 = 2{,}1\), \(g(x) = -x\) ve \(x_0 = 1\) için deneyiniz.
Çözüm
DivergenceError sınıfının gövdesinde yalnız belgesi var; davranışını tamamen ArithmeticError’dan alıyor. Fonksiyon her adımda önce büyüklüğü, sonra yakınsamayı denetliyor:
import math
class DivergenceError(ArithmeticError):
"""İterasyon sınırsız büyüdüğünde fırlatılır."""
def fixed_point(g, x0, tol=1e-12, maxiter=200, bound=1e8):
x = x0
for n in range(1, maxiter + 1):
x_new = g(x)
if abs(x_new) > bound:
raise DivergenceError(f"{n}. adımda |x| > {bound:g}")
if abs(x_new - x) < tol:
return x_new, n
x = x_new
raise RuntimeError(f"{maxiter} adımda yakınsamadı")
print(fixed_point(math.cos, 1.0))
for g, x0 in [(lambda x: x * x - 2, 2.1), (lambda x: -x, 1.0)]:
try:
fixed_point(g, x0)
except (DivergenceError, RuntimeError) as e:
print(type(e).__name__, "-", e)Çıktı:
(0.7390851332147725, 69)
DivergenceError - 6. adımda |x| > 1e+08
RuntimeError - 200 adımda yakınsamadı
Üç davranış, sabit noktadaki türevin büyüklüğüyle açıklanır (bkz. Nümerik Analiz):
- \(\cos x = x\) denkleminin kökü \(p \approx 0{,}7390851332\)’dir ve \(|g'(p)| = \sin p \approx 0{,}67 < 1\) olduğundan iterasyon yakınsar. Hata her adımda yalnız yaklaşık \(0{,}67\) katına indiği için \(69\) adım gerekti.
- \(x^2 - 2 = x\) denkleminin \(2{,}1\)’e yakın kökü \(p = 2\)’dir ve \(|g'(2)| = 4 > 1\) olduğundan iterasyon kökten uzaklaşır: \(2{,}41\), \(3{,}8081\), \(12{,}50\ldots\) diye büyüyen değerler \(6\). adımda \(10^8\)’i aştı.
- \(g(x) = -x\) için tek sabit nokta \(0\)’dır ve \(|g'(0)| = 1\)’dir. İterasyon \(1, -1, 1, \ldots\) diye ne yaklaşır ne uzaklaşır; \(200\) adımın sonunda
RuntimeErrorfırlatıldı.
İki hata türü bir demetle birlikte yakalandı ve type(e).__name__ hangisinin çıktığını gösterdi. \(\blacksquare\)
Bu bölümle Python’un temel araçlarını tamamladık: artık kodumuzu modüllere bölebiliyor, hataları yakalayıp anlamlı biçimde bildirebiliyor, verileri dosyalardan okuyup yazabiliyor ve matematik nesnelerini kendi sınıflarımızla temsil edebiliyoruz. Bir sonraki bölümde bilimsel hesabın temel taşı olan NumPy Dizileri ile tanışacağız. NumPy bir pakettir ve dizileri, bu bölümde gördüğümüz özel metotlarla +, * ve @ işlemlerini binlerce sayıya aynı anda uygulayan bir sınıfın nesneleridir.