Python之classmethod和staticmethod的觀念理解

最近在模組開發完後需要轉換成 API 的接口服務,當時遇到一些流程問題需要解決,這篇算是探討 Python 的用法吧。

大概情境如下,模型訓練完成後,正常情況使用者會接續進行預測的動作,但也有可能因為使用者在自己的排程上暫時不需要再行預測,因此會擱置一段時間。在一段時間的靜置後,server 端會把目前 keep 住的模型消滅掉,以避免資源占用,然而當使用者要重啟服務時,重新訓練模型又會是成本,故希望重新將模型 load 回來,再直接進行預測。所以可使用 classmethod 協助我們在實例化之前把必要的資訊重載,並再行實例化。

上面大概是問題的情境,不甚清楚可以跳過,僅僅算是個紀錄,可直接看下方的內容。

使用時機

通常要使用 class 下方的 method 時(指的是我們經常使用的 instance method ),應是要先把這個 class 先給實例化(補一下實例化和把物件初始化,不確定兩者說法是否指同一件事),完成後便可應用承接了這個實例下的方法,如下面的程式:

1
2
3
4
5
6
7
8
9
10
11
12
class TemperatureSimulator:
def __init__(self, sample_rate):
self.fs = sample_rate

def train_model(self, sensor_data):
print('Detect your sample rate is %d'%self.fs)
print('I am training model now...')
return model

if __name__ == '__main__':
simulator = TemperatureSimulator(sample_rate=2)
simulator.train_model(data)

目前開發中最常使用到的就是上述的用法。那麼 classmethod 和 staticmethod 的用法差異在哪?可來看個簡單的範例,重新建立一下概念。

流程建立

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class ClassA:
attrB = 5
def instance_method(self, n): # 我們最常使用的method方式,默認第一個參數self是instance本身
print(n)

@classmethod
def class_method(cls, n): # 默認第一個參數是class
print(n)

@staticmethod
def static_method(n): # 指傳入自定義的參數即可
print(n)

a = ClassA()
a.instance_method(1) # self: <__main__.A object at 0x00000122EA903908>
ClassA.class_method(1) # cls: <class '__main__.ClassA'>
ClassA.static_method(1)

首先,程式碼會在第一段先執行 ClassA 下面的程式碼,創建一個 ClassA 的 class object,同時初始化裡面的attribute 和 method。

再來到了a = ClassA()時,才是進行實例化,這時會創建 instance object,而這個 a 會指向這個 instance object。調用a.instance_method(1)時,因為這是一個 instance method,所以 self 參數會與剛剛的 instance object 綁定(總之這時候 a 和 self 都指向同一個地方)。

最後在ClassA.class_method(1)被使用時,cls 則是指向了剛剛提到的 class object。而 ClassA.static_method(1)則是永遠指向同一個記憶體位置,也就是無論創建了多少實例,它的記憶體位置是永不變的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
### 1. 實體方法已實體化後綁定
ClassA.instance_method(1)
# <function ClassA.instance_method at 0x00000122EA8D4620>
a = ClassA()
a.instance_method(1)
# <bound method ClassA.instance_method of <__main__.ClassA object at 0x00000122EA903908>>


### 2. 靜態方法的位置不隨實體物件改變
a1 = ClassA()
a2 = ClassA()

a1.static_method # <function ClassA.static_method at 0x00000122EA8D4E18>
a2.static_method # <function ClassA.static_method at 0x00000122EA8D4E18>

a1.instance_method # <bound method ClassA.instance_method of <__main__.ClassA object at 0x00000122EA903978>>
a2.instance_method # <bound method ClassA.instance_method of <__main__.ClassA object at 0x00000122EA903A90>>

### 3. 類方法就是綁定類物件
ClassA.class_method # <bound method ClassA.class_method of <class '__main__.ClassA'>>
a1.class_method # <bound method ClassA.class_method of <class '__main__.ClassA'>>
a2.class_method # <bound method ClassA.class_method of <class '__main__.ClassA'>>

簡單來說,static method 的使用時機可以是在當這個方法裡不需要有 self 或是 cls 時,使用靜態方法能夠比較有效的完成工作,既不需要接收用不到的參數,另外效率也會好一些,因為 instance method,會在實際要使用它時才生成,然而 static method 不會有這個問題。

然而個人目前使用上的經驗來看,雖然感受上有點雞肋,因為常常開發時總歸大多還是使用 instance method,若遇到這種靜態的狀況,又可以以直接建立 function 的方式去進行,也就是該 function 和 class 包在同一個 module 下,可是實際上若考量到程式編寫時的架構性,當某一個方法雖然不需傳入 self 或 cls,仍然將該方法歸屬於它要服務的那個類之下,是非常有助於程式的可讀性的。

使用實例

但是在開發階段會遇到一些狀況,例如說我其實根本不需要承接實例化,可是這個方法的整體概念是要在這個 class 下進行的,那麼這個時候就可以使用 staticmethod。只要在 method 前面加上裝飾器 @staticmethod 即可,如下面的程式:

1
2
3
4
5
6
7
8
9
10
11
12
class TemperatureSimulator:
def __init__(self, sample_rate):
self.fs = sample_rate

@staticmethod
def get_preprocessed_data(data):
print('Data preprocessing...')
return preprocessed_data

if __name__ == '__main__':
simulator = TemperatureSimulator(sample_rate=2)
new_date = simulator.get_preprocessed_data(data)

那麼在剛剛提到這次 API 遇到的問題則是 model 重啟的問題,便是藉由 classmethod 內部先行準備好要實例化時 initial 的數據,再將整個物件實例化並傳出,如下面的程式:

1
2
3
4
5
6
7
8
9
10
11
12
class TemperatureSimulator:
def __init__(self, sample_rate):
self.fs = sample_rate

@classmethod
def load_model(cls, load_path):
print('Loading info from load_path...')

return cls(fs_from_loaded_info)

if __name__ == '__main__':
simulator = TemperatureSimulator.load_model('our_path')

如同上述的內容,其實就可以清晰的區分使用時機。

reference: