首页 » Python编程快速上手 » Python编程快速上手全文在线阅读

《Python编程快速上手》第15章 保持时间、计划任务和启动程序

关灯直达底部

坐在电脑前运行程序是不错的,但在你没有直接监督时运行程序,也是有用的。计算机的时钟可以调度程序,在特定的时间和日期运行,或定期运行。例如,程序可以每小时抓取一个网站,检查变更,或在凌晨4点你睡觉时,执行CPU密集型任务。Python的time和datetime模块提供了这些函数。

利用subprocess和threading模块,你也可以编程按时启动其他程序。通常,编程最快的方法是利用其他人已经写好的应用程序。

15.1 time模块

计算机的系统时钟设置为特定的日期、时间和时区。内置的time模块让Python程序能读取系统时钟的当前时间。在time模块中,time.time和time.sleep函数是最有用的模块。

15.1.1 time.time函数

Unix纪元是编程中经常参考的时间:1970年1月1日0点,即协调世界时(UTC)。time.time函数返回自那一刻以来的秒数,是一个浮点值(回想一下,浮点值只是一个带小数点的数)。这个数字称为UNIX纪元时间戳。例如,在交互式环境中输入以下代码:

>>> import time>>> time.time1425063955.068649  

这里,我在2015年2月27日,太平洋标准时间11:05(或7:05 PM UTC),调用time.time。返回值是Unix纪元的那一刻与time.time被调用的那一刻之间的秒数。

注意

交互式环境的例子得到的日期和时间,是我在2015年2月写这一章的时间。除非你是时间旅行者,否则得到的日期和时间会不同。

纪元时间戳可以用于剖析代码,也就是测量一段代码的运行时间。如果在代码块开始时调用time.time,并在结束时再次调用,就可以用第二个时间戳减去第一个,得到这两次调用之间经过的时间。例如,打开一个新的文件编辑器窗口,然后输入以下程序:

 import time❶ def calcProd:     # Calculate the product of the first 100,000 numbers.     product = 1     for i in range(1, 100000): product = product * i     return product❷ startTime = time.time prod = calcProd❸ endTime = time.time❹ print('The result is %s digits long.' % (len(str(prod))))❺ print('Took %s seconds to calculate.' % (endTime - startTime))  

在❶行,我们定义了函数calcProd,循环遍历1至99999的整数,返回它们的乘积。在❷行,我们调用time.time,将结果保存在startTime中。调用calcProd后,我们再次调用time.time,将结果保存endTime中❸。最后我们打印calcProd返回的乘积的长度❹,以及运行calcProd的时间❺。

将该程序保存为calcProd.py,并运行它。输出看起来像这样:

The result is 456569 digits long.Took 2.844162940979004 seconds to calculate.  

注意

另一种剖析代码的方法是利用cProfile.run函数。与简单的time.time技术相比,它提供了详细的信息。cProfile.run函数在https://docs.python.org/3/library/ profile.html有解释。

15.1.2 time.sleep函数

如果需要让程序暂停一下,就调用time.sleep函数,并传入希望程序暂停的秒数。在交互式环境中输入以下代码:

 >>> import time >>> for i in range(3):❶   print('Tick')❷   time.sleep(1)❸   print('Tock')❹   time.sleep(1) Tick Tock Tick Tock Tick Tock❺ >>> time.sleep(5)  

for循环将打印Tick❶,暂停一秒钟❷,打印Tock❸,暂停一秒钟❹,打印Tick,暂停,如此继续,直到Tick和Tock分别被打印3次。

time.sleep函数将阻塞(也就是说,它不会返回或让程序执行其他代码),直到传递给time.sleep的秒数流逝。例如,如果输入time.sleep(5) ❺,会在5秒后才看到下一个提示符(>>>)。

请注意,在IDLE中按Ctrl-C不会中断time.sleep调用。IDLE会等待到暂停结束,再抛出KeyboardInterrupt异常。要绕过这个问题,不要用一次time.sleep(30)调用来暂停30秒,而是使用for循环执行30次time.sleep(1)调用。

 >>> for i in range(30):     time.sleep(1)  

如果在这30秒内的某个时候按Ctrl-C,应该马上看到抛出KeyboardInterrupt异常。

15.2 数字四舍五入

在处理时间时,你会经常遇到小数点后有许多数字的浮点值。为了让这些值更易于处理,可以用Python内置的round函数将它们缩短,该函数按照指定的精度四舍五入到一个浮点数。只要传入要舍入的数字,再加上可选的第二个参数,指明需要传入到小数点后多少位。如果省略第二个参数,round将数字四舍五入到最接近的整数。在交互式环境中输入以下代码:

>>> import time>>> now = time.time>>> now1425064108.017826>>> round(now, 2)1425064108.02>>> round(now, 4)1425064108.0178>>> round(now)1425064108  

导入time,将time.time保存在now中之后,我们调用round(now, 2),将now舍入到小数点后两位数字,round(now, 4)舍入到小数点后四位数字,round(now)舍入到最接近的整数。

15.3 项目:超级秒表

假设要记录在没有自动化的枯燥任务上花了多少时间。你没有物理秒表,要为笔记本或智能手机找到一个免费的秒表应用,没有广告,且不会将你的浏览历史发送给市场营销人员,又出乎意料地困难(在你同意的许可协议中,它说它可以这样做。你确实阅读了许可协议,不是吗?)。你可以自己用Python写一个简单的秒表程序。

总的来说,你的程序需要完成:

  • 记录从按下回车键开始,每次按键的时间,每次按键都是一个新的“单圈”。
  • 打印圈数、总时间和单圈时间。

这意味着代码将需要完成以下任务:

  • 在程序开始时,通过调用time.time得到当前时间,将它保存为一个时间戳。在每个单圈开始时也一样。
  • 记录圈数,每次用户按下回车键时加1。
  • 用时间戳相减,得到计算流逝的时间。
  • 处理KeyboardInterrupt异常,这样用户可以按Ctrl-C退出。

打开一个新的文件编辑器窗口,并保存为stopwatch.py。

第1步:设置程序来记录时间

秒表程序需要用到当前时间,所以要导入的time模块。程序在调用input之前,也应该向用户打印一些简短的说明,这样计时器可以在用户按下回车键后开始。然后,代码将开始记录单圈时间。在文件编辑器中输入以下代码,为其余的代码编写TODO注释,作为占位符:

#! python3# stopwatch.py - A simple stopwatch program.import time# Display the program's instructions.print('Press ENTER to begin. Afterwards, press ENTER to "click" the stopwatch.Press Ctrl-C to quit.')input  # press Enter to beginprint('Started.')startTime = time.time      # get the first lap's start timelastTime = startTimelapNum = 1# TODO: Start tracking the lap times.  

既然我们已经编码显示了用户说明,那就开始第一圈,记下时间,并将圈数设为1。

第2步:记录并打印单圈时间

现在,让我们编码开始每一个新的单圈,计算前一圈花了多少时间,并计算自启动秒表后经过的总时间。我们将显示的单圈时间和总时间,为每个新的单圈增加圈计数。将下面的代码添加到程序中:

#! python3# stopwatch.py - A simple stopwatch program.import time--snip--# Start tracking the lap times.❶ try:❷     while True:input❸ lapTime = round(time.time - lastTime, 2)❹ totalTime = round(time.time - startTime, 2)❺ print('Lap #%s: %s (%s)' % (lapNum, totalTime, lapTime), end='')lapNum += 1lastTime = time.time # reset the last lap time❻ except KeyboardInterrupt:    # Handle the Ctrl-C exception to keep its error message from displaying.    print('/nDone.')  

如果用户按Ctrl-C停止秒表,KeyboardInterrupt异常将抛出,如果程序的执行不是一个try语句,就会崩溃。为了防止崩溃,我们将这部分程序包装在一个try语句中❶。我们将在except子句中处理异常❻,所以当Ctrl-C按下并引发异常时,程序执行转向except子句,打印Done,而不是KeyboardInterrupt错误消息。在此之前,执行处于一个无限循环中❷,调用input并等待,直到用户按下回车键结束一圈。当一圈结束时,我们用当前时间time.time减去该圈开始的时间lastTime,计算该圈花了多少时间❸。我们用当前时间减去秒表最开始启动的时间startTime,计算总共流逝的时间❹。

由于这些时间计算的结果在小数点后有许多位(如4.766272783279419),所以我们在❸和❹行用round函数,将浮点值四舍五入到小数点后两位。

在❺行,我们打印出圈数,消耗的总时间和单圈时间。由于用户为input调用按下回车时,会在屏幕上打印一个换行,所以我们向print函数传入end='',避免输出重复空行。打印单圈信息后,我们将计数器lapNum加1,将lastTime设置为当前时间(这就是下一圈的开始时间),从而为下一圈做好准备。

第3步:类似程序的想法

时间追踪为程序打开了几种可能性。虽然可以下载应用程序来做其中一些事情,但自己编程的好处是它们是免费的,而且不会充斥着广告和无用的功能。可以编写类似的程序来完成以下任务:

  • 创建一个简单的工时表应用程序,当输入一个人的名字时,用当前的时间记录下他们进入或离开的时间。
  • 为你的程序添加一个功能,显示自一项处理开始以来的时间,诸如利用requests模块进行的下载(参见第11章)。
  • 间歇性地检查程序已经运行了多久,并为用户提供了一个机会,取消耗时太长的任务。

15.4 datetime模块

time模块用于取得Unix纪元时间戳,并加以处理。但是,如果以更方便的格式显示日期,或对日期进行算术运算(例如,搞清楚205天前是什么日期,或123天后是什么日期),就应该使用datetime模块。

datetime模块有自己的datetime数据类型。datetime值表示一个特定的时刻。在交互式环境中输入以下代码:

 >>> import datetime❶ >>> datetime.datetime.now❷ datetime.datetime(2015, 2, 27, 11, 10, 49, 55, 53)❸ >>> dt = datetime.datetime(2015, 10, 21, 16, 29, 0)❹ >>> dt.year, dt.month, dt.day (2015, 10, 21)  >>> dt.hour, dt.minute, dt.second (16, 29, 0)  

调用datetime.datetime.now❶返回一个datetime对象❷,表示当前的日期和时间,根据你的计算机的时钟。这个对象包含当前时刻的年、月、日、时、分、秒和微秒。也可以利用datetime.datetime函数❸,向它传入代表年、月、日、时、分、秒的整数,得到特定时刻的datetime对象。这些整数将保存在datetime对象的year、month、day❹、hour、minute和second❺属性中。

Unix纪元时间戳可以通过datetime.datetime.fromtimestamp,转换为datetime对象。datetime 对象的日期和时间将根据本地时区转换。在交互式环境中输入以下代码:

>>> datetime.datetime.fromtimestamp(1000000)datetime.datetime(1970, 1, 12, 5, 46, 40)>>> datetime.datetime.fromtimestamp(time.time)datetime.datetime(2015, 2, 27, 11, 13, 0, 604980)  

调用datetime.datetime.fromtimestamp并传入1000000,返回一个datetime对象,表示Unix纪元后1000000秒的时刻。传入time.time,即当前时刻的Unix纪元时间戳,则返回当前时刻的datetime对象。因此,表达式datetime.datetime.now和 datetime.datetime.fromtimestamp(time.time)做的事情相同,它们都返回当前时刻的datetime对象。

注意

这些例子是在一台设置了太平洋标准时间的计算机上输入的。如果你在另一个时区,结果会有所不同。

datetime对象可以用比较操作符进行比较,弄清楚谁在前面。后面的datetime对象是“更大”的值。在交互式环境中输入以下代码:

