Introduction
Dunder Method 就是 Python 中 Class 內建用兩個底線包夾名稱的方法,例如 __init__
(唸 Dunder Init)。除了初始化之外,還能夠讓你自行發揮創意,客製化各種 Python 內建的語法,使得程式碼看起來更像Python。
來看看如何使用吧~
__dunder__ Methods
__repr__ 與 __str__
字串雙兄弟,前者用字串來描述我是誰,後者單純將自己輸出成字串格式。一個常見的 __repr__
寫法是直接輸出與自己等價的創建呼叫(直接複製貼上就可以再 New 一個跟自己一樣的複製物件的懶人作法)。
class Person:
def __init__(self, name):
self._n = name
def __repr__(self):
return f'{self.__class__.__name__}({self._n!r})'
另一個例子是輸出 str
的時候,如果需要引號以表明型態,__repr__
此時就能派上用場。
>>> f'A function name {method}' # call __str__
A function name __init__
>>> f'A function name {method!r}' # call __repr__
A function name '__init__'
__getitem__ 與 __setitem__
切片語法(Slice Notation)就靠它們了。
## __getitem__
>>> foo[2]
'n'
>>> foo[:1]
'p'
>>> foo[1:]
'en'
當中括號裡面出現冒號的時候,輸入的 index
會變成時 slice
type(包含 start
、stop
、step
Attributes),並且再透過 __setitem__
就可以實作一個神奇的 Mutable(內容可變型)自製 str
:
class MutableStr:
def __init__(self, txt):
self._txt = txt
def __repr__(self):
return self._txt.__repr__()
def __str__(self):
return self._txt
def __getitem__(self, index):
return self._txt.__getitem__(index)
def __setitem__(self, index, txt):
if type(index) == int:
start, end = index, index + 1
else:
start, end = index.start or 0, index.stop or None
tail = self._txt[end:] if end is not None else ''
self._txt = self._txt[:start] + txt + tail
來玩一下 MutableStr。
## __setitem__
>>> m = MutableStr('00000')
>>> m
'00000'
>>> m[3:] = 'zzz'
>>> m
'000zzz'
>>> m[:2] = '22'
>>> m
'220zzz'
__getattr__ 與 __setattr__
如果覺得 dict
每次都要寫中括號很麻煩,可以用 SimpleNamespace
或 namedtuple
來簡單改寫,改成以 Attribute 的方式(Dot Notation)來存取變數。而若想要進一步客製化屬性的行為,就需要用到這 2 個 Dunder Method。
例如以下 Class。
class DefaultZero:
def __getattr__(self, key):
return 0
def __setattr__(self, key, value):
self.__dict__[key] = value
def print_1(self):
print(1)
來看一下行為:
>>> z = DefaultZero()
>>> z.Apple
0
>>> z.banana = 777
>>> z.banana
777
>>> z.print_1()
1
>>> z.print_2() # z.print_2 fallback to 0, so not callable
TypeError: 'int' object is not callable
注意:被設值之後的屬性(Attribute)或有實作的方法(Method)就不會再走 __getattr__。
__enter__ 與 __exit__
有一些操作需要前置作業以及收拾殘局,例如 open
一個檔案之後需要 close
檔案。Python 提供了一個方便的 with
語法(Context Management)可以輕鬆解決這個問題,而我們可以輕鬆的客製化這個行為。
例如一個簡單的 Class 將 stdout
暫時關閉。
import sys
import io
class MuteStdout:
def __init__(self):
self._backup = sys.stdout
self.buffer = io.StringIO()
def __enter__(self):
sys.stdout = self.buffer
return self
def __exit__(self, exc_type, exc_value, traceback):
sys.stdout = self._backup
然後試玩一下:
>>> with MuteStdout() as m:
>>> print(123)
>>> print(456)
>>> ## nothing print out here
>>> m.buffer.getvalue()
'123\n456\n'
在 __enter__
之前會先走 __init__
,這樣就可以簡單的暫時把 stdout
重新導向至 buffer
而不用使用者麻煩的切換及備份。如果中途有發生任何的 Exception 也都會由 __exit__
提供的參數傳入。
順便介紹一個漂亮的 with 用法:
from contextlib import suppress
with suppress(ValueError):
raise ValueError # some codes may raise error that we don't care
如此即可免去如下的 try…catch…
的寫法:
try:
raise ValueError # some codes may raise error that we don't care
except ValueError:
pass
延伸閱讀:處理 Python 拋出的 Exception 例外異常並使用 Assert 確保程式正常執行
各種算數符(四則運算、比較符號等)
根據參考官方文件 Basic customization,可以進一步客製化:
- 加減乘除等:§3.3.8. Emulating numeric types
- 比較符號,懶的話可以用
total_ordering
,只需要實作__eq__
以及其餘的 4 選 1) - ⋯
Abstract Base Classes (ABC)
只要實作 Collections Abstract Base Classes 中的 Abstract Methods,就能立即享有所有 Mixin Methods 中的內建方法。
例如我想要自製一種 Sequece 可以走訪某個資料夾底下的檔案們。
from abc import abstractmethod
from collections.abc import Sequence
import os, glob
class LS(Sequence):
def __init__(self, directory):
self.file_list = self.list_files(directory)
def cd(self, directory):
self.file_list = self.list_files(directory)
def __getitem__(self, index):
return self.file_list[index]
def __len__(self):
return len(self.file_list)
@abstractmethod
def list_files(self, directory):
return []
class ListDir(LS):
# 歷遍當層資料夾的所有檔案
def list_files(self, directory):
return os.listdir(directory)
class ListExe(LS):
# 歷遍當層資料夾的所有.exe檔,遞迴要用**並且加recursive=True參數
def list_files(self, directory):
directory += '/' # add tailing slash to directory
flist = glob.glob(directory + '*.exe')
return [f.replace(directory, '') for f in flist]
來實驗看看:
>>> path = 'path/to/a/dir'
>>> ls1 = ListDir(path)
>>> ls2 = ListExe(path)
>>> list(ls1)
['index.html', 'test.exe', 'canvas.cpp', 'makefile', 'a.exe', 'input.txt']
>>> list(ls2)
['test.exe', 'a.exe']
如果想要建立一個完全空白的 Interface,可以繼承 abc.ABC
、並且標記所必須的 abc.abstractmethod
就好。
Pythonic 精神就在於用更簡潔易懂的程式來實現同樣的功能,而善用這些 Dunder Method 就能過使得我們自製的Python API 更加平易近人、並且寫出更 Python 的程式。
References
- Data model – Python Documentation
(網頁的 Top Bar 左邊可以切換顯示語言與 Python 版本) - Raymond Hettinger – Beyond PEP 8 — Best practices for beautiful intelligible code | PyCon 2015
- Raymond Hettinger – Build powerful, new data structures with Python’s abstract base classes | PyCon 2019
好像是第一次放影片的連結上來,最近很愛這名講者:)