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