❶ >>> halloween2015 = datetime.datetime(2015, 10, 31, 0, 0, 0)❷ >>> newyears2016 = datetime.datetime(2016, 1, 1, 0, 0, 0) >>> oct31_2015 = datetime.datetime(2015, 10, 31, 0, 0, 0)❸ >>> halloween2015 == oct31_2015 True❹ >>> halloween2015 > newyears2016 False❺ >>> newyears2016 > halloween2015 True >>> newyears2016 != oct31_2015 True  

为2015年10月31日的第一个时刻(午夜)创建一个datetime对象,将它保存在halloween2015中❶。为2016年1月1日的第一个时刻创建一个datetime对象,将它保存在newyears2016中❷。然后,为2015年10月31日的午夜创建另一个对象,将它保存在oct31_2015中。比较halloween2015和oct31_2015,它们是相等的❸。比较newyears2016和halloween2015,newyears2016大于(晚于)halloween2015 ❹❺。

15.4.1 timedelta数据类型

datetime模块还提供了timedelta数据类型,它表示一段时间,而不是一个时刻。在交互式环境中输入以下代码:

❶ >>> delta = datetime.timedelta(days=11, hours=10, minutes=9, seconds=8)❷ >>> delta.days, delta.seconds, delta.microseconds (11, 36548, 0) >>> delta.total_seconds 986948.0 >>> str(delta) '11 days, 10:09:08'  

要创建timedelta对象,就用datetime.timedelta函数。datetime.timedelta函数接受关键字参数weeks、days、hours、minutes、seconds、milliseconds和microseconds。没有month和year关键字参数,因为“月”和“年”是可变的时间,依赖于特定月份或年份。timedelta对象拥有的总时间以天、秒、微秒来表示。这些数字分别保存在days、seconds和microseconds属性中。total_seconds方法返回只以秒表示的时间。将一个timedelta对象传入str,将返回格式良好的、人类可读的字符串表示。

在这个例子中,我们将关键字参数传入datetime.delta,指定11天、10小时、9分和8秒的时间,将返回的timedelta对象保存在delta中❶。该timedelta对象的days属性为11,seconds属性为36548(10小时、9分钟、8秒,以秒表示)❷。调用total_seconds告诉我们,11天、10小时、9分和8秒是986948秒。最后,将这个timedelta对象传入str,返回一个字符串,明确解释了这段时间。

算术运算符可以用于对datetime值进行日期运算。例如,要计算今天之后1000天的日期,在交互式环境中输入以下代码:

>>> dt = datetime.datetime.now>>> dtdatetime.datetime(2015, 2, 27, 18, 38, 50, 636181)>>> thousandDays = datetime.timedelta(days=1000)>>> dt + thousandDaysdatetime.datetime(2017, 11, 23, 18, 38, 50, 636181)  

首先,生成表示当前时刻的datetime对象,保存在dt中。然后生成一个timedelta对象,表示1000天,保存在thousandDays中。dt与thousandDays相加,得到一个datetime对象,表示现在之后的1000天。Python将完成日期运算,弄清楚2015年2月27日之后的1000天,将是2017年11月23日。这很有用,因为如果要从一个给定的日期计算1000天之后,需要记住每个月有多少天,闰年的因素和其他棘手的细节。datetime模块为你处理所有这些问题。

利用+和-运算符,timedelta对象与datetime对象或其他timedelta对象相加或相减。利用*和/运算符,timedelta对象可以乘以或除以整数或浮点数。在交互式环境中输入以下代码:

❶ >>> oct21st = datetime.datetime(2015, 10, 21, 16, 29, 0)❷ >>> aboutThirtyYears = datetime.timedelta(days=365 * 30) >>> oct21st datetime.datetime(2015, 10, 21, 16, 29) >>> oct21st - aboutThirtyYears datetime.datetime(1985, 10, 28, 16, 29) >>> oct21st - (2 * aboutThirtyYears) datetime.datetime(1955, 11, 5, 16, 29)  

这里,我们生成了一个DateTime对象,表示2015年10月21日❶,以及一个timedelta对象,表示大约30年的时间(我们假设每年为365天)❷。从oct21st中减去aboutThirtyYears,我们就得到一个datetime对象,表示2015年10月21日前30年的一天。从oct21st中减去2 * aboutThirtyYears,得到一个datetime对象,表示2015年10月21日之前60年的一天。

15.4.2 暂停直至特定日期

time.sleep方法可以暂停程序若干秒。利用一个while循环,可以让程序暂停,直到一个特定的日期。例如,下面的代码会继续循环,直到2016年万圣节:

import datetimeimport timehalloween2016 = datetime.datetime(2016, 10, 31, 0, 0, 0)while datetime.datetime.now <  halloween2016:    time.sleep(1)  

time.sleep(1)调用将暂停你的Python程序,这样计算机不会浪费CPU处理周期,一遍又一遍地检查时间。相反,while循环只是每秒检查一次,在2016年万圣节(或你编程让它停止的时间)后继续执行后面的程序。

15.4.3 将datetime对象转换为字符串

Unix纪元时间戳和datetime对象对人类来说都不是很友好可读。利用strftime方法,可以将datetime对象显示为字符串。(strftime函数名中的f表示格式,format)。

该的strftime方法使用的指令类似于Python的字符串格式化。表15-1列出了完整的strftime指令。

表15-1 strftime指令

strftime指令

含义

%Y

带世纪的年份,例如'2014'

%y

不带世纪的年份,'00'至'99'(1970至2069)

%m

数字表示的月份, '01'至'12'

%B

完整的月份,例如'November'

%b

简写的月份,例如'Nov'

%d

一月中的第几天,'01'至'31'

%j

一年中的第几天,'001'至'366'

%w

一周中的第几天,'0'(周日)至'6'(周六)

%A

完整的周几,例如'Monday'

%a

简写的周几,例如'Mon'

%H

小时(24小时时钟),'00'至'23'

%I

小时(12小时时钟),'01'至'12'

%M

分,'00'至'59'

%S

秒,'00'至'59'

%p

'AM'或'PM'

%%

就是'%'字符

向strftime传入一个定制的格式字符串,其中包含格式化指定(以及任何需要的斜线、冒号等),strftime将返回一个格式化的字符串,表示datetime对象的信息。在交互式环境中输入以下代码:

>>> oct21st = datetime.datetime(2015, 10, 21, 16, 29, 0)>>> oct21st.strftime('%Y/%m/%d %H:%M:%S')'2015/10/21 16:29:00'>>> oct21st.strftime('%I:%M %p')'04:29 PM'>>> oct21st.strftime("%B of '%y")"October of '15"  

这里,我们有一个datetime对象,表示2015年10月21日下午4点29分,保存在oct21st中。向strftime传入定制的格式字符串'%Y/%m/%d %H:%M:%S,返回一个字符串,包含以斜杠分隔的2015、10和21,以冒号分隔的16、29和00。传入'%I:%M% p'则返回'04:29 PM',传入"%B of '%y"则返回"October of '15"。请注意,strftime不是以datetime.datetime开始。

15.4.4 将字符串转换成datetime对象

如果有一个字符串的日期信息,如'2015/10/21 16:29:00'或'October 21, 2015',需要将它转换为datetime对象,就用datetime.datetime.strftime函数。strptime函数与strftime方法相反。定制的格式字符串使用相同的指令,像strftime一样。必须将格式字符串传入strptime,这样它就知道如何解析和理解日期字符串(strptime函数名中p表示解析,parse)。

在交互式环境中输入以下代码:

❶ >>> datetime.datetime.strptime('October 21, 2015', '%B %d, %Y') datetime.datetime(2015, 10, 21, 0, 0) >>> datetime.datetime.strptime('2015/10/21 16:29:00', '%Y/%m/%d %H:%M:%S') datetime.datetime(2015, 10, 21, 16, 29) >>> datetime.datetime.strptime("October of '15", "%B of '%y") datetime.datetime(2015, 10, 1, 0, 0) >>> datetime.datetime.strptime("November of '63", "%B of '%y") datetime.datetime(2063, 11, 1, 0, 0)  

要从字符串'October 21, 2015'取得一个datetime对象,将'October 21, 2015'作为第一个参数传递给strptime,并将对应于'October 21, 2015' 的定制格式字符串作为第二个参数❶。带有日期信息的字符串必须准确匹配定制的格式字符串,否则Python将抛出ValueError异常。

15.5 回顾Python的时间函数

在Python中,日期和时间可能涉及好几种不同的数据类型和函数。下面回顾了表示时间的3种不同类型的值:

  • Unix纪元时间戳(time模块中使用)是一个浮点值或整型值,表示自1970年1月1日午夜0点(UTC)以来的秒数。
  • datetime对象(属于datetime模块)包含一些整型值,保存在year、month、day、hour、minute和second等属性中。
  • timedelta对象(属于datetime模块)表示的一段时间,而不是一个特定的时刻。

下面回顾了时间函数及其参数和返回值:

  • time.time函数返回一个浮点值,表示当前时刻的Unix纪元时间戳。
  • time.sleep(seconds)函数让程序暂停seconds参数指定的秒数。
  • datetime.datetime(year, month, day, hour, minute, second)函数返回参数指定的时刻的datetime对象。如果没有提供hour、minute或second参数,它们默认为0。
  • datetime.datetime.now函数返回当前时刻的datetime对象。
  • datetime.datetime.fromtimestamp(epoch)函数返回epoch时间戳参数表示的时刻的datetime对象。
  • datetime.timedelta(weeks, days, hours, minutes, seconds, milliseconds, microseconds)函数返回一个表示一段时间的timedelta对象。该函数的关键字参数都是可选的,不包括month或year。
  • total_seconds方法用于timedelta对象,返回timedelta对象表示的秒数。
  • strftime(format)方法返回一个字符串,用format字符串中的定制格式来表示datetime对象表示的时间。详细格式参见表15-1。
  • datetime.datetime.strptime(time_string, format)函数返回一个datetime对象,它的时刻由time_string指定,利用format字符串参数来解析。详细格式参见表15-1。

15.6 多线程

为了引入多线程的概念,让我们来看一个例子。假设你想安排一些代码,在一段延迟后或在特定时间运行。可以在程序启动时添加如下代码:

import time, datetimestartTime = datetime.datetime(2029, 10, 31, 0, 0, 0)while datetime.datetime.now <  startTime:    time.sleep(1)print('Program now starting on Halloween 2029')--snip--  

这段代码指定2029年10月31日作为开始时间,不断调用time.sleep(1),直到开始时间。在等待time.sleep的循环调用完成时,程序不能做任何事情,它只是坐在那里,直到2029年万圣节。这是因为Python程序在默认情况下,只有一个执行线程。

要理解什么是执行线程,就要回忆第2章关于控制流的讨论,当时你想象程序的执行就像把手指放在一行代码上,然后移动到下一行,或是流控制语句让它去的任何地方。单线程程序只有一个“手指”。但多线程的程序有多个“手指”。每个“手指”仍然移动到控制流语句定义的下一行代码,但这些“手指”可以在程序的不同地方,同时执行不同的代码行(到目前为止,本书所有的程序一直是单线程的)。

不必让所有的代码等待,直到time.sleep函数完成,你可以使用Python的threading模块,在单独的线程中执行延迟或安排的代码。这个单独的线程将因为time.sleep调用而暂停。同时,程序可以在原来的线程中做其他工作。

要得到单独的线程,首先要调用threading.Thread函数,生成一个Thread对象。在新的文件中输入以下代码,并保存为threadDemo.py:

 import threading, time print('Start of program.')❶ def takeANap:     time.sleep(5)     print('Wake up!')❷ threadObj = threading.Thread(target=takeANap)❸ threadObj.start print('End of program.')  

在❶行,我们定义了一个函数,希望用于新线程中。为了创建一个Thread对象,我们调用threading.Thread,并传入关键字参数target=takeANap❷。这意味着我们要在新线程中调用的函数是takeANap。请注意,关键字参数是target=takeANap,而不是target=takeANap。这是因为你想将takeANap函数本身作为参数,而不是调用takeANap,并传入它的返回值。

