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 赋值一个类实例,才能正常启动该函数;

Tags: