Python-多程式中關於類以及類例項的一些思考
1. 背景
在最近完成了一個小工具,完成了關於日誌識別、比較的相關功能,雖然目前這個小工具很多功能需要進行完善,但是並不影響我想在這裡推薦的決心: CessTop - CessTop ---- A Smart Tool written in Python to Parse and Compare the Cisco Firewall Config File with TopSec Firewall Config File
在這個過程中,因為需要重構我的程式碼,我需要為三個不同的程式需要扮演不同的角色,第一個程式負責處理 Cisco 的配置文件內容, 第二個程式負責處理 TopSec 的配置文件內容,第三個程式等待前兩個程式處理完相關的資料結構之後,再進行比對,即第三個相當於在執行的前期起到了一個服務監聽的功能。
在這個過程中,為每一個程式都設計了一個獨立的類來定義不同的資料變數,因此需要為每一個類的例項物件建立一個程式;
這些是撰寫這篇部落格的一個背景....
一點點小的思路 - 火花(或者撰寫這篇部落格的動力?!):
-
在 Pycharm IDE 中如果不定義
@staticmethod
就會一直提示建議將你的新定義的函式轉換成為 Global 的函式定義,我不明白為什麼會出現這個問題,但是我覺得有必要了解一下類中函式的定義規則; -
在程式的建立中,都知道 Python 的多程式實現是基於
multiprocessing
的Package來實現的,至於怎麼實現多程式,在Windows 和 類Unix 的系統是不同的,在這裡我只研究 類Unix 的實現,即呼叫fork
函式帶來的問題;1.在對於執行緒池 (pool) 的呼叫
apply
以及apply_async
函式時候的問題;2.怎麼去實現多程式間的通訊來保證程式之間的引數傳遞?使用Pipe還是Queue?
2. Python 類中的函式 - staticmethod / classmethod
肯定很多朋友對這個概念已經很熟悉了,下邊簡單的說一下並舉幾個例子:
staticmethod
@staticmethod
定義了類中的靜態函式,這個靜態函式有幾個特性:
-
可以不被類的例項呼叫,即直接從類就可以呼叫,即不需要宣告一個例項:
class A(object): @staticmethod def demo_method(info:str): print(info) A.demo_method("This is staticmethod") # This is staticmethod
-
靜態方法相當於已經從類中分出去了,但是也可以通過
self
來呼叫類中的私有變數,但前提是必須要建立一個類的例項,因為這個函式不可以與類例項繫結,因此需要為self
進行傳參,即self
的值為一個新的類例項變數:class A(object): __private_var = "This is Private Variable" @staticmethod def demo_method(self): print(self.__private_var) A_instance = A() # 這裡需要為 self 制定一個引數,引數為新建立的 temp A.demo_method(A_instance)
-
靜態方法是可以被子類所繼承的,繼承的方式遵循類的繼承方式:
class A(): @staticmethod def A_method(info): print(info) # B類繼承A類的函式 A_method class B(A): @staticmethod def B_method(self, info): super().A_method(info) # 這裡建立一個新的B的例項 B_instance = B() B_instance.B_method(B_instance, "This is B invokes A staticmethod") # 這裡可以列印出 This is B invokes A staticmethod # 即B呼叫了A的A_method
我們都知道雖然
@staticmethod
定義了一個類外的函式,因此繼承過的類例項是不能訪問被繼承類中的私有變數的,除非你為被繼承的類宣告一個類例項;上邊的靜態也可以被寫成以下的形式:
class A(): @staticmethod def A_method(info): print(info) # B類繼承A類的函式 A_method class B(A): @classmethod def B_method(cls, info): super().A_method(info) B().B_method("This is B invokes A staticmethod") # 這裡可以列印出 This is B invokes A staticmethod # 即B呼叫了A的A_method
具體的解釋由
classmethod
的章節來進行進一步的講解;
classmethod
classmethod
定義了一個類中的 類方法, 由 @classmethod
裝飾器進行定義,其特點如下:
-
由於 以裝飾器
@staticmethod
進行修飾的類方法可以直接將類通過cls
繫結,因此呼叫不需要宣告一個類的例項:class A(): @classmethod def A_method(cls): print("This is A classmethod method") A.A_method() # 列印出: This is A classmethod method
當然,這並不影響你建立一個新的類例項,然後呼叫函式:
class A(): @classmethod def A_method(cls): print("This is A classmethod method") A_instance = A() A_instance.A_method() # 列印出: This is A classmethod method
-
對於一個被宣告瞭 類方法 的函式想要呼叫類中定義的變數,以及私有變數,可以嗎?答案是可以的!
class A(): class_var = "This is Class Variable\n" __private_var = "This is Class Private Variable\n" @classmethod def A_method(cls): print(cls.class_var) print(cls.__private_var) A.A_method() # 列印出: # This is Class Variable # This is Class Private Variable
但是這裡就涉及到了一個問題,在沒有例項的情況下,即在 堆疊中沒有建立一個 類例項,如果改變類的變數,這個類的變數會被修改嗎? - - - 好像會被改變......
class A(): num = 1 @classmethod def A_method(cls): print(cls.num) @classmethod def change_method(cls): cls.num = 2 A.change_method() A.A_method() # 我一開始認為是不能修改的,但是結果讓我很吃驚,居然被更改了.... 分析一下為啥被修改了還是有什麼影響... # 輸出是: 2 # 但是,目前我不認為這個類的定義被修改了,因此嘗試 新定義一個 類的例項 A_instance = A() print(A_instance.num) # 輸出是: 2 # 好吧, 被修改了.... # 分析一下這個過程
接著上邊的繼續分析,我們需要了解一下 Python 對類的定義,即 宣告這個類 到底被存在什麼位置?
class A(): num = 1 @classmethod def A_method(cls): print(cls.num) @classmethod def change_method(cls): cls.num = 2 print(cls) # 140683689759152 A.change_method() # 140683689759152 A.A_method() # 列印一下 python 的函式在記憶體中的位置 print(id(A)) # 140683689759152
即在上邊呼叫的類是儲存到相同地址的定義;
因此,因為引用了相同地址的類變數,因此存在了可能會改變類定義變數的情況;
-
現在,已經明白了在Pyton 類定義的常量可能會發生改變,那麼繼承的子類呼叫super的地址是什麼呢? 即:super 呼叫的是在全域性變數的類中定義? 還是類例項的地址?
-
如果直接呼叫子類 (B、C)而不建立一個子類的例項,那麼呼叫的父類不是直接定義的父類,即不會改變原來父類中的定義!從下邊的程式碼可以看到,在全域性變數中建立的 A、B 兩個類的地址是不一樣的; 在 B 中列印超類(父類)的地址與全域性變數的地址是不相同的,那麼就不會存在改變父類定義屬性的情況;
class A(): def A_method(): print("This is A method!") class B(A): @classmethod def B_method(cls): print(id(super())) class C(A): @classmethod def C_method(cls): print(id(super())) print(id(A)) # 140512863619088 print(id(B)) # 140512863620032 B.B_method() # 140511333031744 C.C_method() # 140511869048192
-
驗證一下上邊的給出的定義:
class A(): num = 1 def A_method(): print("This is A method!") @classmethod def A_ChangeMethod(cls): cls.num = 2 @classmethod def A_PrintNum(cls): print(cls.num) class B(A): @classmethod def B_method(cls): # print(id(super())) super().A_ChangeMethod() super().A_PrintNum() class C(A): @classmethod def C_method(cls): print(super().num) # print(id(B)) B.B_method() # 2 # print(id(A)) C.C_method() # 1
-
-
生成類的例項,再次驗證,即不會被修改!
class A(): num = 1 def A_method(): print("This is A method!") @classmethod def A_ChangeMethod(cls): cls.num = 2 @classmethod def A_PrintNum(cls): print(cls.num) class B(A): @classmethod def B_method(cls): super().A_ChangeMethod() super().A_PrintNum() class C(A): @classmethod def C_method(cls): print(super().num) B_instance = B() B.B_method() # 2 C_instance = C() C.C_method() # 1
-
定義的類例項的
cls
的地址是什麼?class A(): def A_method(): print("This is A method!") class B(): @classmethod def B_Method(cls): print(id(cls)) print(id(B)) # 140512865761952 B_instance = B B_instance.B_Method() # 140512865761952 B_instance_2 = B B_instance.B_Method() # 140512865761952
staticmethod 以及 classmethod 的比較
-
cls
以及self
的區別:-
兩者都有一種 C++ 中指標的感覺;
-
從上邊的例子可以看出,
cls
被用來指代函式定義的當前類中的指向儲存地址的變數:- 如果沒有宣告類的例項,那麼
cls
在被直接呼叫的時候被指向(引用)第一次定義類的地址,即全域性變數類的地址,即如果直接呼叫cls
修改類的屬性,就會被修改,這點要非常注意!; - 建立了類的例項,那麼
cls
也被指向第一次定義類的地址,因此做到cls
來呼叫屬性 或者 修改類的屬性要非常小心,可能會存在意外改變的情況,因此cls
可以做到對類屬性的追加;
- 如果沒有宣告類的例項,那麼
-
self
被用來指代 當前的類例項變數,並沒有什麼可以探討的;
-
一點小思考
- 在直接呼叫類引用的時候,是: 定義全域性變數類的呼叫,因此如果修改屬性會導致修改;
- 在考慮到繼承的因素的情況下,每一次繼承,編譯器會建立(深拷貝)一個臨時的父類來提供繼承的屬性以及方法,這種情況不考慮是否建立類例項,即不管建立一個例項與否編譯器都會深拷貝一個父類,因此
super
不會改變定義的全域性變數類的定義,super
我認為是非常安全的; - 在 Python 的類繼承中,子類會深拷貝 一個父類,從而實現呼叫 父類的屬性以及功能
- 這點帶來的優點是: 對於一個定義來說是非常安全的,即不會出現意外的錯誤;
- 缺點: 佔用資源;
3. Python 中的程式間的通訊 - multiprocessing/Queue
在最近的構建的小工具,中間用到了程式中的通訊,具體的實現過程請參考我的程式碼;
這裡說一下遇到的一個小問題,即 multiprocessing.Pool
中不同的程式之間的通訊問題,特此說明一下;
都知道在 Python 下有程式池的相關概念,呼叫非常簡單,即使用 Pool.add
以及 Pool.apply_async
或者 Pool.apply
來開啟相關的程式;
三個程式中,需要 前兩個程式來處理檔案,第三個程式來分析處理完的相關資料,因此我希望設計第三個程式為一個服務,等待前兩個程式處理完相關資料並返回結果在進行處理,有很多實現方式,我選擇了 Queue
來處理程式間的通訊,下邊的演示程式碼說明了這個過程:
from multiprocessing import Process, Pool, Queue
if __name__=='__main__':
queue_1 = Queue(maxsize=3)
pool = Pool(3)
with pool as pro:
result = pro.apply_async(f, args=(queue_1,),)
pool.close()
pool.join()
即我需要將 Queue
傳入到啟動函式中,完成引數在程式中的通訊,這個時候遇到了報錯:
RuntimeError: Queue objects should only be shared between processes through inheritance
分析一下這個錯誤:
-
首先,查詢了相關的 API 即,
apply_async
返回一個AsyncResult
型別的引數,這個引數可以返回程式的狀態,因為呼叫apply_async
之後,queues_1
不支援直接傳入到apply_async
的函式中; -
但是在
Process
中定義可以直接被傳入,即下邊的這種是被支援的:from multiprocessing import Process, Pool, Queue if __name__=='__main__': queue_1 = Queue(maxsize=3) process_1 = Process(target=f, args=(queue_1,)) process_2 = Process(target=f, args=(queue_1,)) process_3 = Process(target=f, args=(queue_1,))
解決方法:
- 呼叫
multiprocess.Manager
來建立一個允許多程式之間通訊的multiprocess.Manager.Queue
,然後被 Pool 物件呼叫; - 將
Pool
物件換成Process
物件;
寫到最後:
-
在多程式的呼叫中, 如果你自己寫了一個啟動程式函式而不重新覆蓋
Process.Run
函式,那麼你需要定義一個啟動函式,如果類中該函式的被定義為staticmethod
並定義了self
, 那麼你需要定義一個類的例項,然後通過Process
傳參:在類中的定義的啟動函式:
@staticmethod # def Start_Processing(self): def Start_Processing(self, queue: multiprocessing.Queue): try: self.access_list = self.Process_Cisco_LogFile_ToList(filename=self.filename) self.LogFileList_toPandasDF(self, Logfile_List=self.access_list) except Exception as err: raise err finally: queue.put(self.df_cisco) self.df_cisco.to_csv(config.default_config_dict["default"].cisco_csv_Name, sep=',', header=config.default_config_dict["default"].df_format, index=True)
呼叫啟動函式:
cisco_instance = cisco_function.Cisco_Function(filename_dict["cisco_filename"]) cisco_process = Process(target=cisco_instance.Start_Processing, args=(cisco_instance, queue_cisco,))
可以看到,必須為
Start_processing
函式的self
賦值一個類例項,才能正常啟動該函式;