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 Filepython

在這個過程當中,由於須要重構個人代碼,我須要爲三個不一樣的進程須要扮演不一樣的角色,第一個進程負責處理 Cisco 的配置文檔內容, 第二個進程負責處理 TopSec 的配置文檔內容,第三個進程等待前兩個進程處理完相關的數據結構以後,再進行比對,即第三個至關於在運行的前期起到了一個服務監聽的功能。git

在這個過程當中,爲每個進程都設計了一個獨立的類來定義不一樣的數據變量,所以須要爲每個類的實例對象建立一個進程;github

這些是撰寫這篇博客的一個背景....shell


一點點小的思路 - 火花(或者撰寫這篇博客的動力?!):安全

  • 在 Pycharm IDE 中若是不定義 @staticmethod 就會一直提示建議將你的新定義的函數轉換成爲 Global 的函數定義,我不明白爲何會出現這個問題,可是我以爲有必要了解一下類中函數的定義規則;數據結構

  • 在進程的建立中,都知道 Python 的多進程實現是基於 multiprocessing 的Package來實現的,至於怎麼實現多進程,在Windows 和 類Unix 的系統是不一樣的,在這裏我只研究 類Unix 的實現,即調用 fork 函數帶來的問題;app

    1.在對於線程池 (pool) 的調用 apply 以及 apply_async 函數時候的問題;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 賦值一個類實例,才能正常啓動該函數;

相關文章
相關標籤/搜索