原想换过一季,歇口气,不曾想已是冬歇到夏。
不由想起烂柯人的故事。晋中王质上山砍柴,见山中有人弈棋,停下观看。不知不觉,树叶黄了又绿,几度变换,手中的斧柄都腐烂了。等到回到村中,已是数百年后。
晋代的传说满是时间的歧路。有山中樵夫,也有桃源捕鱼人误入不知魏晋的桃源仙境。也许这与当时谈佛论道的风气有关。佛教的时间换算层层叠叠,刹那(10毫秒)于人寿(100年)不值一提,人寿于极乐世界一昼夜(13.44亿年),更是不足道。
根据经纬度划分的时区,在1884年才确立。在那之后,同一个时刻,在地球不同地方,有了不同的时间表示。这个小小的迷宫,在侦探小说推理游戏中屡试不爽。远在魏晋的古人,则只能在同地的不同时空中,上下求索。
折回尘世,时间就具体得多。它的单位不那么多,跨度也不那么长。它有自己的【度量】,【时刻表示】与【计时工具】,有条不紊,记录着每个程序的进程。
度量
在物理世界中,时间有自己的尺度:
- 直接度量单位
年(day),月(month), 日(day), 时(hour), 分(minute), 秒(second), 毫秒(millisecond), 纳秒(nanosecond)
要计算一段程序运行的时间,因为代码运行在CPU上,因此可以使用CPU的时钟周期度量。
- 间接度量
CPU时钟周期(clock)
二者通过CPU的时钟频率换算。
// steady_clock example, from cppreference
#include <iostream>
#include <ctime>
#include <ratio>
#include <chrono>
int main ()
{
using namespace std::chrono;
steady_clock::time_point t1 = steady_clock::now();
std::cout << "printing out 1000 stars...\n";
for (int i=0; i<1000; ++i) std::cout << "*";
std::cout << std::endl;
steady_clock::time_point t2 = steady_clock::now();
duration<double> time_span = duration_cast<duration<double>>(t2 - t1);
std::cout << "It took me " << time_span.count() << " seconds.";
std::cout << std::endl;
return 0;
}
时刻表示
度量回答了时间的长短,要回答今夕何夕,还需要知道时刻的表示。
从Epoch(0时区的1970-01-01)时刻算起,到某个时间点经过的时间里程数可以称为【timestamp】(时间戳),它在不同时区的数值都是一样的。
>>> import time
>>> import os
>>> os.environ['TZ'] = 'GMT'
>>> time.tzname # 现在时区名是CST
('CST', 'CST')
>>> time.time() # 时间戳
1620143611.841546
>>> time.tzset() # 时区改为0时区
>>> time.tzname # 确实变更为GMT时区
('GMT', 'GMT')
>>> time.time() # 时区相差8小时,时间戳大致接近
1620143625.263896
它也可以换算成一个按年月日、时分秒给出的日期。换算日期,就涉及到时区的问题。
如果不考虑时区,我们得到的是UTC时间下的日期——【utctime】,别名【gmtime】,和GMT(0时区名称)的结果一致。
>>> import time
>>> help(time.gmtime) # a.k.a 瞩目
Help on built-in function gmtime in module time:
gmtime(...)
gmtime([seconds]) -> (tm_year, tm_mon, tm_mday, tm_hour, tm_min,
tm_sec, tm_wday, tm_yday, tm_isdst)
Convert seconds since the Epoch to a time tuple expressing UTC (a.k.a.
GMT). When 'seconds' is not passed in, convert the current time instead.
转换到本地的时区,得到就是【localtime】
>>> import time
>>> time.gmtime()
time.struct_time(tm_year=2021, tm_mon=5, tm_mday=4, tm_hour=16, tm_min=6, tm_sec=9, tm_wday=1, tm_yday=124, tm_isdst=0)
>>> time.tzname
('CST', 'CST')
>>> time.localtime() # 在CST(+8时区)下,gmtime和localtime差了8小时,导致日期不一致
time.struct_time(tm_year=2021, tm_mon=5, tm_mday=5, tm_hour=0, tm_min=6, tm_sec=19, tm_wday=2, tm_yday=125, tm_isdst=0)
如果直接拿到的是一串日期
可以通过strftime, strptime这两个方法,进行日期和字符串的转换。
这两个函数默认的日期都是localtime, 其他时区的日期需要注意时区转换。
>>> import time
>>> t1 = time.localtime()
>>> t1
time.struct_time(tm_year=2021, tm_mon=5, tm_mday=5, tm_hour=0, tm_min=17, tm_sec=11, tm_wday=2, tm_yday=125, tm_isdst=0)
>>> time.strftime("%s", t1)
'1620145031'
>>> time.time() # 时间戳接近
1620145059.413488
>>> t2 = time.gmtime() # 使用utc得到的时间对象
>>> time.strftime("%s", t2) # 得到偏小的时间戳,说明函数将这个时间对象当成8小时前的local时间
'1620116297'
计时工具
我们关心的,还有如何衡量度过的时光。
有了python的time模块和时间戳,我们可以在想测量的开头结尾一减,这样就完成了一个自制的一次性定时器。
>>> import time
>>> t1 = time.time()
>>> # do something slow
>>> t2 = time.time()
>>> delta_t = t2 - t1
更进一步,我们希望它能变成一个库,方便多次调用。python的timeit.timeit模块提供的就是这个功能
>>> import timeit
>>> timeit.timeit('a=1+1') # 但是不能测试像time.sleep这类代码,计时器会无法正常退出
即使不能更改代码,要获取整个代码的运行时间,也可以使用linux的time命令
在代码中用timeit测量,需要编写额外的测试代码;更优雅的做法是使用decorator技术进行封装,这个话题,咱们下回再议。