我们将threading.Thread创建的Thread对象保存在threadObj中,然后调用threadObj.start❸,创建新的线程,并开始在新线程中执行目标函数。如果运行该程序,输出将像这样:

Start of program.End of program.Wake up!  

这可能有点令人困惑。如果print('End of program.')是程序的最后一行,你可能会认为,它应该是最后打印的内容。Wake up!在它后面是因为,当threadObj.start被调用时,threadObj的目标函数运行在一个新的执行线程中。将它看成是第二根“手指”,出现在takeANap函数开始处。主线程继续print('End of program.')。同时,新线程已执行了time.sleep(5)调用,暂停5秒钟。之后它从5秒钟小睡中醒来,打印了'Wake up!',然后从takeANap函数返回。按时间顺序,'Wake up!'是程序最后打印的内容。

通常,程序在文件中最后一行代码执行后终止(或调用sys.exit)。但threadDemo.py有两个线程。第一个是最初的线程,从程序开始处开始,在print('End of program.')后结束。第二个线程是调用threadObj.start时创建的,始于takeANap函数的开始处,在takeANap返回后结束。

在程序的所有线程终止之前,Python程序不会终止。在运行threadDemo.py时,即使最初的线程已经终止,第二个线程仍然执行time.sleep(5)调用。

15.6.1 向线程的目标函数传递参数

如果想在新线程中运行的目标函数有参数,可以将目标函数的参数传入threading.Thread。例如,假设想在自己的线程中运行以下print调用:

>>> print('Cats', 'Dogs', 'Frogs', sep=' & ')Cats & Dogs & Frogs  

该print调用有3个常规参数:'Cats'、 'Dogs'和'Frogs',以及一个关键字参数:sep= ' & '。常规参数可以作为一个列表,传递给threading.Thread中的args关键字参数。关键字参数可以作为一个字典,传递给threading.Thread中的kwargs关键字参数。

在交互式环境中输入以下代码:

>>> import threading>>> threadObj = threading.Thread(target=print, args=['Cats', 'Dogs', 'Frogs'],kwargs={'sep': ' & '})>>> threadObj.startCats & Dogs & Frogs  

为了确保参数'Cats'、'Dogs'和'Frogs'传递给新线程中的print,我们将args=['Cats', 'Dogs', 'Frogs']传入threading.Thread。为了确保关键字参数sep=' & '传递给新线程中的print,我们将kwargs={'sep': '& '}传入threading.Thread。

threadObj.start调用将创建一个新线程来调用print函数,它会传入'Cats'、'Dogs'和'Frogs'作为参数,以及' & '作为sep关键字参数。

下面创建新线程调用print的方法是不正确的:

threadObj = threading.Thread(target=print('Cats', 'Dogs', 'Frogs', sep=' & '))  

这行代码最终会调用print函数,将它的返回值(print的返回值总是无)作为target关键字参数。它没有传递print函数本身。如果要向新线程中的函数传递参数,就使用threading.Thread函数的args和kwargs关键字参数。

15.6.2 并发问题

可以轻松地创建多个新线程,让它们同时运行。但多线程也可能会导致所谓的并发问题。如果这些线程同时读写变量,导致互相干扰,就会发生并发问题。并发问题可能很难一致地重现,所以难以调试。

多线程编程本身就是一个广泛的主题,超出了本书的范围。必须记住的是:为了避免并发问题,绝不让多个线程读取或写入相同的变量。当创建一个新的Thread对象时,要确保其目标函数只使用该函数中的局部变量。这将避免程序中难以调试的并发问题。

注意

在http://nostarch.com/automatestuff/,有关于多线程编程的初学者教程。

15.7 项目:多线程XKCD下载程序

在第11章,你编写了一个程序,从XKCD网站下载所有的XKCD漫画。这是一个单线程程序:它一次下载一幅漫画。程序运行的大部分时间,都用于建立网络连接来开始下载,以及将下载的图像写入硬盘。如果你有宽带因特网连接,单线程程序并没有充分利用可用的带宽。

多线程程序中有一些线程在下载漫画,同时另一些线程在建立连接,或将漫画图像文件写入硬盘。它更有效地使用Internet连接,更迅速地下载这些漫画。打开一个新的文件编辑器窗口,并保存为multidownloadXkcd.py。你将修改这个程序,添加多线程。经过全面修改的源代码可从http://nostarch.com/automatestuff/下载。

第1步:修改程序以使用函数

该程序大部分是来自第 11 章的相同下载代码,所以我会跳过 Requests 和BeautifulSoup代码的解释。需要完成的主要变更是导入threading模块,并定义downloadXkcd函数,该函数接受开始和结束的漫画编号作为参数。

例如,调用downloadXkcd(140,280)将循环执行下载代码,下载漫画http://xkcd. com/140、http://xkcd.com/141、http://xkcd.com/142等,直到http://xkcd.com/279。你创建的每个线程都会调用downloadXkcd,并传入不同范围的漫画进行下载。

将下面的代码添加到multidownloadXkcd.py程序中:

 #! python3 # multidownloadXkcd.py - Downloads XKCD comics using multiple threads. import requests, os, bs4, threading❶ os.makedirs('xkcd', exist_ok=True)      # store comics in ./xkcd❷ def downloadXkcd(startComic, endComic):❸      for urlNumber in range(startComic, endComic): # Download the page. print('Downloading page http://xkcd.com/%s...' % (urlNumber))❹ res = requests.get('http://xkcd.com/%s' % (urlNumber)) res.raise_for_status❺ soup = bs4.BeautifulSoup(res.text) # Find the URL of the comic image.❻ comicElem = soup.select('#comic img') if comicElem == :     print('Could not find comic image.') else:❼     comicUrl = comicElem[0].get('src')     # Download the image.     print('Downloading image %s...' % (comicUrl))❽     res = requests.get(comicUrl)     res.raise_for_status     # Save the image to ./xkcd.     imageFile = open(os.path.join('xkcd', os.path.basename(comicUrl)), 'wb')     for chunk in res.iter_content(100000): imageFile.write(chunk)     imageFile.close # TODO: Create and start the Thread objects. # TODO: Wait for all threads to end.  

导入需要的模块后,❶行创建了一个目录来保存漫画,并开始定义downloadxkcd❷。循环遍历指定范围中的所有编号❸,并下载每个页面❹。用Beautiful Soup查看每一页的HTML❺,找到漫画图像❻。如果页面上没有的漫画图像,就打印一条消息。否则,取得图片的URL❼,并下载图像❽。最后,将图像保存到我们创建的目录中。

第2步:创建并启动线程

既然已经定义 downloadXkcd,我们将创建多个线程,每个线程调用downloadXkcd,从 XKCD 网站下载不同范围的漫画。将下面的代码添加到multidownloadXkcd.py中,放在downloadXkcd函数定义之后:

#! python3# multidownloadXkcd.py - Downloads XKCD comics using multiple threads.--snip--# Create and start the Thread objects.downloadThreads =        # a list of all the Thread objectsfor i in range(0, 1400, 100):       # loops 14 times, creates 14 threads    downloadThread = threading.Thread(target=downloadXkcd, args=(i, i + 99))    downloadThreads.append(downloadThread)    downloadThread.start  

首先,我们创建了一个空列表downloadThreads,该列表帮助我们追踪创建的多个Thread对象。然后开始for循环。在每次循环中,我们利用threading.Thread创建一个Thread对象,将它追加到列表中,并调用start,开始在新线程中运行downloadXkcd。因为for循环将变量i设置为从0到1400,步长为100,所以i在第一次迭代时为0,第二次迭代时为100,第三次为200,以此类推。因为我们将args=(I, I+99)传递给threading.Thread,所以在第一次迭代时,传递给downloadXkcd的两个参数将是0和99,第二次迭代是100和199,第三次是200和299,以次类推。

由于调用了Thread对象的start方法,新的线程开始运行downloadXkcd中的代码,主线程将继续for循环的下一次迭代,创造下一个线程。

第3步:等待所有线程结束

主线程正常执行,同时我们创建的其他线程下载漫画。但是假定主线程中有一些代码,你希望所有下载线程完成后再执行。调用Thread对象join方法将阻塞,直到该线程完成。利用一个for循环,遍历downloadThreads列表中的所有Thread对象,主线程可以调用其他每个线程的join方法。将以下代码添加到程序的末尾:

#! python3# multidownloadXkcd.py - Downloads XKCD comics using multiple threads.--snip--# Wait for all threads to end.for downloadThread in downloadThreads:    downloadThread.joinprint('Done.')  

所有的join调用返回后,'Done.'字符串才会打印,如果一个Thread对象已经完成,那么调用它的join方法时,该方法就会立即返回。如果想扩展这个程序,添加一些代码,在所有漫画下载后运行,就可以用新的代码替换print('Done.')。

15.8 从Python启动其他程序

利用内建的subprocess模块中的Popen函数,Python程序可以启动计算机中的其他程序(Popen函数名中的P表示process,进程)。如果你打开了一个应用程序的多个实例,每个实例都是同一个程序的不同进程。例如,如果你同时打开了Web浏览器的多个窗口,每个窗口都是Web浏览器程序的不同进程。参见图15-1,这是同时打开多个计算器进程的例子。

图15-1 相同的计算器程序,六个正在运行的进程

每个进程可以有多个线程。不像线程,进程无法直接读写另一个进程的变量。如果你认为多线程程序是多个手指在追踪源代码,那么同一个程序打开多个进程就像有一个朋友拿着程序源代码的独立副本。你们都独立地执行相同的程序。

如果想在Python脚本中启动一个外部程序,就将该程序的文件名传递给subprocess.Popen(在Windows中,右键点击该应用程序的开始菜单项,然后选择“属性”,查看应用程序的文件名。在OS X上,按住Ctrl键单击该应用程序并选择“显示包内容”,找到可执行文件的路径)。Popen函数随后将立即返回。请记住,启动的程序和你的Python程序不在同一线程中运行。

在Windows计算机上,在交互式环境中输入以下代码:

>>> import subprocess>>> subprocess.Popen('C://Windows//System32//calc.exe')< subprocess.Popen object at 0x0000000003055A58>  

在Ubuntu Linux上,可以输入以下代码:

>>> import subprocess>>> subprocess.Popen('/usr/bin/gnome-calculator')< subprocess.Popen object at 0x7f2bcf93b20>  

在OS X上,过程稍有不同。参见15.8.5节“用默认应用程序打开文件”。

返回值是一个Popen对象,它有两个有用的方法:poll和wait。

可以认为poll方法是问你的朋友,她是否执行完毕你给她的代码。如果这个进程在poll调用时仍在运行,poll方法就返回None。如果该程序已经终止,它会返回该进程的整数退出代码。退出代码用于说明进程是无错终止(退出代码为0),还是一个错误导致进程终止(退出代码非零,通常为1,但可能根据程序而不同)。

wait方法就像是等着你的朋友执行完她的代码,然后你继续执行你的代码。wait方法将阻塞,直到启动的进程终止。如果你希望你的程序暂停,直到用户完成与其他程序,这非常有用。wait的返回值是进程的整数退出代码。

在Windows上,在交互环境中输入以下代码。请注意, wait的调用将阻塞,直到退出启动的计算器程序。

❶ >>> calcProc = subprocess.Popen('c://Windows//System32//calc.exe')❷ >>> calcProc.poll == None True❸ >>> calcProc.wait 0 >>> calcProc.poll 0  

这里,我们打开了计算器程序❶。在它仍在运行时,我们检查poll是否返回None❷。它应该返回None,因为该进程仍在运行。然后,我们关闭计算器程序,并对已终止的进程调用wait❸。wait和poll现在返回0,说明该进程终止且无错。

15.8.1 向Popen传递命令行参数

