AST415 Astronomide Sayısal Çözümleme - I

Ders - 09 Python'da Sınıf (Class) Yapısına Giriş

Doç. Dr. Özgür Baştürk
Ankara Üniversitesi, Astronomi ve Uzay Bilimleri Bölümü
obasturk at ankara.edu.tr
http://ozgur.astrotux.org

Sınf Yapısına Neden İhtiyaç Duyulur?

En basit tanımıyla sınıf, bir veri setini (değişkenler), bu veri seti üzerinde işlemler gerçekleştiren fonksiyonlarla birlikte paketlemeye verilen isimdir. Sınıflara neden ihtiyaç duyulduğu aşağıdaki örnek üzerinden takip edilebilir.

Örnek: Dikey Atış ve Harmonik Hareket

Gerek dikey atış, gerekse harmonik hareket problemi dersin başından beri kullanılan örnekler olduğu için sınıflara olan ihtiyacı örneklemek için de seçilmiştir. Problemlerin temelinde sırasıyla $y$ ve $g$ değişkenlerinin alacağı değerleri belirlemek yatar.

$$ y(t) = V_0 t - \frac{1}{2} g t^2 $$$$ g(x; A, a) = A e^{-a x} $$

Yukarıda metamatiksel ifadeleri verilen bu problemlerden dikey atış probleminde $y$ (düşey konum) sadece $t$ bağımsız değişkeninin bir fonksiyonudur. Ancak $V_0$ (ilk hız) da programcılık bağlamında baktığınızda bir değişkendir. $g$ (yer çekimi ivmesi) ise problemin yapısına bağlı olarak (sadece Dünya dikkate alındığında) bir sabit şekinde düşünülebilir. Bu durumda fonksiyon $y = y(t; V_0)$ şeklinde düşünülebilir. Benzer şekilde harmonik hareket de $g = g(x; A, a)$ şeklinde düşünülebiilr. Bu iki problemi çözmek üzere yazılabilecek iki Python fonksiyonu aşağıda verilmiştir.

In [1]:
def y(t, v0):
    g = 9.81
    return v0*t - 0.5*g*t**2

def g(x, A, a):
    from math import exp
    return A*exp(-a*x)

! Problem: Matematiksel fonksiyonlar üzerine uygulanabilecek pek çok başka fonksiyon, tek değişkenli bir fonksiyonun bir programlama diliyle yazılmış halinin de sadece bir argümanı olduğunu varsayar. Örnek olarak bir $f(x)$ fonksiyonunun $x$ noktasındaki türevini hesaplayan $turev$ fonksiyonunu düşünelim. Bu fonksiyon yukarıda verilen iki fonksiyon için de çalışmaz!

$$ f^{\prime}(x) \approx \frac{f(x+h) - f(x)}{h} $$
In [2]:
def turev(f, x, h=1E-10):
    return (f(x+h) - f(x)) / h

Problemin Kötü Bir Çözümü:

Global Değişken Kullanmak

Fonksiyonlar aşağıdaki şekilde değiştirilir ve bu fonksiyonları çağırmadan önce $V_0$ ve $A$, $a$ değişkenleri global değişken olarak tanımlanırsa amaca ulaşılmış, türev fonksiyonunun da üzerinde kullanılabileceği daha genel bir yapı oluşturmuş olunur.

In [3]:
def y(t):
    g = 9.81
    return v0*t - 0.5*g*t**2

def g(x):
    from math import exp
    return A*exp(-a*x)
In [4]:
v0 = 1
dy = turev(y, x=1)
A = 1; a = 0.1
dg = turev(g, x=1.5)

Ancak global değişken kullanımı genellikle “kötü bir programcılık” pratiği olarak değerlendirilir. Bunun bir nedeni örneğin $y$ fonksiyonunu farklı $V_0$ değerleri için her çalıştırışımızda $v0$ değişkenini yeniden tanılmamamız gerekliliği, diğer bir nedeni ise $v0$, $A$, $a$ değişkenlerinin programımızın başka bir noktasında değiştirilme ihtimalidir. Uzun ve global değişkenlerin sık kullanıldığı bir programda bu durumu kontrol etmek hiç kolay olmayabilir.

Sınıf yapısı tüm bu problemleri çözer ve “iyi programcılık” sınıf yapısını doğru ve yerli yerinde kullanmaktan geçer!

Bir Fonksiyonu Sınıf Yapısı İçerisinde Temsil Etmek

Sınıf yapısı fonksiyonları ve değişkenleri bir arada tek bir birim halinde tutar. Değişkenler sınıfın içerisindeki tüm fonksiyonlar tarafında “görülür”. Bir başka deyişle bir sınıfın içinde tanımlı bir değişken o sınıfın içerisindeki tüm fonksiyonların görebildiği bir global değişken gibi davranır.

Dikey atış problemine iyi bir programcılık çözümü, zamanı bağımsız değişken kabul eden bir fonksiyon (y(t)) ve $V_0$ ile $g$'ye ulaşımın sağlandığı bir sınıf kullanılarak getirilebilir.

y(t) fonksiyonuna ek olarak sınıf yapısında genellikle bulunan ve adı her zaman \__init__ olan ve tüm sınıf yapısında geçerli değişikenleri başlatan bir fonksiyona daha ihtiyaç duyulur. Python programcılığında sınıf isimleri genellikle büyük harfle başlayacak şekilde verilir.

Sonuç olarak \__init__ fonksiyonu ile düşey konumu hesaplayacak bir fonksiyon (dusey_konum) ile iki de sınıf değişkeni ($v0$ ve $g$) bulunmaktadır. Aşağıda bu sınıfın nasıl tanımlandığı görülmektedir.

In [5]:
class Y:
    def __init__(self,v0):
        self.v0 = v0
        self.g = 9.81

    def dusey_konum(self,t):
        return self.v0*t - 0.5*self.g*t**2

self parametresinin ne işe yaradığı öncelikle yukarıda verilen sınıf yapısının düşey konumu nasıl hesapladığına bakılarak görülebilir. Oluşturulan $Y$ sınıfı başlatıldığında $Y$ adında yeni bir veri türü yaratır. Bu veri türü üzerinden yeni nesneler tanımlanabilir. Kullanıcı tarafından tanımlanan bir sınıfın nesnelerine olgu (ing. instance) adı verilir. Liste (list), demet (tuple), metin (string), noktalı sayı (float), tam sayı (integer) gibi nesneler özünde bu isimlerle yaratılmış birer Python sınıfıdır.

Aşağıdaki ifade y değişkenine bağlı bir olgu (instance) yaratır.

In [6]:
y = Y(3)

Python bu ifadedeki $Y(3)$ 'ü hemen $Y$ sınıfındaki \__init__ fonksiyonunu çağırmak üzere kullanır. Bu çağrı yapılırken kullanılan değer(ler) (burada sadece 3 nümerik değeri), \__init__ fonksiyonunda self parametresinin hemen arkasından gelen değişken(ler)e transfer edilir (örnekte $v0$ bu şekilde 3 değerini alır). Sınıf yapısındaki fonksiyonlarda self parametresine hiçbir değer gönderilmez. Gönderilen değer ya da değerler bu parametreden sonra gelen değişkenlere atanır.

y olgusuyla (instance) $t = 0.1$ saniye ve $V_0 = 3$ m/s için düşey konum aşağıdaki ifadeyle elde edilebilir.

In [7]:
v = y.dusey_konum(0.1)

Görüldüğü üzere dusey_konum fonksiyonundaki self parametresine de değer gönderilmemekte, 0.1 değeri fonksiyon tanımında ondan hemen sonra gelen $t$ değişkenine atanmaktadır.

$y$ olgusunun parametrelerine (fonksiyon ve değişkenlerine), bu olgunun adının arkasına istenen parametreyi koyarak ulaşmak mümkündür.

In [8]:
print(y.v0)
3

Bir sınıf nesnesine olgu (instance), sınıftaki fonksiyonlara metotlar (method), değişkenlere (veri) ise öznitelikler (attribute) denir. Örnekteki $Y$ sınıfında iki metot (__init ve dusey_konum), iki de öznitelik (attribute) (v0,g) bulunmaktadır. Tüm Python fonksiyonlarında olduğu gibi isimlendirme kurallarına uymak koşuluyla metot ve öznitelik isimleri de özgürce seçilebilir. Ancak başlatıcı (ing. constructor) fonksiyonun adı \init__ olarak verilmek zorundadır. Aksi takdirde yeni olgular (instances) oluştururken bu fonksiyon (metot) otomatik olarak çağrılamaz ve istenen öznitelikler (attributes) oluşmaz.

Herhangi bir metot herhangi bir işi yapmak için oluşturulurken, __init__ metodu öznitelikleri (sınıf değişkenleri, attribute) yaratmak için oluşturulur.

self Değişkeni: Oluşturulan olguyu (instance, örnekte $y$) __init__ fonksiyonu içerisinde tutan değişkendir.

$y = Y(3)$ yazıldığı vakit, Python bu ifadeyi geri planda "Y.__init__(y,3)" ifadesine dönüştürür. Yani self.v0 yazıldığı vakit de $y.v0$ özniteliği “başlatılmış” olur.

Aynı şekilde konum = y.dusey_konum(0.1) yazıldığı vakit Python bu ifadeyi konum = y.dusey_konum(y,0.1) 'e dönüştürür.

Dolayısı ile dusey_konum fonksiyonu içerisindeki $self.v0*t – 0.5*self.g*t**2$ ifadesi $y.v0*t - 0.5*y.g*t**2$ ile aynı işlevi görür. Özet olarak self, yaratılan olgunun (instance) yerini tutar.

self değişkeni ile ilgili kurallar aşağıdaki gibidir:

  • Her sınıf metodunun ilk argümanı self değişkeni olmak zorundadır!
  • self, sınıfın (herhangi) bir olgusunun (instance) yerini tutar!
  • Sınıfın içindeki diğer metot ve özniteliklere ulaşmak için, ulaşılmak istenen metot ya da özniteliğin adı self değişkenin arkasına yazılır (self.metotadi ya da self.degiskenadi gibi)
  • Sınıfın fonksiyonları (metotlar) çağrılırken self bir argüman olarak verilmez. Fonksiyona (varsa) gönderilen değer self argümanından bir sonrakine atanır!

Bir sınıfa istenildiği kadar metot ya da öznitelik eklenebilir. Dikey atış problemini çözmek üzere geliştirilen $Y$ sınıfına bir ekrana formül basan bir $formul$ fonksiyonu aşağıdaki şekilde eklenebilir.

In [9]:
def formul(self):
    return 'v0*t - 0.5*g*t**2; v0={:g}'.format(self.v0)

Bu durumda $Y$ sınıfı aşağıdaki şekle dönüşmüş olur.

In [10]:
class Y:
    def __init__(self,v0):
        self.v0 = v0
        self.g = 9.81

    def dusey_konum(self,t):
        return self.v0*t - 0.5*self.g*t**2
    
    def formul(self):
        return 'v0*t - 0.5*g*t**2; v0={:g}'.format(self.v0)

Programın en altına aşağıdaki satırlar eklenerek çalıştırılıp, çıktısı böylece güzel bir şekilde gösterilebilir. Unutulmaması gereken, ana programın class ifadesi ile aynı düzeyde (aynı miktarda bloklanmış şekilde) yazılması gerekliliğidir.

In [11]:
y = Y(5)
t = 0.2
v = y.dusey_konum(t)
print('y(t={:g}; v0={:g}) = {:g}'.format(t, y.v0, v))
print(y.formul())
y(t=0.2; v0=5) = 0.8038
v0*t - 0.5*g*t**2; v0=5

Farklı $V_0$ değerleri için farkı $y$ örnekleri oluşturularak, bu örneklerin her birinin sonucu herhangi bir fonksiyona gönderilebilir. Örnek olarak $turev$ fonksiyonuna bu sonuçlar ve fonksiyonlar gönderilebilir. Böylece bu fonksiyona görünürde sadece bir değer ($t$) gönderildiği halde $y$ örnekleri aracılığıyla $v0$ ve $g$'ye de erişim sağlanır ve istendiği takdirde bu değerler de değiştirilebilir! Böylece problem çözülmüş olur!

In [12]:
def turev(f, x, h=1E-10):
    return (f(x+h) - f(x)) / h
In [13]:
y1 = Y(1)
y2 = Y(1.5)
y3 = Y(-3)
dy1dt = turev(y1.dusey_konum,0.1)
dy2dt = turev(y2.dusey_konum,0.1)
dy3dt = turev(y3.dusey_konum,0.2)
print(dy1dt, dy2dt, dy3dt)
0.019000009898739734 0.5189998431021081 -4.96200081023801

Sonuç olarak elde edilen kodun tamamı bir miktar iç dokümantasyonla aşağıdaki şekilde oluşmuş olur.

In [14]:
class Y:
    """
    Dikey atilan bir cismin t anindaki dusey konumunu hesaplayan sinif
    Metotlar (Methods):
    __init__(v0): baslangici hizi v0 'i belirler
    dusey_konum(t): cismin t'nin fonksiyonu olarak dusey konumunu hesaplar
    formul(): formulu ekrana yazdirir
    Oznitelikler (Attributes):
    v0: cismin ilk hizi (t=0 anindaki hiz)
    g: yercekimi ivmesi (sabit)
    Kullanim:
    >>> y = Y(3)
    >>> konum1 = y.value(0.1)
    >>> konum22 = y.value(0.3)
    >>> print y.formul()
    v0*t - 0.5*g*t**2; v0=3
    """
    def __init__(self,v0):
        self.v0 = v0
        self.g = 9.81
        
    def dusey_konum(self,t):
        return self.v0*t - 0.5*self.g*t**2

    def formul(self):
        return 'v0*t - 0.5*g*t**2; v0=%g' % self.v0

def turev(f, x, h=1E-10):
    return (f(x+h) - f(x)) / h

y = Y(5)
t = 0.2
v = y.dusey_konum(t)
print('y(t={:g}; v0={:g}) = {:g}'.format(t, y.v0, v))
print(y.formul())

y1 = Y(1)
y2 = Y(1.5)
y3 = Y(-3)
dy1dt = turev(y1.dusey_konum,0.1)
dy2dt = turev(y2.dusey_konum,0.1)
dy3dt = turev(y3.dusey_konum,0.2)
print(dy1dt, dy2dt, dy3dt)
y(t=0.2; v0=5) = 0.8038
v0*t - 0.5*g*t**2; v0=5
0.019000009898739734 0.5189998431021081 -4.96200081023801

Örnek: Jeans Kütlesi Hesabı

Bir gaz bulutunun kendi çekim etkisi altında çökerek yıldız oluşturabileceği limit kütleye Jeans Kütlesi adı verilir. Jeans kütlesi gaz bulutunun sıcaklığı ($T$), ortalama molekül ağırlığı ($\mu$) ve ortalama yoğunluğa ($\rho$) bağlıdır. Yıldız oluşumunun gerçekleştiği gaz bulutlarının ortalama molekül ağırlığı ile yoğunluğunu ölçmek kolay olmadığı için genellikle bu değerler teorik bazı değerlere eşit kabul edilerek işlem yapılır. Bu nedenle Jeans Kütlesi, sıcaklığın temel bağımsız parametre olarak kabul edilebileceği M($T$; $\mu$, $\rho$)) bir fonksiyonla ifade edilebilir. $k$ (Boltzman sabiti), $m_H$ (Hidrojen atomunun kütlesi) ve $G$ (evrensel çekim sabiti) ise fonksiyonun sabitleridir. $\pi$ ise kolaylıkla math modülünden çekilebilecek matematiksel bir sabittir.

$$ M_{Jeans} = (\frac{5 k T}{G \mu m_H})^{\frac{3}{2}} (\frac{3}{4 \pi \rho})^{\frac{1}{2}} $$

Bu hesap için kodlanabilecek sınıf doğal olarak bir başlatıcı fonksiyon (init) ve bir de hesabı yapan fonksiyon ($hesap$) içermelidir. Sınıfın adı $JeansKutlesi$ olarak belirlenmiş olsun.

In [15]:
class JeansKutlesi:
    """
    Bir gaz bulutunun kendi cekim etkisi altinda cokmesi icin sahip olmasi
    gereken limit kutleye Jean's Limiti ya da Jean's kutlesi adi verilir.
    Bu sinif bu limit kutleyi hesaplamaktadir.
    Metotlar (Methods):
    __init__(mu,rho): Gaz bulutunun yogunlugu (rho) ve kimyasal yapisini
    (ortalama molekul agirligi, mu) belirleyen baslatici metot.
    hesap(T): Verilen bir gaz bulutu icin sicakliga bagli olarak Jean's kutlesi
    hesabini yapan metot
    Oznitelikler (Attributes):
    mu: Ortalama molekul agirligi (kg)
    rho: Ortalama yogunluk (kg / m^3)
    k: Boltzmann sabiti
    mH: Hidrojen atomunun kutlesi (kg)
    G: Evrensel cekim sabiti (m^3 / (kgs^2))
    T: sicaklik (K)
    Kullanim:
    >>> m = JeansKutlesi(100)
    >>> Mj = m.hesap(mu=2.,rho=3.3e-18)
    >>> print Mj
    """
    def __init__(self,mu,rho):
        # Boltzman sabiti
        self.k = 1.3806488e-23 # J/K
        # Hidrojen atomunun kutlesi
        u = 1.660538921e-27 # kg (atomik birim kutle)
        self.mH = 1.00784*u # kg
        # Evrensel cekim sabiti
        self.G = 6.67408e-11 # m3 / (kg s^2)
        # Ortalama Molekul Agirligi ve Ortalama Yogunluk
        self.mu,self.rho = mu,rho

    def hesap(self,T):
        from math import pi
        mu,rho,k,mH,G = self.mu,self.rho,self.k,self.mH,self.G
        M = ((5*k*T)/(G*mu*mH))**(3./2.)*(3./(4*pi*rho))**(1./2.)
        return M

Sınıfın ne şekilde kullanılabileceği aşağıdaki şekilde örneklenmiştir. $T$ = 10, 50, 100, 250, 500, 1000 K sıcaklığında, ortalama molekül kütlesi $\mu$ = 2, yoğunluğu $\rho = 3.3 \times 10^{-18} kg m^{-3}$ bir bulutun kendi külte çekim etkisi altında çökmesi için sahip olması gereken minimum kütleyi scaıkligin bir fonksiyonu olarak grafik eden bu kodda $JeansKutlesi$ sınıfı fonksiyonları kullanılmaktadır.

In [16]:
# Ortalama molekul kutlesi 2, bulutun yogunlugu 3.3e-18 kg/m^3 olsun
m = JeansKutlesi(mu=2.0,rho=3.3e-18)
# Farkli sicakliklarda boyle bir gaz bulutunun kendi cekim etkisi altinda
# cokebilmesi icin hangi kutleye sahip olmasi gerektigine bakilabilir
import numpy as np
T = np.array([10,50,100,250,500,1000])
Mj = m.hesap(T)
# Kutle gunes kutlesi cinsinden ifade edildiginde
Mgunes = 1.989e30 #kg
Mj = Mj / Mgunes
# Ornek olarak sicakliga 
from matplotlib import pyplot as plt
plt.plot(T,Mj,"ro")
plt.xlim((-100,1100))
plt.ylim((-1000,25000))
plt.xlabel("T [K]")
plt.ylabel("M [$M_{\odot}$]")
plt.show()

Örnek: Çember Nesnesi

Geometrik şekiler (örneğin bir çember) sınıf kavramınıın anlaşılabilmesi açısından iyi bir örnek oluşturabilir. Bir çemberin merkez koordinatları ($x_0$,$y_0$) ve yarıçapı ($R$) ile tekil olarak tanımlanabilir. Bu üç sayıyı bir sınıfın öznitelikleri olarak değerlendirebilliriz. Bu sayılar başlatıcı metotta (__init__) başlatılabilir. Sınıfın diğer metotları ise çemberin sırasıyla alan ve çevresini hesaplayan $alan$ ve $cevre$ olabilir.

In [17]:
class Cember:
    def __init__(self, x0, y0, R):
        self.x0, self.y0, self.R = x0, y0, R
    def alan(self):
        from math import pi
        return pi*self.R**2
    def cevre(self):
        from math import pi
        return 2*pi*self.R

Sınıfın kullanımına bir örnek aşağıda verilmiştir:

In [18]:
c = Cember(2,-1,5)
r,x,y,A,C = c.R, c.x0, c.y0, c.alan(), c.cevre()
print("""
      {:g} yaricapina sahip merkez koordinatlari ({:g},{:g}) olan 
      bir cemberin alani {:g}, cevresi {:g} dir
      """.\
      format(r, x, y, A, C))
      5 yaricapina sahip merkez koordinatlari (2,-1) olan 
      bir cemberin alani 78.5398, cevresi 31.4159 dir
      

Bu kavram pek çok geometrik şeklin (dikdörtgen, üçgen, elips, dikdörtgenler prizması olarak düşünülebilecek bir kutu, küre …) alan ve çevresini hesap etmek üzere uygulanabilir.

Programcılıkta bir problemin genellikle pek çok çözümü bulunur. Yukarıdaki örnekte çemberin merkez koordinatları ve yarıçapı bir listenin üyeleri olarak düşünülebilir ve metotlar buna uygun olarak da düzenlenebilir.

In [19]:
class Cember2:
    def __init__(self, x0, y0, R):
        self.cember = [x0, y0, R]
    def alan(self):
        from math import pi
        return pi*self.cember[2]**2
    def cevre(self):
        from math import pi
        return 2*pi*self.cember[2]
In [20]:
c2 = Cember2(2,-1,5)
Rxy,A,C = c2.cember, c2.alan(), c2.cevre()
print("""
      {:g} yaricapina sahip merkez koordinatlari ({:g},{:g}) olan 
      bir cemberin alani {:g}, cevresi {:g} dir
      """.\
      format(Rxy[0], Rxy[1], Rxy[2], A, C))
      2 yaricapina sahip merkez koordinatlari (-1,5) olan 
      bir cemberin alani 78.5398, cevresi 31.4159 dir
      

Ya da çember, koordinatları ve yarıçapını anahtar olarak alan sözlük (dictionary) türünde bir öznitelik olarak da tanımlanabilir.

In [21]:
class Cember3:
    def __init__(self, x0, y0, R):
        self.cember = {'merkez':(2,-1),'yaricap':5}
    def alan(self):
        from math import pi
        return pi*self.cember['yaricap']**2
    def cevre(self):
        from math import pi
        return 2*pi*self.cember['yaricap']
In [22]:
c3 = Cember3(2,-1,5)
Rxy,A,C = c3.cember, c3.alan(), c3.cevre()
print("""
      {:g} yaricapina sahip merkez koordinatlari {:} olan 
      bir cemberin alani {:g}, cevresi {:g} dir
      """.\
      format(Rxy['yaricap'], Rxy['merkez'], A, C))
      5 yaricapina sahip merkez koordinatlari (2, -1) olan 
      bir cemberin alani 78.5398, cevresi 31.4159 dir
      

Özel Metotlar

call Metodu

Daha önce gördüğünüz başlatıcı metot __init gibi “\” ile başlayıp biten, başka bazı “özel metotlar” da bulunmaktadır. Bu metotlar olgular arasında aritmetik işlemler, karşılaştırmalar (>, <, ==, != gibi) yapmak, sıradan bir fonksiyon çağırır gibi olguları çağırmak ve bir olgunun Boolean ($True$ ya da $False$) değerini belrilemek gibi işlevleri görürler.

Bir olgunun (instance) tıpkı bir fonksyon gibi çağrılabilmesi için (örneğin dikey atışta düşey konum hesaplayan dusey_konum(t) fonksiyonunu çağırmak için y.dusey_konum(t) yerine y(t) yazılıp aynı hesaplama yaptırmak istenirse) kullanılması gereken özel metot \__call__ metodudur. Pek çok sınıf bu şekilde doğrudan fonksiyon olarak da kullanılmak üzere bir \__call__ metoduna sahiptir.

In [23]:
class Y:
    def __init__(self,v0):
        self.v0 = v0
        self.g = 9.81

    def __call__(self,t):
        return self.v0*t - 0.5*self.g*t**2
    
    def formul(self):
        return 'v0*t - 0.5*g*t**2; v0={:g}'.format(self.v0)

İyi bir programcılık prensibi matematiksel bir fonksiyon işlevi içeren tüm sınıfların \__call__ metoduna sahip olmaları ve işlemlerin bu metot içerisinde yapılmasıdır. Bu şekilde \__call__ metodu içeren tüm olgular, “çağrılabilir nesneler” (ing. callable objects) olarak tanımlanır (tıpkı fonksiyonlar gibi!). Bu yolla programlanmış $Y$ sınıfının bir olgusu, daha önce tanımlanan $turev$ fonksiyonuna bir fonksiyon argümanı olarak gönderilebilir.

In [24]:
y = Y(v0 = 5)
dydt = turev(y,0.1)
print(dydt)
4.019000687804919

Örnek: Nümerik Türev

Python diline entegre edilmiş bir $f(x)$ matematiksel fonksiyonu için bir Python fonksiyonu gibi davranan ve $f(x)$'in türevini alan ($^{\prime}(x)$) nesnesi tanımlanmak isteniyor olsun. $Turev$ adı verilen sınıfın $dfdx$ olgusu tıpkı bir fonksiyon gibi örneğin $x^3$ fonksiyonunun türevini alıp ($3x^2$) herhangi bir $x$ için değerini döndürebilmelidir. Türev fonksiyonu için basit bir yaklaşım kulllılabilir. Daha iyi bir hassasiyet için başka bir yaklaşım (nümerik algoritma) kullanmak gerekecektir.

In [25]:
class Turev:
    def __init__(self, f, h=1E-9):
        self.f = f
        self.h = float(h)
    def __call__(self, x):
        f, h = self.f, self.h # daha kisa yazabimek icin donusum
        return (f(x+h) - f(x))/h

Örnek olarak sinüs fonksiyonunun $x = \pi$ noktasındaki türevini alıp, gerçek değeri ile ($sin^{\prime}(x=\pi) = cos(x=\pi) = -1$) ile karşılaştırarak kodun doğru çalışıp çalışmadığı kontrol edilebilir.

In [26]:
from math import cos,sin,pi
df = Turev(sin)
x = pi
print("f'(x) = sin'(x=pi) =  {:g}".format(df(x)))
print("cos(x=pi) =  {:g}".format(cos(x)))
f'(x) = sin'(x=pi) =  -1
cos(x=pi) =  -1

Bir başka örnek olarak $x^3$ fonksiyonunu tanımlayarak ve $x = 1$ noktasındaki türevi alınıp, gerçek değeri ($f^{\prime}(x= 1) = 3x^2 = 3$) ile karşılaştırılabilir.

In [27]:
def g(t):
    return t**3
dg = Turev(g)
t = 1
print("g'(x=1) =  {:g}".format(dg(t)))
g'(x=1) =  3

Örnek: Nümerik İntegrasyon

Python diline entegre edilmiş bir $f(x)$ matematiksel fonksiyonu için bir Python fonksiyonu gibi davranan ve $f(x)$'in integralini alan bir nesne tanımlanmak isteniyor olsun. Bu nesnein türü $Integral$ ve nümerik integrasyon yöntemi de yamuk yöntemi olsun. Yöntem aşağıdaki şekilde ifade edilir.

$$ \int_{a}^{x} f(t)dt = h ~ (\frac{1}{2} f(a) + \sum\limits_{i = 1}^{n-1} f(a + ih) + \frac{1}{2} f(x)) $$
In [28]:
class Integral:
    def __init__(self, f, a, n=100):
        self.f, self.a, self.n = f, a, n
    def yamuk_yontemi(self):
        f, a, n = self.f, self.a, self.n
        h = (x-a)/float(n)
        I = 0.5*f(a)
        for i in range(1, n):
            I += f(a + i*h)
        I += 0.5*f(x)
        I *= h
        return I
    def __call__(self, x):
        return self.yamuk_yontemi()

Aslında yamuk_yonteminin tamamı \__call__ fonksiyonunun içine de yazılabilirdi ama başka yöntemler de bu sınıfın altına farklı fonksiyonlar olarak kodlanıp, bunlardan biri varsayılan integrasyon yöntemi olarak \__call__ fonksiyonu tarafından çağrılabilir. Bu yöntem \__call__ fonksiyonunu yapısının da basit tutulmuş olmasını sağlar. Bu sınıfın kullanıldığı örnek bir çalışma aşağıdaki gibidir.

In [29]:
from math import sin,pi
G = Integral(sin,0,200)
print(G(2*pi))
1.9999588764792162

str Metodu

Bir başka önemli özel metot \__str__ özel metodudur. Bu metot bir sınıfa ait olgu ekranda gösterilmek ($print$) istendiğinde çağrılır. Eğer olgunun bir \__str__ metodu varsa ve bu metot bir metin (string) döndürüyorsa döndürülen metin, aksi takdirde sınıfın adı yazdırılır. Örneğin dikey atış probleminin çözümü için yazdığımız $Y$ sınıfının bir olgusunu ekrana yazdırmak üzere \__str__ metodu $Y$ sınıfında aynı görevi gören $formul$ fonksiyonunun yerini alabilir.

In [30]:
class Y:
    def __init__(self,v0):
        self.v0 = v0
        self.g = 9.81
        
    def __call__(self,t):
        return self.v0*t - 0.5*self.g*t**2

    def __str__(self):
        return 'v0*t - 0.5*g*t**2; v0=%g' % self.v0

Kodun bir örnek çalışmasıyla \__str__ metodunun fonksiyonalitesi görülebilir.

In [31]:
v0 = 1.5
y = Y(v0)
t = 0.2
print(y(t))
print(y)
0.1038
v0*t - 0.5*g*t**2; v0=1.5

add Metodu

Eğer bir $C$ sınıfında tanımlı bir \__add__ özel metodu bulunuyor ise bu sınıfın iki ayrı olgusu (instance) $a$ ve $b$ toplanabilir ve toplam (yeni bir olgu olarak) bu metodla döndürülür.

Örnek Problem (Polinomlar): Polinomlar üzerinde işlem yapacak bir program yazmak üzere $Polinom$ adında bir sınıf tanımlamak üzere sınıfın başlatıcı fonksiyonuna (\__init__ polinomun katsayıları bir sözlük halinde geçirilmek isteniyor olsun. Polinom({0:1,2:-1,3:2]) bu durumda $1 – x^2 + 2x^3$ polinomunun karşılığı olur. İki polinom toplanabileceği için sınıfın bir \__add__ metodu olması doğaldır. Verilen bir $x$ değeri için polinomun değerini döndürecek bir \__call__ metodu da olmalıdır.

In [32]:
class Polinom:
    def __init__(self, katsayilar):
        self.katsayi = katsayilar
    def __call__(self, x):
        s = 0
        for k in self.katsayi:
            s += self.katsayi[k]*x**k
        return s
    def __add__(self, diger):
        # Oncelikle p1'i katsayilarini toplama atalim
        toplam_katsayi = dict(self.katsayi)
        # p2'ninkileri de atarken p1'le esit olanlari p2'den alalim
        toplam_katsayi.update(diger.katsayi)
        # p1 ve p2'nin esit katsayilarini toplayalarak duzeltelim
        for k in self.katsayi:
            for j in diger.katsayi:
                if k == j:
                    toplam_katsayi[k] = self.katsayi[k] + diger.katsayi[j]
        return Polinom(toplam_katsayi)

Şu ana kadar $Polinom$ sınıfı için tanımlanmış olan iki fonksiyon için birer örnek aşağıda verilmiştir.

$$ p_1(x) = 1 – x $$


$$ p_2(x) = x – 6x^4 - x^5 $$

In [33]:
p1 = Polinom({0:1,1:-1})
p2 = Polinom({1:1,4:-6,5:-1})
x = 1
print("p1(x=1) = {:g}".format(p1(x)))
print("p2(x=1) = {:g}".format(p2(x)))
p1(x=1) = 0
p2(x=1) = -6
In [34]:
p3 = p1 + p2
print(p3.katsayi)
x = 1
print("p3(x=1) = {:g}".format(p3(x)))
{0: 1, 1: 0, 4: -6, 5: -1}
p3(x=1) = -6

mul Metodu

Polinom sınıfına bir de çarpma işlemi eklemek üzere \__mul__ özel metodu kullanılarak polinom nesneleri arasında çarpma işleminin nasıl yapılacağı da tanımlanabilir. Bu işlem biraz daha komplike bir matematiğe sahiptir.

$$ (\sum\limits_{i=0}^{M} c_i x^i) (\sum\limits_{j=0}^{N} d_j x^j) = \sum\limits_{i=0}^{M} \sum\limits_{j=0}^{N} c_i d_j x^{i+j} $$

Sonuçta iki toplam işlemi içiçe görünüyor dolayısıyla Python'a entegrasyon da içiçe iki döngü kullanmayı gerektirecektir. Ancak öncelikle yapılması gereken, sonucu saklamak üzere boş bir sözlük oluşturmaktır.

In [35]:
class Polinom:
    def __init__(self, katsayilar):
        self.katsayi = katsayilar
    def __call__(self, x):
        s = 0
        for k in self.katsayi:
            s += self.katsayi[k]*x**k
        return s
    def __add__(self, diger):
        # Oncelikle p1'i katsayilarini toplama atalim
        toplam_katsayi = dict(self.katsayi)
        # p2'ninkileri de atarken p1'le esit olanlari p2'den alalim
        toplam_katsayi.update(diger.katsayi)
        # p1 ve p2'nin esit katsayilarini toplayalarak duzeltelim
        for k in self.katsayi:
            for j in diger.katsayi:
                if k == j:
                    toplam_katsayi[k] = self.katsayi[k] + diger.katsayi[j]
        return Polinom(toplam_katsayi)
    def __mul__(self, diger):
        c = self.katsayi
        d = diger.katsayi
        carpim_katsayi = {}
        for i in c:
            for j in d:
                # eger i+j. kuvvetin katsayisi zaten olusmussa
                # carpimdan gelen katsayiyi uzerine ekle
                if i+j in carpim_katsayi:
                    carpim_katsayi[i+j] += c[i]*d[j]
                else:
                    carpim_katsayi[i+j] = c[i]*d[j]
        return Polinom(carpim_katsayi)

Örnekte, $p_4 = p_1 \times p_2$ işleminin sonucunun aşağıdaki şekilde oluşması beklenir:

$$ p_4(x) = p_1 \times p_2 = (1 – x) (x – 6x^4 - x^5) = x - x^2 - 6x^4 + 5x^5 + x^6 $$$$ p_4(x = 1) = 1 - 1^2 - 6 \times 1^4 + 5 \times 1^5 + 1^6 = 1 - 1 - 6 + 5 + 1 = 0 $$
In [36]:
p1 = Polinom({0:1,1:-1})
p2 = Polinom({1:1,4:-6,5:-1})
x = 1
p4 = p1*p2
print(p4.katsayi)
print(p4(x))
{1: 1, 4: -6, 5: 5, 2: -1, 6: 1}
0

Polinom Türevi

Polinom sınıfına ayrıca verilen polinomun aşağıdaki formüle göre türevini alan bir metot daha eklenebilir.

$$ \frac{d}{dx} \sum\limits_{x=0}^{n} c_i x^i = \sum\limits_{i = 1}^{n} i c_i x^{i - 1} $$

Polinomların \__str__ İle Ekrana Yazdırılması

Polinom sınıfına ayrıca verilen polinomun ekrana güzel bir şekilde yazdırılacağı bir \__str__ metodu da eklenmelidir. Burada hem $Polinom$ sınıfının hem de bu sınıfın nesnesinin sözlük nesne yapısı üzerine kurulu olmasının avantajı iyice belirginleşecektir. İstendiği takdirde aynı sınıf liste nesnesi üzerinden de kodlanabilirdi (deneyiniz!)

In [37]:
class Polinom:
    def __init__(self, katsayilar):
        self.katsayi = katsayilar
    def __call__(self, x):
        s = 0
        for k in self.katsayi:
            s += self.katsayi[k]*x**k
        return s
    def __add__(self, diger):
        # Oncelikle p1'i katsayilarini toplama atalim
        toplam_katsayi = dict(self.katsayi)
        # p2'ninkileri de atarken p1'le esit olanlari p2'den alalim
        toplam_katsayi.update(diger.katsayi)
        # p1 ve p2'nin esit katsayilarini toplayalarak duzeltelim
        for k in self.katsayi:
            for j in diger.katsayi:
                if k == j:
                    toplam_katsayi[k] = self.katsayi[k] + diger.katsayi[j]
        return Polinom(toplam_katsayi)
    def __mul__(self, diger):
        c = self.katsayi
        d = diger.katsayi
        carpim_katsayi = {}
        for i in c:
            for j in d:
                # eger i+j. kuvvetin katsayisi zaten olusmussa
                # carpimdan gelen katsayiyi uzerine ekle
                if i+j in carpim_katsayi:
                    carpim_katsayi[i+j] += c[i]*d[j]
                else:
                    carpim_katsayi[i+j] = c[i]*d[j]
        return Polinom(carpim_katsayi)
    def turev(self):
        """Yeni bir nesne dondurmeden gelen polinomun turevini alan metot"""
        turev_katsayi = {}
        for i in self.katsayi:
            turev_katsayi[i-1] = i*self.katsayi[i]
        return Polinom(turev_katsayi)
    def __str__(self):
        s = ''
        for i in self.katsayi:
            if self.katsayi[i] != 0:
                s += ' + %g*x^%d' % (self.katsayi[i], i)
        # ciktiyi guzellestir:
        s = s.replace('+ -', '- ')
        s = s.replace('x^0', '1')
        s = s.replace(' 1*', ' ')
        s = s.replace('x^1 ', 'x ')
        s = s.replace('x^1', 'x')
        if s[0:3] == ' + ': # basa gelen + isaretini kaldir
            s = s[3:]
        if s[0:3] == ' - ': # basa gelen - isaretini kaydir
            s = '-' + s[3:]
        return s
In [38]:
p1 = Polinom({0:1,1:-1})
p2 = Polinom({1:1,4:-6,5:-1})
print("p1 = ", p1)
print("p2 = ", p2)
p3 = p1 + p2
print("p3 = p1 + p2 = ", p3)
p4 = p1*p2
print("p4 = p1 * p2 = ", p4)
p5 = p2.turev()
print("p2 = ", p2)
print("p5 = p2' = ", p5)
p1 =  1 - x
p2 =  x - 6*x^4 - x^5
p3 = p1 + p2 =  1 - 6*x^4 - x^5
p4 = p1 * p2 =  x - 6*x^4 + 5*x^5 - x^2 + x^6
p2 =  x - 6*x^4 - x^5
p5 = p2' =  1 - 24*x^3 - 5*x^4

Aritmetik İşlemler ve Diğer Özel Metotlar

Bir sınıfın iki olgusu olan $a$ ve $b$ olguları için standart aritmetik işlemler aşağıdaki özel metotlarla tanımlanır.

  • a + b : a.__add__(b)
  • a – b : a.__sub__(b)
  • a * b : a.__mul__(b)
  • a / b : a.__div__(b)
  • a ** b : a__pow__(b)

Diğer kullanışlı özel metotlar:

  • a olgusunun uzunluğu: len(a) : a.__len__()
  • a olgusunun mutlak değeri: abs(a) : a.__abs__()
  • a == b : a.__eq__(b)
  • a > b : a.__gt__(b)
  • a >= b : a.__ge__(b)
  • a < b : a.__lt__(b)
  • a <= b : a.__le__(b)
  • a != b : a.__ne__(b)
  • -a : a.__neg__()

Bu özel metotlardan polinomlar için uygun olanları $Polinom$ sınıfına eklemeyi deneyiniz.

repr Metodu

jupyter (ya da ipython) gibi interaktif bir kabukta sadece olgunun adı yazıldığında, her ne kadar bir __str__ özel metodu olsa da Python öncelikle \__repr__ özel metodunu arar. Bu metot \__str__'ye çok benzer ancak olgunun içeriğinin ekrana yazımını sağlayan __str__ 'den farklı olarak olgunun içeriğinin tamamını temsil eder. Pek çok Python nesnesi için (int, float, complex, list, tuple, dict) \__repr__ ve \__str__ aynı çıktıyı verir.

Teknik olarak str(a) ifadesi a.\__str__() metodunu çağırırken, interaktif kabukta sadece a, a.\__repr__() metodunu çağırır.

Python'daki metin ($string$), tamsayı ($int$), kayan noktalı sayı ($float$), liste ($list$) gibi tüm nesnelerin birer \__repr__ metodu bulunmaktadır. Python bu nesneler ekrana getirilmek istendiğinde bu metoda bakarak nesnenin ekrana ne şekilde getirilmesi gerektiğine karar verir. Kayan noktalı sayılar bu nedenle 16 basamakla ekrana gelirler.

Aşağıdaki örnek incelenerek aradaki fark anlaşılabilir.

In [39]:
class BenimSinifim:
    def __init__(self):
        self.veri = 2
    def __str__(self):
        return 'In __str__: %s' % (self.veri)
In [40]:
bs = BenimSinifim()
print(bs)
bs
In __str__: 2
Out[40]:
<__main__.BenimSinifim at 0x7f8e3c41dcf8>

Bu durumdan kaçınmak için aşağıdaki bir \__repr() fonksiyonu sınıfa eklenebilir.

In [41]:
class BenimSinifim2:
    def __init__(self):
        self.veri = 2
    def __str__(self):
        return 'In __str__: %s' % (self.veri)
    def __repr__(self):
        return self.__str__() # ya da return str(self)
In [42]:
bs2 = BenimSinifim2()
print(bs2)
bs2
In __str__: 2
Out[42]:
In __str__: 2

Kullanıcıdan girdi alınırken sıklıkla kullanılan $eval(e)$ fonksiyonu argümanı olan $e$ metnini bir Python ifadesi olarak çalıştırır. \__repr__ özel metodunun asıl niteliği eval fonksiyonu uygulandığında Python ifadesi olarak çalıştırılabilecek (aynı olguyu tekrar oluşturabilecek) bir metin döndürmesidir.

Örneğin, bu derste dikey atış problemini çözmek üzere tanımladığımız Y sınıfında $v0 = 10$ için \__repr__ metodu 'Y(10)' metnini döndürmelidir ki $eval('Y(10)')$ ifadesi Y(10) ifadesi ile aynı işi görsün!

Aynı şekilde bu derste geliştirilen diğer sınıflarda da aynı düzenlemeler yapılıp, kodlar harici birer Python dosyasına kaydedlimiştir ve linkinden indirilebilir. Bu linkteki dosyaları bu Jupyter defteriyle aynı yere (ya da PYTHONPATH 'inize) kopyalanması durumunda aşağıdaki örneklerle aynı sonuçlar elde edilecektir.

Harici bir dosyada yer alan Python kodundaki bir sınıfa erişebilmek için onun öncelikle $import$ edilmesi gerektiği unutulmamalıdır.

In [43]:
from dikey_atis import Y
v0 = 1.5
y = Y(v0)
t = 0.2
print(y(t))
print(y)
y
0.1038
v0*t - 0.5*g*t**2; v0=1.5
Out[43]:
Y(v0=1.5)
In [44]:
from polinomlar import Polinom as p
p1 = p({2:2,0:-1})
print(p1)
p1
2*x^2 - 1
Out[44]:
Polinom(katsayilar={2: 2, 0: -1})

Örnek: Vektörel İşlemler

İki boyutlu bir düzlemde vektörler $(a,b)$ reel sayı çiftiyle tanımlanırlar. Vektörler üzerine tanımlanan bazı işlemler aşağıdaki gibidir:

$$ (a,b) + (c,d) = (a + c, b + d) $$$$ (a,b) - (c,d) = (a - c, b - d) $$$$ (a,b) . (c,d) = ac + bd $$$$ ||(a,b)|| = \sqrt{(a,b) . (a,b)} $$$$ (a,b) = (c,d) \Rightarrow a = c, b = d $$

Vektor adında bir sınıf oluşturulup, yukarıdaki işlemler de özel metotlardan yararlanarak tanımlanabilir. Vektör koordinatlarını göstermek üzere iki adet özniteliğe (attribute) (x,y) ve bir de çıktı veren metoda ihtiyaç olacaktır.

In [45]:
from math import sqrt
class Vektor:
    def __init__(self, x, y):
        self.x = x
        self.y = y
    def __add__(self, diger):
        return Vektor(self.x + diger.x, self.y + diger.y)
    def __sub__(self, diger):
        return Vektor(self.x - diger.x, self.y - diger.y)
    def __mul__(self, diger):
        return self.x*diger.x + self.y*diger.y
    def __abs__(self):
        return sqrt(self.x**2 + self.y**2)
    def __eq__(self, diger):
        fark = 1e-16 # iki reel sayiyi == operatoruyle karsilastirmak risklidir!
        return abs(self.x - diger.x) <= fark and abs(self.y - diger.y) <= fark
    def __str__(self):
        return '(%g, %g)' % (self.x, self.y)
    def __ne__(self, diger):
        return not self.__eq__(diger) 
In [46]:
u = Vektor(0,1)
v = Vektor(1,0)
w = Vektor(1,1)
a = u + v
print(a)
(1, 1)
In [47]:
a == w
Out[47]:
True
In [48]:
a = u - v
print(a)
(-1, 1)
In [49]:
a = u*v
print(a)
0

Statik Metot ve Öznitelikler

Her bir olgun (instance) kendi özniteliklerine (attribute) sahiptir. Buna ek olarak sınıfın farklı olguları arasında paylaşılan özniteliklere de ihtiyaç duyulur. Örneğin bir sınıftan kaç tane olgu üretildiğini tutan bir öznitelik (sınıf değişkenlerine öznitelik –attribute- diyoruz) faydalı olur. Bunun için özniteliği sınıfın metotlarıyla aynı hizadan (indentation level) başlatmak ve $self$ öneki (prefix) yerine sınıfın adını önek olarak kullanmak yeterlidir. Bu şekilde aynı sınıfın tüm olguları tarafından erişlebilen özniteliklere statik öznitelikler adı verilir.

In [50]:
class UzaydaNokta:
    sayac = 0
    def __init__(self,x,y,z):
        self.nokta = (x,y,z)
        UzaydaNokta.sayac += 1
In [51]:
p1 = UzaydaNokta(0,0,0)
UzaydaNokta.sayac
Out[51]:
1
In [52]:
for i in range(400):
    p = UzaydaNokta(i*0.5,i,i+1)
UzaydaNokta.sayac
Out[52]:
401

Şu ana kadar görülen tüm sınıf metotları da bir olgu tarafından “çağrılıyor” ve $self$ değişkeniyle “besleniyorlardı”. Herhangi bir olguya bağlı olmaksızın çalışan metotlar yaratmak da mümkündür. Bu durumda metot, bir sınıf yapısı içinde yer alması ve bu nedenle o sınıfın adının önek (prefix) olarak verilmesi gerekliliği dışında tipik bir Python fonksiyonu gibi davranır. Bu tür metotlar statik metotlar adı verilir. $Integral$ sınıfı içinde yer alan yamuk_yontemi fonksiyonu da böyle bir statik metottur.

In [53]:
class A:
    @staticmethod
    def mesajyaz(mesaj):
        print(mesaj)
In [54]:
A.mesajyaz('Merhaba Dunyali Biz Tostuz!')
Merhaba Dunyali Biz Tostuz!
In [55]:
a = A() # istenirse bir olgu da bu fonksiyonu kullanabilir!
a.mesajyaz('Tost degil benim canim, Dost!')
Tost degil benim canim, Dost!