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
賦值一個類實例,才能正常啟動該函數;