特殊方法
April 26, 2022在 Python 中可以定義特定的 __xxx__
方法名稱,這是一種協定,用以定義某些特定行為,像是到目前為止看過的 __init__
、__str__
、__repr__
等方法,這邊要再來看看運算子、建構、刪除等行為的定義。
定義運算子
型態遇到運算子時應該具有的行為,是可以藉用特殊方法定義的,例如:
>>> x = 10
>>> y = 3
>>> x + y
13
>>> x.__add__(y)
13
>>> x % 3
1
>>> x.__mod__(3)
1
>>>
可以看到,+
運算子實際上是由 int
的 __add__
方法定義,而 %
運算子是由 int
的 __mod__
方法定義,為了實際瞭解這些方法如何定義,先來個具體的範例,建立一個有理數類別,並定義其 +
、-
、*
、/
等運算子的行為。
class Rational:
def __init__(self, numer, denom):
self.numer = numer
self.denom = denom
def __add__(self, that):
return Rational(
self.numer * that.denom + that.numer * self.denom,
self.denom * that.denom
)
def __sub__(self, that):
return Rational(
self.numer * that.denom - that.numer * self.denom,
self.denom * that.denom
)
def __mul__(self, that):
return Rational(
self.numer * that.numer,
self.denom * that.denom
)
def __truediv__(self, that):
return Rational(
self.numer * that.denom,
self.denom * that.numer
)
def __str__(self):
return f'{self.numer}/{self.denom}'
def __repr__(self):
return f'Rational({self.numer}, {self.denom})'
在建立 Rational
實例之後,會經用 __init__
初始分子與分母,物件常見的 +
、-
、*
、/
等操作,分別是由 __add__
、__sub__
、__mul__
、__truediv__
定義(//
是由 __floordiv__
定義),至於物件的字串描述,想要以 1/2 這樣的形式呈現運算結果,這定義在 __str__
方法之中,而 __repr__
方法的實作,採用 'Rational(1, 2)'
這類的字串描述。
來看看 REPL 中的執行結果:
>>> r1 = Rational(1, 2)
>>> r2 = Rational(2, 3)
>>> print(r1 + r2)
7/6
>>> print(r1 - r2)
-1/6
>>> print(r1 * r2)
2/6
>>> print(r1 / r2)
3/6
>>>
〈算術運算〉曾經談過的 decimal.Decimal
類別,該類別建立的實例可以直接使用 +
、-
、*
、/
進行運算,就是因為 decimal.Decimal
類別定義了相對應的方法。
類似地,如果想定義 >
、>=
、<
、<=
、==
、!=
等比較,可以分別實作 __gt__
、__ge__
、__lt__
、__le__
、__eq__
或 __comp__
等方法。
operator
模組定義了一組運算子對應的函式,例如 add(a, b)
相當於 a + b
,如果需要將運算子當成是函式傳遞時可以使用,文件中也可看到特殊方法對應的運算符號;Python 3.5 新增了 __matmul__
方法,用於矩陣乘法,對應的運算符號是@,你可能會在NumPy 之類的第三方程式庫看到它。
__new__、__init__ 方法
到目前為止只要談到 __init__
,都是說這個方法是在類別的實例建構之後,進行初始化的方法,而不是說 __init__
是用來建構類別實例,這樣的說法是有意義的,因為類別的實例如何建構,是由 __new__
方法來定義,__new__
方法的第一個參數是類別本身,之後可定義任意參數作為建構物件之用。
__new__
方法可以傳回物件,若傳回的物件是第一個參數的類別實例,接下來就會執行 __init__
方法,而 __init__
方法的第一個參數就是 __new__
傳回的物件。__new__
如果沒有傳回第一個參數的類別實例(傳回別的實例或 None
),就不會執行 __init__
方法。
藉由定義 __new__
方法,可以決定如何建構物件與初始物件,一個應用的例子如下:
TLogger = Type['Logger']
class Logger:
__loggers = {}
def __new__(cls, name):
if name not in cls.__loggers:
logger = object.__new__(cls)
cls.__loggers[name] = logger
return logger
return cls.__loggers[name]
def __init__(self, name):
if 'name' not in vars(self):
self.name = name
def log(self, message):
print(f'{self.name}: {message}')
這個 Logger
類別的設計想法是,每個指定名稱下的 Logger
實例只會有一個,因此使用了一個 dict
來保存已建立的實例。如果以 Logger('某名稱')
呼叫時會先執行 __new__
方法,這時檢查 dict
中是否有指定名稱的鍵存在,若沒有表示先前沒有建立Logger實例,此時使用 object.__new__(cls)
建立物件,並以 name
作為鍵而建立的物件作為值,保存在 dict
中,接著傳回建立的物件;如果指定名稱已有對應的物件就直接傳回。
由於這個範例的 __new__
都會傳回 Logger
實例,在 __init__
方法中,為了不重複設定 Logger
實例的 name
屬性,使用 vars(self)
取得了 Logger
實例上的屬性清單,並看看 name
是否為 Logger
實例的屬性之一,如果不是,表示這是新建的 Logger
,為其設定 name
屬性,最後為了方便進行示範,定義了一個簡單的 log
方法來模擬日誌(Logging)的行為。
以下是個簡單的測試程式:
logger1 = Logger('xlogging')
logger1.log('一些日誌訊息....')
logger2 = Logger('xlogging')
logger2.log('另外一些日誌訊息....')
logger3 = Logger('xlog')
logger3.log('再來一些日誌訊息....')
print(logger1 is logger2)
print(logger1 is logger3)
程式中 logger1
與 logger2
參考的物件,都是使用 xlogging.Logger('xlogging')
來取得,根據 Logger
的定義,應該會是相同的 Logger
實例,而 logger3
使用的名稱不同,因此會是不同的 Logger
實例。一個執行結果如下:
xlogging: 一些日誌訊息....
xlogging: 另外一些日誌訊息....
xlog: 再來一些日誌訊息....
True
False
__del__ 方法
如果一個物件不再被任何名稱參考,就無法在程式流程中繼續被使用,那麼這個物件就是個垃圾了,執行環境在適當的時候會刪除這個物件,以回收相關的資源。如果想在物件被刪除時,自行定義一些清除相關資源的行為,可以實作 __del__
方法。例如:
>>> class Some:
... def __del__(self):
... print('__del__')
...
>>> s = Some()
>>> s = None
__del__
>>>
在這個例子中,原本被 s
參考的物件,由於 s
被指定了 None
,而不再有任何名稱參考了,就這個單純的例子來說,該物件馬上就被回收資源了,因此你看到 __del__
方法被執行了。
不過實際上,物件被回收的時機並不一定,也就無法預期 __del__
會被執行的時機,因此 __del__
中最好只定義一些不急著執行的資源清除行為,如果有些資源清除行為,希望能夠掌控執行的時機,那麼最好定義其他方法,並在必要時明確呼叫它。
想要知道還有哪些方法的話,可以看看〈Special method names〉。