3.1. Python Fonksiyonları

Kayıt Tarihi:

Son Güncelleme:

Özet:

Bu derste kendi fonksiyonlarımızı tanımlamanın ve kullanmanın yollarını öğreneceğiz. Fonksiyon tanımlama işlemi programlama dilleri için kritik öneme sahiptir ve Python'da bu işlem son derece basittir.

Anahtar Kelimeler: doc string · global değişken · hata fonksiyonu · lambda · lokal değişken · None · python fonksiyonu · return · Simpson kuralı · test fonksiyonu · yaklaşık türev

Programlarımızda sık kullanacağımız verileri değişken olarak tanımladığımız gibi sık yapacağımız işlemleri birer fonksiyon olarak tanımlayıp isimleriyle çağırdığımızda söz konusu işlemlerin tekrar kodlarını yazmadan yapılmasını sağlayabiliriz. Şu ana kadar range(), len(), math.sqrt() gibi bir çok hazır fonksiyon ile çalıştık, şimdi kendi fonksiyonlarımızı tanımlayıp kullanacağız.

Basit Fonksiyonlar

Örnek olarak ilk bölümde ele aldığımız $$y(t) = v_0t-\frac{1}{2}gt^2$$ matematik fonksiyonunu bir Python fonksiyonu olarak tanımlayalım.


def y(t):
    v0 = 2
    g = 9.81
    y = v0*t - 0.5*g*t**2
    return y

print y(0.1)

Python'da bir fonksiyon tanımlamak için satıra def ifadesi ile başlayıp hemen sonrasında yukarıda görüldüğü gibi fonksiyon ismi ve parametreleri belirtilir. Alt satırlarda sağdan hizalanmış satırlarda belirtilen değişkenler ile yapılacak işlemler kodlandıktan sonra return ifadesi ile başlayıp sonrasında fonksiyonun döndüreceği değerin belirtildiği bir satırla tanımı sonlandırırız. Bu örnekte tek bir parametresi ($t$) olan bir y(t) fonksiyonu tanımladık. Fonksiyon tanımında v0 = 2 ve g = 9.81 değişkenlerini kullanarak y = v0*t - 0.5*g*t**2 hesabını yaptırıp bunun sonucunu return değeri olarak tanımladık. Burada t değişkenini tanımlamadığımıza dikkat edin, bu değişkeni fonksiyonun bir parametresi olarak belirttik. Fonksiyonu tanımladıktan sonra artık programımız içinde herhangi bir yerde y(0.1) gibi fonksiyonu çağırarak ilgili işlemin sonucunu getirebiliriz, yukarıda bu işlemin sonucunu ekrana yazdırdık. Bu programın çıktısı aşağıdaki gibi olacaktır.


Terminal > python fonk1.py
0.15095

Parametreler, Değişkenler ve Dahası

Burada tanımladığımız v0, g ve y değişkenleri fonksyion içindeki lokal değişkenlerdir, fonksiyonun dışında ana programda tanımlanmış değişkenlere ise global değişkenler denir. Program içinde hesaplama yapılırken, örneğin v0*t - 0.5*g*t**2 hesabı yapılırken, Python burada geçen v0 ve g değişkenlerini önce lokal değişkenler içinde arar, bulamazsa sonra global değişkenlerde arar. Dolayısıyla yukarıdaki programda 2 ve 3 numaralı satırları silersek yine hatasız bir fonksiyon tanımlamış oluruz ama bu durumda programımızda fonksiyonu çağırmadan önce bu değişkenleri global olarak tanımlamamız gerekir. Aksi taktirde python bu değişkenlerin tanımlı olmadığını bildiren bir hata mesajı gösterecektir. Şunu da ekleyelim, lokal değişkenlere ilgili fonksiyon dışından erişemeyiz, bunlar fonksiyon işlemi yaptıktan sonra bellekten silinirler. Aşağıdaki program da aynı çıktıyı üretecektir.


def y(t):
    return v0*t - 0.5*g*t**2

v0 = 2
g = 9.81
print y(0.1)

Bir Python fonksiyonu birden fazla parametreye sahip olabilir, örneğin yukarıda tanımladığımız y(t) fonksiyonunu geliştirerek bir y(v0, t) fonksiyonu tanımlayabiliriz. Böylece fonksiyon sabit bir ilk hız için değil, verilecek olan herhangi bir ilk hız için de çalışabilir.


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

print y(2, 0.1)

Çok değişkenli fonksiyonlar çağrılırken parametre değerleri def satırında belirtilen sıra ile verilmesi gerekir fakat değişkenlerin isimleri açıkça belirtilirse sıranın önemi yoktur. Yani yukarıda tanımladığımız fonksiyonu y(t=0.1, v0=2) biçiminde çağırırsak da aynı sonucu alırız ama y(0.1, 2) biçiminde çağırırsak farklı bir işlem yaptırmış oluruz.

Python fonksiyonlarında bazı parametrelere varsayılan değerler tanımlayabiliriz. Böylece fonksiyon çağrılırken o parametreler verilmemişse fonksiyon belirtilen varsayılan değerleri kullanır, eğer bu parametreler belirtilerek fonksiyon çağrılırsa fonksiyon bu değerleri dikkate alır. Böyle parametrelere anahtar kelime argümanı (keyword argument) denir, varsayılan değeri olmayan sıradan parametrelere ise sıradan veya pozisyonel argüman (veya parametre) denir.


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

print y(2, 0.1)
print y(2, 0.1, 9.81)
print y(2, 0.1, 12.0)

Yukarıdaki örnekte görüldüğü gibi varsayılan değerli parametrelerin varsayılan değerleri def satırında belirtilir. Burada dikkat edilmesi gereken husus şudur, varsayılan değerli parametreler def saturında mutlaka pozisyonel parametrelerden sonra tanımlanmalıdır. Yukarıdaki programın çıktısı aşağıda görüldüğü gibi olacaktır.


Terminal > fonk4.py
0.15095
0.15095
0.14

Python'da basit fonksiyonları tek satırda kompakt olarak tanımlamak için bir lambda ifadesi vardır. Bu yöntemle örneğin $f(x)=x^2 + 3x$ fonksiyonunu f = lambda x: x**2 + 3*x biçiminde hızlıca tanımlayabiliriz. Bu yöntemle çok değişkenli fonksiyonlar ve varsayılan argümanlı fonksiyonlar da tanımlanabilir, aşağıdaki örnekleri inceleyin.


>>> y1 = lambda t: v0*t - 0.5*g*t**2
>>> y2 = lambda v0, t: v0*t - 0.5*g*t**2
>>> y3 = lambda v0, t, g=9.81: v0*t - 0.5*g*t**2
>>>
>>> v0 = 2; g = 9.81
>>>
>>> y1(0.1)
0.15095
>>> y2(3, 0.1)
0.25095
>>> y3(3, 0.1)
0.25095
>>> y3(3, 0.1, 1.62)
0.29190000000000005

Bir Python fonksiyonu birden fazla değer döndürebilir, bu değerleri return ifadesinden sonra virülle ayırarak belirtiriz. Bu durumda geri dönecek veri bir tuple nesnesidir ve elemanları da belirttiğimiz return değerleridir. Örnek olarak tanımladığımız y(v0, t, g=9.81) fonksiyonunu, fırlatılan topun belirtilen $t$ anındaki konumunun yanında hızını da döndürecek şekilde geliştirelim. Bu hızın $v0 - gt$ (konumun türevi) olarak hesaplandığını hatırlayın.


def y(v0, t, g=9.81):
    y = v0*t - 0.5*g*t**2
    v = v0 - g*t
    return y, v

v0 = 2
t = 0.1
g = 1.62
konum, hiz = y(v0, t, g)

print "g=%g, v0=%g, t=%g\nkonum: %g\nhiz: %g " % (g, v0, t, konum, hiz)

Burada konum ve hiz global değişkenlerini nasıl tanımladığımıza dikkat edin. Bu programın çıktısı aşağıda verilmiştir.


Terminal > python fonk5.py
g=1.62, v0=2, t=0.1
konum: 0.1919
hiz: 1.838

Bir Python fonksiyonunda return ifadesi bulunmayabilir, fonksiyon bir nesne döndürmeden bazı işlemler yapabilir. Aşağıdaki örneği inceleyin.


def y(v0, t, g=9.81):
    y = v0*t - 0.5*g*t**2
    v = v0 - g*t
    print "g=%g, v0=%g ve t=%g\nkonum: %g\nhiz: %g " % (g, v0, t, y, v)

y(2, 0.1, 1.62)

Burada fonksiyon bir return değeri döndürmek yerine hesaplama sonucu elde ettiği değerleri doğrudan yazdırıyor. Yani yukarıdaki program da bir öncekiyle aynı çıktıyı üretecektir. Python'da bir değer döndürmeyen fonksiyonlar aslında özel bir nesne olan None nesnesini döndürür, bir fonksiyona return satırını koymazsak Python otomatik olarak ona return None satırını ekler.


>>> def f():
...     print "merhaba"
...
>>> f()
merhaba
>>> a = f()
merhaba
>>> type(a)
type 'NoneType'>

Yukarıdaki örnekte Terminal içinde interaktif olarak da bir fonksiyon tanımlamış oluyoruz. Ayrıca bu örnekte hiç bir argümanı (parametresi) olmayan bir fonksiyon da tanımlanabileceğini görmüş oluyoruz.

Daha önce Python fonksiyonları içinde tanımladığımız lokal değişkenlere fonksiyon dışından ulaşamayacağımızı belirtmiştik, bunlara ulaşmak için return ifadesiyle fonksiyonun bunları döndürmesi gerekir. Diğer yandan fonksiyon içinden global değişkenlere her zaman ulaşılabilir fakat aksi belirtilmedikçe değiştirilemez. Bunun için global olarak aynı isimde bir değişken tanımlanmalıdır, fonksiyon içinden global bir x değişkeni tanımlamak için global x biçiminde bir ifade sonrasında değer atanmalıdır.


def f(x):
    a = 2
    return a + x

def g(x):
    global a
    a += x
    return a + x

a = 0
print f(5), a, g(5), a, f(5), a, g(5), a

Yukarıdaki programı dikkatli bir şekilde inceleyin. Dikkat ederseniz her iki fonksiyon da a isimli bir değişken kullanıyor, ayrıca aynı isimde bir de global değişken var. f içinde tanımlanan a değişkeni lokal bir değişken olarak kalıyor ve global olan a değişkenini etkilemiyor. Fakat g fonksiyonu içinde kullanılan a değişkeni global olarak alınıyor ve yapılan yeni atamalar global a değişkeninin değerini de değiştirmiş oluyor. Programın çıktısı aşağıdaki gibi olacaktır.


Terminal > python fonk7.py
7 0 10 5 7 5 15 10

Python fonksiyonları parametre olarak başka fonksiyonları alabilirler. Örneğin bir $f$ fonksiyonunun ikinci türevini yaklaşık olarak hesaplamakta kullanılan $$f''(x)\approx\frac{f(x-h)-2f(x)+f(x+h)}{h^2}$$ formülünde $f''$ fonksiyonu tanımlanırken $f$ fonksiyonu bir değişken olarak kullanılır. Başka programlama dillerinde bu işlemler biraz karmaşıktır ama pythonda sıradan bir argüman tanımlamaktan hiçbir farkı yoktur. Örneğin yukarıdaki yaklaşık türev hesabını yapan fonksiyon aşağıdaki gibi tanımlanabilir.


from math import cos, pi

def turev2m(f, x, h=1E-6):
    r = (f(x-h) -2*f(x) +f(x+h))/(h**2)
    return r

def f(x):
    return cos(x)

x = pi
for k in range(1, 15):
    h = 10**(-k)
    yaklasik_turev = turev2m(f, x, h)
    net_turev = -f(x)
    hata = abs(turev2m(f, x, h) + f(x))
    print "h=%.3e, yaklasik turev=%f, kesin turev=%g, hata=%g" \
    % (h, yaklasik_turev, net_turev, hata)

Burada kullandığımız abs() fonksyionu verilen argümanın mutlak değerini döndürür. Ayrıca uzun satırları \ karakteri ile bölebilirsiniz, yukarıda print ifadesinde bunu kullandık. Yukarıdaki programın çıktısı aşağıda verilmiştir.


Terminal > python fonk8.py
h=1.000e-01, yaklasik turev=0.999167, kesin turev=1, hata=0.000833056
h=1.000e-02, yaklasik turev=0.999992, kesin turev=1, hata=8.33331e-06
h=1.000e-03, yaklasik turev=1.000000, kesin turev=1, hata=8.3349e-08
h=1.000e-04, yaklasik turev=1.000000, kesin turev=1, hata=6.07747e-09
h=1.000e-05, yaklasik turev=1.000000, kesin turev=1, hata=8.27404e-08
h=1.000e-06, yaklasik turev=1.000089, kesin turev=1, hata=8.89006e-05
h=1.000e-07, yaklasik turev=0.988098, kesin turev=1, hata=0.0119015
h=1.000e-08, yaklasik turev=0.000000, kesin turev=1, hata=1
h=1.000e-09, yaklasik turev=0.000000, kesin turev=1, hata=1
h=1.000e-10, yaklasik turev=0.000000, kesin turev=1, hata=1
h=1.000e-11, yaklasik turev=0.000000, kesin turev=1, hata=1
h=1.000e-12, yaklasik turev=0.000000, kesin turev=1, hata=1
h=1.000e-13, yaklasik turev=0.000000, kesin turev=1, hata=1
h=1.000e-14, yaklasik turev=0.000000, kesin turev=1, hata=1

Matematiksel olarak $h\rightarrow0$ için yukarıda tanımladığımız oranın $f''(x)$ kesin değerine yakınsadığı kanıtlanabilir. Ama burada programımızın çıktısında belirli bir noktadan sonra h küçüldükçe hatanın büyüdüğünü görüyoruz. Bunun sebebi yuvarlama hatalarıdır, $h$ sayısı çok hızlı bir şekilde küçüldüğünde $h^{-2}$ sayısı hızlı bir şekilde büyür. Dolayısıyla $h$ hızlıca küçülürken $h^{-2}$ float nesnesindeki küçük yuvarlama hataları belli bir noktadan sonra yakınsamayı bozacak kadar büyür.

Pythonda bir fonksiyon tanımlandığında kısa bir dokümantasyon metni ile fonksiyonu tanıtmak adettir. Bu metne doc string denir üç tırnak işareti içinde def satırından hemen sonra yazılır.


def erf(x, n):
    """
    Bu fonksiyon erf(x) hata fonksiyonunun yaklasik degerini hesaplar.
    Kullanimi erf(x, n) bicimindedir. x bir float, n bir int.
    Ornek: erf(1.0, 100)
    """
    from math import factorial, sqrt, pi
    s = 0
    for k in range(n + 1):
        terim = ((-1)**k)*(x**(2*k+1))/(factorial(k)*(2*k+1))
        s = s + terim
    return 2*s/sqrt(pi)

n = 100
xlist = [-3 + i*0.5 for i in range(13)]
for x in xlist:
    print "x = %4.1f, erf(x)=%10f" % (x, erf(x, n))

Çalıştığımız fonksiyonların doc string metnine help(fonksiyon) veya fonksiyon.__doc__ komutlarıyla ulaşabiliriz.


Terminal > import math
>>> help(math.sin)
Help on built-in function sin in module math:

sin(...)
sin(x)

Return the sine of x (measured in radians).
>>>
>>> print math.sin.__doc__
sin(x)

Return the sine of x (measured in radians).

Örnek: Hata Fonksiyonu

Bilimsel hesaplamalarda sıklıkla bir sonlu toplamı kullanarak fonksiyonlar tanımlamaya ihtiyaç duyarız. Bunları yapmak için fonksiyon tanımında bir döngü kurmamız gerekir. Örneğin istatistikte 0 ortalama değere ve $1/2$ standart sapmaya sahip olan normal dağılımlı bir rastgele değişkenin $[-x, x]$ aralığına düşme olasılığı $$\text{erf}(x):=\frac{2}{\sqrt{\pi}}\int_{0}^{x}\text{e}^{-t^2}\text{dt}$$ ile verilir, bu fonksiyon hata fonksiyonu olarak adlandırılır. Bu integral elemanter fonksiyonlar cinsinden ifade edilemez fakat yaklaşık olarak $$\text{erf}(x)\approx\frac{2}{\sqrt{\pi}}\sum_{k=0}^{n}\frac{(-1)^{k}x^{2k+1}}{k!(2k+1)}$$ formülü ile hesaplanabilir.

Bu fonksiyonu Python'da basit bir döngü yardımıyla tanımlayıp bazı değerlerini hesaplatabiliriz.


def erf(x, n):
    from math import factorial, sqrt, pi
    s = 0
    for k in range(n + 1):
        terim = ((-1)**k)*(x**(2*k+1))/(factorial(k)*(2*k+1))
        s = s + terim
    return 2*s/sqrt(pi)

n = 100
xlist = [-3 + i*0.5 for i in range(13)]
for x in xlist:
    print "x = %4.1f, erf(x)=%10f" % (x, erf(x, n))

Programın çıktısı aşağıdaki gibi olacaktır.


Terminal > python erf1.py
x = -3.0, erf(x)= -0.999978
x = -2.5, erf(x)= -0.999593
x = -2.0, erf(x)= -0.995322
x = -1.5, erf(x)= -0.966105
x = -1.0, erf(x)= -0.842701
x = -0.5, erf(x)= -0.520500
x =  0.0, erf(x)=  0.000000
x =  0.5, erf(x)=  0.520500
x =  1.0, erf(x)=  0.842701
x =  1.5, erf(x)=  0.966105
x =  2.0, erf(x)=  0.995322
x =  2.5, erf(x)=  0.999593
x =  3.0, erf(x)=  0.999978

Test Fonksiyonları

Python'da tanımladığımız bir fonksiyon kodlama açısından hiç bir hata içermeyebilir, çalıştırıldığında Python hiç bir hata mesajı göstermeyebilir. Fakat fonksiyonun yaptığı matematiksel veya diğer genel işlemler hatalı olabilir. Örneğin yukarıda tanımladığımız erf(x, n) fonksiyonu hatalı hesaplama yapıyor olabilir, bu gibi durumları tespit etmek için her yazdığımız programı test etmeliyiz. Yazdığımız fonksiyonlar geliştikçe bu testleri manuel olarak yapmak zorlaşır, bunun için test fonksiyonları tanımlarız. Bir test fonksiyonu, başka bir fonksiyonun önceden bilinen argüman ve değeleri ile test edip beklenen sonucun dönüp dönmediğini kontrol eden bir fonksiyondur. Örneğin yukarıda tanımladığımız $\text{erf}(x)$ fonksiyonu için matematiksel olarak $\text{erf}(0)=0$ olduğunu biliyoruz. Yazdığımız fonksiyonu test etmek için bu veriyi kullanabiliriz.


def erf_test():
    x = 0
    n = 100
    if erf(x, n) == 0:
        print "test basarili"
    else:
        print "test basarisiz"

Bu tanımladığımız fonksiyon amacımıza hizmet etse de modern test fonksiyonları bu şekilde olmamalıdır. Yazılımları test etmek önemli bir iştir ve bunun için çok geniş paketler bulunur Python'da. Modern bir test fonksiyonu aşağıdaki özelliklere sahip olmalıdır.

  • İsmi test_ ile başlamalı.
  • Hiçbir argümanı olmamalı.
  • Bir bool değişkeni tanımlanmalı, örneğin test. Test başarılı olursa True, başarısız olursa False değerini almalı.
  • Neyin başarısız olduğunu bildiren bir mesaj üretmeli, hata gibi.
  • assert test, hata komutunu kullanmalı. Bu komut; success = False ise msg hata mesajını döndürür.

Bunlara uygun bir test fonksiyonu aşağıdaki gibi olabilir.


def test_erf():
    hesaplanan = erf(0, 100)
    beklenen = 0.0
    tolerans = 1E-12
    test = abs(beklenen - hesaplanan) < tolerans
    msg = "hatali hesap"
    assert test, msg

erf(x, n) fonksiyonu yanlış hesap yapmazsa bu test fonksiyonu hiç bir çıktı üretmez. Fakat erf(x, n) fonksiyonu hatalı işlem yaparsa test_erf() fonksiyonu hata üretir ve belirtilen hata mesajını gösterir. 5. satırda doğrudan karşılaştırma yerine belirli bir tolerans ile kıyaslama yaptık, yuvarlama hatalarını göz önüne almazsak fonksiyonumuz doğru hesap yapsa bile test fonksiyonu yanlış algılayabilir.

Örnek: Simpson Kuralı

Başka bir örnek olarak nümerik integrasyon için kullanılan ve Simpson kuralı olarak bilinen yöntemi ele alalım. Bu yöntemle bir $$\int_{a}^{b}f(x)dx$$ integrali $$\int_{a}^{b}f(x)dx \approx \frac{h}{3}\left( f(a)+ f(b) +4\sum_{i=1}^{n/2}f(a+(2i-1)h) + 2\sum_{i=1}^{n/2-1}f(a+2ih) \right) $$ formülü ile yaklaşık olarak hesaplanabilir. Burada $n$ bir çift tamsayı ve $h=(b-a)/n$ dir. Şimdi bu hesaplamayı yapacak olan bir Simpson(f, a, b, n=100) fonksiyonu tanımlayalım.


def Simpson(f, a, b, n=100):
    h = (b-a)/float(n)
    toplam1 = 0
    toplam2 = 0
    
    for i in range(1, n/2 + 1):
        toplam1 += f(a + (2*i-1)*h)
    
    for i in range(1, n/2):
        toplam2 += f(a + 2*i*h)
    
    integral = (h/3)*(f(a) + f(b) + 4*toplam1 + 2*toplam2)
    return integral

Bu fonksiyonu kullanarak bir işlem yapalım. $\int_{0}^{\pi}\sin^3(x)\text{d}x=4/3$ olduğunu biliyoruz. Aşağıdaki döngüyle farklı adım sayılarıyla Simpson kuralı yaklaşık hesabını yaptıralım.


from math import sin, pi

f = lambda x: sin(x)**3
a = 0; b = pi

for n in 2, 5, 10, 50, 100, 500, 1000:
    hesap = Simpson(f, a, b, n)
    kesin = 4.0/3
    hata = abs(hesap - kesin)
    print "n = %4d, Hesaplanan=%.12f, Hata=%g" % (n, hesap, hata)

Bu döngü ile aşağıdaki tablo üretilecektir, göreceğimiz gibi n adım sayısı arttıkça hata azalıyor.


n =    2, Hesaplanan=2.094395102393, Hata=0.761062
n =    5, Hesaplanan=1.251135387587, Hata=0.0821979
n =   10, Hesaplanan=1.332599724263, Hata=0.000733609
n =   50, Hesaplanan=1.333332289401, Hata=1.04393e-06
n =  100, Hesaplanan=1.333333268318, Hata=6.50158e-08
n =  500, Hesaplanan=1.333333333229, Hata=1.03907e-10
n = 1000, Hesaplanan=1.333333333327, Hata=6.49392e-12

Şimdi bir de bu fonksiyon için test yazalım. Bunu yazabilmemiz için Simpson kuralının kesin değer verdiği bir durum kullanmalıyız. Simpson kuralının şöyle ilginç bir özelliği var, bu kural ile 2 veya daha küçük dereceli polinomların integrali hatasız olarak hesaplanıyor. Bunu kullanarak aşağıdaki test fonksiyonu tanımlanabilir.


def test_Simpson():
    a = 1.5; b = 2.0; n = 10; tolerans = 1E-14
    g = lambda x: 3*x**2 - 2*x + 2#integrand
    G = lambda x: x**3 - x**2 + x#integral
    kesin = G(b) - G(a)
    hesap = Simpson(g, a, b, n)
    test = abs(kesin - hesap) < tolerans
    msg = "Simpson: %g, Analitik: %g" % (hesap, kesin)
    assert test, msg
Önceki Ders Notu:
2.3. If-Else Koşullu Yapısı
Dersin Ana Sayfası:
Python ve Bilimsel Hesaplama
Sonraki Ders Notu:
3.2. Kullanıcıdan Veri Alma