Full Circle - lite

社区杂志中文在线lite版 (under developing).

53期 - Python 编程 - 第27部分

| Comments

作者:Greg Walters | 翻译:文宁 校对:李蹊 吴云

如果你曾经排队买过电影票,或者曾经在交通高峰期等车,又或者曾经拿着写有数字98的小票在政府办公室等待,并且一个标志写着:“正在为42号服务”,那么你就在队列里呆过。

在电脑的世界里,队列是很常见的。作为一个用户,通常你不必考虑它们。对于用户而言它们是不可见的。但是,如果你必须处理实时事件,就得和它们打交道了。它只不过是一些正在排队等待被执行的数据。一旦数据进入队列,直到被访问后才会出队列。除非你让前面的数据项出队,否则就不能获得下一个数据项的值。例如,如果想获得队列里第15项的值,你就必须先访问前14项。一旦某项被访问,它就会出队列。一旦它离开队列,就不能再取回这个数据了,除非你把它保存到一个持久变量中。

队列有很多种。最常见的一些是FIFO (First In, First Out,先进先出), LIFO (Last In, First Out,后进先出), Priority(优先级队列), 以及 Ring(队列环)。关于环型队列的讨论我们将另找时间。

FIFO队列在我们日常生活中很常见。之前我所列举的有关队列的例子都是FIFO队列。队列里的第一个人首先获得处理,接着里面的每个人都向前走一步。在FIFO缓冲区里,项目所持有的号码是没有限制的(在合理范围内)。它们仅仅是按顺序堆在一起。当一个数据项被处理后,它将从这个队列里被推出去(或者叫出队),而它之后的每一个都会往前移动一个位置,使其更加靠近队列的前端。

LIFO队列在生活中就相对不常见了,但是还是有的。最容易想到的是橱柜里的一叠盘子。当盘子被清洗晾干后,它们将被叠放在橱柜里。最后一个放在它们上面的将被第一个用到。其余的都必须等待,也许很多天后才会被用上。这样看来,排队买电影票是FIFO队列的确是件好事儿,对吧?和FIFO队列一样,在合理的范围内,LIFO队列的大小也是没有限制的。队列里的第一项必须等待较新加入的项陆续被推出缓冲区(从堆里取出盘子)直到只剩它一个时方可被调用。

对于许多人来讲,要立刻想到一个Priotiry队列或许有点困难。想象一个公司只有一台打印机,每个人都要用到那台打印机,而打印工作由部门的权限控制。工资单,比如说,相对于你这个程序员有较高的权限(应该庆幸如此)而你相对于接待者有较高的权限(庆幸如此)。因此简单说就是,权限较高的那些数据先于权限较低的被处理,并离开队列。

FIFO

与数据有关的FIFO队列是比较容易想象的。一个python链表就是一个容易想到的。考虑一下这个链表……

[1,2,3,4,5,6,7,8,9,10]

这个列表中有10个元素。在列表中,你可以根据序号访问它们。然而,在队列中却不能根据序号访问这些元素。只能处理最近的一个,而且这个列表不是静态的,它是动态的。当我们请求队列中的下一个元素,当前元素就会被移除。那么用上面的例子,你请求队列中的一个元素。它会返回第一个元素(1)并且队列将变成这样:

[2,3,4,5,6,7,8,9,10]

再请求两个你将获得2,接着是3,然后返回,接着队列将变成这样:

[4,5,6,7,8,9,10]

我敢肯定你明白了。Python提供了一个简单的库,令人惊奇的是,就叫Queue。对于中小型的队列(大概可以容纳500个元素)来说,它运行得还是不错的。上面的代码就是一个简单的例子。

在这个例子中,我们初始化了这个队列(fifo = Queue.Queue()),接着把数字0到4放到了队列(fifo.put(i))中。然后调用内部方法.get()使队中元素出队,直到队列为空(.empty())。返回的是0,1,2,3,4。你也可以通过初始化队列的大小来设置队列项数目的最大值,就像这样:

fifo = Queue.Queue(300)

一旦队列中元素的数量达到了最大值,队列会阻止任何对其进行添加的操作。不过,这将产生一个使程序看起来像是被“锁住”了的副作用。为了避免这种情况,最简单的方法就是用Queue.full()函数进行检查(右上图)。在这个例子里,队列的最大容量被设置成了12个元素。我们把’0’到’11’这些数据项放入队列。当我们遇到数字12时,就说明缓冲区已经满了。因为在将这个数据项放进去之前我们已经对缓冲区是否为满进行了检查,所以最后一项就被丢弃了。

当然也有其他的选择,但它们也会有它们的副作用,我们将在今后的文章中说到。因此,大部分时间里,原则上还是使用无限制的队列或保证你队列里的空间多于你所需要的。

LIFO

Queue库也支持LIFO队列。我们将用上面的列表作为一个形象的例子。建立一个如下的队列:

[1,2,3,4,5,6,7,8,9,10]

从这个队列里推出三个数据项,然后它就变成这样:

[1,2,3,4,5,6,7]

记住,在一个LIFO队列里,数据项按照后进先出的顺序被移除。这里有一个LIFO队列的简单例子: 当我们运行它,将会得到”4,3,2,1,0”。

同FIFO队列一样,你可以设置队列的大小,并且用.full()来检查队列是否已满。

PRIORITY

虽然PRIORITY队列不常用,但有时也很有用。它和其他队列在结构上几乎是一样的,但是我们需要传入一个含有优先权(priority)和数据的元组。下面是一个用Queue库的例子:

首先,我们初始化队列。接着将4个数据项放入到这个队列里。注意,用(优先级,数据)这种形式来存放数据。库将把我们的数据按照优先级升序排列。当进行数据出队操作时,一个元组将被推出,(形式)就像我们入队时一样。你可以按照数据索引来寻址。将获得的是……

在前面两个例子,我们只输出了从队列里出来的数据。对于那些例子这就够了,但在实际编程中,你可能需要尽快处理那些出队的数据,否则它们就会丢失。当使用“print fifo.get”时,数据就被送到了终端,然后它就被销毁了。只有我们脑袋还记得有它。

现在来用我们已经学过的关于tkinter的东西创建一个队列演示程序。这个演示程序将有两个窗框。第一个将包含三个按钮(呈现给用户的)。一个是给FIFO队列用的,一个给LIFO队列,还有一个给PRIORITY队列。第二个窗框将包含一个输入控件,两个按钮(一个用来入队列操作,一个用来出队操作),以及三个标签(一个用来显示队空,一个显示队满,一个显示出队元素)。我们也将写一些代码使窗口在屏幕中自动居中。左上图是代码的开头部分。

这是imports部分和类开始的部分。和以前一样,我们用DefineVars, BuildWidgets, 和PlaceWidgets 函数来创建init例程。还有一个叫ShowStatus的函数(右上图),嗯,用它来显示队列的状态。

现在开始创建DefineVars函数。我们有四个StringVar()对象,一个叫QueueType的空变量,以及三个队列对象——每一个将要演示的队列类型都需要一个对象。为了便于演示,我们已经设定这个队列的最大容量为10。也创建了一个叫obj的对象,并让它指向FIFO队列。当从按钮里选择一个队列类型时,就把这个对象指向我们需要的那个队列。这样,这个队列在我们切换至到另一个队列类型时也能被保留下来(代码在前一页的右下方)。

现在开始定义控件。创建第一个窗框,三个按钮,以及它们的绑定项。请注意,要用同一个函数来处理绑定回调。每一个按钮在点击时都将传入一个值给回调函数,我们将以此来表示哪个按钮被点击了。也可以简单地给每一个按钮创建一个专用函数。不过,既然三个按钮处理的是一个共同任务,我想让它们作为一个组来工作会更好些(代码在右边)。