用Popen创建进程时,可以向进程传递命令行参数。要做到这一点,向Popen传递一个列表,作为唯一的参数。该列表中的第一个字符串是要启动的程序的可执行文件名,所有后续的字符串将是该程序启动时,传递给该程序的命令行参数。实际上,这个列表将作为被启动程序的sys.argv的值。

大多数具有图形用户界面(GUI)的应用程序,不像基于命令行或基于终端的程序那样尽可能地使用命令行参数。但大多数GUI应用程序将接受一个参数,表示应用程序启动时立即打开的文件。例如,如果你使用的是Windows,创建一个简单的文本文件C:/hello.txt,然后在交互式环境中输入以下代码:

>>> subprocess.Popen(['C://Windows//notepad.exe', 'C://hello.txt'])< subprocess.Popen object at 0x00000000032DCEB8>  

这不仅会启动记事本应用程序,也会让它立即打开C:/hello.txt。

15.8.2 Task Scheduler、launchd和cron

如果你精通计算机,可能知道 Windows 上的 Task Scheduler,OS X 上的launchd,或Linux上的cron调度程序。这些工具文档齐全,而且可靠,它们都允许你安排应用程序在特定的时间启动。如果想更多地了解它们,可以在http://nostarch. com/automatestuff/找到教程的链接。

利用操作系统内置的调度程序,你不必自己写时钟检查代码来安排你的程序。但是,如果只需要程序稍作停顿,就用time.sleep函数。或者不使用操作系统的调度程序,代码可以循环直到特定的日期和时间,每次循环时调用time.sleep(1)。

15.8.3 用Python打开网站

webbrowser.open函数可以从程序启动Web浏览器,打开指定的网站,而不是用subprocess.Popen打开浏览器应用程序。详细内容参见第11章的“项目:利用webbrowser模块的mapIt.py”一节。

15.8.4 运行其他Python脚本

可以在Python中启动另一个Python脚本,就像任何其他的应用程序一样。只需向Popen传入python.exe可执行文件,并将想运行的.py脚本的文件名作为它的参数。例如,下面代码将运行第1章的hello.py脚本:

>>> subprocess.Popen(['C://python34//python.exe', 'hello.py'])< subprocess.Popen object at 0x000000000331CF28>  

向Popen传入一个列表,其中包含Python可执行文件的路径字符串,以及脚本文件名的字符串。如果要启动的脚本需要命令行参数,就将它们添加列表中,放在脚本文件名后面。在Windows上,Python可执行文件的路径是C:/python34/ python.exe。在OS X上,是/Library/Frameworks/Python.framework/ Versions/3.3/bin/python3。在Linux上,是/usr/bin/python3。

不同于将Python程序导入为一个模块,如果Python程序启动了另一个Python程序,两者将在独立的进程中运行,不能分享彼此的变量。

15.8.5 用默认的应用程序打开文件

双击计算机上的.txt文件,会自动启动与.txt文件扩展名关联的应用程序。计算机上已经设置了一些这样的文件扩展名关联。利用Popen,Python也可以用这种方式打开文件。

每个操作系统都有一个程序,其行为等价于双击文档文件来打开它。在Windows上,这是start程序。在OS X上,这是open程序。在Ubuntu Linux上,这是see程序。在交互式环境中输入以下代码,根据操作系统,向Popen传入'start'、'open'或'see':

>>> fileObj = open('hello.txt', 'w')>>> fileObj.write('Hello world!')12>>> fileObj.close>>> import subprocess>>> subprocess.Popen(['start', 'hello.txt'], shell=True)  

这里,我们将Hello world!写入一个新的hello.txt文件。然后调用Popen,传入一个列表,其中包含程序名称(在这个例子中,是Windows上的'start'),以及文件名。我们也传入了shell=True关键字参数,这只在Windows上需要。操作系统知道所有的文件关联,能弄清楚应该启动哪个程序,比如Notepad.exe,来处理hello.txt文件。

在OS X上,open程序用于打开文档文件和程序。如果你有Mac,在交互式环境中输入以下代码:

>>> subprocess.Popen(['open', '/Applications/Calculator.app/'])< subprocess.Popen object at 0x10202ff98>  

计算器应用程序应该会打开。

Unix哲学

程序精心设计,能被其他程序启动,这样的程序比单独使用它们自己的代码更强大。Unix的哲学是一组由UNIX操作系统(现代的Linux和OS X也是基于它)的程序员建立的软件设计原则。它认为:编写小的、目的有限的、能互操作的程序,胜过大的、功能丰富的应用程序。

较小的程序更容易理解,通过能够互操作,它们可以是更强大的应用程序的构建块。智能手机应用程序也遵循这种方式。如果你的餐厅应用程序需要显示一间咖啡店的方位,开发者不必重新发明轮子,编写自己的地图代码。餐厅应用程序只是启动一个地图应用程序,同时传入咖啡店的地址,就像Python代码调用一个函数,并传入参数一样。

你在本书中编写的Python程序大多符合Unix哲学,尤其是在一个重要的方面:它们使用命令行参数,而不是input函数调用。如果程序需要的所有信息都可以事先提供,最好是用命令行参数传入这些信息,而不是等待用户键入它。这样,命令行参数可以由人类用户键入,也可以由另一个程序提供。这种互操作的方式,让你的程序可以作为另一个程序的部分而复用。

唯一的例外是,你不希望口令作为命令行参数传入,因为命令行可能记录它们,作为命令历史功能的一部分。在需要输入口令时,程序应该调用input函数。

在https://en.wikipedia.org/wiki/Unix_philosophy/,你可以阅读更多有关Unix哲学的内容。

15.9 项目:简单的倒计时程序

就像很难找到一个简单的秒表应用程序一样,也很难找到一个简单的倒计时程序。让我们来写一个倒计时程序,在倒计时结束时报警。

总的来说,程序要做到:

  • 从60倒数。
  • 倒数至0时播放声音文件(alarm.wav)。

这意味着代码将需要做到以下几点:

  • 在显示倒计时的每个数字之间,调用time.sleep暂停一秒。
  • 调用subprocess.Popen,用默认的应用程序播放声音文件。

打开一个新的文件编辑器窗口,并保存为countdown.py。

第1步:倒计时

这个程序需要time模块的time.sleep函数,subprocess模块的subprocess. Popen函数。输入以下代码并保存为countdown.py:

 #! python3 # countdown.py - A simple countdown script. import time, subprocess❶ timeLeft = 60 while timeLeft > 0:❷      print(timeLeft, end='')❸      time.sleep(1)❹      timeLeft = timeLeft - 1 # TODO: At the end of the countdown, play a sound file.  

导入time和subprocess后,创建变量timeleft,保存倒计时剩下的秒数❶。它从60开始,或者可以根据需要更改这里的值,甚至通过命令行参数设置它。

在while循环中,显示剩余次数❷,暂停一秒钟❸,再减少timeleft变量的值❹,然后循环再次开始。只要timeleft大于0,循环就继续。在这之后,倒计时就结束了。

第2步:播放声音文件

虽然有第三方模块,播放各种声音文件,但快速而简单的方法,是启动用户使用的任何播放声音文件的应用程序。操作系统通过.wav文件扩展名,会弄清楚应该启动哪个应用程序来播放该文件。这个.wav文件很容易变成其他声音文件格式,如.mp3或.ogg。

可以使用计算机上的任何声音文件,在倒计时结束播放,也可以从http://nostarch. com/automatestuff/下载alarm.wav。

在程序中添加以下代码:

#! python3# countdown.py - A simple countdown script.import time, subprocess--snip--# At the end of the countdown, play a sound file.subprocess.Popen(['start', 'alarm.wav'], shell=True)  

while循环结束后,alarm.wav(或你选择的声音文件)将播放,通知用户倒计时结束。在Windows上,要确保传入Popen的列表中包含'start',并传入关键字参数shell=True。在OS X上,传入'open',而不是'start’,并去掉shell=True。

除了播放声音文件之外,你可以在一个文本文件中保存一条消息,例如Break time is over!。然后在倒计时结束时用Popen打开它。这实际上创建了一个带消息的弹出窗口。或者你可以在倒计时结束时,用webbrowser.open函数打开特定网站。不像在网上找到的一些免费倒计时应用程序,你自己的倒计时程序的警报可以是任何你希望的方式!

第3步:类似程序的想法

倒计时是简单的延时,然后继续执行程序。这也可以用于其他应用程序和功能,诸如:

  • 利用time.sleep给用户一个机会,按下Ctrl-C取消的操作,例如删除文件。你的程序可以打印“Press Ctrl-C to cancel”,然后用try和except语句处理所有KeyboardInterrupt异常。
  • 对于长期的倒计时,可以用timedelta对象来测量直到未来某个时间点(生日?周年纪念?)的天、时、分和秒数。

15.10 小结

对于许多编程语言,包括Python,Unix纪元(1970年1月1日午夜,UTC)是一个标准的参考时间。虽然time.time函数模块返回一个Unix纪元时间戳(也就是自Unix纪元以来的秒数的浮点值),但datetime模块更适合执行日期计算、格式化和解析日期信息的字符串。

time.sleep函数将阻塞(即不返回)若干秒。它可以用于在程序中暂停。但如果想安排程序在特定时间启动,http://nostarch.com/automatestuff/上的指南可以告诉你如何使用操作系统已经提供的调度程序。

threading 模块用于创建多个线程,如果需要下载多个文件或同时执行其他任务,这非常有用。但是要确保线程只读写局部变量,否则可能会遇到并发问题。

最后,Python程序可以用subprocess.Popen函数,启动其他应用程序。命令行参数可以传递给Popen调用,用该应用程序打开特定的文档。另外,也可以用Popen启动start、open或see程序,利用计算机的文件关联,自动弄清楚用来打开文件的应用程序。通过利用计算机上的其他应用程序,Python程序可以利用它们的能力,满足你的自动化需求。

15.11 习题

1.什么是Unix纪元?

2.什么函数返回自Unix纪元以来的秒数?

3.如何让程序刚好暂停5秒?

4.round函数返回什么?

5.datetime对象和timedelta对象之间的区别是什么?

6.假设你有一个函数名为spam。如何在一个独立的线程中调用该函数并运行其中的代码?

7.为了避免多线程的并发问题,应该怎样做?

8.如何让Python程序运行<em>C</em>:/ <em>Windows</em>/<em>System32</em>文件夹中的calc.exe程序?

15.12 实践项目

作为实践,编程完成下列任务。

15.12.1 美化的秒表

扩展本章的秒表项目,让它利用rjust和ljust字符串方法来“美化”的输出。(这些方法在第6章中介绍过)。输出不是像这样:

Lap #1: 3.56 (3.56)Lap #2: 8.63 (5.07)Lap #3: 17.68 (9.05)Lap #4: 19.11 (1.43)  

…而是像这样:

Lap # 1:   3.56 ( 3.56)Lap # 2:   8.63 ( 5.07)Lap # 3:  17.68 ( 9.05)Lap # 4:  19.11 ( 1.43)  

请注意,对于lapNum、lapTime和totalTime等整型和浮点型变量,你需要字符串版本,以便对它们调用字符串方法。接下来,利用第6章中介绍的pyperclip模块,将文本输出复制到剪贴板,以便用户可以将输出快速粘贴到一个文本文件或电子邮件中。

15.12.2 计划的Web漫画下载

编写一个程序,检查几个Web漫画的网站,如果自该程序上次访问以来,漫画有更新,就自动下载。操作系统的调度程序(Windows上的Task Scheduler,OS X上的launchd,以及Linux上的cron)可以每天运行你的Python程序一次。Python程序本身可以下载漫画,然后将它复制到桌面上,这样很容易找到。你就不必自己查看网站是否有更新(在http://nostarch.com/automatestuff/上有一份Web漫画的列表)。