Polimorfizm ve duck typing
Geçen hafta alt sınıfın üst sınıftaki bir metodu override edebildiğini gördünüz. Bu hafta farklı sınıflardan nesneleri aynı kodla kullanacağız. Farklı sınıflardan nesnelerin aynı metot çağrısıyla, aynı döngüde ya da aynı fonksiyonda kullanılabilmesine polimorfizm denir. Python’da bunun için sınıfların ortak bir üst sınıftan türemesi her zaman gerekmez. Nesnenin sınıfına değil, hangi metotları olduğuna bakan bu yaklaşıma duck typing denir. Adı “Ördek gibi yürüyor ve ördek gibi vaklıyorsa ördektir” sözünden gelir.
Bu sayfadaki kod hücreleri kendiliğinden çalışmaz. Her hücrede önce çıktıyı tahmin edin, sonra Run Code düğmesine basın. Kodu değiştirip yeniden çalıştırabilir, Start Over ile ilk hâline döndürebilirsiniz.
Bu bölümün kapsamı
- Aynı metot çağrısının farklı nesnelerde farklı iş yapması
- Ortak üst sınıf olmadan polimorfizm (duck typing)
- Her yeni türde uzayan tür koşulu zincirinin neden kod kokusu sayıldığı
isinstance()kullanımının yerinde olduğu durumlarhasattr()sonucunun neyi söyleyip söylemediği- EAFP ile LBYL yaklaşımlarının farkı ve yalnız beklenen istisnayı yakalamak
- Polimorfizm ile kalıtımın farkı
- Soyut temel sınıf (ABC) ve
@abstractmethodyazımı typing.Protocolile yapısal alt tipleme- Çoklu gönderim (multiple dispatch)
Bu başlıklar konunun devamıdır. İleride karşınıza çıkar, ama bu derste ezberlemeniz beklenmiyor.
Duyuru programındaki dört işi C’de nasıl yapardınız?
Okulun duyuru programını yazıyorsunuz. Bir duyuru e-postayla, SMS’le ve okulun mobil uygulamasından gönderiliyor. Her kanal mesajı kendi biçiminde gönderiyor: E-posta: Toplantı 14.00'te, SMS: Toplantı 14.00'te. Program şu dört işi yapacak:
- Bir duyuruyu bütün kanallardan tek bir döngüyle göndermek
- Mobil uygulama kanalını eklemek: bu kanalın sınıfını başka bir ekip yazdı, sizin sınıflarınızdan türetmedi
- Yeni bir kanal eklendiğinde gönderme fonksiyonunu değiştirmemek
- Listeye gönderme işi olmayan bir nesne karışırsa programı çökertmeden “Desteklenmiyor” yazmak
Bu işlerin her birini C’de nasıl yapardınız? Geçen haftaki kalıtım bilgisiyle Python’da nasıl yazacağınızı düşündüyseniz onu da yazabilirsiniz. Okumaya devam etmeden önce her iş için bir satır yazın: “Tür alanına bakıp switch ile ayırırım” da bir cevap.
Bölümde bu dört işin Python’da nasıl yazıldığını göreceğiz. Bölümün sonunda listenize döneceğiz.
Aynı çağrı, farklı davranış
Birinci iş: bir duyuruyu bütün kanallardan tek döngüyle göndermek. Aşağıdaki iki sınıfın ortak bir üst sınıfı yok, ama ikisinde de send(message) metodu var. Döngü çalışır mı? Çalışırsa ne yazar?
Döngü, nesnenin hangi sınıftan geldiğini bilmeden send() metodunu çağırır. Her nesne kendi sınıfındaki send() metodunu çalıştırır. Aynı çağrının farklı nesnelerde farklı iş yapması polimorfizmdir.
Kalıtım olmadan da polimorfizm
İkinci iş: başka bir ekibin yazdığı mobil uygulama kanalını eklemek. Geçen haftaki bilgiyle bütün kanalları ortak bir Notification üst sınıfından türetebilirdiniz. Aşağıda EmailNotification böyle yazıldı. AppNotification sınıfını ise başka ekip yazdı ve Notification sınıfından türetmedi. Döngü ikinci nesnede hata verir mi? isinstance() satırları ne yazar?
AppNotification nesnesi bir Notification sayılmıyor, ama döngü onunla da çalıştı. Çağrı için gereken tek şey nesnede send(message) metodunun bulunması.
Nesnenin hangi sınıftan olduğuna değil, gereken metodu olup olmadığına bakın.
Duck typing’in kuralı budur.
Her yeni türde uzayan koşul zinciri
Üçüncü iş: yeni bir kanal eklendiğinde gönderme fonksiyonunu değiştirmemek. C’de bu iş için çoğu zaman bir tür alanı ve switch yazılır. Python’daki karşılığı, nesnenin türüne bakan bir if/elif zinciridir. type(nesne) nesnenin sınıfını verir.
Aşağıdaki sınıflarda gönderme metotlarının adları farklı. Bu yüzden send_notification() her türü ayrı bir dalda tanıyor. AppNotification sonradan eklendi ve fonksiyona dokunulmadı. Son satır ne yazar?
Her yeni kanal bu fonksiyona bir elif daha ekletir. Eklemeyi unutursanız fonksiyon hata da vermez: hiçbir dal çalışmayınca None döndürür. Kodun çalıştığı ama tasarımda bir sorun olduğunu gösteren böyle işaretlere kod kokusu (code smell) denir. Buradaki sorun, mesajın nasıl gönderileceğini nesnenin değil fonksiyonun bilmesi. Bütün sınıflarda aynı adlı send() metodu olursa fonksiyon tek satıra iner ve yeni bir kanal eklemek için onu değiştirmeniz gerekmez:
def send_notification(notification, message):
return notification.send(message)isinstance() nerede yerinde?
Tür kontrolü bazı yerlerde yerindedir: programa dışarıdan gelen verinin beklenen türde olup olmadığını denetlerken (sınır doğrulaması), bir nesneyi dosyaya ya da ağa yazmak için metne çevirirken (serileştirme) ya da karar gerçekten türe bağlıysa. Kod kokusu, nesnenin kendi metodunu çağırmak yerine her yeni türde uzayan bir koşul zinciriyle ne yapılacağını seçmektir. Tür kontrolü gerekiyorsa çoğu zaman isinstance() seçilir: type(x) is Sınıf yalnız tam o sınıfı kabul eder, isinstance(x, Sınıf) alt sınıfların nesnelerini de kabul eder.
hasattr() ne söyler, ne söylemez?
Dördüncü iş: gönderme işi olmayan bir nesne listeye karışırsa “Desteklenmiyor” yazmak. Önce şunu sorabilirsiniz: nesnede send var mı? Bir nesnede belirli bir adda nitelik ya da metot bulunup bulunmadığını hasattr(nesne, "ad") ile sorarsınız. İki satır ne yazar?
Ancak hasattr(obj, "send") yalnızca nesnede send adında bir şey bulunduğunu söyler. Bunun çağrılabilir bir metot mu yoksa sıradan bir nitelik mi olduğunu, message parametresini alıp almadığını ya da gerçekten mesaj gönderip göndermediğini söylemez. Örneğin obj.send = "evet" atanmış bir nesnede de hasattr(obj, "send") True verir. Bu yüzden duck typing’i “önce her şeyi hasattr ile kontrol et” diye anlamayın.
EAFP ve LBYL
Python kodunda iki yaklaşımın adını sık görebilirsiniz:
- LBYL (Look Before You Leap, “atlamadan önce bak”): İşi yapmadan önce koşulu kontrol edin.
- EAFP (Easier to Ask Forgiveness than Permission, “özür dilemek izin istemekten kolaydır”): İşi doğrudan yapmayı deneyin. Beklediğiniz hata çıkarsa onu uygun
exceptile yakalayın.
hasattr() ile önce sormak LBYL yaklaşımıdır:
if hasattr(notification, "send"):
result = notification.send(message)
else:
result = "Desteklenmiyor"EAFP yaklaşımında send() doğrudan çağrılır. Nesnede send yoksa Python AttributeError yükseltir ve except bu hatayı yakalar. Aşağıdaki Printer sınıfında send yok. SmsNotification sınıfının send() metodunda ise upper yanlış yazılmış. Üç satır ne yazar?
except Exception gibi geniş bir yakalama her hatayı yutar. Yalnız beklediğiniz hata türünü yakalayın. Hücre bir tuzak daha gösteriyor: AttributeError, send() metodunun içinde de oluşabilir. SmsNotification mesaj gönderebilen bir sınıf, ama metodun içindeki yazım hatası yüzünden “Desteklenmiyor” sonucunu aldı. Kendi programlama hatanız böylece gözden kaçar. Her bildirim nesnesinde send() bulunacağı baştan belliyse çoğu zaman en sade çözüm metodu doğrudan çağırmaktır.
Bu iki yaklaşımda 5. haftada gördüğünüz try/except bilgisini nesnelerin metotlarına uyguluyorsunuz. Hangisinin her zaman doğru olduğunu söyleyen bir kural yok. Bir yanda gereksiz ön kontrollerle uzayan kod, öbür yanda gerçek hataları da yutan geniş bir except var. İkisinin arasındaki dengeyi gözetin.
Davranış sözleşmesi
Duck typing’de de kurallar vardır. send(message) gibi ortak bir metodun ne aldığı, ne döndürdüğü ve hangi durumda hangi hatayı yükselttiği belli olmalıdır. Örneğin bu bölümdeki bütün bildirim sınıflarında send() bir metin alır ve bir metin döndürür. Bu kuralları yazan ayrı bir sınıf olmasa bile kuralların kendisi bir davranış sözleşmesidir: çağıran kod bu kurallara güvenerek yazılır.
Fonksiyonlar da polimorfik davranabilir
len() tek bir fonksiyon, ama aşağıda üç farklı türle çağrılıyor. Üç satır ne yazar?
len() metinle, listeyle ve sözlükle aynı biçimde çalışır, çünkü üç türde de uzunluğu veren __len__ adlı özel bir metot vardır. len(x) yazdığınızda Python nesnenin __len__() metodunu çağırır. Bir işlemin çalışması için nesnede bulunması gereken metotlara protokol denir. 12. haftada kendi sınıflarınıza bu tür özel metotlar yazıp onları print() ve == gibi Python işlemleriyle çalıştıracaksınız.
Polimorfizm ve kalıtım ayrımı
Kalıtım iki sınıf arasındaki bir ilişkidir: class SmsNotification(Notification) yazdığınızda SmsNotification üst sınıfın metotlarını devralır. Polimorfizm ise farklı nesnelerin aynı metot çağrısıyla kullanılabilmesidir. Kalıtım polimorfizmi kolaylaştırabilir. Ama Python’da ortak üst sınıf zorunlu değildir: bu bölümün ilk örneğinde kalıtım yoktu.
Tanımanız yeterli: soyut temel sınıf (ABC)
from abc import ABC, abstractmethod
class Payment(ABC):
@abstractmethod
def pay(self, amount):
passABC ile alt sınıfların hangi metotları yazması gerektiğini kodda açıkça belirtirsiniz: buradaki Payment sınıfından türeyen her sınıfta bir pay() metodu olmalıdır. ABC yazmak sınavda sorulmaz.
Soruya dönelim: duyuru programı Python’da
Bölümün başında duyuru programının dört işini nasıl yapacağınızı sormuştuk. Python’daki karşılıkları şöyle:
| İş | C’de ya da önceki haftalarda | Bu haftaki Python yolu | Dikkat |
|---|---|---|---|
| Bir duyuruyu bütün kanallardan tek döngüyle göndermek | C’de tür alanı ve döngüde switch, ya da 10. haftadaki gibi ortak üst sınıf ve override |
Her sınıfta send(message), döngüde notification.send(message) |
Çağrı her nesnenin kendi sınıfındaki metoda gider |
| Başka ekibin yazdığı kanalı eklemek | C’de onların struct’ını kendi tür alanınıza uydurursunuz, Python’da sınıflarını Notification sınıfından türetmelerini istersiniz |
Sınıfta send(message) bulunması yeter (duck typing) |
isinstance(x, Notification) False döndürse de çağrı çalışır |
| Yeni kanalda gönderme fonksiyonunu değiştirmemek | switch’e yeni bir case ya da if/elif zincirine yeni bir dal eklersiniz |
return notification.send(message) |
Dal eklenmeyen türde zincir hata vermez, None döndürür |
| Gönderemeyen nesnede “Desteklenmiyor” yazmak | switch’in default dalında yazdırırsınız |
hasattr() ile önce sormak (LBYL) ya da try/except AttributeError (EAFP) |
except AttributeError, send() içindeki yazım hatasını da yakalar |
Listenizi tabloyla karşılaştırın. C’de switch ya da Python’da if/elif zinciri yazdıysanız üçüncü satırdaki sorunla karşılaşırsınız: her yeni kanal gönderme fonksiyonunu değiştirmeyi gerektirir. Ortak üst sınıf ve override cevabı da çalışır, ama Python’da şart değil: ikinci satırdaki kanal üst sınıf olmadan da döngüde çalıştı. Gönderme işini fonksiyon yerine nesnenin kendisi bildiğinde dördüncü satırdaki kontrole de çoğu zaman gerek kalmaz. Her nesnede send() olacağı belliyse metodu doğrudan çağırırsınız.
Alıştırma: Koşul zincirini kaldır
Tür kontrollerini kaldırın: döngü yalnızca animal.speak() çağırsın ve çıktı aynı kalsın. Ardından bir Bird sınıfı ekleyin ve döngüye dokunmadan onun da çalıştığını gösterin.
Alıştırma: Ortak davranışı tamamla
return payment.pay(amount)Sıra sizde: Rapor çıktıları
ConsoleReport, TextReport ve CompactReport sınıflarının her birine bir render(data) metodu yazın. Tek bir show_report(report, data) fonksiyonuyla üçünü de kullanın. Dördüncü bir rapor sınıfı ekleyin ve show_report() fonksiyonunu değiştirmeden onunla da çalıştığını gösterin.
Çalışır kod görevi: Dört rapor nesnesini bir listeye koyun, döngüyle dolaşıp her birinin render() sonucunu yazdırın. Ayrıca bir nesnede hasattr(..., "render") sonucuna bakın. Sonra çözümünüzün neden tür ya da nitelik kontrol eden bir if zincirine ihtiyaç duymadığını iki cümleyle açıklayın.
Tek sayfa özet
- Polimorfizmde aynı metot çağrısı her nesnede o nesnenin kendi işini yapar.
- Duck typing nesnenin sınıfına değil metotlarına bakar. Bu yüzden ortak bir üst sınıf her zaman gerekmez.
- Nesnenin türüne bakan
if/elifzinciri her yeni türde uzar ve dal eklenmeyen türde hata vermedenNonedöndürebilir. Nesnenin metodunu çağırırsanız yeni bir tür eklemek için çağıran kodu değiştirmeniz gerekmez. isinstance(), dışarıdan gelen veriyi doğrulamak gibi kararın gerçekten türe bağlı olduğu yerlerde kullanılır.hasattr()yalnız bir adın bulunup bulunmadığını söyler. Metodun sözleşmeye uygun çalışacağını garanti etmez.- LBYL işten önce koşulu kontrol eder, EAFP işi dener ve hatayı yakalar. EAFP’de yalnız beklediğiniz hata türünü yakalayın ve metodun içinde oluşan aynı türden hatanın da yakalanacağını unutmayın.
- Kalıtım sınıflar arasındaki bir ilişkidir. Polimorfizm nesneleri kullanma biçimidir ve kalıtım olmadan da mümkündür.
Bu bölümün kazanımları
Bu bölümü bitiren öğrenci:
- Aynı metot çağrısının farklı nesnelerde farklı iş yaptığını örnekle gösterir.
- Ortak üst sınıf olmadan da polimorfizmin mümkün olduğunu açıklar.
- Her yeni türde uzayan tür koşulu zincirini kod kokusu olarak tanır ve yerine ortak bir metot çağrısı koyar.
isinstance()kullanımının hangi durumda yerinde olduğunu belirler.hasattr()sonucunun neyi söyleyip neyi söylemediğini açıklar.- EAFP ile LBYL yaklaşımlarını karşılaştırır ve yalnız beklenen istisnayı yakalamanın gerekçesini açıklar.
- Polimorfizm ile kalıtımı birbirinden ayırır.