接下来(右下方),我们建立第二个窗框,输入控件,以及两个按钮。这里唯一与众不同的是entry控件的绑定项。我们把self.AddToQueue函数绑定到键(回车键)。这样,用户就不必用鼠标来添加数据了。只需在输入控件里输入数据,然后按下即可。

这一段(下一页底部)是最后三个控件的定义。三个都是标签。为之前我们定义的变量设置textvariable属性。如果你还记得,当那个变量改变时,标签的文本也会同时改变。我们对lblData标签也做一点不同的东西。使用另一种字体来显示出队的数据从而突出它们。请记住,必须返回窗框对象,这样才能在PlaceWidget函数中使用它。

这是PlaceWidgets函数开始部分(下一页中间)。注意,我们在根窗口(root window)的最顶部放置了5个空标签。我这样做是为了设置间隔。这个简单的“伎俩(cheat)”会使得窗口布局更加容易。接着设置第一个窗框,然后是另一个“cheater”标签,最后是三个按钮。

下面我们放置第二个窗框,另一个“cheater”标签还有剩下的控件。

def Quit(self):
    sys.exit()

下来是我们“standard”退出函数,它只是简单地调用了sys.exit()

现在看看主按钮btnMain的回调函数。记住,我们将在按下按钮时将数据传给回调函数(通过p1参数)。用self.QueueType变量表示一个正在处理的队列类型,接着我们把self.obj分配给合适的队列,并最终通过改变根窗口的标题来显示正在使用的队列类型。在这之后,再把队列类型打印到终端窗口(当然你并不一定需要这样做),同时调用ShowStatus函数。接下来(下一页,右上方),我们要用ShowStatus函数了。

正如你所见,非常得简单。我们把标签变量设置成合适的状态,以使它们可以显示正在使用的队列是否为满、为空还是介于两者之间。

AddToQueue函数(下一页底部的右边)也相当直观。我们利用.get()函数从文本输入框里获得数据。接着检查当前队列类型是否是priority队列。如果是,就需要确保它的格式正确。检查它是否包含一个逗号(译者注:priority队列格式为(优先级,数据))。如果没有,就通过一条错误信息反映给用户。如果每件事都看似正确,那么我们就该检查正在使用的队列是否满了。请切记,如果队列满了,入队函数会被阻塞,程序将挂起。如果万事俱备了,就可以向队列里添加数据项并更新状态了。

GetFromQueue函数(见中偏右)甚至更简单。检查队列是否为空以防止遇到阻塞问题。如果不为空,就从队列里弹出并显示数据,然后更新状态即可。

下面就是我们程序的最后部分了。这是中心窗口函数(左上方)。首先获得屏幕的长和宽。接着我们用tkinter库内建的winfo_reqwidth()和winfo_reqheight()函数来获得根窗口的长和宽。在适当的时刻被调用时,这些函数将根据控件布局返回根窗口的长和宽。如果你太早调用它,你将得到错误的数据。接着我们用屏幕宽度减去请求窗口的宽度,并除以2,同时对高度做同样处理。然后用这些信息来设置geometry调用。大部分情况下,它都运行很棒。但是,有时候你需要手动设置所需的宽度和高度。

最后,实例化根窗口,设置基本标题,实例化QueueTest类。接着调用root.after,当根窗口实例化后它会等待x毫秒(这里是3毫秒),然后才调用Center函数。这样,根窗口就被完全建立并做好了运行准备,因此我们就能获得根窗口的高和宽了。你可能得调节一下延迟时间。有些机器比其他的快很多。3毫秒在我的机器上运行得很好,你的情况可能会不同。最后,通过调用根窗口的主循环来使程序运行。

当你操作队列时,请注意如果你把一些数据放入一个队列(比方说FIFO队列),接着切换到另一个队列(比方说LIFO队列),放在FIFO队列里的数据还在那儿等着你。你可以完全或部分地填充这三个队列,下来就开始折腾它们吧。


好吧,上述的就是这期内容了。祝你和队列玩得愉快。QueueTest的代码可以到这儿找到:http://pastebin.com/5BBUiDce

Comments