Python Dekoratörleri Anlamak İçin 12 Kavram

Bu yazıda, Python programlama dilinde dekoratör kavramını anlamak için gerekenler, temelden başlayarak ve üzerlerine ekleme yapılarak sunulmuştur. Amacınız dekoratör yaratmayı öğrenmek olmasa bile bu kavramların çoğunu (özellikle closure) bilmeniz gerekir.

Dekoratörler kesinlikle bu şekilde uygulanır diye bir koşul yoktur. Ama fikri anlamak için işe yarar bir yol haritasıdır.

1.Fonksiyonlar

Python’da fonksiyonlar def komutu ile yaratılırlar. Bir isim ve de opsiyonel bir parametre listesi alırlar. Bir fonksiyondan değer döndürmek için return komutu kullanılır. Olabilecek en basit fonksiyon şuna benzer;

>>> def foo():
...     return 1
>>> foo()
1

Fonksiyon deklarasyonunda içerik (Python’daki diğer bütün tek satırdan uzun deklarasyon için olduğu gibi) gereklidir ve satır başı boşluklarıyla seviyesi belirlenir. Fonksiyonları, isimlerinin sonuna () ekleyerek çağırabiliriz.

2. Kapsam (Scope)

Python’da, her yeni fonksiyon bir kapsam yaratır. Pythonista’lar bu kavrama isim uzayı da (namespace) derler. Bunun anlamı, Python’ın bir fonksiyonu çözümlerken ilk önce o fonksiyona ait isim uzayına bakarak değişkeni bulmaya çalışmasıdır. (Kapsam kelimesiyle devam edeceğim.) Kapsama bakacağımız bir takım fonksiyon Python içerisinde gelmektedir. Yerel (local) ve genel (global) kapsamlar arasındaki farkı anlamak için şu kodu inceleyin;

>>> a_string = "This is a global variable"
>>> def foo():
...     print locals()
>>> print globals() # doctest: +ELLIPSIS
{..., 'a_string': 'This is a global variable'}
>>> foo() # 2
{}

Yerleşik (built-in) globals() fonksiyonu, Python’ın o kapsamda varlığından haberdar olduğu bütün değişken isimlerini bir dictionary olarak verir. #2’de foo() fonksiyonunu çağırıyoruz ve fonksiyon içerisindeki yerel kapsamdaki değişkenlerin listesini alıyoruz. Bu örnekte, foo() fonksiyonunun kendine mahsus ayrı, boş bir kapsamı olduğunu görüyoruz.

3. Değişken Çözümleme Sırası Kuralları (Variable Resolution Rules)

Bu demek değil ki fonksiyonun içinden genel kapsamdaki değişkenlere ulaşamıyoruz. Python’ın kapsam kurallarına göre; bir değişken her zaman yerel kapsamda yaratılır ancak değişkene ulaşmaya çalışırken (içeriğini değiştirirken de ulaşılması gerekir), uyan bir tane bulunana kadar sırasıyla bir üstteki kapsama bakılır. Eğer foo() fonksiyonunu değiştirip genel kapsamdaki değişkeni yazdırmak istersek, çalışacaktır.

>>> a_string = "This is a global variable"
>>> def foo():
...     print a_string # 1
>>> foo()
This is a global variable

#1 adımında, Python değişkeni önce fonksiyonun yerel kapsamında arıyor. Bulamadığı için bir sonraki adımda (aynı değişken ismi için) genel kapsama bakıyor.

Ancak, aynı isimli bir değişkeni fonksiyonun içinde tanımlarsak istediğimiz sonucu alamıyoruz.

>>> a_string = "This is a global variable"
>>> def foo():
...     a_string = "test" # 1
...     print locals()
>>> foo()
{'a_string': 'test'}
>>> a_string # 2
'This is a global variable'

Gördüğümüz üzere, genel kapsamdaki değişkenlere fonksiyon içlerinden ulaşabiliriz ama (bu yazı için mühim olmayan istisnalar dışında) içeriklerini değiştiremeyiz.

#1 adımında, fonksiyonun içinde, genel kapsamdaki (aynı isimli) değişkeni gölgeliyoruz (shadow).

Bunu görmek için foo() fonksiyonunun içindeki yerel kapsamı (locals) yazdırıyoruz. Dikkat ederseniz, bu sefer kapsam boş değil ve değişkenin yereldeki değeri listeleniyor. Sonrasında, #2 adımında, genel kapsama çıkmış oluyoruz ve değişkenin bu noktadaki (üst kapsamdaki/kümedeki) değeri de değişmiş oluyor.

4. Değişken Ömrü (Lifetime)

Son kullanım tarihi olarak da düşünebiliriz. Şöyle ki;

>>> def foo():
...     x = 1
>>> foo()
>>> print x # 1
Traceback (most recent call last):
  ...
NameError: name 'x' is not defined

#1 adımında, hata sadece kapsam kurallarına aykırı gelindiğinden kaynaklanmıyor. Konsol NameError hatasını bu yüzden veriyor ancak ana nedeni Python’da (ve diğer bir çok programlama dilinde) fonksiyon çağırma kurallarının şekli.

Fonksiyondan çıktıktan sonra (yerelde tanımladığımız) değişkene erişmek için kullanabileceğimiz bir sözdizimi (syntax) yok. Kapsam dışında, yani varlığı sona ermiş. Fonksiyonumuz foo() için tanımlanan kapsam her çağrılışında baştan yaratılıyor ve fonksiyon sonlandığında yok oluyor.

5. Fonksiyonlara Beslenen Argümanlar ve Deklarasyondaki Parametreler

Fonksiyonlara argümanlar besleyebiliyoruz. Argümanlar fonksiyon tanımında parametre adıyla geçiyorlar ve deklarasyon içinde (fonksiyon metninde) yerel kapsamda kullanılan değişkenler oluyorlar. Basit bir şekilde anlatılırsa; etrafa saçılan değerler; kodu yazarken parametre, kullanırken (değerlendirirken) argüman.

>>> def foo(x):
...     print locals()
>>> foo(1)
{'x': 1}

Python’da fonksiyon parametrelerini deklere etmek için birden fazla yöntem vardır. Çok zor bir kavram değil ama detaylı bir açıklamasını (fonksiyon yaratma kurallarının açıklamasını) okumanızı tavsiye ederim. Özeti; parametreler konumsal (positional) ya da isimlendirilmiş (named) olabilirler. Konumsal parametreler zorunludur, yani fonksiyonu çağırırken verilmesi (beslenmesi) gerekirler. İsimlendirilmiş parametreler ise verilmek zorunda değildirler. İsimlendirilmiş parametrelere, fonksiyon tanımlanırken bir varsayılan (default) değer verilir.

>>> def foo(x, y=0): # 1
...     return x - y
>>> foo(3, 1) # 2
2
>>> foo(3) # 3
3
>>> foo() # 4
Traceback (most recent call last):
  ...
TypeError: foo() takes at least 1 argument (0 given)
>>> foo(y=1, x=3) # 5
2

#1 adımında deklere ettiğimiz fonksiyonun bir tane konumsal parametresi (x) ve bir tane de isimlendirilmiş parametresi (y) var.

#2 adımına bakacak olursak, bu fonksiyonu daha önceden gördüğümüz şekilde çağırabiliyoruz: beslediğimiz değerler konumlarına göre fonksiyon tanımındaki parametrelere bağlanıyorlar ve yerel kapsamda bu şekilde işlem görüyorlar.

#3 adımında ise, isimlendirilmiş parametre için bir değer beslemiyoruz. Bu durumda, y parametresi için fonksiyon deklere edilirken verilmiş varsayılan değer (0) kullanılıyor. Ancak konumsal parametre için bir değer beslemek zorundayız.

#4 adımı da bunu gösteriyor. TypeError: foo() en az 1 argüman alır (0 verildi)

#5 adımında iş biraz karışıyor ama bu da aslında kavramın doğal bir uzantısı. Fonksiyonu çağırırken parametrelere değer belirtebiliriz. Sonuçta parametrelerin isimlerini biliyoruz; x ve y. Değerlerini çağırma esnasında belirliyoruz ve fonksiyonun yerel kapsamında da bunlar kullanılıyor. Dikkatli inceliyorsanız (kül yutmam!) parametrelerin sıraları ters. Değer vererek çağırırsanız, parametre isimlerini de belirtmeniz gerektiği için, konumun önemi kalmıyor.

Mantıklı ama dikkat edilmesi gereken bir husus: #2 adımında isimlendirilmiş parametreye (varsayılanı 0 olan) değer atadık ve istediğimiz sonucu aldık. Çünkü konumsal parametrelere değer belirttikten sonra argüman listesi hala devam ediyorsa, sırada hangi parametre (bu durumda isimlendirilmiş bir tane) varsa değer ona atanır. Özetle, fonksiyon tanımlama kurallarında iki tip parametre var; konumsal ve isimlendirilmiş. Fonksiyon tanımlama ve çağırma adımlarında bu kavramların anlamları biraz farklılık gösteriyor.

6. İçiçe (Nested) Fonksiyonlar

Fonksiyonların içinde başka fonksiyonlar tanımlayabiliriz. Kapsam ve ömür (son kullanım) kuralları aynı şekilde burada da geçerli.

>>> def outer():
...     x = 1
...     def inner():
...         print x # 1
...     inner() # 2
...
>>> outer()
1

#1 adımında, x adındaki yerel değişken aranıyor. Kapsamda bulunamayınca, bir üsttekine bakıyor. Bu örnekte bir üst kapsam başka bir fonksiyon. Outer() fonksiyonunun yerel kapsamında olan x değişkenine inner() fonksiyonu da erişebiliyor.

#2 adımında, (hala outer() fonksiyonunun deklarasyonu aşamasındayız) inner() fonksiyonu çağırılıyor. Fonksiyon deklarasyonu bittikten sonra, genel kapsama çıkıyoruz. Burada outer() fonksiyonunu çağırıyoruz. Fonksiyonun tanımında “inner() çağır” var. Değişkeni (x) bulabiliyor çünkü bir üst kapsamı da görebiliyor.

7. Python’da Fonksiyonlar Üst Seviye (First Class) Objelerdir

Basit bir kavram; Python’da fonksiyonlar (diğer her şey gibi) bir objedir.

>>> issubclass(int, object) # all objects in Python inherit from a common baseclass
True
>>> def foo():
...     pass
>>> foo.__class__ # 1
<type 'function'>
>>> issubclass(foo.__class__, object)
True

Python’da, fonksiyonlar programda kullanılan diğer bütün değerler gibi değerlendirilir (görülür). Bu da demek oluyor ki, bir fonksiyonu bir başka fonksiyona argüman olarak besleyebilirsiniz ya da bir fonksiyonun dış kapsama döndürdüğü (return) şey bir başka fonksiyon olabilir.

>>> def add(x, y):
...     return x + y
>>> def sub(x, y):
...     return x - y
>>> def apply(func, x, y): # 1
...     return func(x, y) # 2
>>> apply(add, 2, 1) # 3
3
>>> apply(sub, 2, 1)
1

#1 adımında bir fonksiyon deklere ediyoruz. Konumsal olarak ilk sıradaki parametre bir fonksiyon besleyeceğimizi varsayıyor ancak deklere edilirken aynı diğer parametreler gibi tanımlanıyor. Bir fonksiyona başka bir fonksiyonu beslemek için Python’da özel bir sözdizimi yapmanıza gerek yok.

#2 adımında, parantezleri kullanarak func parametresini çağırıyoruz. Bunu iyi kavramanız lazım; fonksiyonun bir ismi var (func) ve argüman olarak besleniyor. Arkasına parantezleri ekleyerek bu ismin içindekini çağırıyoruz. Her şey bir obje sonuçta. Verilen ismin içinde bir şeyler var. Bu örnekte, verilen ismin içerisinde işlemler yapan bir fonksiyon var. Parantezleri eklediğimizde, Python’a “Bu ismin içindekileri değerlendir.” (evaluate) diyoruz. İçindekinin ne olduğunu ve ne şekilde kurallarla çağırılması gerektiğini bildiğimiz için (ve x ile y parametreleri de yerel kapsamımızda olduğu için) çağırma işlemini usulüne göre yapabiliyoruz.

#3 adımında bunun uygulamasını görebilirsiniz. Farklı bir söz dizimine gerek olmadan argüman olarak bir fonksiyon besliyoruz.

Üst kapsama bir fonksiyon döndürmek (return) de şöyle;

>>> def outer():
...     def inner():
...         print "Inside inner"
...     return inner # 1
...
>>> foo = outer() #2
>>> foo # doctest:+ELLIPSIS
<function inner at 0x...>
>>> foo()
Inside inner

#1 adımında, outer() fonksiyonunun döndürdüğü değer inner() fonksiyonu: return inner inner() fonksiyonunu, outer() döndürmediği sürece göremiyoruz. Değişken ömrü kurallarına istinaden, inner() fonksiyonu, outer()’ın her çağırılışında baştan yaratılıyor ve bittiğinde de bellekten siliniyor. Ancak fonksiyonun ismi döndürüldüğü için, bir üstteki kapsam inner()’ın varlığından haberdar.

#2 adımında, outer() tarafından döndürülen değeri (inner) foo değişkenine atıyoruz. Konsola “foo değişkeninde ne var?” diye sorduğumuzda, görüyoruz ki inner isimli fonksiyona işaret ediyor. Sonra da parantezleri kullanarak “foo değişkeninin içindekileri değerlendir.” diyoruz.

Kavramı anlamak için mühim değil ama şunun farkında olun; foo = outer() dediğimiz zaman Python’a “outer isminin içindekileri değerlendir ve foo içerisinde sakla” diyoruz. Böylece şunu yapmış oluyoruz; inner() fonksiyonunu daha değerlendirmiyoruz (çalıştırmıyoruz) ama bir sonraki adımda foo sonuna parantez eklediğimizde “Bazı işlemlere işaret ediyorsun ya, şimdi onları değerlendir.” diyoruz. İhtiyacımız olana kadar, bellekte (ya da isim uzayı içerisinde) inner() ve kapsamı olmayacak.

8. Hatırlanan/Taşınan Kapsamlar (Closures)

Bu kavram biraz karışık. Nasıl bir resim bin kelime anlatırsa, burada da aşağıdaki kod altındaki açıklamayı çok iyi tasvir ediyor. Dikkatli inceleyin ve lütfen her satırın ne yapmaya çalıştığını (kapsam kümelerini aklınızda tutarak) düşünün.

Üstte verilen koddaki outer() deklerasyonuna bir satır ekleyip bir başka satırı da değiştiriyoruz;

>>> def outer():
...     x = 1
...     def inner():
...         print x # 1
...     return inner
>>> foo = outer()
>>> foo.func_closure # doctest: +ELLIPSIS
(<cell at 0x...: int object at 0x...>,)

Kapsam ve değişken ömrü kurallarını göz önünde bulundurduğumuzda, kodda çalışmamasını bekleyeceğimiz bir şey yapıyoruz. Görmediyseniz bir daha üzerinden geçin.

Ama kod çalışıyor. Şöyle ki;

Python’ın kapsam kurallarına göre her şey düzgün: x, outer() fonksiyonunun yerel kapsamındaki bir değişken. Önceden gördüğümüz gibi, #1 adımında inner() fonksiyonu bu değişkeni yazdırmaya çalıştığı zaman, Python önce yerel kapsama bakıyor, orada bulamıyor ve bir üst seviyedeki kapsama (outer) geçiyor ve orada buluyor.

Ama işe değişken ömrü tarafından baktığımız zaman kurallara aykırı bir durum görüyoruz. Değişkenimiz (x) outer() fonksiyonunun yerel kapsamında, yani sadece outer() değerlendirilirken var. inner’ı sadece outer() değerlendirilmesi bittikten sonra çağırabiliyoruz. Şöyle ki; inner ismi üst kapsama outer() tarafından döndürülüyor, evet, ama değerlendirileceği sırada bizim outer() ile işimiz bitmiş oluyor.

Şu ana kadar gördüğümüz kapsam kurallarına aykırı durum da burada oluşuyor: Değişkenin (x) ömrü outer() değerlendirildikten sonra bitiyorsa, niye inner’la işimiz olduğu zaman kapsam içinde olsun?

Ama içinde. Closure kavramı da bu. İç içe geçmiş fonksiyonlardan bahsettiğimizde, bir yerel kapsamın içinde bir başka yerel kapsam oluyor. Alt küme. İçteki kapsam, üsttekinin hangi isimleri barındırdığını hatırlıyor. Üst kapsamın içeriğinin bir kopyasını yanında taşımıyor, ama o kapsamın hangi isimlerden haberi olduğunu biliyor.

Dikkat etmeniz gereken bir nüans var burada: içerideki kapsam, dışarıdakinin deklarasyon sonucunda neye benzediğini biliyor. Eğer üst kapsamın içeriğinde değerlendirilme sırasında (veya başka bir nedenden) bir değişiklik olursa, iç kapsam bunu göremez. Tanımlananı bilir, değişiklikleri takip etmez.

