特殊方法

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)   

程式中 logger1logger2 參考的物件,都是使用 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〉。

分享到 LinkedIn 分享到 Facebook 分享到 Twitter