在Python中记住过去(模型状态)的五种方法 从封闭函数和迭代器到状态机Python库
有人说...
"那些不能记住过去的人,注定要重复它"。G. Santayana, 1905
就像在现实生活中一样,在软件中,重要的是要知道 "状态 "是如何保存的,这可能是理想的行为或不是。状态是一个动态系统在某一时期的行为。
在软件中,系统的状态是通过保留一组相关变量的值来建模的。在一个更复杂的形式中,软件中的状态是通过状态机来建模的,其特征是状态、过渡,通常还有从一个状态过渡到另一个状态的概率。机器学习中的有限状态机(简称状态机)的例子是马尔科夫链和马尔科夫模型[1]。状态机在各种应用中都很重要。
安全关键型系统。例如,医疗设备[2]、配电系统[3]和运输系统,如铁路[4]、飞机[5]和自动驾驶汽车[6]。
区块链数据传输、存储和检索系统。区块链也被称为无限状态机[7]。
视频游戏[8],对话式人工智能[9],图形用户界面的实现[10]。
用BLAST(Basic Local Alignment Search Tool)寻找蛋白质序列的相似性[11]。
用于时间序列预测的深度状态空间模型[12]。
每个软件开发者都知道,要想在不同的函数调用之间保留一个变量的状态,最简单(也是最丑陋)的方法就是将这个变量声明为一个全局变量。但是,让我们忘掉丑陋,在管理一个虚构的、风景如画的地中海酒店的客人的用例中,描述五种不同的方法来模拟 Python 的状态。我们的酒店叫做Artemis Cypress(Artemis是古希腊的女神,而柏树是她的圣树)。
在创建管理酒店客人的功能时,我们将研究以下在 Python 中管理状态的方法。
空列表作为默认的函数参数。
Python闭包
迭代器
类变量
状态机Python库
1、默认函数参数的空列表
在我们讨论第一个用例之前,让我们注意以下几点。一般来说,将空列表作为默认函数参数是个坏主意。我们将在下面的用例中解释原因。
def roomsWithPet(roomNumber,lr=[]):
lr.append(roomNumber)
print(f"The rooms with pets are: {lr}")
return lr
L=roomsWithPet(300)
L=roomsWithPet(101)
L=roomsWithPet(200)
在我们的第一个用例中,酒店的预订专家想跟踪有宠物的客人预订的房间,以便分配额外的清洁人员。因此,每当有客人预订了带宠物的房间时,她的软件就会调用下面的函数 roomsWithPet()
。
该函数需要一个位置参数(roomNumber
)和一个命名参数lr
,其默认值为一个空列表。这个函数被调用了三次,只有roomNumber
这个参数。
lr
发生了什么? 在函数第一次被调用时,lr
被创建并填充了roomNumber
的值,随后的每一次,新的roomNumber
值都被附加到第一次调用时创建的lr
列表上。
因此,输出是:
The rooms with pets are: [300]
The rooms with pets are: [300, 101]
The rooms with pets are: [300, 101, 200]
在我们的例子中,这是理想的行为,但在许多情况下,你想在每次函数调用时都创建一个新的列表,这就不是了。因此,重要的是要知道,把一个空列表作为默认参数传递给一个函数,在函数调用之间保留列表的状态。
2.Python 闭包
在我们的第二个用例中,四个在酒店参加会议的客人来到前台办理退房手续。这些客人是同一家公司的员工,在酒店共用一个大套房。接待员需要计算每个人的应付余额。她可以调用下面的函数,输入的参数是套房号(501)、入住天数(3)、套房价格/天(400美元),以及每个人的额外费用(在酒店餐厅用餐)。但是调用一个函数四次,每次只有最后一个参数不同,这有点麻烦。
def checkOut(roomN,rate,nights,extra):
amountOwned=rate*nights+extra
return amountOwned
Python闭包为我们提供了一种更优雅的方式来做到这一点。它们是内部函数,可以访问外部函数中的值,甚至在外部函数执行完毕之后[13]。换句话说,闭包函数有能力记住外层函数的值,即使这些值已经不在内存中了[14]。总的来说,当我们不想写一个类时,Python闭包提供了一个很好的方法来进行数据隐藏,这是实现数据隐藏最常见的方法。
def balanceOwned(roomN,rate,nights):
def increaseByMeals(extra):
amountOwned=rate*nights+extra
print(f"Dear Guest of Room {roomN}, you have",
"a due balance:", "${:.2f}".format(amountOwned))
return amountOwned
return increaseByMeals
ba = balanceOwned(201,400,3)
ba(200)
ba(150)
ba(180)
ba(190)
在下面的代码片段中,内部函数 increaseByMeals()
是一个闭包函数,因为它记住了外部函数 balanceOwned()
的值,即使在后者执行之后。因此,这使得我们可以写出以下代码,其中balanceOwned()
只被调用一次,并在其执行后,我们用每个客人的餐费调用它四次。
更加优雅了。现在的输出是。
Dear Guest of Room 201, you have a due balance: $1400.00
Dear Guest of Room 201, you have a due balance: $1350.00
Dear Guest of Room 201, you have a due balance: $1380.00
Dear Guest of Room 201, you have a due balance: $1390.00
3.迭代器
在我们的第三个用例中,酒店的客人可以预约瑜伽教练或体育教练的个人课程。在下面的代码中,D是一个字典,键是房间号,值是 "Y "或 "PT"("Y "表示要求的瑜伽教练,"PT "表示要求的身体教练)。
D = {"123":"Y","111":"PT","313":"Y","112":"Y","201":"PT"}
ff = filter(lambda e:e[1]=="Y", D.items())
print(next(ff))
print(next(ff))
每天早上,一个新的字典被创建,其中房间号是根据请求的时间输入的。然后,为了得到请求瑜伽的房间,执行随后的 filter() 命令。现在中央服务器已经为瑜伽教练准备好了。
每次瑜伽教练要求去下一个房间时,服务器只需调用 next(ff),就会返回下一个排队等候瑜伽教练的房间。这样一来,瑜伽教练就不需要就下一个房间的去向相互沟通。一个简单的迭代器保留了房间如何被服务的整体状态,而next()命令将我们带到下一个需要被服务的房间。
输出结果是。
('123', 'Y')
('313', 'Y')
4.类变量和状态机Python库
在本节中,我们将讨论上述在Python中保留状态的最后两种方法。
类变量。这些是在类的结构中定义的变量,在类被构造时声明,并由类的所有实例共享。如果一个类的实例改变了一个类的变量,那么该类的所有实例都会受到影响,通过这种方式,类的变量在类的实例之间保留它们的状态。
Python 状态机库,是一个开源的库[15],允许定义状态和从状态到状态的转换动作。
我们将在最后一个用例中展示类变量和Python状态机库的使用,这个用例为我们酒店的桑拿房的使用提供了模型。酒店在两个不同的地方有两个桑拿房,每个都可以容纳两个人。
class Sauna(StateMachine):
#Class variable that counts the number of class instances
inst_counter=0
def __init__(self):
super().__init__()
self.entryTimes=[]
self.guestcount=0
self.__roomnumbers=[]
Sauna.inst_counter+=1
#States
empty = State('Empty', initial=True)
space_avail = State('OneSpaceOccupied')
full = State('NoSpace')
#Transitions
occupied = empty.to(space_avail)
guestleft = space_avail.to(empty)
nospace = space_avail.to(full)
spacefortwo = full.to(empty)
nomorefull = full.to(space_avail)
#Actions on Transitions
def on_enter_empty(self):
print("Welcome to the Sauna Room!")
def on_enter_space_avail(self):
print("One sauna seat available!")
currentT = DT.now()
self.__roomnumbers.append(random.randint(1,30))
self.entryTimes.append(currentT.strftime("%Y-%m-%d %H:%M:%S"))
self.guestcount+=1
def on_enter_full(self):
print("Sorry, the Sauna Room is full!")
self.__roomnumbers.append(random.randint(1, 30))
currentT = DT.now()
self.entryTimes.append(currentT.strftime("%Y-%m-%d %H:%M:%S"))
self.guestcount+=1
def on_enter_full(self):
print("Sorry, the Sauna Room is full!")
self.__roomnumbers.append(random.randint(1, 30))
currentT = DT.now()
self.entryTimes.append(currentT.strftime("%Y-%m-%d %H:%M:%S"))
self.guestcount+=1
def getEntryTimes(self):
return self.entryTimes
def getGuestCount(self):
return self.guestcount
@classmethod
def getSaunaCount(cls):
return Sauna.inst_counter
@property
def getroomnumbers(self):
return self.__roomnumbers
cycle= occupied | nospace | spacefortwo
我们下面的桑拿房类继承自状态机,并有一个类变量inst_counter
,用于计算该类的实例数量,每次调用该类的构造函数时都会递增。我们为每个桑拿房定义了三种状态:空(0个客人)、空闲(一个客人)和满(两个客人)。然后,我们给可能的过渡起名字。例如,nospace
意味着我们已经从space_avail
状态过渡到full
状态。
在定义了过渡之后,我们定义了过渡的动作。例如,函数on_enter_space_avail()
实现了当一个客人进入先前空的桑拿房时的动作。除了打印出有一个桑拿房座位可用外,该函数还增加实例变量guestcount
。工作人员想知道每个房间有多少客人使用过,以评估特定桑拿房位置的受欢迎程度。
获取当前时间。然后将其追加到列表entryTimes
中,这是一个实例变量。知道进入的时间是很有用的,这样在繁忙的时候,工作人员可以安排额外的毛巾等。
记录正在使用该房间的客人的房间号,将其追加到列表roomnumbers
中。这是为了计费的目的(超过两个疗程/周,这是免费的,客人要为额外的疗程计费)。
在我们的案例中,房间号是随机生成的。另外,值得注意的是,roomnumbers
列表是类的一个私有成员,为了访问它,我们定义了getroomnumbers
这个属性。
最后,我们定义了循环,它是一个状态的管道(序列):已被占用、无空间、空间二。
下面我们创建一个桑拿房的实例,并通过调用cycle()进行状态转换。
ArtemisSauna1 = Sauna()
ArtemisSauna1.cycle()
ArtemisSauna1.cycle()
ArtemisSauna1.cycle()
输出显示如下。在第一次调用cycle()后,它处于被占用的状态,所以它打印出 "有一个桑拿座位"。在第二次调用后,它处于nospace状态,并打印出 "对不起,桑拿房已满"。在第三次调用后,它处于spacefortwo状态,并打印出 "欢迎来到桑拿房!"。
One sauna seat available!
Sorry, the Sauna Room is full!
Welcome to the Sauna Room!
现在,让我们得到一些额外的信息:客人的进入时间、当前状态、到目前为止的客人数量和房间号。
L=ArtemisSauna1.getEntryTimes()
print(L)
print(ArtemisSauna1.current_state)
c=print(f"The number of guests is: {ArtemisSauna1.getGuestCount()}")
ic=print(f"The number of used saunas is: {ArtemisSauna1.getSaunaCount()}")
L3=ArtemisSauna1.getroomnumbers
print(L3)
下面是上述代码的输出。正如预期的那样,我们目前处于空状态,到目前为止有两个客人使用了ArtemisSauna1,而且我们目前有一个Sauna类的实例。
['2022-06-10 14:46:35', '2022-06-10 14:46:35']
State('Empty', identifier='empty', value='empty', initial=True)
The number of guests is: 2
The number of used saunas is: 1
[28, 26]
Python 提供了许多保留状态的方法,或者换句话说,保持对进程的跟踪。这很有价值,特别是对于安全关键系统,软件同时跟踪许多物理过程的状态,并将状态变化映射到适当的行动。
另一方面,正如许多GUI(图形用户界面)开发者所证实的那样,最恼人的缺陷之一是GUI不响应用户的行动,而是被锁定在一个特定的状态。总之,无论我们的目标是否是保留状态,我们都需要注意Python的保留状态的特异性。
本文由 mdnice 多平台发布