关于复制SimPy仿真环境的生成器的讨论

文章目录


参考文章:

  1. SimPy Discrete event simulation for Python
  2. python离散事件仿真库SimPy官方教程
  3. 离散事件仿真原理DES

基础内容

生成器介绍

离散事件仿真库Simpy的执行效率之所以很高,关键在于生成器的使用,在Python中通过yield来暂时停止进程,再次调用时才从中断的位置开始。这会有什么特点呢?就是程序并不需要在一开始完全执行,而是随着一点点推进(调用)的时候,才执行相应的操作,因此它不像常规的序列(如列表)一样在内存中保存所有的元素值,这种惰性计算地特性显著地减少了内存使用,但也使得生成器对象的状态不容易序列化和反序列化。

如下是一个简单的例子:

python 复制代码
def generator(n):
    while True:
        for j in range(n):
            yield j+100

先创建一个生成器,并进行调用,得到的返回值如下:

python 复制代码
a = generator(3)
print(next(a))
print(next(a))
print(next(a))

注意:yield 中断的是具体生成器的进程,因此创建多个不同生成器(一个对象也可以有多个生成器),可以实现多事件线的启停,这种特性非常适合用来进行离散仿真。

对于同一个生成器,调用3次,返回的结果为:

text 复制代码
100
101
102

那继续调用呢?调用6次后的返回结果如下:

text 复制代码
100
101
102
100
101
102

由于 while True 的存在,且暂无终止条件,只要一直调用,生成器就会一直抛出计算结果。

保存和恢复生成器的状态

现在,我想实现这么个功能,就是生成器在调用到一半的时候,对他进行复制,并希望复制的新的生成器能从源生成器的中断位置继续执行,如下代码,在第一次调用 a 生成器时,返回 100,复制为 b 生成器,希望 b 的下次调用抛出 101,那么,执行后会出现什么情况:

python 复制代码
def generator(n):
    while True:
        for j in range(n):
            yield j+100

a = generator(3)
print(next(a))

b = deepcopy(a)
print(next(b))

结果会返回类型错误。

text 复制代码
TypeError: cannot pickle 'generator' object

前面提到,生成器对象的状态不容易序列化和反序列,计算结果并不存储在内存中,这就使得我们如果想要保存生成器的状态,并进行复制,就只能考虑将这些生成器的值保存到可序列化的数据结构当中,比如列表或元组,通过这些可序列化的数据结构,来保存生成器的状态。那如何基于这些保存的可序列化的数据进行恢复呢?我们知道,生成器暂停进程,以及抛出一次次的计算结果,是因为函数 yield,因此恢复生成器状态只需要模拟生成器照常记录计算结果,在计算结果到达记录状态之前,跳过yield

为了恢复数据,需要记录生成器的当前状态,以及需要恢复的状态,这里我们建立一个 event 类,初始化的成员属性如下:

python 复制代码
class event:
    def __init__(self) -> None:
        self.record = []		# 当前状态
        self.from_record = []	# 待恢复状态

记录当前状态需要在每次运行到 yield 之前进行记录,这里记录的内容不一定是要yield语句的执行内容,可以只是一些标记的关键数据。

python 复制代码
class event:
    def __init__(self) -> None:
        self.record = []
        self.from_record = []

    def generator(self, n):
        while True:
            for j in range(n):
                self.record.append(j+100)
                yield j+100

记录完当前状态后,将当前状态与待恢复状态进行比较,如果不同,则跳过 yield,若相同,则释放到 yield 并完成状态的恢复。并建立一个成员方法 set_init_record 用来定义生成器的起始状态。

这里记录的状态可以是任何数据结构,当前状态和待恢复状态之间的比较也可以是任何逻辑运算,但要注意不论是记录状态还是对比恢复状态的操作,都是放在 yield 语句之前,不改变外层的循环。

python 复制代码
class event:
    def __init__(self) -> None:
        self.record = []
        self.from_record = []

    def generator(self, n):
        while True:
            for j in range(n):
                self.record.append(j+100)

                if len(self.record) <= len(self.from_record):
                    continue
                else:
                    yield j+100

    def set_init_record(self, init_record):
        self.from_record = init_record

此时,通过复制一个 event 对象的当前记录信息,将记录信息作为起始状态传入另一个 event 对象,即实现了生成器的状态复制,进行如下实验:

python 复制代码
a = event()
a_g = a.generator(3)
print(f"\na的抛出: ", next(a_g))

b = event()
b.set_init_record(deepcopy(a.record))
b_g = b.generator(3)
print(f"\nb的抛出: ", next(b_g))
print(f"b的抛出: ", next(b_g))
print(f"b的抛出: ", next(b_g))
print(f"b的抛出: ", next(b_g))
print(f"b的抛出: ", next(b_g))

输出结果为:

text 复制代码
a的抛出:  100

b的抛出:  101
b的抛出:  102
b的抛出:  100
b的抛出:  101
b的抛出:  102

显然的,这种方式相当于做了个标记,跳过了事件抛出的过程,但是所有的计算步骤都会重新执行一遍,但它是完完整整地恢复了整个生成器。因此换个思路,有没有必要完整地恢复整个生成器的抛出序列,还是记录待恢复状态,然后基于这个状态出发进行仿真。

相关推荐
秋难降4 分钟前
Python 知识点详解(三)
python·编程语言
chao_7891 小时前
二分查找篇——寻找旋转排序数组中的最小值【LeetCode】
python·线性代数·算法·leetcode·矩阵
金玉满堂@bj1 小时前
PyCharm 中 Python 解释器的添加选项及作用
ide·python·pycharm
程序员三藏1 小时前
如何使用Pytest进行测试?
自动化测试·软件测试·python·测试工具·职场和发展·测试用例·pytest
随心点儿2 小时前
使用python 将多个docx文件合并为一个word
开发语言·python·多个word合并为一个
不学无术の码农2 小时前
《Effective Python》第十三章 测试与调试——使用 Mock 测试具有复杂依赖的代码
开发语言·python
sleepybear11132 小时前
在Ubuntu上从零开始编译并运行Home Assistant源码并集成HACS与小米开源的Ha Xiaomi Home
python·智能家居·小米·home assistant·米家·ha xiaomi home
纪伊路上盛名在2 小时前
(鱼书)深度学习入门1:python入门
人工智能·python·深度学习
夏末蝉未鸣012 小时前
python transformers笔记(TrainingArguments类)
python·自然语言处理·transformer
德育处主任Pro2 小时前
「py数据分析」04如何将 Python 爬取的数据保存为 CSV 文件
数据库·python·数据分析