Hangi kapsamın hatırlandığını görmek için de func_closure niteliğini (attribute) kullanabiliriz. Hatırlarsanız foo isminin içinde inner ismine bir işaret var. foo.func_closure dediğimizde, bellekte nelerden haberdar olunduğunun bir listesini alabiliriz.

Hatırlayalım: outer() her çağırıldığında, inner ismi (ve haliyle kapsamı) silbaştan yaratılıyor. Yukarıdaki kodda, x değişkeninin değeri hep aynı. Bu nedenle, inner fonksiyonunun her misali (instance) diğerleriyle tıpatıp aynı şeyi yapıyor.

>>> def outer(x):
...     def inner():
...         print x # 1
...     return inner
>>> print1 = outer(1)
>>> print2 = outer(2)
>>> print1()
1
>>> print2()
2

Bir closure tanımı daha şöyle olabilir; fonksiyonlar üst kapsamlarını hatırlarlar. Yani üst kapsamdaki bir değişken, alt kapsamdaki fonksiyona görünen (efektif olarak o fonksiyona bağlanmış) olan bir objedir. Inner’a 1 ya da 2 argümanlarını direkt beslemiyoruz. Fonksiyonun (inner) özelleştirilmiş (custom) türevlerini (misallerini) yaratıyoruz. Bu özelleştirilmiş misaller de (üst kapsamlarını görebilmeleri sayesinde) verilen değişkeni (outer tarafından) kullanabiliyorlar.

Bu kavramın tek başına önemli olmasının ana nedeni de bu. Tam bir OOP bakış açısı aslında. outer(), inner() için bir constructor. Aldığı x argümanı da inner()’ın bir niteliği.

Kullanım alanları da çok. Python’ın sorted() fonksiyonundaki key parametresinin nasıl çalıştığına aşinaysanız, bir closure yazmışsınızdır. Mesela; argüman olarak bir liste alınıyor; bu listenin elemanları başka listeler. Bir lambda fonksiyonu kullanarak her listenin 2. konumdaki elemanına göre sıralıyorsunuz üst listenizi. Bu da closure kullanmaktır.

OOP tarzında bir getter fonksiyonu yazarken şöyle yapabilirsiniz; önce değişken/obje alım işlerinin yönetimini yapan bir motor yazarsınız. Sonra da bu motoru bir başka fonksiyonun içine koyup etrafını başka bir kapsamla sarmalarsınız. Üstteki fonksiyona beslediğiniz argümanlara göre oluşturulan bir kapsam ile birlikte içerideki fonksiyonu kullanan, özelleştirilmiş bir fonksiyon yaratmış olursunuz.

9. Dekoratörler

Tanım: argüman olarak başka bir fonksiyon alan (beslenen) ve özelleştirilmiş/farklı bir fonksiyon döndüren, çağırılabilen (evaluate edilebilen) bir obje.

Yorum: Python’ın tasarımcıları bu kavramın önemli olduğunu düşünmüş olmasalardı dilin içine standart desteğini koymazlardı.

>>> def outer(some_func):
...     def inner():
...         print "before some_func"
...         ret = some_func() # 1
...         return ret + 1
...     return inner
>>> def foo():
...     return 1
>>> decorated = outer(foo) # 2
>>> decorated()
before some_func
2

İlk satırda, outer() fonksiyonunu tanımlıyoruz. Bu fonksiyon tek bir argüman alıyor ve o da bir başka fonksiyon.

İkinci satırda fonksiyonun metnini yazmaya başlıyoruz. Burada da inner() fonksiyonunu tanımlamaya başladık. Bu içerideki fonksiyon, önce ekrana bir string yazdıracak, sonra da some_func parametresiyle gelen ismi değerlendirecek (sonundaki parantezler sayesinde).

#1 adımında, some_func’ın döndürdüğü değeri ret değişkeni içerisinde saklıyoruz. some_func’ın değeri outer()’i her çağırdığımızda farklı olabilir. Bunda bir mahsur yok. Biz ne değer gelirse onu değerlendireceğiz.

Sonraki adımda ise inner() tarafından beslenen fonksiyonun değeri +1 döndürülüyor.

foo 1 rakamını döndürüyor. Bu fonksiyonu kullanarak outer()’ı besliyoruz ve bunu da decorated değişkeni içerisinde saklıyoruz. Parantezleri kullanarak decorated değişkeninin değerlendirilmesini talep ettiğimiz zaman da son 2 satırı alıyoruz.

Beklediğimiz string ekrana yazılmış, demek ki inner()’ın kapsamına girmişiz. Son satırda da 2 rakamı döndürülmüş. Yani foo’nun döndürdüğü değer (1) + 1.

Şimdi de dekoratör kelimesiyle ne kastedildiğini görebiliriz; decorated değişkeni, foo değişkenini dekore eden bir obje. Verdiği sonuç, (bu örnekte) foo + 1.

Bu kavramı biraz daha optimize eder ve takip etmemiz gereken kodun hacmini düşürürsek, foo değişkenini, onu dekore eden başka bir değişkenin içine atmak yerine, foo değişkeninin içeriğini dekorasyon sonrasındaki ile güncellemeyi tercih edebiliriz. Şöyle ki;

>>> foo = outer(foo)
>>> foo # doctest: +ELLIPSIS
<function inner at 0x...>

Böylece, foo() fonksiyonunu çağıranlar original foo değişkenini almayacaklar, dekore edilmiş olanı alacaklar. foo değişkeninin dekorasyonunu otomize etmiş olduk. Kodumuzun üzerine eklemeler yaparak devam edelim.

Koordinat objeleri aldığımız bir kütüphane (library) kullandığımızı varsayalım. Velev ki, x ve y sayılarından oluşan bir obje. Ancak, aldığımız koordinat değerleri matematik işlemlerini desteklemiyorlar ve de kaynak koduna müdahale edemiyor: yani bu desteği kütüphanenin içine ekleyemiyoruz. Ancak, bu koordinatlarla bir sürü matematik işlemi yapacağız o yüzden bu desteği kodumuza eklememiz lazım. Toplama ve çıkarma yapan iki fonksiyonla işe başlayalım.

>>> class Coordinate(object):
...     def __init__(self, x, y):
...         self.x = x
...         self.y = y
...     def __repr__(self):
...         return "Coord: " + str(self.__dict__)
>>> def add(a, b):
...     return Coordinate(a.x + b.x, a.y + b.y)
>>> def sub(a, b):
...     return Coordinate(a.x - b.x, a.y - b.y)
>>> one = Coordinate(100, 200)
>>> two = Coordinate(300, 200)
>>> add(one, two)
Coord: {'y': 400, 'x': 400}

__init__: Objenin x ve y niteliklerini tanımlıyoruz.

__repr__: Python’a, “Bu objeyi temsil ederken (represent) şöyle bir şey döndür.” diyoruz.

add: Verdiğim iki koordinatın x ve y niteliklerini topla ve yeni bir Coordinate() objesi döndür.

sub: Verdiğim iki koordinatın x ve y niteliklerini birbirinden çıkart ve yeni bir Coordinate() objesi döndür.

Buna ek olarak, add() ve sub() fonksiyonlarımızın beslenen argümanları belirli sınırlar içinde değerlendirmelerini istiyoruz. Mesela; toplama ve çıkarma işlemlerini sadece pozitif koordinat değerleriyle yapmak istiyoruz. Döndürülen objeler de (koordinatlar) pozitif değerlerle sınırlanmalı. Yukarıdaki kodun çıktısı şu olmaktadır;

>>> one = Coordinate(100, 200)
>>> two = Coordinate(300, 200)
>>> three = Coordinate(-100, -100)
>>> sub(one, two)
Coord: {'y': 0, 'x': -200}
>>> add(one, three)
Coord: {'y': 100, 'x': 0}

Ama;

  • sub(one, two) sonucunda Coord: {y: 0, x: 0}
  • add(one, three) sonucunda Coord: {y: 200, x: 100}

almak istiyoruz ve bunu da one, two, three değerlerini değiştirmeden yapmak istiyoruz. Fonksiyona girilecek argümanları beslemeden önce teker teker limitleri kontrol etmek ve de fonksiyon işini bitirdikten sonra dönülen değeri bir kere daha limitler için kontrol etmek yerine, bunu yapan bir dekoratör yazabiliriz.

>>> def wrapper(func):
...     def checker(a, b): # 1
...         if a.x < 0 or a.y < 0:
...             a = Coordinate(a.x if a.x > 0 else 0, a.y if a.y > 0 else 0)
...         if b.x < 0 or b.y < 0:
...             b = Coordinate(b.x if b.x > 0 else 0, b.y if b.y > 0 else 0)
...         ret = func(a, b)
...         if ret.x < 0 or ret.y < 0:
...             ret = Coordinate(ret.x if ret.x > 0 else 0, ret.y if ret.y > 0 else 0)
...         return ret
...     return checker
>>> add = wrapper(add)
>>> sub = wrapper(sub)
>>> sub(one, two)
Coord: {'y': 0, 'x': 0}
>>> add(one, three)
Coord: {'y': 200, 'x': 100}

Bu dekoratör aynen bu maddenin başındaki örnekteki gibi; verilen bir fonksiyonun özelleştirilmiş bir versiyonunu döndürüyor. Bu sefer, farklı olarak, işe yarar bir şey yapıyor. Girdi olarak verilen argümanları ve döndürülen objeyi (bu örnekte girdiler de aynı obje) limitler için kontrol ediyor ve negatif bir x ya da y değerini 0 ile değiştiriyor.

Böyle yapmanın kodumuzu daha temiz yapıp yapmadığı tartışmaya açık: limitleri kontrol etmeyi kendi fonksiyonu içinde izole ediyoruz ve kontrol edilmesini istediğimiz her fonksiyona uyguluyoruz.

Alternatifi şöyle olabilirdi; her girdi argüman (input argument) için ve çıktı (output) için çağırılacak bir fonksiyon. Bu fonksiyon çağırmalarını her matematik fonksiyonumuz (add ve sub) içinde birden fazla kere yapmamız gerekirdi. Bu iki olasılığı karşılaştırdığımızda, görüyoruz ki dekoratör kullanmak bizi aynı işlevi gören kodu birden fazla yerde kullanmaktan kurtarıyor.

10. @ Sembolü Fonksiyona Dekoratör Atar

Python 2.4 ile birlikte, fonksiyonların başlarına @ işareti ve dekoratörün ismini (isim uzayı?) koyarak, dekoratör ile etrafını sarma (dekorasyon atama) özelliği geldi. Yukarıdaki örneklerde, fonksiyonumuzu sarmalanmış (wrap edilmiş) versiyonuyla değiştirerek dekorasyon efektini sağladık.

>>> add = wrapper(add)

Bu yöntemi herhangi bir fonksiyonu sarmalamak için kullanabiliriz. Ama yapılmışı var; bir fonksiyon deklere ederken @ sembolünü kullanarak dekoratör atayabiliriz;

>>> @wrapper
... def add(a, b):
...     return Coordinate(a.x + b.x, a.y + b.y)

Üstteki kodun, add ismini wrapper ile sarmalayıp, sonrasında da özelleştirilmiş versiyonunu add ismi içine döndürmekten bir farkı yok. Python biraz “syntactic sugar” ekleyerek kodun akışını daha anlaşılır kılıyor.

Dekoratörleri kullanmak kolay bir şey. İşe yarar staticmethod ve classmethod gibi dekoratörleri yazmak zor olsa bile, kullanması sadece @dekoratörismi ibaresini fonksiyonun başına eklemekten ibaret.

11. *args ve **kwargs

İşe yarayan bir dekoratör yazdık ama sadece belirli bir (iki argüman alan) fonksiyon tipinde çalışıyor. İçerideki checker isimli kontrol fonksiyonumuz, girdi olarak iki argüman kabul ediyor ve bu değerleri işledikten sonra da closure içinde hatırladığı fonksiyona (üst kapsama) iletiyor.

Velev ki, olası bütün fonksiyonlarda işe yarar bir şey yapmasını istediğimiz bir dekoratör lazım. Mesela, her fonksiyon çağırılmasında bir sayıcıyı (counter) 1 arttıran bir dekoratöre ihtiyaç var. Bu dekoratöre beslenen fonksiyonlar zaten dekore edilmiş olabilir. Bizim ekleyeceğimiz sayıcı dekoratörünün, kendisine beslenen her fonksiyonun deklere edilen argüman imzasını (signature) alabilip, işini yaptıktan sonra da üst kapsama (bu isimleri) geçirebilmesi lazım. Var olan dekorasyonun içeriğini değiştirmeden.

Python’da bu işlem için syntactic sugar var. Detayları bu kaynakta. Özetle, * operatörü fonksiyon deklere ederken kullanılırsa, tanımlanmamış bütün konumsal parametreleri * sembolünden sonra verilen ismin (argümanın) içine atar.

Örnekle açıklamak gerekirse;

>>> def one(*args):
...     print args # 1
>>> one()
()
>>> one(1, 2, 3)
(1, 2, 3)
>>> def two(x, y, *args): # 2
...     print x, y, args
>>> two('a', 'b', 'c')
a b ('c',)

İlk satırda tanımlanan one() fonksiyonu, beslenen bütün (eğer varsa) konumsal argümanları ekrana yazdırıyor.

#1 adımında görüldüğü gibi, args parametresini fonksiyonun içinde (sonuçta kapsamda) kullanıyoruz. *args ibaresi sadece fonksiyon deklere edilirken (imzasında) kullanılıyor.

#2 adımında da, tanımladığımız konumsal parametrelere ek olarak, “Beslenen ilave argüman olursa onları da args ismi içine at.” diyoruz.

* operatörü, fonksiyonları çağırırken de kullanılabilir. Argümanın adının başında kullanılan * sembolü, “bunun içindeki değerleri ayrıştır ve de konumsal parametreler olarak kullan.” anlamına gelir.

Örnekle açıklamak gerekirse;

>>> def add(x, y):
...     return x + y
>>> lst = [1,2]
>>> add(lst[0], lst[1]) # 1
3
>>> add(*lst) # 2
3

#1 adımında yapılan ile #2 adımında yapılan aynı şeyler. Python, bizim #1 adımında elle yaptığımız şeyi #2 adımında otomatikman yapıyor. Yani; *args şeklindeki bir tanımlama;

  • Fonksiyon çağırırken: yinelebilen (iterable) bir objeyi (mesela list) elementlerine ayırarak konumsal değişkenlere kullan
  • Fonksiyon deklere ederken: verilen konumsal değişkenlerden, deklerasyon sırasında özellikle belirtilmemiş olanları şu ismin içinde depola

demek.

** operatörü de buna benzeyen bir mantık. * operatörünün yinelenebilen objelerde yaptığını, sözlük (dictionary) objelerinde yapıyor. * operatörü konumsal parametrelerle ilgili, ** operatörü de key/value çiftleri (opsiyonel parametreler) ile ilgili.

>>> def foo(**kwargs):
...     print kwargs
>>> foo()
{}
>>> foo(x=1, y=2)
{'y': 2, 'x': 1}

Bir fonksiyon deklere ederken, **kwargs tanımını kullandığımızda şöyle demiş oluyoruz: “Özellikle tanımlanmamış olan bütün anahtar kelimeli parametreleri (keyword arguments) kwargs isminin içinde depola.” Ne args ismi ne de kwargs ismi Python’da rezerve edilmiş isimler değiller. Ama, başkalarının kodlarını incelerken çokça karşınıza çıkacak isimler.

Aynı * gibi, ** operatörünün de fonksiyon deklere ederken ve çağırırken ayrı anlamları var.

>>> dct = {'x': 1, 'y': 2}
>>> def bar(x, y):
...     return x + y
>>> bar(**dct)
3

12.Daha Genel Dekoratörler

Öğrendiklerinizi şöyle bir uygulama için kullanabilirsiniz; Bir fonksiyona beslediğiniz argümanların çetelesini tutan (günlüğe kaydeden) bir sarmalayıcı (wrapper) dekoratör tasarlayabilirsiniz. İşleyişi göstermek adına örneği basit tutacağız ve stdout’a (terminale) çıktı alacağız;

>>> def logger(func):
...     def inner(*args, **kwargs): #1
...         print "Arguments were: %s, %s" % (args, kwargs)
...         return func(*args, **kwargs) #2
...     return inner

Görüldüğü üzere, inner isimli fonksiyon, #1 adımında, herhangi bir sayıda argüman alıyor. #2 adımında, argümanları sarmaladığı func fonksiyonuna aktarıyor. Böylece, argüman imzası ne olursa olsun, herhangi bir fonksiyonu sarmalayabiliyoruz. Bir başka tanımla, dekore edebiliyoruz.

>>> @logger
... def foo1(x, y=1):
...     return x * y
>>> @logger
... def foo2():
...     return 2
>>> foo1(5, 4)
Arguments were: (5, 4), {}
20
>>> foo1(1)
Arguments were: (1,), {}
1
>>> foo2()
Arguments were: (), {}
2

Fonksiyonları çağırdığımızda, günlük tutucunun (logger) yazdırdığı satırı (argümanların listesi) alıyoruz. Argümanlar da fonksiyona dekoratör tarafından doğru aktarılmış ki, beklediğimiz return değerini alıyoruz.


Kaynak: istihza.com & simeonfranklin.com