Python - 關於類(self/cls) 以及 多進程通訊的思考


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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM