接著上篇文章"Python OOP 01: 其實class不難,就是個班級",我們今天來討論討論dunder method,也可以叫做magic method,為何可以叫做magic method呢? 我們不彷先來了解python這個語言的中心思想。
Python black box
大家不知道有沒有在學習python的時候,常常聽到,python是個很適合入門的程式語言,而且語法簡潔,容易理解也很易容教。其實很大部分的原因可以歸功於python將很多東西都黑盒化了,至於什麼是黑盒化呢? 簡單來說黑盒化我們可以讓我們輸入跑進一個黑色的盒子的加工廠(我們看不到裡面),然後他的輸出就跑出來了,這樣的過程我們不用去理解中間經過了甚麼,只要知道我們怎麼輸入輸出就好,就像是魔術一樣酷炫,至於這樣有甚麼好處呢?
假如是統計分析家,我不用琢磨太多python語法的問題,我可以輕易地用python達到目的
多虧python黑盒化的設計風氣,許多第三方的套件也是這樣設計,我們管用就好
- pd.read_excel('data.xlsx')就可以輕鬆把DATA讀取出來了,我們不太需要去煩惱中間的過程

Dunder method
聊完黑盒的概念,那代表python幫我們隱藏了好多東西,我們要怎麼找出來呢?
來請dir()這個built-in function幫我們找找:
print(dir('hello world'))
---------------output----------------------
['__add__', '__class__', '__contains__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__getnewargs__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__iter__', '__le__', '__len__', '__lt__', '__mod__', '__mul__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__rmod__', '__rmul__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'capitalize', 'casefold', 'center', 'count', 'encode', 'endswith', 'expandtabs', 'find', 'format', 'format_map', 'index', 'isalnum', 'isalpha', 'isascii', 'isdecimal', 'isdigit', 'isidentifier', 'islower', 'isnumeric', 'isprintable', 'isspace', 'istitle', 'isupper', 'join', 'ljust', 'lower', 'lstrip', 'maketrans', 'partition', 'replace', 'rfind', 'rindex', 'rjust', 'rpartition', 'rsplit', 'rstrip', 'split', 'splitlines', 'startswith', 'strip', 'swapcase', 'title', 'translate', 'upper', 'zfill']
還記得class是個班級的概念吧 ? 而string本身就是一個class,dir能幫我們把class中的小朋友能做的事情(methods)拿出,然後就有__add__,__class__,__dir__...這些跟其他行為長得不太一樣的特別行為,這些就是俗稱的dunder methods,英文可以把__add__唸成underscore underscore add underscore underscore,但太長了所以python community有個python specific的叫法dunder or magic,這些dunder在黑盒化的貢獻功不可沒,你可曾想過 a = 1 + 1 以及 a = 1 > 2在python中如何運作嗎...? 而dunder就默默幫我們完成這一切。
建立自己的class用上這些小朋友看看
直接解釋dunder怎麼運作的實在是太過複雜了,因此我們來嘗試看看在我們的class裡面用看看,我們最早學到的dunder很高機率應該是__init__,這個行為是叫小朋友去做初始化的動作( initialization method )顧名思義,我們初始化一個班級時常常會使用,例如我們要幫班級建立時,就會有些小朋友幫忙當收錢的管帳的,那小朋友就會乖乖把這個東西丟到self內,變為共同財產(attribute)。
from datetime import date
class MusicClass:
"""
音樂班
name = 音樂作品名稱
instruments = 用的樂器
"""
def __init__(self,name,instruments,play_date=None):
self.name = name
self.instruments = instruments
if not play_date:
self.play_date = date.today()
song1 = MusicClass('song1',['trumpet','piano'])
print(song1)
---------------output----------------------
<__main__.MusicClass object at 0x0000023A24765048>
這個案例實在太尷尬了,我們小朋友辛苦表現的song1竟然是顯示記憶體位置,我們有義務為小小朋友的表現(instance)印個名片,這時就要叫我們的小朋友遞名片了,而負責公關的methods就是我們的__str__以及__repr__:
from datetime import date
class MusicClass:
"""
音樂班
name = 音樂作品名稱
instruments = 用的樂器
"""
def __init__(self,name,instruments,play_date=None):
self.name = name
self.instruments = instruments
if not play_date:
self.play_date = date.today()
def __str__(self):
return 'This song is %s' %self.name
def __repr__(self):
return 'This song is %s,palyed at %s' \
%(self.name,self.play_date)
song1 = MusicClass('song1',['trumpet','piano'])
print(song1)
print(repr(song1))
---------------output----------------------
This song is song1
This song is song1,palyed at 2022-04-25
可以看到我們創建的__str__以及__repr__很好的公關了音樂作品的特徵,官方文檔是建議__str__是用來大致描述,而__repr__則用來多多表述更多細節(represent),要注意不是寫print而是return,因為我們要讓我們寫出來的班級(物件)是可以神不知鬼不覺的與其他python物件互動的ie.print所以這邊使用return,可以理解我們寫入了__str__ 或__repr__之後我們Python print就知道要怎麼跟我們設計的音樂班物件互動了。
__add__的用法
有了上面的例子我們再來看一個__add__小朋友,add顧名思義,就是把我們小朋友作品統整起來的一位小朋友,我們需要建構他時+上一個other,讓他知道怎麼跟別人互動(other instance created by MusicClass),我們來舉一個要用到很多作品的場合,演唱會:
from datetime import date
class MusicClass:
"""
音樂班
name = 音樂作品名稱
instruments = 用的樂器
"""
def __init__(self,name,instruments,play_date=None):
self.name = name
self.instruments = instruments
if not play_date:
self.play_date = date.today()
def __add__(self, other): # other instance created by MusicClass
return (
f'{self.name} {other.name}',
self.instruments + other.instruments,
)
song1 = MusicClass('song1',['trumpet','piano'])
song2 = MusicClass('song2',['Afghani guitar','piano'])
song3 = MusicClass('song3',['fluet','Bagpipes'])
concert = song1 + song2 #開演唱會要唱兩首歌
print(concert)
concert += song3
print(concert)
---------------output----------------------
('song1 song2', ['trumpet', 'piano', 'Afghani guitar', 'piano'])
TypeError: can only concatenate tuple (not "MusicClass") to tuple ##我們自創的class return 的是tuple
可以看到開演唱會要統計歌曲跟樂器時,兩首歌可以順利相加,但是加到第三首時出現問題了tuple不能與我們定義的class相加,這個時候該怎麼辦呢? 其實我們只要讓__add__ return MusicClass的作品就好,而且我們還可以更進一步多利用isinstance()來多做個判斷,讓我們自創的資料型態跟python內建的dictionary也可以做相加:
from datetime import date
class MusicClass:
"""
音樂班
name = 音樂作品名稱
instruments = 用的樂器
"""
def __init__(self,name,instruments,play_date=None):
self.name = name
self.instruments = instruments
if not play_date:
self.play_date = date.today()
def __str__(self):
if len(self.name.split(' ')) <=1:
return 'This contains %s' %self.name
if len(self.name.split(' ')) >1:
return 'This contains %s, total %s songs' \
%(self.name,len(self.name.split(' ')))
else: return
def __add__(self, other):
if isinstance(other,MusicClass):
return MusicClass(
name = f'{self.name} {other.name}',
instruments = self.instruments + other.instruments,
)
elif isinstance(other, dict):
if other.get('name') and other.get('instruments'):
return MusicClass(
name = self.name +' '+ other.get('name'),
instruments = self.instruments + other.get('instruments'),
)
else:
print('dictionary must include "name" and "instruments" keys')
raise KeyError
else:
raise TypeError
song1 = MusicClass('song1',['trumpet','piano'])
song2 = MusicClass('song2',['Afghani guitar','piano'])
song3 = MusicClass('song3',['fluet','Bagpipes'])
print(song1)
concert = song1 + song2
print(concert)
concert += song3
print(concert)
concert += {'name':'song4','instruments':['france trumpet','Berimbau']}
print(concert)
---------------output----------------------
This contains song1
This contains song1 song2, total 2 songs
This contains song1 song2 song3, total 3 songs
This contains song1 song2 song3 song4, total 4 songs
結論
dunder是python一大精髓,讓python設計時黑盒化的關鍵
想要讓python operators, 或是built-in funcion等等可以用於我們自己class,需要請他們出場
我們之所以可以寫出1+1就是python已經幫我們定義好了int class中的__add__ methods,這只是dunder methods的冰山一角,想要更加了解可以參考這個連結 ,希望這篇有幫助到大家,如果我有寫錯或是寫不好的地方也歡迎提出指教。下篇我來談談繼承