1 Python ile İlk Adımlar: Sayılar, Değişkenler ve İfadeler
\(2^{100}\) sayısının bütün basamaklarını yazmak, \(100!\)’in kaç basamaklı olduğunu bulmak, \(\sqrt{2}\)’nin elli basamağını hesaplamak ya da \(\left(1 + \frac{1}{n}\right)^n\) dizisinin \(e\)’ye ne hızla yaklaştığını görmek elle uzun sürer; bilgisayarla ise birkaç satırlık iştir. Bu ders Python’ı tam olarak böyle bir araç olarak öğretir: her yeni yapıyı, zaten bildiğimiz bir matematik problemi üzerinde göreceğiz.
İlk bölümde Python’ı gelişmiş bir hesap makinesi gibi kullanacağız. Ama bu hesap makinesi farklı sayı türleriyle çok farklı davranır. Tam sayılarla istediğimiz büyüklükte ve hiç hata yapmadan hesap yapar; ondalıklı sayılarla ise hemen her işlemde küçük bir yuvarlama hatası yapar. \(0{,}1 + 0{,}2\) toplamının Python’da tam olarak \(0{,}3\) çıkmamasının nedeni budur.
Bölümün asıl amacı, hangi sayı türünün ne zaman kullanılacağını ve bir sonuca ne kadar güvenilebileceğini öğrenmektir. Bunun için tam sayıları, kayan noktalı sayıları, karmaşık sayıları, tam kesirleri ve ondalık sayıları sırayla ele alacağız.
1.1 Python’u Çalıştırmak
Python kodu birkaç farklı ortamda çalıştırılabilir; hepsinde dil aynıdır, yalnız kodu yazıp sonucu görme biçimi değişir.
Python, okunaklı söz dizimiyle bilinen genel amaçlı bir programlama dilidir. Python yorumlanan (interpreted) bir dildir: yazdığımız kodu yorumlayıcı (interpreter) adı verilen program doğrudan çalıştırır; kullanıcının ayrıca yürüttüğü bir derleme adımı yoktur. Bilimsel hesaptaki yaygınlığını büyük ölçüde NumPy, SciPy, SymPy, Matplotlib ve pandas kütüphanelerine borçludur; bu kütüphaneleri ilerleyen bölümlerde tek tek göreceğiz.
Python kodunu çalıştırmanın üç yaygın yolu vardır:
- Etkileşimli yorumlayıcı. Terminalde
pythonyazıldığında>>>istemi belirir. Bu isteme yazılan her ifade hemen hesaplanır ve değeri bir alt satırda gösterilir. Kısa denemeler için en hızlı yoldur. - Betik. Kod
ilk.pygibi.pyuzantılı bir dosyaya yazılır ve terminaldepython ilk.pykomutuyla baştan sona çalıştırılır. Betikte ifadelerin değeri kendiliğinden gösterilmez; görmek istediğimiziprintile yazdırırız. - Jupyter defteri. Kodun, açıklamaların ve çıktıların hücreler hâlinde bir arada durduğu bir belgedir. Google Colab, Jupyter defterlerini hiçbir kurulum yapmadan tarayıcıda çalıştırır.
Etkileşimli yorumlayıcıda kısa bir oturum şöyle görünür:
>>> 2 + 3 * 4
14
>>> 7 / 2
3.5
>>> 2 ** 10
1024
Python, python.org adresinden ya da bilimsel kütüphanelerle birlikte gelen Anaconda dağıtımıyla kurulabilir. Kurulumla uğraşmadan başlamak için Google Colab yeterlidir. Bu notlardaki kodlar Python 3.12, NumPy 2.5, SciPy 1.18, SymPy 1.14, Matplotlib 3.11 ve pandas 3.0 ile çalıştırılmıştır.
Bu notlarda kod betik olarak verilir; hemen altında, “Çıktı:” satırından sonra, kodun çalıştırıldığında ekrana yazdığı her şey aynen gösterilir. İlk programımız şudur:
# İlk programımız: bir selam ve basit bir hesap
print("Merhaba, matematik!")
print(2 + 3 * 4)Çıktı:
Merhaba, matematik!
14
# işaretinden satır sonuna kadar olan kısım bir yorumdur (comment); Python onu okumaz, yalnız okura açıklama yapar. print parantez içinde verilen değerleri ekrana yazar. Tırnak içindeki "Merhaba, matematik!" gibi metinlere dize (string) denir; dizeler Türkçe karakter de içerebilir.
1.2 İfadeler ve İşlem Önceliği
Python’a verdiğimiz her hesap bir ifadedir; önce ifadenin ne olduğunu ve Python’un onu hangi sırayla hesapladığını görelim.
Tanım 1.1 (İfade) Sayılardan, değişken adlarından, işlem işaretlerinden ve fonksiyon çağrılarından oluşan ve bir değeri olan kod parçasına ifade (expression) denir. Python bir ifadeyi değerlendirerek (evaluate) onu tek bir değere indirger.
Yani 2 + 3 * 4 bir ifadedir ve değeri 14’tür; print(2 + 3 * 4) ise bu değeri ekrana yazan bir komuttur. Etkileşimli yorumlayıcı her ifadenin değerini kendiliğinden gösterir, betik göstermez.
Python’un aritmetik işlemleri aşağıdaki tablodadır.
| İşlem | Python | Örnek | Değer |
|---|---|---|---|
| toplama | + |
7 + 2 |
9 |
| çıkarma | - |
7 - 2 |
5 |
| çarpma | * |
7 * 2 |
14 |
| bölme | / |
7 / 2 |
3.5 |
| tam bölme | // |
7 // 2 |
3 |
| kalan | % |
7 % 2 |
1 |
| üs alma | ** |
7 ** 2 |
49 |
İşlem önceliği matematikteki gibidir: önce üs alma, sonra çarpma ve bölmeler, en son toplama ve çıkarmalar yapılır; parantez içi her zaman önce hesaplanır. Aynı öncelikteki işlemler soldan sağa yapılır. Tek istisna üs almadır: 2 ** 3 ** 2 ifadesi \(2^{3^2} = 2^9\) olarak, yani sağdan sola hesaplanır. Üs alma, önündeki eksi işaretinden de önce gelir; -2 ** 2 ifadesi, matematikteki \(-2^2\) gibi, \(-(2^2) = -4\)’tür.
print(2 + 3 * 4) # önce çarpma yapılır
print((2 + 3) * 4) # parantez önceliği değiştirir
print(2 ** 3 ** 2) # üs sağdan sola: 2 ** 9
print(-2 ** 2) # -(2 ** 2)
print((-2) ** 2)
print(10 - 4 - 3) # soldan sağa: (10 - 4) - 3
print(7 / 2, 6 / 3) # / her zaman ondalıklı sonuç verirÇıktı:
14
20
512
-4
4
3
3.5 2.0
print’e virgülle ayrılmış birden çok değer verilirse onları aralarına birer boşluk koyarak aynı satıra yazar. Son satırda 6 / 3 işleminin 2 değil 2.0 verdiğine dikkat edin: / işlemi, bölüm tam sayı olsa bile ondalıklı bir sayı üretir. Tam sayı sonucu veren // işlemini Bölüm 1.7 kısmında göreceğiz.
Birçok hesap makinesinde ve LaTeX’te \(2^3\) için 2^3 yazılır, ama Python’da ^ bit düzeyinde “dışlayıcı ya da” (XOR) işlemidir ve hata vermeden bambaşka bir sayı üretir. Üs almak için daima ** kullanılır.
print(2 ^ 3) # XOR: 10 ve 11 bit dizilerinden 01 çıkar
print(2 ** 3) # üs almaÇıktı:
1
8
Örnek 1.1 (Kesirli Bir İfadeyi Python’a Çevirmek) \(\dfrac{2^{10} - 1}{3 \cdot 11}\) sayısını Python ile hesaplayınız.
Çözüm
Doğrudan yazım. İfadeyi kâğıttaki sırayla 2 ** 10 - 1 / 3 * 11 diye yazarsak öncelik kuralları yüzünden Python önce \(1/3\)’ü hesaplar, sonucu \(11\) ile çarpar ve bunu \(2^{10}\)’dan çıkarır:
# Yanlış: yalnız 1 / 3 bölünür, sonra 11 ile çarpılır
print(2 ** 10 - 1 / 3 * 11)Çıktı:
1020.3333333333334
Parantezli yazım. Kesir çizgisi payı ve paydayı birer bütün yapar; Python’da bunu parantezle söyleriz:
# Doğru: pay ve payda ayrı ayrı parantez içinde
print((2 ** 10 - 1) / (3 * 11))Çıktı:
31.0
Sağlama. \(2^{10} - 1 = 1023 = 3 \cdot 11 \cdot 31\) olduğundan aranan değer gerçekten \(31\)’dir. \(\blacksquare\)
1.3 Değişkenler ve Atama
Bir değeri tekrar tekrar kullanmak ya da adım adım güncellemek için ona bir ad veririz.
Tanım 1.2 (Değişken ve Atama) ad = ifade biçimindeki komuta atama (assignment) denir. Python sağdaki ifadeyi değerlendirir ve elde ettiği değeri soldaki ada bağlar. Bir değere bağlanmış ada değişken (variable) denir.
Yani = işareti matematikteki eşitlik değil, bir emirdir: “sağdakini hesapla, sonucu bu adla sakla”. Bu yüzden matematikte anlamsız olan n = n + 1 Python’da anlamlıdır: n’nin şimdiki değerine \(1\) ekler ve sonucu yine n adına bağlar. İki değerin eşit olup olmadığını sınamak için == kullanılır; sonuç True (doğru) ya da False (yanlış) olur.
r = 2.5 # yarıçap
pi = 3.141592653589793
area = pi * r ** 2 # dairenin alanı
perimeter = 2 * pi * r # dairenin çevresi
print(area, perimeter)
n = 10
n = n + 1 # n'nin eski değerine 1 ekle
n += 1 # aynı işin kısa yazılışı
print(n, n == 12)Çıktı:
19.634954084936208 15.707963267948966
12 True
n += 1 yazımı n = n + 1’in kısaltmasıdır; aynı biçimde -=, *=, /=, //=, %= ve **= de vardır.
Değişken adları harf, rakam ve alt çizgiden (_) oluşur ve rakamla başlayamaz. Büyük ve küçük harf ayrıdır: x ile X iki ayrı değişkendir. for, if, def, lambda gibi Python’un kendi anahtar kelimeleri ad olarak kullanılamaz. Türkçe harfler kurallara aykırı değildir, ama bu notlarda değişken adları İngilizce ya da matematikteki alışılmış adlardır (x, n, f, A, tol). print, sum, max gibi hazır fonksiyonların adlarını değişkenlere vermekten de kaçınmak gerekir; aksi hâlde o ad artık fonksiyonu değil, bizim değerimizi gösterir.
Python’da bir değişkenin türü yoktur, değerin türü vardır: aynı ad önce bir tam sayıya, sonra bir ondalıklı sayıya bağlanabilir. Bir değerin türünü type fonksiyonu söyler.
x = 7
print(type(x))
x = 7 / 2 # aynı ad şimdi başka türde bir değere bağlı
print(type(x))
print(type(3 + 4j), type("yedi"), type(7 > 2))Çıktı:
<class 'int'>
<class 'float'>
<class 'complex'> <class 'str'> <class 'bool'>
Bu bölümde int (tam sayı), float (kayan noktalı sayı) ve complex (karmaşık sayı) türlerini ayrıntılı göreceğiz. str dize türüdür; bool ise yalnız True ve False değerlerinden oluşan mantıksal türdür: 7 > 2 karşılaştırmasının değeri True’dur.
Birden çok değişkene aynı satırda atama yapılabilir: a, b = 1, 2 yazımı a’ya \(1\)’i, b’ye \(2\)’yi bağlar. Bu eşzamanlı atamada sağ taraftaki bütün ifadeler, hiçbir ad değişmeden önce hesaplanır. Bu yüzden a, b = b, a iki değişkenin değerlerini takas eder.
Döngüleri Kontrol Yapıları ve Fonksiyonlar bölümünde ayrıntılı göreceğiz. Bu bölümdeki birkaç örnek için şu kadarı yeterli: for k in range(a, b): satırının altındaki girintili (dört boşlukla içeri yazılmış) satırlar, k sırasıyla \(a, a + 1, \ldots, b - 1\) değerlerini alarak tekrar tekrar çalışır. range(n) ise \(0, 1, \ldots, n - 1\) değerlerini verir.
for k in range(1, 6):
print(k, k ** 2, k ** 3)Çıktı:
1 1 1
2 4 8
3 9 27
4 16 64
5 25 125
Örnek 1.2 (Fibonacci Sayıları ve Eşzamanlı Atama) \(F_0 = 0\), \(F_1 = 1\) ve \(n \ge 0\) için \(F_{n+2} = F_{n+1} + F_n\) ile tanımlanan Fibonacci dizisinin \(F_{10}\) terimini eşzamanlı atamayla hesaplayınız.
Çözüm
Fikir. a ve b ardışık iki terimi, yani \((F_k, F_{k+1})\) çiftini tutsun. a, b = b, a + b ataması bu çifti \((F_{k+1}, F_k + F_{k+1}) = (F_{k+1}, F_{k+2})\) çiftine taşır. \((F_0, F_1)\)’den başlayıp bu adımı \(10\) kez uygularsak a’da \(F_{10}\) kalır.
a, b = 0, 1 # F0 ve F1
for k in range(10): # adımı 10 kez uygula
a, b = b, a + b # (Fk, Fk+1) -> (Fk+1, Fk+2)
print(a, b) # F10 ve F11Çıktı:
55 89
Neden eşzamanlı? Atamayı iki ayrı satıra bölersek ikinci satır a’nın eski değerini değil, yeni değerini kullanır:
a, b = 0, 1
a = b # a artık 1
b = a + b # yeni a kullanıldı: 1 + 1
print(a, b) # doğrusu 1 1 olmalıydıÇıktı:
1 2
Doğru bir adım \((F_0, F_1) = (0, 1)\) çiftini \((F_1, F_2) = (1, 1)\) çiftine götürmeliydi. Eşzamanlı atamayla \(F_{10} = 55\) bulunur. \(\blacksquare\)
1.4 Çıktıyı Biçimlendirmek
Hesap sonuçlarını okunaklı, hizalı ve istediğimiz basamak sayısıyla yazdırmak için print’i biçimlendirilmiş dizelerle birlikte kullanırız.
print virgülle ayrılmış değerleri varsayılan olarak bir boşlukla ayırır ve sonunda satırı bitirir. Ayıracı sep, satır sonunu end ile değiştirebiliriz:
print(1, 2, 3)
print(1, 2, 3, sep=" < ")
print("a", end="; ") # satırı bitirme, sonuna "; " koy
print("b")Çıktı:
1 2 3
1 < 2 < 3
a; b
Tanım 1.3 (f-string ve Biçim Belirteci) Önüne f harfi konmuş bir dizeye f-string denir. Böyle bir dizede süslü parantez içine yazılan {ifade} değerlendirilir ve değeri dizeye yerleştirilir. {ifade:biçim} yazımında iki noktadan sonra gelen biçim belirteci (format specifier) değerin nasıl yazılacağını belirler.
Yani f-string bir şablondur: sabit metin aynen kalır, süslü parantezlerin yerine ifadelerin değerleri istenen biçimde yazılır. Bilimsel hesapta en çok kullanılan biçim belirteçleri şunlardır:
| Belirteç | Anlamı |
|---|---|
.4f |
noktadan sonra \(4\) basamak |
.3e |
bilimsel gösterim, noktadan sonra \(3\) basamak |
.5g |
\(5\) anlamlı basamak |
.1% |
yüzde olarak, noktadan sonra \(1\) basamak |
, |
binlik ayırıcı |
b, x |
ikili ve onaltılı taban |
>8, <8, ^8 |
\(8\) karakterlik alanda sağa, sola, ortaya yasla |
10.4f |
\(10\) karakterlik alanda, noktadan sonra \(4\) basamak |
x = 2 / 3
n = 2 ** 32
print(f"x = {x}")
print(f"{x:.4f} | {x:.3e} | {x:.5g} | {x:.1%}")
print(f"{n:,} | {n:.2e}")
print(f"{10:b} | {255:x}") # ikili ve onaltılı
print(f"[{7:>6}] [{7:<6}] [{7:^6}]") # sağa, sola, ortaya
print(f"{x = :.3f}") # adı ve değeri birlikteÇıktı:
x = 0.6666666666666666
0.6667 | 6.667e-01 | 0.66667 | 66.7%
4,294,967,296 | 4.29e+09
1010 | ff
[ 7] [7 ] [ 7 ]
x = 0.667
Son satırdaki {x = :.3f} yazımı, ifadenin kendisini ve değerini birlikte yazar; ara sonuçları kontrol ederken çok işe yarar.
Python sayıları her zaman ondalık noktayla yazar ve okur: 0.5 bir sayıdır, 0,5 ise virgülle ayrılmış iki ayrı sayıdır; print(0,5) komutu 0 5 yazar. Bu notlarda metin ve formüllerde Türkçedeki gibi ondalık virgül (\(0{,}5\)), kodda ve kod çıktısında ise Python’un yazdığı biçimde ondalık nokta kullanılır.
Örnek 1.3 (Hizalı Bir Karekök Tablosu) \(n = 1, 2, \ldots, 6\) için \(n\), \(\sqrt{n}\) ve \(1/n^{2}\) değerlerini sütunları hizalı bir tablo olarak yazdırınız; \(\sqrt{n}\) noktadan sonra \(6\) basamakla, \(1/n^2\) ise bilimsel gösterimle yazılsın.
Çözüm
Biçimleri seçelim. \(n\) için \(2\) karakterlik sağa yaslı alan (>2), \(\sqrt{n} = n^{1/2}\) için \(10\) karakterlik alanda \(6\) ondalık basamak (10.6f), \(1/n^2\) için \(12\) karakterlik alanda \(3\) basamaklı bilimsel gösterim (12.3e) kullanalım. Karekökü n ** 0.5 ile alıyoruz.
print(f"{'n':>2} {'kök n':>10} {'1/n^2':>12}")
for n in range(1, 7):
print(f"{n:>2} {n ** 0.5:10.6f} {1 / n ** 2:12.3e}")Çıktı:
n kök n 1/n^2
1 1.000000 1.000e+00
2 1.414214 2.500e-01
3 1.732051 1.111e-01
4 2.000000 6.250e-02
5 2.236068 4.000e-02
6 2.449490 2.778e-02
Hizalama. Başlık satırındaki dizeler de aynı genişliklerle sağa yaslandığından başlıklar sütunların tam üstüne düştü. Sayılar ne kadar basamaklı olursa olsun her sütun aynı genişlikte kaldı. \(\blacksquare\)
1.5 Modüller ve math Modülü
Karekök, logaritma ve trigonometrik fonksiyonlar dilin çekirdeğinde değil, Python’la birlikte gelen math modülündedir.
Tanım 1.4 (Modül ve İçe Aktarma) Birbiriyle ilgili fonksiyon ve sabitleri bir arada tutan Python dosyasına modül denir. import math komutu math modülünü yükler; modüldeki bir ada math.sqrt gibi modül.ad yazımıyla ulaşılır. from math import sqrt, pi komutu ise yalnız sqrt ve pi adlarını, önlerine math. yazmadan kullanılabilir hâle getirir.
Yani modül, hazır araçların durduğu bir kutudur; import bu kutuyu masaya koyar. Modül adını önde yazmak (math.sqrt) bir adın nereden geldiğini açıkça gösterdiği için genellikle tercih edilir.
import math
print(math.sqrt(2), math.pi, math.e)
print(math.exp(1), math.log(math.e), math.log10(1000))
print(math.log(8, 2), math.log2(8)) # 2 tabanında logaritma
print(math.sin(math.pi / 6), math.cos(0))
print(math.floor(-2.5), math.ceil(-2.5), math.trunc(-2.5))
print(math.factorial(10), math.comb(5, 2), math.gcd(84, 36))Çıktı:
1.4142135623730951 3.141592653589793 2.718281828459045
2.718281828459045 1.0 3.0
3.0 3.0
0.49999999999999994 1.0
-3 -2 -2
3628800 10 12
Bu çıktıda birkaç noktaya dikkat edelim:
math.log(x)doğal logaritmadır, yani \(\ln x\)’tir; başka bir taban ikinci argümanla verilir:math.log(8, 2).- Trigonometrik fonksiyonlar açıyı radyan cinsinden alır. Dereceyi radyana
math.radians, radyanı dereceyemath.degreesçevirir. - \(\sin(\pi/6) = 1/2\) olduğu hâlde Python
0.49999999999999994yazdı. Nedeni,math.pi’nin \(\pi\)’nin kendisi değil, ona çok yakın bir ondalıklı sayı olmasıdır. Bu küçük farkları Bölüm 1.8 kısmında inceleyeceğiz. flooraşağı,ceilyukarı yuvarlar;truncise ondalık kısmı atar, yani sıfıra doğru yuvarlar. Negatif sayılardaflooriletruncfarklı sonuç verir.factorial,comb(n, k)\(= \binom{n}{k}\),perm(n, k)\(= \frac{n!}{(n-k)!}\),gcdvelcmtam sayılarla çalışır ve sonucu tam verir.
Sık kullanılan diğer adlar şunlardır: tan, asin, acos, atan, atan2(y, x), sinh, cosh, hypot(x, y) \(= \sqrt{x^2 + y^2}\), tam sayı karekökü isqrt(n) \(= \lfloor \sqrt{n} \rfloor\), sonsuzu gösteren inf ve “sayı değil” anlamına gelen nan. Bir fonksiyonun ne yaptığını help(math.comb) komutu yazdırır. Mutlak değer için math.fabs(x) de vardır, ama sonucu her zaman float’tır; genellikle yerleşik abs(x) fonksiyonu kullanılır. abs(x) de \(|x|\)’i verir, tam sayıyı tam sayı olarak döndürür ve karmaşık sayılarla da çalışır.
from yazımıyla adlar doğrudan kullanılır:
from math import sqrt, pi
print(sqrt(16), 2 * pi)Çıktı:
4.0 6.283185307179586
from math import * yazımı modüldeki bütün adları getirir; ama hangi adın nereden geldiğini gizlediği ve aynı adlı değişkenleri sessizce ezebildiği için önerilmez.
math modülü gerçel sayılarla çalışır ve tanım kümesi dışındaki bir değer hata verir. Etkileşimli yorumlayıcıda bu şöyle görünür:
>>> import math
>>> math.sqrt(-1)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ValueError: math domain error
Negatif sayıların karekökü için karmaşık sayılarla çalışan cmath modülü vardır; onu Bölüm 1.10 kısmında göreceğiz.
Örnek 1.4 (Kosinüs Teoremiyle Üçüncü Kenar) Bir üçgenin iki kenarı \(a = 7\), \(b = 8\) ve bu kenarlar arasındaki açı \(\gamma = 60^\circ\)’dir. Üçüncü kenarı \(c^2 = a^2 + b^2 - 2ab\cos\gamma\) kosinüs teoremiyle Python’da hesaplayınız.
Çözüm
Açıyı radyana çevirelim. math.cos açıyı radyan cinsinden beklediğinden math.cos(60) yazmak \(60\) radyanın kosinüsünü hesaplar. Bu yüzden açıyı önce math.radians ile çeviriyoruz; karşılaştırma için yanlış yazımın sonucunu da yazdıralım:
import math
a, b = 7, 8
gamma = math.radians(60) # 60 derece = pi/3 radyan
c = math.sqrt(a**2 + b**2 - 2 * a * b * math.cos(gamma))
print(c)
print(math.sqrt(57), math.cos(gamma))
print(math.sqrt(a**2 + b**2 - 2 * a * b * math.cos(60))) # yanlışÇıktı:
7.549834435270749
7.54983443527075 0.5000000000000001
14.82127706395429
Elle sağlama. \(\cos 60^\circ = \frac{1}{2}\) olduğundan \(c^2 = 49 + 64 - 56 = 57\) ve \(c = \sqrt{57} \approx 7{,}5498\) olur. Python’un bulduğu c değeri math.sqrt(57) ile yalnız son basamakta farklıdır: math.cos(gamma) tam \(0{,}5\) değil, 0.5000000000000001 çıktığı için küçük bir yuvarlama farkı oluştu.
Yanlış satır. Son satırdaki \(14{,}82\), kosinüsü yaklaşık \(-0{,}95\) olan \(60\) radyanlık açıyla hesaplanmıştır ve \(c \approx 7{,}55\) ile hiç ilgisi yoktur. \(\blacksquare\)
1.6 Tam Sayılar
Python’un tam sayıları, matematikteki \(\mathbb{Z}\)’nin bilgisayardaki en sadık modelidir.
Tanım 1.5 (Tam Sayı Türü) Python’da tam sayılar int türündendir. Bir int değeri, büyüklüğüne üst sınır konmadan ve tam olarak saklanır. İki int arasında toplama, çıkarma, çarpma, tam bölme, kalan ve doğal sayı kuvvetine üs alma işlemlerinin sonucu yine tam olarak hesaplanmış bir int’tir.
Yani int ile yapılan hesapta yuvarlama yoktur; sayının büyüklüğünü yalnız bilgisayarın belleği sınırlar. Birçok programlama dili tam sayıları \(64\) bitle saklar ve \(2^{63} - 1\)’i aşan sonuçlarda taşma yaşar (bkz. Nümerik Analiz); Python’da böyle bir sınır yoktur.
import math
print(2 ** 63 - 1) # 64 bitlik işaretli en büyük tam sayı
print(2 ** 100)
print(math.factorial(30)) # 30!
print(2 ** 100 + 1 - 2 ** 100) # tam hesap: 1 kaybolmaz
print(2 ** -1) # negatif üs: sonuç artık int değilÇıktı:
9223372036854775807
1267650600228229401496703205376
265252859812191058636308480000000
1
0.5
Son satır, negatif kuvvetin sonucunun bir float olduğunu gösteriyor. Uzun sayıları okunaklı yazmak için rakamların arasına alt çizgi konabilir: 1_000_000 ile 1000000 aynı sayıdır.
Bir tam sayının basamaklarını saymak için onu str ile dizeye çevirir, dizenin uzunluğunu len ile alırız: len(str(2 ** 100)) değeri \(31\)’dir.
Örnek 1.5 (Yüz Faktöriyelin Basamak Sayısı) \(100!\) sayısının kaç basamaklı olduğunu bulunuz ve sonucu \(\log_{10}\) ile sağlayınız.
Çözüm
Doğrudan sayım. \(100!\)’i tam olarak hesaplayıp dizeye çevirelim ve uzunluğuna bakalım. math.log10 çok büyük tam sayıları da kabul eder:
import math
N = math.factorial(100)
print(len(str(N))) # basamak sayısı
print(math.log10(N))
print(math.floor(math.log10(N)) + 1)Çıktı:
158
157.97000365471578
158
Logaritmayla sağlama. Pozitif bir \(N\) tam sayısı \(d\) basamaklıysa \(10^{d-1} \le N < 10^{d}\), yani \(d - 1 \le \log_{10} N < d\) olur. Buradan \(d = \lfloor \log_{10} N \rfloor + 1\) bulunur. \(\log_{10}(100!) \approx 157{,}97\) olduğundan \(d = 158\)’dir; iki yol aynı sonucu verdi. \(\blacksquare\)
1.7 Tam Bölme ve Kalan
Sayılar teorisinin temel aracı olan bölme algoritması Python’da üç işlemle karşılanır: //, % ve divmod.
Tanım 1.6 (Tam Bölme ve Kalan) \(a\) ve \(b \neq 0\) tam sayılar olsun. Python’da a // b tam bölümü \(q = \lfloor a/b \rfloor\) sayısıdır, a % b kalanı ise \(r = a - bq\) sayısıdır. divmod(a, b) bu ikisini (q, r) çifti olarak birlikte verir.
Yani // bölümü daima aşağı, \(-\infty\)’a doğru yuvarlar ve \(a = bq + r\) eşitliği her zaman sağlanır. \(b > 0\) ise \(0 \le r < b\) olur; bu, Sayılar Teorisi notlarındaki bölme algoritmasının verdiği bölüm ve kalanın ta kendisidir, \(a\) negatif olsa bile. \(b < 0\) ise \(b < r \le 0\) olur, yani kalan bölenin işaretini alır.
print(17 // 5, 17 % 5, divmod(17, 5))
print(-17 // 5, -17 % 5, divmod(-17, 5))
print(5 * (-4) + 3) # a = bq + r sağlaması
print(7.5 // 2, 7.5 % 2) # ondalıklı sayılarda da tanımlıÇıktı:
3 2 (3, 2)
-4 3 (-4, 3)
-17
3.0 1.5
Bölüm sıfıra doğru değil aşağı yuvarlandığından \(-17/5 = -3{,}4\) için tam bölüm \(-3\) değil \(-4\) olur ve kalan negatif değil, \(3\) çıkar. (Sayının önündeki eksi işareti //’den önce uygulanır, yani -17 // 5 ifadesi (-17) // 5 demektir.) Aşağıdaki şekil iki durumu sayı doğrusu üzerinde gösteriyor: \(q\), \(a\)’yı aşmayan en büyük \(5\) katının katsayısıdır.
divmod(17, 5) ve divmod(-17, 5) sayı doğrusunda. Mavi bloklar |q| tane 5 uzunluğunda adımdır ve 0'dan, a'yı aşmayan en büyük 5 katı olan q · 5'e ulaşır; turuncu blok r = a − 5q kalanıdır. İki durumda da 0 ≤ r < 5 olur; −17 için bölüm −3 değil −4'tür.Kalan işlemi, büyük kuvvetlerin modüler hesabında da kullanılır. pow(a, n, m) çağrısı \(a^n\)’yi hiç hesaplamadan, her adımda \(m\)’ye göre kalan alarak \(a^n \bmod m\) değerini bulur. Kuvvet ardışık kare almalarla oluşturulduğundan adım sayısı yaklaşık \(\log_2 n\)’dir; \(n\) yüzlerce basamaklı olsa bile hesap anında biter.
Örnek 1.6 (Fermat Testiyle Bileşiklik) Fermat’nın küçük teoremini kullanarak \(341\)’in asal olmadığını gösteriniz.
Çözüm
Test. Fermat’nın küçük teoremine göre \(p\) asal ve \(p \nmid a\) ise \(a^{p-1} \equiv 1 \pmod{p}\) olur. Bu yüzden \(341 \nmid a\) olan bir \(a\) için \(a^{340} \not\equiv 1 \pmod{341}\) çıkarsa \(341\) asal olamaz. \(a = 2\) ve \(a = 3\)’ü deneyelim:
print(pow(2, 340, 341)) # 2^340 mod 341
print(pow(3, 340, 341)) # 3^340 mod 341
print(2 ** 340 % 341) # aynı sonuç, ama önce 2^340 hesaplanır
print(11 * 31)Çıktı:
1
56
1
341
Yorum. \(2^{340} \equiv 1 \pmod{341}\) olduğundan \(a = 2\) ile yapılan test karar veremez; \(341\), \(2\) tabanına göre bir sahte asaldır. Buna karşılık \(3^{340} \equiv 56 \not\equiv 1 \pmod{341}\) olduğundan \(341\) asal değildir. Gerçekten \(341 = 11 \cdot 31\)’dir. Üçüncü satırdaki doğrudan hesap da aynı kalanı verdi, ama önce \(103\) basamaklı \(2^{340}\) sayısını hesapladı. \(\blacksquare\)
1.8 Kayan Noktalı Sayılar
Tam sayılardan sonra gerçel sayılara geçiyoruz: 2.5 gibi ondalık noktayla yazılan her sayı ve iki tam sayının / ile bölümü Python’da float türündendir; bu tür, gerçel sayıların sonlu bir alt kümesidir.
Tanım 1.7 (Kayan Noktalı Sayı Türü) Python’un float türü, IEEE 754 standardının \(64\) bitlik (çift duyarlıklı) makine sayılarıdır. Sıfırdan farklı her normalize float
\[x = \pm\,(1 + m)\, 2^{e}, \qquad -1022 \le e \le 1023\]
biçimindedir; burada \(m = (0{,}b_1 b_2 \ldots b_{52})_2\), yani \(0 \le m < 1\) ve \(2^{52} m\) bir tam sayıdır. Bir sayı yazıldığında ya da bir işlem yapıldığında sonuç, kendisine en yakın float değerine yuvarlanır.
Yani float, gerçel sayıları \(53\) anlamlı ikili basamakla, yaklaşık \(16\) ondalık basamakla temsil eder. Bitlerin bellekte nasıl dizildiği Nümerik Analiz notlarında ayrıntılı anlatılmıştır. Ondalık tabanda sonlu olan \(0{,}1\) sayısının ikili açılımı \(0{,}000110011001100\ldots_2\) sonsuzdur; bu yüzden \(0{,}1\) bir float olarak tam saklanamaz, ona en yakın makine sayısı saklanır.
print(0.1 + 0.2)
print(0.1 + 0.2 == 0.3)
print(f"{0.1:.20f}") # 0.1'in saklanan değeri
print(2.0 ** 100 + 1 - 2.0 ** 100) # 1 kayboldu
print(1e308 * 10, -1e308 * 10) # taşma: sonsuzÇıktı:
0.30000000000000004
False
0.10000000000000000555
0.0
inf -inf
Satırları sırayla okuyalım:
- Python bir
float’ı yazarken, geri okunduğunda aynıfloat’ı veren en kısa ondalık diziyi yazar.0.1için bu dizi0.1’dir, ama saklanan değer, üçüncü satırın gösterdiği gibi,0.1’den biraz büyüktür. \(0{,}1\) ile \(0{,}2\)’nin saklanan değerlerinin toplamı da \(0{,}3\)’e en yakınfloat’a değil, onun bir sonraki komşusuna yuvarlanır. - \(2^{100}\) civarında ardışık iki
floatarasındaki uzaklık \(2^{48}\)’dir. \(2^{100} + 1\) bu aralığa göre \(2^{100}\)’e o kadar yakındır ki toplam yine \(2^{100}\)’e yuvarlanır. Aynı hesapintile tam olarak \(1\) vermişti. 1e308yazımı \(1 \cdot 10^{308}\) demektir. En büyükfloat’ı aşan sonuçlarinf, yani sonsuz olur.
Bir float’ı tam sayıya int(x) çevirir (ondalık kısmı atarak), bir tam sayıyı float’a float(n) çevirir. float sisteminin sınırlarını sys modülündeki sys.float_info söyler:
import sys
print(sys.float_info.max)
print(sys.float_info.min)
print(sys.float_info.mant_dig, sys.float_info.dig)
print(2.0 ** 1023 * (2 - 2.0 ** -52) == sys.float_info.max)
print(2.0 ** -1022 == sys.float_info.min)Çıktı:
1.7976931348623157e+308
2.2250738585072014e-308
53 15
True
True
Yani en büyük float \(2^{1023}(2 - 2^{-52}) \approx 1{,}798 \cdot 10^{308}\), en küçük pozitif normalize float ise \(2^{-1022} \approx 2{,}225 \cdot 10^{-308}\)’dir; bunlar Nümerik Analiz notlarında bulunan sınırlardır. mant_dig anlamlı ikili basamak sayısını (\(52\) mantis biti ve gizli baştaki \(1\) ile \(53\)) verir. dig ise \(15\)’tir: \(15\) anlamlı basamaklı her ondalık sayı float’a çevrilip geri yazıldığında aynen elde edilir. Standart, \(2^{-1022}\)’nin altında daha az anlamlı basamaklı alt normal (subnormal) sayılara da izin verir; bunların en küçüğü \(2^{-1074} \approx 4{,}9 \cdot 10^{-324}\)’tür.
Kayan noktalı sayılar sayı doğrusuna eşit aralıklarla dağılmaz. Bunu görmek için mantisi yalnız \(2\) ikili basamaklı olan oyuncak bir sistem düşünelim: sayılar \((1 + m/4) \cdot 2^{e}\) biçiminde olsun, burada \(m \in \{0, 1, 2, 3\}\) ve \(-2 \le e \le 2\)’dir.
# 2 bitlik mantisli oyuncak sistem: (1 + m/4) * 2**e
for e in range(-2, 3):
print(e, 2.0**e, 1.25 * 2.0**e, 1.5 * 2.0**e, 1.75 * 2.0**e)Çıktı:
-2 0.25 0.3125 0.375 0.4375
-1 0.5 0.625 0.75 0.875
0 1.0 1.25 1.5 1.75
1 2.0 2.5 3.0 3.5
2 4.0 5.0 6.0 7.0
Her \([2^e, 2^{e+1})\) aralığında dört sayı vardır ve bunlar eşit aralıklıdır; ama aralık \(2^{e-2}\)’dir ve bir sonraki ikilik aralıkta iki katına çıkar. Gerçek float sisteminde de her \([2^e, 2^{e+1})\) aralığında \(2^{52}\) tane eşit aralıklı sayı vardır ve aralık \(2^{e-52}\)’dir. Mutlak aralık büyüdükçe artar, ama sayının büyüklüğüne oranı hep aynı mertebede kalır.
float yalnız \(53\) anlamlı ikili basamak tutar; bu yüzden \(2^{53}\)’ten büyük tam sayıların hepsi float olarak saklanamaz. / işleminin sonucu her zaman float olduğundan büyük tam sayılarla bölmede / yerine // kullanılmalıdır.
n = 2 ** 53 + 1
print(n, float(n), float(n) == n)
print(10 ** 20 // 3) # tam
print(int(10 ** 20 / 3)) # float üzerinden: son basamaklar bozukÇıktı:
9007199254740993 9007199254740992.0 False
33333333333333333333
33333333333333331968
1.9 Makine Epsilonu ve Sayıları Karşılaştırmak
Yuvarlama hatasının büyüklüğünü tek bir sayı belirler: \(1\) ile ondan büyük ilk float arasındaki uzaklık.
Tanım 1.8 (Makine Epsilonu) \(1\)’den büyük en küçük float \(1 + 2^{-52}\)’dir. \(\varepsilon = 2^{-52} \approx 2{,}22 \cdot 10^{-16}\) sayısına makine epsilonu (machine epsilon) denir.
Yani \(1\)’in sağındaki ilk komşu \(1 + \varepsilon\)’dur ve \(1\) ile \(1 + \varepsilon\) arasındaki bir sayı bu ikisinden birine yuvarlanır. Daha genel olarak, bir x değeri ile büyüklükçe ondan sonra gelen ilk float arasındaki uzaklığa x’in ulp’u (unit in the last place) denir ve math.ulp(x) ile bulunur; \(\varepsilon\), math.ulp(1.0) değeridir.
import math
import sys
eps = sys.float_info.epsilon
print(eps, eps == 2 ** -52)
print(1 + eps > 1, 1 + eps / 2 == 1)
print(math.nextafter(1, 2) - 1) # 1'den sonraki float eksi 1
print(math.ulp(1.0), math.ulp(1000.0), math.ulp(1e16))Çıktı:
2.220446049250313e-16 True
True True
2.220446049250313e-16
2.220446049250313e-16 1.1368683772161603e-13 2.0
math.nextafter(x, y), x’ten y yönünde ilerleyince karşılaşılan ilk float’tır. \(1 + \varepsilon/2\) sayısı \(1\) ile \(1 + \varepsilon\)’un tam ortasındadır; IEEE 754’ün “eşitlikte son biti çift olana yuvarla” kuralıyla \(1\)’e yuvarlanır. Son satır, aralığın sayıyla birlikte büyüdüğünü gösteriyor: \(10^{16}\) civarında ardışık float’lar \(2\) aralıklıdır, yani \(10^{16} + 1\) tam olarak saklanamaz.
Önerme 1.1 (Yuvarlamanın Bağıl Hatası) \(x\), \(2^{-1022} \le |x| \le 2^{1023}(2 - 2^{-52})\) koşulunu sağlayan bir gerçel sayı ve \(\mathrm{fl}(x)\), \(x\)’e en yakın float olsun. Bu durumda
\[\mathrm{fl}(x) = x\,(1 + \delta), \qquad |\delta| \le \frac{\varepsilon}{2} = 2^{-53}\]
olur. IEEE 754 standardı ayrıca dört işlemin ve karekökün sonucunun, tam sonucun yuvarlanmışı olmasını şart koşar: float sayılar \(x, y\) ve \(\circ \in \{+, -, \times, /\}\) için, tam sonuç \(x \circ y\) yukarıdaki normal aralıkta kaldıkça (yani taşma ya da alttan taşma olmadıkça) \(\mathrm{fl}(x \circ y) = (x \circ y)(1 + \delta)\), \(|\delta| \le 2^{-53}\) olur.
Yani tek bir işlemin bağıl hatası en fazla \(2^{-53} \approx 1{,}1 \cdot 10^{-16}\)’dır; tek bir işlemden sonra bir float sonucuna yaklaşık \(16\) anlamlı basamakla güvenebiliriz. Hatalar ise art arda yapılan işlemler boyunca birikebilir. Sınırın ispatı, ondalık tabanda \(k\) basamakla yuvarlamanın bağıl hata sınırının (bkz. Nümerik Analiz) ikili tabanda \(53\) basamakla aynen tekrarıdır.
Aşağıdaki şekil math.ulp(x)/x bağıl aralığının \(x\)’e göre nasıl değiştiğini gösteriyor. Bağıl aralık her ikilik aralığın başında \(\varepsilon\)’dur, aralığın sonuna doğru \(\varepsilon/2\)’ye iner ve bir sonraki \(2\) kuvvetinde yeniden \(\varepsilon\)’a sıçrar.
math.ulp(x)/x, logaritmik x ekseninde. Bir [2e, 2e+1) aralığında ardışık float'lar arasındaki uzaklık sabit olduğundan bağıl aralık ε'dan ε/2'ye iner ve her 2 kuvvetinde yeniden ε'a sıçrar. Yuvarlamanın bağıl hatası bunun yarısıyla, en fazla ε/2 ile sınırlıdır.Yuvarlama hatası ile yöntemin kendi hatasının birlikte nasıl davrandığını tanıdık bir limitte görelim.
Örnek 1.7 (e Sayısına Yaklaşan Dizi) \(e = \lim_{n\to\infty} \left(1 + \frac{1}{n}\right)^n\) olduğunu biliyoruz (bkz. Analiz 1). \(n = 10^k\), \(k = 1, 2, \ldots, 16\) için \(\left(1 + \frac{1}{n}\right)^n\) değerini float ile hesaplayıp math.e ile arasındaki farkı inceleyiniz.
Çözüm
Hesap.
import math
for k in range(1, 17):
n = 10 ** k
approx = (1 + 1 / n) ** n
print(k, approx, abs(approx - math.e))Çıktı:
1 2.5937424601000023 0.12453936835904278
2 2.7048138294215285 0.01346799903751661
3 2.7169239322355936 0.0013578962234515046
4 2.7181459268249255 0.000135901634119584
5 2.7182682371922975 1.359126674760347e-05
6 2.7182804690957534 1.359363291708604e-06
7 2.7182816941320818 1.3432696333026684e-07
8 2.7182817983473577 3.011168736577474e-08
9 2.7182820520115603 2.2355251516614771e-07
10 2.7182820532347876 2.2477574246337895e-07
11 2.71828205335711 2.248980650598753e-07
12 2.7185234960372378 0.00024166757819266138
13 2.716110034086901 0.002171794372144209
14 2.716110034087023 0.0021717943720220845
15 3.035035206549262 0.31675337809021675
16 1.0 1.718281828459045
İlk kısım: yöntemin hatası. \(k \le 7\) iken hata her adımda yaklaşık on kat azalır. Bu, dizinin kendi yakınsama hızıdır: Taylor açılımıyla \(e - \left(1 + \frac{1}{n}\right)^n \approx \frac{e}{2n}\) olduğu gösterilebilir.
İkinci kısım: yuvarlama hatası. \(k = 9\)’dan sonra hata yeniden büyür. \(1 + \frac{1}{n}\) toplamı Önerme 1.1 gereği \(2^{-53}\)’e kadar bağıl hatayla saklanır; \(n\)’inci kuvvet alınırken bu bağıl hata yaklaşık \(n\) ile çarpılır. \(n = 10^9\) için \(n \cdot 2^{-53} \approx 1{,}1 \cdot 10^{-7}\) olur ve bu, artık yöntemin \(\frac{e}{2n} \approx 1{,}4 \cdot 10^{-9}\) hatasından çok büyüktür.
Son satır. \(n = 10^{16}\) için \(10^{-16} < \frac{\varepsilon}{2}\) olduğundan \(1 + 10^{-16}\) toplamı doğrudan \(1\)’e yuvarlanır ve \(1^n = 1\) çıkar. En iyi sonuç, iki hatanın dengelendiği \(k = 8\) civarında elde edilir. \(\blacksquare\)
İki hata kaynağı log-log ölçekte açıkça ayrılır: soldaki doğru parçası yöntemin \(\frac{e}{2n}\) hatasıdır, sağdaki yükselen kısım ise yuvarlama hatasının \(n\) ile büyümesidir.
Yuvarlama hataları yüzünden matematikte eşit olan iki niceliğin float değerleri son basamaklarda farklı çıkabilir. Bu yüzden float sayılar == ile değil, bir toleransla karşılaştırılır.
Tanım 1.9 (Toleransla Karşılaştırma) math.isclose(a, b, rel_tol=1e-09, abs_tol=0.0) çağrısı, rel_tol değerine \(\rho\) ve abs_tol değerine \(\tau\) dersek,
\[|a - b| \le \max\bigl(\rho \cdot \max(|a|, |b|),\ \tau\bigr)\]
eşitsizliği sağlanıyorsa True, sağlanmıyorsa False verir. \(\rho\)’ya bağıl tolerans, \(\tau\)’ya mutlak tolerans denir; verilmeyen tolerans için varsayılan değer kullanılır.
Yani bağıl tolerans farkı sayıların kendi büyüklüğüyle ölçer: varsayılan \(\rho = 10^{-9}\), iki sayının yaklaşık dokuz anlamlı basamağının aynı olmasını ister. Mutlak tolerans ise büyüklükten bağımsız bir eşiktir. Sayılardan biri \(0\) ise bağıl koşul \(|a| \le \rho\,|a|\) biçimine iner ve yalnız \(a = 0\) iken sağlanır; bu yüzden sıfırla karşılaştırırken mutlak tolerans şarttır. abs_tol=1e-12 yazımında olduğu gibi, bir argümanı adıyla vermek mümkündür.
==kullanma. Matematikte eşit olan iki niceliğinfloatdeğerleri son basamaklarda farklı olabilir.- Bağıl tolerans seç. Sayıların büyüklüğüne uygun bir
rel_tolilemath.isclose(a, b, rel_tol=...)kullan; varsayılan \(10^{-9}\) çoğu hesap için yeterlidir. - Sıfırın yakınında mutlak tolerans ekle. Beklenen değer \(0\) ise problemin ölçeğine uygun bir
abs_tolver.
Örnek 1.8 (Sıfırla Karşılaştırma) \(\sin \pi = 0\) eşitliğini Python’da math.sin ve math.pi ile sınayınız.
Çözüm
Değeri ve üç karşılaştırmayı yazdıralım.
import math
y = math.sin(math.pi)
print(y)
print(y == 0)
print(math.isclose(y, 0)) # yalnız bağıl tolerans
print(math.isclose(y, 0, abs_tol=1e-12)) # mutlak tolerans eklendiÇıktı:
1.2246467991473532e-16
False
False
True
Neden sıfır değil? math.pi \(\pi\)’nin kendisi değil, ona en yakın float’tır ve \(\pi\) ile farkı yaklaşık \(1{,}22 \cdot 10^{-16}\)’dır. \(\pi\) civarında \(\sin(\pi - h) = \sin h \approx h\) olduğundan Python bu küçük farkı doğru olarak hesapladı; sonuç \(0\) değil, bu yüzden == karşılaştırması False verdi.
Toleranslar. Sayılardan biri \(0\) olduğu için yalnız bağıl toleransla yapılan karşılaştırma da False verdi. Sinüs değerleri \(1\) mertebesinde olduğundan \(\tau = 10^{-12}\) mutlak toleransı makul bir eşiktir ve bununla karşılaştırma True verdi. \(\blacksquare\)
1.10 Karmaşık Sayılar
Python karmaşık sayıları da yerleşik bir tür olarak tanır; temel işlemler için bir modül yüklemek gerekmez.
Tanım 1.10 (Karmaşık Sayı Türü) Python’da \(a + bi\) karmaşık sayısı a + bj biçiminde yazılır ve complex türündendir; sanal birim 1j ile gösterilir. Bir z karmaşık sayısının gerçel ve sanal kısımları z.real ve z.imag, eşleniği z.conjugate(), modülü abs(z) ile bulunur.
Yani Python, mühendislikteki geleneği izleyerek \(i\) yerine \(j\) harfini kullanır. Tek başına j bir değişken adıdır; sanal birim daima önünde bir sayıyla, 1j olarak yazılır. Gerçel ve sanal kısımlar float olarak saklanır, bu yüzden karmaşık hesap da float hesabının yuvarlama hatalarını taşır. z.conjugate() gibi bir değerin arkasına nokta konarak çağrılan fonksiyonlara metot denir.
z = 3 + 4j
w = 1 - 2j
print(z + w, z * w, z / w)
print(z.real, z.imag, z.conjugate(), abs(z))
print(1j ** 2)Çıktı:
(4+2j) (11-2j) (-1+2j)
3.0 4.0 (3-4j) 5.0
(-1+0j)
Karekök, üstel fonksiyon ve logaritmanın karmaşık sürümleri cmath modülündedir:
import cmath
print(cmath.sqrt(-1), cmath.sqrt(-4))
print(cmath.exp(1j * cmath.pi)) # Euler: e^(i pi) = -1
print(cmath.polar(3 + 4j)) # (modül, argüman)
print(cmath.rect(2, cmath.pi / 2)) # 2 e^(i pi/2) = 2iÇıktı:
1j 2j
(-1+1.2246467991473532e-16j)
(5.0, 0.9272952180016122)
(1.2246467991473532e-16+2j)
cmath.polar(z), \(z = re^{i\theta}\) olacak biçimdeki \((r, \theta)\) çiftini \(\theta \in [-\pi, \pi]\) olacak biçimde verir; cmath.rect(r, theta) bunun tersidir. Euler formülüne göre \(e^{i\pi} = -1\)’dir. Python’un sonucundaki 1.2246467991473532e-16j sanal kısmı yine cmath.pi ile \(\pi\) arasındaki farktan gelir; aynı fark cmath.rect satırının gerçel kısmında da görünür.
Örnek 1.9 (Karmaşık Kökleri Olan İkinci Derece Denklem) \(x^2 + 2x + 5 = 0\) denkleminin köklerini Python ile bulunuz ve sağlamasını yapınız.
Çözüm
Formül. Kökler \(x_{1,2} = \dfrac{-b \pm \sqrt{b^2 - 4ac}}{2a}\) ile verilir. Diskriminant \(\Delta = 4 - 20 = -16 < 0\) olduğundan math.sqrt hata verir; cmath.sqrt ise \(\sqrt{-16} = 4i\) sonucunu döndürür.
import cmath
a, b, c = 1, 2, 5
d = cmath.sqrt(b**2 - 4 * a * c) # diskriminantın karekökü
x1 = (-b + d) / (2 * a)
x2 = (-b - d) / (2 * a)
print(x1, x2)
print(x1**2 + 2 * x1 + 5, x2**2 + 2 * x2 + 5) # sağlamaÇıktı:
(-1+2j) (-1-2j)
0j 0j
Sonuç. Kökler \(-1 + 2i\) ve \(-1 - 2i\)’dir; katsayılar gerçel olduğundan birbirinin eşleniğidir. Her iki kökte de polinomun değeri tam olarak \(0\) çıktı, çünkü bu hesaptaki bütün ara değerler float olarak tam saklanabilen küçük tam sayılardır. \(\blacksquare\)
Örnek 1.10 (Birimin Altıncı Kökleri) \(z^6 = 1\) denkleminin altı kökünü \(\omega_k = e^{2\pi i k/6}\), \(k = 0, 1, \ldots, 5\) formülüyle hesaplayınız ve her birinin denklemi ne kadar iyi sağladığını inceleyiniz.
Çözüm
Hesap. Formül, Kompleks Analiz notlarındaki \(n\)-inci kök formülünün \(z_0 = 1\), \(n = 6\) hâlidir.
import cmath
n = 6
for k in range(n):
w = cmath.exp(2j * cmath.pi * k / n)
print(k, w, abs(w ** n - 1))Çıktı:
0 (1+0j) 0.0
1 (0.5000000000000001+0.8660254037844386j) 8.617648093045562e-16
2 (-0.4999999999999998+0.8660254037844387j) 1.2412670766236366e-15
3 (-1+1.2246467991473532e-16j) 7.347880794884119e-16
4 (-0.5000000000000004-0.8660254037844385j) 2.975418419039454e-15
5 (0.5000000000000001-0.8660254037844386j) 8.617648093045562e-16
Yorum. Tam değerler \(\pm 1\) ve \(\pm\frac{1}{2} \pm \frac{\sqrt{3}}{2}\, i\)’dir; \(\frac{\sqrt{3}}{2} \approx 0{,}8660254\). Hesaplanan değerler son basamaklarda bunlardan biraz farklıdır ve \(|\omega_k^6 - 1|\) farkları \(0\) değil, \(10^{-15}\) mertebesindedir: her kök \(\varepsilon\) mertebesinde bir bağıl hatayla bulunmuştur ve altıncı kuvvet bu hatayı birkaç kat büyütür. \(\blacksquare\)
Altı kök, birim çember üzerinde düzgün bir altıgenin köşelerine dizilir; ardışık iki kök arasındaki açı \(\frac{2\pi}{6} = \frac{\pi}{3}\)’tür.
cmath.exp(2j * cmath.pi * k / 6) ile hesaplanan altı kökü. Kökler birim çember üzerinde düzgün bir altıgenin köşeleridir ve ardışık iki kök arasındaki açı π/3'tür.1.11 Tam Kesirler
Rasyonel sayılarla hiç yuvarlama yapmadan hesap yapmak için standart kütüphanedeki fractions modülü kullanılır.
Tanım 1.11 (Kesir Türü) fractions modülündeki Fraction(p, q), \(q \neq 0\) olmak üzere \(p\) ve \(q\) tam sayılarından \(p/q\) rasyonel sayısını kurar ve onu paydası pozitif, en sade kesir olarak saklar. Fraction değerleri arasında dört işlem ve tam sayı kuvvetleri tam olarak yapılır; sonuç yine en sade hâle getirilir.
Yani Fraction, \(\mathbb{Q}\)’nun bilgisayardaki tam modelidir ve int’in kesirlere genişlemesi gibi düşünülebilir. Bedeli hızdır: hesap ilerledikçe pay ve paydalar büyüyebilir ve işlemler float’a göre çok daha yavaştır.
from fractions import Fraction
x = Fraction(1, 3) + Fraction(1, 6)
print(x, x.numerator, x.denominator)
print(Fraction(6, 4), Fraction("0.75"), Fraction(3, 10) * 10)
print(Fraction(1, 10) + Fraction(2, 10) == Fraction(3, 10))
print(Fraction(0.1)) # float 0.1'in tam değeri
print(float(Fraction(22, 7)))Çıktı:
1/2 1 2
3/2 3/4 3
True
3602879701896397/36028797018963968
3.142857142857143
x.numerator ve x.denominator kesrin payını ve paydasını verir. Fraction(0.1) satırı ise bir float’ın aslında hangi rasyonel sayı olduğunu tam olarak gösteriyor: paydası \(36028797018963968 = 2^{55}\)’tir. Bu, 0.1’in \(\frac{1}{10}\) değil, ona çok yakın bir ikili kesir olduğunun kesin kanıtıdır. Bir kesri iki tam sayıdan ya da Fraction("0.75") gibi bir dizeden kurmak tamdır; bir float’tan kurmak ise float’ın hatasını da beraberinde getirir.
Örnek 1.11 (Harmonik Sayının Tam Değeri) \(H_{10} = 1 + \frac{1}{2} + \frac{1}{3} + \cdots + \frac{1}{10}\) toplamını tam kesir olarak bulunuz ve float ile yapılan toplamla karşılaştırınız.
Çözüm
İki toplamı yan yana kuralım. H tam toplamı, h ise float toplamı biriktirsin:
from fractions import Fraction
H = Fraction(0)
h = 0.0
for k in range(1, 11):
H += Fraction(1, k) # tam toplam
h += 1 / k # float toplam
print(H)
print(float(H), h)Çıktı:
7381/2520
2.9289682539682538 2.9289682539682538
Sonuç. \(H_{10} = \frac{7381}{2520}\)’dir; payda \(2520 = \operatorname{ekok}(1, 2, \ldots, 10)\) ve pay \(7381 = 11^2 \cdot 61\) olduğundan kesir en sade hâldedir. Bu kısa toplamda float toplam, tam değere en yakın float ile aynı çıktı; ama tam değeri, yani pay ve paydayı, ancak Fraction verir. \(\blacksquare\)
Örnek 1.12 (Pi Sayısına En Yakın Kesir) Paydası \(1000\)’i geçmeyen kesirler arasında \(\pi\)’ye en yakın olanı bulunuz.
Çözüm
Araç. Fraction değerlerinin limit_denominator(N) metodu, paydası \(N\)’yi geçmeyen kesirler arasında kendisine en yakın olanı verir. Fraction(math.pi), math.pi float’ının tam değeridir; \(\pi\)’den farkı \(10^{-16}\) mertebesinde olduğundan bu soruda \(\pi\) yerine kullanılabilir.
import math
from fractions import Fraction
p = Fraction(math.pi)
print(p.limit_denominator(10))
print(p.limit_denominator(100))
print(p.limit_denominator(1000))
r = p.limit_denominator(1000)
print(float(r) - math.pi)Çıktı:
22/7
311/99
355/113
2.667641894049666e-07
Sonuç. Aranan kesir \(\frac{355}{113}\)’tür ve \(\pi\)’den farkı yaklaşık \(2{,}7 \cdot 10^{-7}\)’dir. Paydası \(114\) ile \(1000\) arasında olan hiçbir kesir bundan daha iyi değildir. \(\blacksquare\)
Paydanın üst sınırı \(N\) büyüdükçe en iyi yaklaşım ancak belirli \(N\) değerlerinde değişir; aradaki \(N\)’lerde aynı kesir en iyi kalır. Aşağıdaki şekil limit_denominator(N) çağrısının \(N = 1, 2, \ldots, 1000\) için verdiği kesirlerin \(\pi\)’ye uzaklığını gösteriyor.
limit_denominator(N) ile N = 1, 2, …, 1000 için. Hata basamak basamak azalır: 22/7, N = 7'den 56'ya kadar en iyi kesirdir ve N = 57'de yerini 179/57'ye bırakır; 355/113 ise N = 113'ten 1000'e kadar en iyi kesir kalır. Turuncu noktalar örnekteki kodun yazdırdığı N = 10, 100, 1000 değerleridir.1.12 Ondalık Hesap
Bazı hesaplarda sayıların onluk tabanda tam saklanması ya da float’ın sunduğundan çok daha fazla basamak gerekir; bunun için decimal modülü vardır.
Tanım 1.12 (Ondalık Sayı Türü) decimal modülündeki Decimal türü sayıları onluk tabanda ve belirli sayıda anlamlı basamakla saklar. İşlemlerin kaç anlamlı basamakla yapılacağını getcontext().prec değeri belirler (varsayılan değeri \(28\)’dir); her işlemin sonucu bu sayıda anlamlı basamağa yuvarlanır.
Yani Decimal da bir kayan noktalı sayı sistemidir, ama tabanı \(10\)’dur ve basamak sayısını biz seçeriz. Decimal("0.1") gibi bir dizeden kurulan ondalık sayılar tam saklanır; \(\frac{1}{3}\) gibi onluk açılımı sonsuz olan sayılar ise yine yuvarlanır.
from decimal import Decimal, getcontext
print(Decimal("0.1") + Decimal("0.2"))
print(Decimal(0.1)) # float'tan kurulunca hatası da gelir
print(Decimal(1) / Decimal(3)) # varsayılan: 28 basamak
getcontext().prec = 50
print(Decimal(1) / Decimal(3))Çıktı:
0.3
0.1000000000000000055511151231257827021181583404541015625
0.3333333333333333333333333333
0.33333333333333333333333333333333333333333333333333
Decimal(0.1) yazıldığında önce 0.1 float’ı oluşur ve Decimal onun tam değerini, yani yukarıdaki uzun sayıyı saklar. Ondalık bir sayıyı tam saklamak için onu dize olarak verin: Decimal("0.1").
Örnek 1.13 (Karekök İkinin Elli Basamağı) \(\sqrt{2}\)’yi \(50\) anlamlı basamakla hesaplayınız ve bulduğunuz sayının karesinin \(2\)’den ne kadar farklı olduğunu bulunuz.
Çözüm
Hassasiyeti ayarlayıp karekökü alalım. Decimal değerlerinin sqrt metodu karekökü, o anki hassasiyetle hesaplar:
from decimal import Decimal, getcontext
getcontext().prec = 50
r = Decimal(2).sqrt()
print(r)
print(r * r - 2)
print(2 ** 0.5) # float ile karşılaştırmaÇıktı:
1.4142135623730950488016887242096980785696718753769
-1E-49
1.4142135623730951
Karenin farkı. r, \(\sqrt{2}\)’nin \(50\) anlamlı basamağa yuvarlanmış hâlidir ve \(\sqrt{2}\)’den küçüktür; bu yüzden karesi \(2\)’den biraz küçük çıkar. Fark \(10^{-49}\) mertebesindedir, yani ancak \(50\). anlamlı basamakta ortaya çıkar. float ise yalnız \(17\) anlamlı basamak yazdı ve sonuncusu bile \(\sqrt{2}\)’nin basamağı değildir: \(\sqrt{2} = 1{,}41421356237309504\ldots\) olduğu hâlde float son basamağı \(1\) olarak yazdı. \(\blacksquare\)
Python’un yerleşik round(x, n) fonksiyonu \(x\)’i noktadan sonra \(n\) basamağa yuvarlar (\(n\) verilmezse en yakın tam sayıya). Tam ortadaki değerlerde “en yakın çift sayıya yuvarla” kuralını uygular; bu kural, çok sayıda yuvarlamada hataların hep aynı yöne birikmesini önler. Okulda öğrenilen “yarımı yukarı yuvarla” kuralı için Decimal’in quantize metodu kullanılır:
from decimal import Decimal, ROUND_HALF_UP
print(round(0.5), round(1.5), round(2.5)) # yarımlar çifte gider
x = Decimal("2.675")
print(x.quantize(Decimal("0.01"), rounding=ROUND_HALF_UP))Çıktı:
0 2 2
2.68
Bölümde gördüğümüz sayı türlerini şöyle özetleyebiliriz:
| Tür | Değerler | Kesinlik | Ne zaman? |
|---|---|---|---|
int |
\(\mathbb{Z}\) | tam | sayma, sayılar teorisi, kombinatorik |
float |
\(53\) ikili basamaklı sayılar | işlem başına bağıl hata \(\le 2^{-53}\) | sayısal hesabın büyük çoğunluğu |
complex |
iki float |
float gibi |
karmaşık sayılarla sayısal hesap |
Fraction |
\(\mathbb{Q}\) | tam | tam kesirli sonuçlar, küçük hesaplar |
Decimal |
onluk kayan nokta | seçilen basamak sayısı | para hesabı, çok basamaklı hesap |
1.13 Alıştırmalar
Aşağıdaki alıştırmalar bölümdeki sayı türlerini tanıdık matematik problemleri üzerinde kullanır; her çözüm çalıştırılmış kodu ve çıktısını içerir. Kodu önce kendiniz yazmayı deneyin, sonra çözümle karşılaştırın.
Alıştırma 1.1 (İki Üzeri Binin Basamakları) \(2^{1000}\) sayısının kaç basamaklı olduğunu ve basamaklarının toplamını bulunuz.
Çözüm
Basamakları dolaşalım. for döngüsü bir dizenin karakterlerini de tek tek dolaşabilir. int("7") gibi bir çağrı, tek rakamlı bir dizeyi tam sayıya çevirir.
n = 2 ** 1000
s = str(n)
total = 0
for c in s: # dizenin karakterlerini tek tek dolaş
total += int(c) # karakteri rakama çevirip ekle
print(len(s), total)Çıktı:
302 1366
Sağlama. Basamak sayısı \(\lfloor 1000 \log_{10} 2 \rfloor + 1\) formülüyle de bulunur: \(1000 \log_{10} 2 \approx 301{,}03\) olduğundan sonuç \(302\)’dir. \(2^{1000}\) sayısı \(302\) basamaklıdır ve basamaklarının toplamı \(1366\)’dır. \(\blacksquare\)
Alıştırma 1.2 (Büyük Bir Kuvvetin Son Basamakları) \(3^{1000}\) sayısının son \(10\) basamağını bulunuz.
Çözüm
Kalana çevirelim. Bir sayının son \(10\) basamağı, onun \(10^{10}\) ile bölümünden kalandır. Bu kalanı pow ile, \(3^{1000}\)’i hesaplamadan bulabiliriz. Sağlama için doğrudan hesabı da yapalım; kalan \(10\) basamaktan kısa çıkarsa baştaki sıfırları da göstermek için 010d biçimini (soldan sıfırla doldurulmuş \(10\) karakterlik tam sayı) kullanıyoruz:
r = pow(3, 1000, 10 ** 10)
print(f"{r:010d}")
print(3 ** 1000 % 10 ** 10 == r)Çıktı:
2855220001
True
Sonuç. \(3^{1000}\) sayısının son on basamağı \(2855220001\)’dir; iki yol aynı kalanı verdi. \(\blacksquare\)
Alıştırma 1.3 (Bin Faktöriyelin Sonundaki Sıfırlar) \(1000!\) sayısının sonunda kaç sıfır olduğunu bulunuz.
Çözüm
Sayarak düşünelim. Sondaki her sıfır bir \(10 = 2 \cdot 5\) çarpanıdır. \(1000!\) içinde \(2\) çarpanları \(5\) çarpanlarından çok olduğundan sıfır sayısı \(5\) çarpanlarının sayısına eşittir. \(1\)’den \(1000\)’e kadar \(5\)’in katları birer, \(25\)’in katları birer fazla, \(125\)’in katları bir fazla daha, \(625\)’in katları bir fazla daha \(5\) çarpanı verir:
\[\left\lfloor \frac{1000}{5} \right\rfloor + \left\lfloor \frac{1000}{25} \right\rfloor + \left\lfloor \frac{1000}{125} \right\rfloor + \left\lfloor \frac{1000}{625} \right\rfloor = 200 + 40 + 8 + 1 = 249.\]
Python ile. Formülü // ile hesaplayalım; sağlama için \(1000!\)’i dizeye çevirip sağdaki sıfırları sayalım. Dizenin rstrip("0") metodu sağdaki 0 karakterlerini atar, uzunluklar arasındaki fark atılan sıfır sayısıdır:
import math
n = 1000
print(n // 5 + n // 25 + n // 125 + n // 625)
s = str(math.factorial(n))
print(len(s) - len(s.rstrip("0"))) # sağdaki sıfırların sayısıÇıktı:
249
249
Sonuç. \(1000!\) sayısının sonunda \(249\) sıfır vardır. \(\blacksquare\)
Alıştırma 1.4 (Bir Milyon Saniye) \(10^{6}\) saniyenin kaç gün, saat, dakika ve saniye ettiğini divmod ile bulunuz.
Çözüm
Birimleri sırayla ayıralım. Önce saniyeleri \(60\)’a bölüp dakika ve artan saniyeyi, sonra dakikaları \(60\)’a bölüp saat ve artan dakikayı, en son saatleri \(24\)’e bölüp gün ve artan saati buluruz. divmod’un verdiği çifti eşzamanlı atamayla iki değişkene ayırıyoruz:
t = 10 ** 6
m, s = divmod(t, 60) # dakika ve artan saniye
h, m = divmod(m, 60) # saat ve artan dakika
d, h = divmod(h, 24) # gün ve artan saat
print(d, h, m, s)
print(((d * 24 + h) * 60 + m) * 60 + s) # sağlamaÇıktı:
11 13 46 40
1000000
Sonuç. \(10^6\) saniye \(11\) gün \(13\) saat \(46\) dakika \(40\) saniyedir; son satır bu parçalardan yeniden \(10^6\)’yı elde ediyor. \(\blacksquare\)
Alıştırma 1.5 (Negatif Bölenle Tam Bölme) \(17\)’yi \(-5\)’e Python’un // ve % işlemleriyle bölünüz ve sonucu, kalanın \(0 \le r < |b|\) koşulunu sağladığı sayılar teorisi bölmesiyle karşılaştırınız.
Çözüm
Python’un bölmesi. Tanım 1.6 gereği \(q = \lfloor 17/(-5) \rfloor\), yani \(q = \lfloor -3{,}4 \rfloor = -4\) ve \(r = 17 - (-5)(-4) = -3\) olur; kalan bölen gibi negatiftir.
Sayılar teorisi bölmesi. Negatif bölenle bölmede kalan \(0 \le r < |b| = 5\) aralığında seçilir: \(17 = (-5)(-3) + 2\). Bu kalanı Python’da a % abs(b) verir, bölüm de \(q = (a - r)/b\) eşitliğinden bulunur:
a, b = 17, -5
print(a // b, a % b, b * (a // b) + a % b)
r = a % abs(b) # 0 <= r < |b| olan kalan
q = (a - r) // b
print(q, r, b * q + r)Çıktı:
-4 -3 17
-3 2 17
Karşılaştırma. İki bölme de \(a = bq + r\) eşitliğini sağlar, ama kalanları farklı aralıkta seçer: Python \((q, r) = (-4, -3)\), sayılar teorisi \((q, r) = (-3, 2)\) verir. Bölen pozitifken bu fark ortaya çıkmaz. \(\blacksquare\)
Alıştırma 1.6 (Makine Epsilonunu Deneyle Bulmak) \(k = 50, 51, \ldots, 55\) için 1 + 2 ** -k == 1 karşılaştırmasının değerini yazdırarak \(1 + 2^{-k}\) toplamının \(1\)’e eşit çıktığı en küçük \(k\)’yi bulunuz ve sonucu makine epsilonuyla açıklayınız.
Çözüm
Deney. Negatif kuvvet aldığımız için 2 ** -k bir float’tır:
import sys
for k in range(50, 56):
print(k, 1 + 2 ** -k == 1)
print(sys.float_info.epsilon == 2 ** -52)Çıktı:
50 False
51 False
52 False
53 True
54 True
55 True
True
Açıklama. \(k \le 52\) iken \(1 + 2^{-k}\) sayısı tam olarak bir float’tır: mantisin \(k\)’inci ikili basamağı \(1\), diğerleri \(0\)’dır. Bu yüzden toplam \(1\)’den büyük kalır. \(k = 53\) için \(1 + 2^{-53} = 1 + \frac{\varepsilon}{2}\) sayısı \(1\) ile \(1 + \varepsilon\)’un tam ortasındadır ve “eşitlikte çifte yuvarla” kuralıyla \(1\)’e yuvarlanır. Daha büyük \(k\)’lerde toplam \(1\)’e daha da yakındır. Aranan en küçük değer \(k = 53\)’tür. \(\blacksquare\)
Alıştırma 1.7 (Onda Birin On Katı) 0.0’dan başlayıp \(0{,}1\)’i döngüyle on kez ekleyiniz. Sonucu \(1\) ile == ve math.isclose kullanarak karşılaştırınız; aynı toplamı Fraction(1, 10) ile de yapınız.
Çözüm
İki toplamı birlikte kuralım.
import math
from fractions import Fraction
total = 0.0
exact = Fraction(0)
for k in range(10):
total += 0.1
exact += Fraction(1, 10)
print(total, total == 1, math.isclose(total, 1))
print(exact, exact == 1)Çıktı:
0.9999999999999999 False True
1 True
Yorum. float toplam her adımda küçük bir yuvarlama hatası yaptı ve sonuç \(1\)’in hemen altındaki float oldu; bu yüzden == False verdi. Fark \(10^{-16}\) mertebesinde olduğundan math.isclose ile karşılaştırma True verdi. Fraction toplamı ise tam olarak \(1\)’dir. \(\blacksquare\)
Alıştırma 1.8 (Sadeleşme Hatası) \(f(x) = \dfrac{1 - \cos x}{x^2}\) fonksiyonunun \(x \to 0\) iken limiti \(\frac{1}{2}\)’dir. \(f(10^{-8})\)’i Python ile doğrudan hesaplayınız ve sonucu, \(1 - \cos x = 2\sin^2(x/2)\) özdeşliğiyle yeniden yazılmış formülün sonucuyla karşılaştırınız.
Çözüm
İki formül.
import math
x = 1e-8
print(math.cos(x)) # 1'e yuvarlandı
print((1 - math.cos(x)) / x ** 2) # doğrudan formül
print(2 * math.sin(x / 2) ** 2 / x ** 2) # özdeşlikleÇıktı:
1.0
0.0
0.5
Ne oldu? \(\cos x = 1 - \frac{x^2}{2} + \cdots\) olduğundan \(x = 10^{-8}\) için \(\cos x \approx 1 - 5 \cdot 10^{-17}\)’dir. \(1\)’in hemen altındaki float \(1 - \frac{\varepsilon}{2}\) olduğundan, \(1\)’e uzaklığı \(\frac{\varepsilon}{4} \approx 5{,}55 \cdot 10^{-17}\)’den küçük olan sayılar \(1\)’e yuvarlanır. \(5 \cdot 10^{-17}\) bu eşiğin altında kaldığı için math.cos(x) tam olarak \(1\) çıkar ve \(1 - \cos x\) farkı \(0\) olur: birbirine çok yakın iki sayının farkı alınırken bütün anlamlı basamaklar kaybolmuştur. Bu olaya yıkıcı sadeleşme (catastrophic cancellation) denir. Özdeşlikle yazılan formülde fark alınmaz; \(\sin(x/2)\) tam bağıl hassasiyetle hesaplanır ve sonuç doğru olarak \(0{,}5\) çıkar. \(\blacksquare\)
Alıştırma 1.9 (Bir Karmaşık Sayının Onuncu Kuvveti) \((1 + i)^{10}\) sayısını Python ile hesaplayınız ve sonucu kutupsal biçimle sağlayınız.
Çözüm
Doğrudan ve kutupsal yoldan hesap.
import cmath
z = 1 + 1j
print(z ** 10)
r, theta = cmath.polar(z)
print(r, theta)
print(cmath.rect(r ** 10, 10 * theta))Çıktı:
32j
1.4142135623730951 0.7853981633974483
(9.797174393178832e-15+32.00000000000002j)
Elle sağlama. \(|1 + i| = \sqrt{2}\) ve \(\arg(1 + i) = \frac{\pi}{4}\) olduğundan
\[(1 + i)^{10} = (\sqrt{2})^{10}\, e^{i\,10\pi/4} = 32\, e^{i\,5\pi/2} = 32\, e^{i\pi/2} = 32i\]
olur. Tam sayı kuvveti ** ile alındığında Python ardışık çarpmalar yapar ve bu çarpımlarda bütün ara değerler küçük tam sayılar olduğundan sonuç tam olarak 32j çıkar. Kutupsal yol ise \(\sqrt{2}\) ve \(\frac{\pi}{4}\)’ün yuvarlanmış değerlerinden geçtiği için son basamaklarda küçük hatalar taşır. \(\blacksquare\)
Alıştırma 1.10 (Teleskopik Bir Toplam) \(\displaystyle\sum_{k=1}^{20} \frac{1}{k(k+1)}\) toplamını Fraction ile tam olarak hesaplayınız ve \(\dfrac{n}{n+1}\) formülüyle karşılaştırınız.
Çözüm
Formül. \(\frac{1}{k(k+1)} = \frac{1}{k} - \frac{1}{k+1}\) olduğundan toplam teleskopiktir:
\[\sum_{k=1}^{n} \frac{1}{k(k+1)} = 1 - \frac{1}{n+1} = \frac{n}{n+1}.\]
Python ile.
from fractions import Fraction
n = 20
S = Fraction(0)
for k in range(1, n + 1):
S += Fraction(1, k * (k + 1))
print(S, S == Fraction(n, n + 1))Çıktı:
20/21 True
Sonuç. Toplam tam olarak \(\frac{20}{21}\)’dir ve formülle uyuşur. \(\blacksquare\)
Alıştırma 1.11 (Beklenmedik Bir Yuvarlama) round(2.675, 2) çağrısının neden 2.68 değil 2.67 verdiğini açıklayınız.
Çözüm
Saklanan değere bakalım. Decimal(2.675) yazımı, 2.675 float’ının tam değerini gösterir:
from decimal import Decimal
print(round(2.675, 2))
print(Decimal(2.675)) # float 2.675'in tam değeriÇıktı:
2.67
2.67499999999999982236431605997495353221893310546875
Açıklama. \(2{,}675\)’in ikili açılımı sonsuzdur; ona en yakın float \(2{,}675\)’ten biraz küçüktür. round bu saklanan değeri yuvarlar. Değer \(2{,}67\) ile \(2{,}68\)’in tam ortasında değil, ortanın biraz altında olduğundan doğru yuvarlama \(2{,}67\)’dir. Buradaki sorun bir yuvarlama kuralı değil, \(2{,}675\)’in float olarak tam saklanamamasıdır. Ondalık sayıları tam yuvarlamak gerekiyorsa Decimal("2.675") ve quantize kullanılır. \(\blacksquare\)
Alıştırma 1.12 (Aynı Türden Beş Kart) \(52\) kartlık bir desteden rastgele çekilen \(5\) kartın hepsinin aynı türden (hepsi kupa, hepsi karo, hepsi maça ya da hepsi sinek) olma olasılığını tam kesir olarak ve yüzde olarak bulunuz.
Çözüm
Sayalım. Bütün eşit olasılıklı eller \(\binom{52}{5}\) tanedir. Aynı türden bir el için önce türü (\(4\) yol), sonra o türün \(13\) kartından \(5\)’ini (\(\binom{13}{5}\) yol) seçeriz. Olasılık
\[P = \frac{4\binom{13}{5}}{\binom{52}{5}}\]
olur.
Python ile. Binom katsayıları math.comb ile tam sayı olarak, oran da Fraction ile tam kesir olarak hesaplanır:
import math
from fractions import Fraction
total = math.comb(52, 5)
flush = 4 * math.comb(13, 5)
P = Fraction(flush, total)
print(total, flush)
print(P, f"{float(P):.4%}")Çıktı:
2598960 5148
33/16660 0.1981%
Sonuç. \(P = \frac{5148}{2598960} = \frac{33}{16660} \approx 0{,}00198\), yani yaklaşık yüzde \(0{,}198\)’dir: bin elden yalnız ikisi aynı türdendir. \(\blacksquare\)
Bu bölümde Python’ı bir hesap makinesi gibi kullandık: kod yukarıdan aşağıya bir kez çalıştı ve döngüyü yalnız bir önizleme olarak gördük. Gerçek programlar karar verir, bir koşul sağlanana kadar tekrar eder ve tekrar tekrar kullanılabilecek parçalar tanımlar. Bir sonraki bölüm Kontrol Yapıları ve Fonksiyonlar, if, for, while yapılarını ve kendi fonksiyonlarımızı yazmayı Öklid algoritması, Collatz dizisi ve Newton yöntemiyle karekök gibi tanıdık problemler üzerinde öğretiyor.