接下來,連續三天,我們會每天分享五個Python小技巧。
# 01a中,x or 'x'使用的原理為bool(None)會被視為False,所以當沒有給定預定值時,self.x會被設定為'x',算是一種常用的語法。但這麼一來,當x給定任何布林值為False的obj時,Python會使用or之後的'x'來作為self.x,包括None、False和空的container等等。
# 01a
class MyClass:
def __init__(self, x=None):
self.x = x or 'x'
if __name__ == '__main__':
my_inst = MyClass(False)
print(my_inst.x) # 'x' (not False)
當不確定參數是否會傳入布林值為False的obj時,我們建議使用# 01b中,顯性判斷其是否為None的語法。看起來比較不酷沒錯,但是想起那些踩坑時的痛苦,建議還是多打幾個字,畢竟The Zen of Python都告誡我們Explicit is better than implicit.。
# 01b
class MyClass:
def __init__(self, x=None):
self.x = x if x is not None else 'x'
if __name__ == '__main__':
my_inst = MyClass(False)
print(my_inst.x) # False
None是一個不錯的預設值。但當您的程式可以接受None為一合法輸入值時,又該以什麼值來當預設值呢?事實上,不管你設什麼樣的值,使用者都可能會直接傳入那個值。
一個常見的方法是使用object()來當預設值,一般我們會稱呼這樣的用法為sentinel。由於每次的sentinel是無法預測的,所以除非於寫code時,顯性傳進這個值,才能確定這個值是由自己傳入的。
假設我們想要寫一個get_given function,其要求如下:
given與default。given時,該function需回傳given,否則即回傳default,而default之預設值為0。# 02a中以None為given的預設值,此時當顯性給予None時,其仍然會回傳2,但這不符合我們所希望的行為。
# 02a
def get_given(given=None, default=0):
return default if given is None else given
if __name__ == '__main__':
print(get_given('abc')) # 'abc'
print(get_given(None, 2)) # 2
# 02b中以object()為sentinel來作為given的預設值,此時只有當顯性傳入sentinel給given時,才能打破回傳值不是給予值的情況。
# 02b
sentinel = object()
def get_given(given=sentinel, default=0):
return default if given is sentinel else given
if __name__ == '__main__':
print(get_given('abc')) # 'abc'
print(get_given(None, 2)) # None
# We can only get 2(default) by passing sentinel to given explicitly
print(get_given(sentinel, 2)) # 2
在建立class的時候,常常會有一種情況是希望於__init__中選擇性接收一些值,來更新內部的某個mutable的container,像是dict、set或list等。
舉例來說,# 03a中的MyDict繼承UserDict(註1)。於__init__中接收一個選擇性的dict_data。當其不為None時,我們希望能將傳入的值,更新到self。
# 03a
from collections import UserDict
class MyDict(UserDict):
def __init__(self, dict_data=None):
super().__init__()
if dict_data is not None:
self.update(dict_data)
由於這樣類似的pattern很常出現,我們就開始動動腦,是不是有辦法免除這個if的確認呢?
首先# 03b試著將dict_data設為{}。修但幾勒,我們了解大家會說你怎麼可以把mutable的obj作為預設值呢?難道你沒讀過Python人必看的The Hitchhiker's Guide to Python嗎?但仔細想想,如果沒有設一個instance variable給dict_data的話,我們「好像」沒有辦法mutate它(除非直接mutate dict_data)。
# 03b
from collections import UserDict
class MyDict(UserDict):
def __init__(self, dict_data={}):
super().__init__()
self.update(dict_data)
當然,如果您寫成# 03c的格式,那麼我們就可以藉由self._dict_data來mutate dict_data。變動d._dict_data也會變動d2._dict_data,因為兩個是同一個obj,這樣的行為相信不是您想要的。
# 03c
from collections import UserDict
class MyDict(UserDict):
def __init__(self, dict_data={}):
super().__init__()
self._dict_data = dict_data
self.update(dict_data)
if __name__ == '__main__':
d, d2 = MyDict(), MyDict()
print(d._dict_data is d2._dict_data) # True
print(d, d2) # {}, {}
d._dict_data['a'] = 1
print(d._dict_data is d2._dict_data) # True
print(d._dict_data, d2._dict_data) # {'a': 1} {'a': 1}
# 03d中,使用空tuple來當預設值,由於tuple是immutable,所以不會有# 03c的情況,是一個我們覺得可以考慮的寫法,尤其是當這個變數代表iterable時。
# 03d
from collections import UserDict
class MyDict(UserDict):
def __init__(self, dict_data=()):
super().__init__()
self._dict_data = dict_data
self.update(dict_data)
if __name__ == '__main__':
d, d2 = MyDict(), MyDict()
print(d._dict_data is d2._dict_data) # True
print(d, d2) # {}, {}
d._dict_data = (1,)
print(d._dict_data is d2._dict_data) # False
print(d._dict_data, d2._dict_data) # (1,) ()
當需要formatdatetime的obj時,一般會使用datetime.strftime,如# 04a。
# 04a
from datetime import datetime
now = datetime.now()
datetime_fmt = '%Y-%m-%d_%H:%M:%S'
if __name__ == '__main__':
now_str = now.strftime(datetime_fmt)
print(f'{now_str}') # 2023-09-01_21:14:41
但其實f-string是可以認得datetime format所用的格式,而且使用起來更為方便。# 04b最後一行的f-string,我們使用兩層{}。外層datetime object的:後,可擺入需要format的格式。
# 04b
from datetime import datetime
now = datetime.now()
datetime_fmt = '%Y-%m-%d_%H:%M:%S'
if __name__ == '__main__':
print(f'{now:{datetime_fmt}}') # 2023-09-01_21:14:41
當定義客製的Exception時,常使用pass,如# 05a。
# 05a
class MyError(Exception):
pass
其實有時候也可以考慮使用docstrings代替pass,如# 05b。除了這是個合法的語法外(相當於指定__doc__),也可以為Exception增加說明。其它像是定義需要略為說明的class或是function時,也可以使用。
# 05b
class BetterMyError(Exception):
"""This error will be raised if..."""
另外,一個有趣的小知識,其實Ellipsis也是合法的語法,如# 05c。
# 05c
class MyCoolError(Exception):
...
註1:UserDict相比於繼承dict更加容易操作,如果有興趣深入研究的朋友,可以參考Trey的說明或是Python docs。