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

DoHerasYang 發表於 2021-04-06

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