15. 系统内置模块
Hi,大家好。我是茶桁。
上一节中,在我们的学习到达一个阶段的时候,我们用之前所学过的知识创建了一个简单的注册登录系统。不知道小伙伴们有没有在课后自己实现一遍呢?编程这种事情,还是要多上手多练才行。
那么今天这节课,我们来学习一下 Python 系统内置模块。
系统内置模块就是安装完 Python 解释器之后,系统本身所提供的模块。我知道,咱们之前的课程里有学习系统的内置函数,这个模块和函数不是一个东西。模块这种东西,是需要导入后才可以使用的,比如:json, re, os
等等。
行,废话不多说,让我们进入正题。
序列化模块
序列化,就是指可以把 Python 中的数据,以文本或者二进制的方式进行转换,并且还能反序列化为原来的数据。数据在程序和网络中进行传输和存储的时候,需要以更加方便的形式进行操作,因此需要对数据进行序列化。
对数据进行序列化主要有两种方法,一种呢是 Python 专用的二进制序列化模块:pickle
, 还有一种呢,是互联网通用的文本序列化模块json
。
pickle
按照官方的定义来讲
pickle 实现了对一个 Python 对象结构的二进制序列化和反序列化
它提供了一些可供使用的函数,下面让我们来一一介绍一下:
当前是将一段字符串使用dumps()
进行了转化,那其他数据类型是否可以呢?我们来一段列表试试看:
可以看到,依然进行了转化,并且类型还是bytes
。其他的诸如字典、元组等都可以进行这样的转化,我们就不一一的在这里展示了。结论为,我们使用pickle.dumps
方法可以进行序列化成为一个二进制的数据。
再让我们来看看反序列化的效果,我在源码中还做过一个元组的序列化,并且给res
进行了赋值,我们就拿最后一次的结果来做演示(res = b'\x80\x04\x95\x10\x00\x00\x00\x00\x00\x00\x00(K\x01K\x02K\x03K\x04K\x05K\x06t\x94.'
):
可以看到, 之前序列化成二进制数据的元组被loads()
反序列化转化回来恢复成了元组,我们打印其类型,为tuple
。
除了以上两个方法之外,还有另外两个方法dump()
和load()
, 这四者的区别如下:
dumps(obj, protocol=None, *, fix_imports=True, buffer_callback=None)
: 序列化,可以把一个 Python 的任意对象序列化成为一个二进制,返回一个序列化后的二进制数据。dump(obj, file, protocol=None, *, fix_imports=True, buffer_callback=None)
: 序列化,把一个数据对象进行序列化并写入到文件中。注意,demps
是返回并不写到文件中,而dump
者是写入到文件中。所以多一个必填参数file
, 就是写入的文件对象。loads(data, /, *, fix_imports=True, encoding='ASCII', errors='strict', buffers=None)
: 反序列化,可以把一个序列化后的二进制数据反序列化为 Python 的对象。返回一个反序列化后的 Python 对象。load(file, *, fix_imports=True, encoding='ASCII', errors='strict', buffers=None)
: 反序列化, 在一个文件中读取序列化的数据,并且完成一个反序列化。和loads
最大的不同是加载的是读取的文件对象file
,而不是data
。
可以看到,基本上来说,dump
和load
是对文件进行操作的方法,那能不能使用dumps
和loads
来完成呢?让我们来试试:
然后我们看,文件夹中确实多了一个 data.txt 文件,当我想要打开的时候,提示我为二进制文件。
那基本上可以确定,咱们所作的操作确实成功了。
借用其他的支持二进制文件的编辑器打开看看:
再来,我们把一个反序列的二进制文件读取处理,并完成反序列化:
以上两个方式,我们其实完全可以使用pickle
模块提供的方法来完成,dump
和load
:
我们又重新创建了一个序列化,保存数据到data2.txt
中,然后反序列化再从文件中读取转化。和之前我们用到的方法得到的结果一样,但是方法我们用的却完全不同。
JSON 序列化
JSON 的全称为: JavaScript Object Notation, 是一个受 JS 的对象字面量语法启发的轻量级数据交换格式。其在 JS 语言中是一个对象的表示方法,和 Python 中的字典的定义规则和语法都很像。
JSON 在互联网中又是一种通用的数据交换,传输,定义的一种数据格式。
和之前的pickle
序列化方法一样,JSON 序列化也有四种函数,其功能基本是一模一样。只是最后转化的数据格式不同:
json.dumps()
: 完成 JSON 格式数据的序列化json.loads()
: 完成 JSON 格式数据的反序列化json.dump()
: 和pickle
模块的dump
方法一致json.load()
: 和pickle
模块的load
方法一致
这里,我们先不着急写代码,我觉得需要对 JSON 简单了解一下,其实很简单,一说就明白了:
我们之前定义了一个字典:myDict = {'name':'茶桁', 'age':32, 'sex':'male'}
, 这个格式的数据在 Python 中是字典,但是在 JS 中,这个玩意是一个对象(Object
),如果它放在一个.json
文件中,这会是正常的json
格式的数据。
我们来做一下操作,上几张图就明白了,为了说明,我们创建一个15_json.html
文件和15_json.json
, 大家来看:
我们在脚本中定义了一个person
,格式和 Python 中的字典一模一样,但是在 JS 中,它被称为对象。我们在浏览器的控制台中打印出来看看:
那如果在一个 JSON 文件中呢,它就是一个最普通的 JSON 数据格式。只是稍微需要注意一下,虽然我们这样写并不会报错,但是总会提示格式问题,JSON 最正规的写法,是需要用“"
,尽量不要使用‘’
。
提前说这么多 JSON 的知识点,是因为接下来,如果我们没有说清楚,可能正的会分辨不清。好了,让我们转回 Python 中:
我们在代码中执行了两次打印,第一次是定义完字典之后,第二次是转换为 JSON 之后。我们可以看到两次打印结果,几乎是一模一样。当然,中文部分转化的比较明显,但是如果我讲name
的值设定为英文,比如Hivan
, 那么可以说几乎看不出区别。比较明显的,是我们将类型打印了出来,一个是dict
, 一个就是str
。
当然,和pickle
一样,loads
方法将会进行反序列化。
JSON 的转化,基本 Python 中所有的数据类型都可以进行序列化而不会出现报错的情况,但是有些数据是真的转为了 JSON 格式的数据,但是有的则只是转为了字符串而已。
让我们尝试将一个复杂的结构数据写到一个 JSON 文件中:
我们去查看一下文件的内容:
读取的话,当然也和pickle
中的用法也是一致的:
数学与数值
数学模块 Math
Python 中的内置数学模块 Math 提供了很多的数学相关运算。整个模块中的方法都非常简单,直接调用就可以了,当然,前提是需要导入数学模块。 下面我们就简单的介绍一下相关方法(当然,其实我们之前已经介绍过一部分了。):
向上取整
math.ceil()
这个函数让你想到了什么?不知道小伙伴们还记得前面学习的内容不,是不是特别像我们曾经学过的round()
方法?不过这两个方法还有不一样的地方,让我们来看:
看到区别了吗?round
实际上是一个四舍五入的函数,而ceil
只是向上取整 ,则不管你小数点后面的数字大小。当然,有向上取整: math.floor()
,这肯定有对应的向下取整,为了很清晰的看出来,我选择了一个向上接近整数的小数:
接下来是求幂次方: math:pow(x,y)
,这个方法会传入两个数值,前一个数值为底数,后一个则为指数,结果是浮点数。
下面一个是求开平方,结果也是浮点数: math.sqrt()
math.fabs()
能够计算绝对值,结果是浮点
math.modf()
: 把一个数值拆分成小数和整数组成的元组
至于最后的打印结果,不用太纠结。这并不是 Python 中的 BUG,而是与计算机如何处理浮点数有关,因为计算机存储数据实际上都是二进制的,而二进制无法正确处理。
举个栗子:1/3
, 在十进制下,这个数是无限循环小数对吧?3.33333333
,二进制实际上也有这种问题,比如说1/10
, 十进制下是0.1
,可是二进制下呢,就成了无限循环小数:0.000111001100110011...
。
其实大部分情况之下,这并不影响我们的使用,只是得到的数值需要处理一下四舍五入就可以了,但是也有不适用的情况。实际上,Python 中有专门处理精度计算的模块:decimal
,这个等我们以后再详细讲。
math.copysign(x,y)
: 把第二个参数的正负符号拷贝给第一个参数,结果为浮点数:
math.fsum()
: 将一个容器类型数据中的元素进行一个求和运算,结果为浮点数
这个方法的参数值需要注意一下,容器中的元素必须是可以运算的number
类型。
math.factorial(x)
: 以一个整数返回 x 的阶乘
除了运算函数外,还有一些常量函数。最典型的就是数学常数 π = 3.141592...
:
基本可以看出来,Python 的数学模块基本都属于很简单的工具类函数, 列举几个之后,大家基本上就都能上手了。其官方的文档地址可以看这里:相关文档地址为:https://docs.python.org/zh-cn/3.10/library/math.html#module-math。
随机模块 random
随机模块也是一个比较简单的模块,大部分时候,我们是使用它来产生随机值使用的。
random
模块中的random
函数直接使用会返回一个 0-1 之间的随机小数(左闭右开)
什么是左闭右开呢?通俗点说,就是random
这个函数,有可能取到0
这个值,而无论如何不会取到1
这个值。
random.randrange([开始值],结束值,[步进值])
:随机获取指定范围内的整数。对于这种需要开始值,结束值和步进值的参数形式的函数我们应该都已经非常熟悉了对吧?
这里有三个值,除了结束值是必选之外,另外两个值都是可选值。当只有一个参数时,默认就是从 0 到整数之间的值,存在两个参数时,就从开始值到结束值之间的随机数,而当有三个参数时,就会按照步进值从开始值到结束值之间产生一个随机数。需要记住一点,这三种参数取值方式,都是左闭右开的形式。也就是说,结束值是不会被取到的。
随机数大量应用在数字验证码,抽奖以及高并发下生成订单号等应用。
random.randint()
会随机产生指定范围内的随机整数
可能有的小伙伴会发出疑问了:茶桁老师,你这个解释是不是写错位置了?将randrange
的解释直接复制了下来。其实没有,这两个的功能几乎一模一样,说几乎的意思当然是还是有不同点,唯一一点不相同的是,randint
产生的随机整数,是左闭右闭的模式,也就是说,它是可以取到结束值的。
当然我们不能只随机整数对吧?实际应用场景中我们也需要大量的浮点数:
random.uniform()
获取指定返回内的随机小数, 实际应用中我们需要注意,这个函数是没有开始值和结束值的,只有范围值,也就是说,你最小值和最大值填入的先后顺序无所谓。
那有的小伙伴会想,如果两个值我填入的数值一样会如何?嗯,那就产生一个唯一的浮点值呗。
random.choice()
, 随机获取容器类型中的值。这个函数的应用范围就非常广了,我们在做数据分析的时候经常会用得到。因为大部分时候,我们说面对的应该都是容器类数据。
random.shuffle()
随机打乱当前列表中的值,没有返回值,仅仅是打乱原数据:
当然,我说介绍的函数都只是一部分,目的是打个样,让大家知道这些函数是个怎么回事。大部分时候会用到的函数抽出来讲解一下,更多的内容,还需要参考官方文档:https://docs.python.org/zh-cn/3.10/library/random.html#module-random
系统操作相关模块
OS 模块
OS 模块,就是操作系统接口模块。这个模块提供了一些方便使用操作系统相关功能的函数。我们之前重点学习的open()
,就是这个模块中的相关函数。现在让我们来看看除了open
之外,还有哪些函数可供我们日常使用:
os.getcwd()
获取当前的工作目录,注意获取的不是当前脚本的目录
不过需要注意一点是,这个函数并不是获取现在这个文件的所在目录,而是当前此文件的执行目录。这个怎么理解呢?我给大家举例说明一下:
我们首先需要知道一个,就是我们在 Linux 中执行cd
和pwd
的时候,一个是进入某个目录,一个是打印当前目录路径:
那么这里的pwd
所执行的结果,是随着进入目录不同而变化的,比如我们进入我们当前的文件目录:
也就是说,我在哪个目录下执行pwd
,那么返回结果就是当前执行的这个目录,而不是pwd
这个执行文件本身所在的目录。
gwtcwd()
文件,和pwd
实际上就是相同的特点,如果在当前目录执行这个脚本文件,那么 getcwd 获取的就是当前的文件目录。如果这个时候我切换到了其他目录,但是写了getcwd()
方法的文件没有挪动位置,那么此时我获取的返回值就是我切换的其他目录,而非文件所在的位置。下面我们可以测试一下:
可以看到,我们执行了和上 main 相同的代码,但是这个时候res
接收返回值发生了变化,其原因就是我使用了os.chdir
来改变了一下当前的工作目录。不知道大家现在是否能理解gwtcwd()
的工作原理?还是无法理解的,可以多自己写一下代码,做做尝试。连我这种笨人之前学习的时候都能很快理解,小伙伴们肯定更没有问题。
刚才我们的实验中,引出了另外一个方法:
os.chdir()
, 如上所见,其功能就是修改当前工作目录。
下面我们直接介绍其他的函数:
os.listdir()
获取当前或指定目录中的所有项(文件,文件夹,隐藏文件),组成的列表
这个方法和 Linux 中的list
命令就十分像了,让我们先将当前工作目录切回我们当前文件本来所在的目录,然后在来执行一下这个函数:
可以看到,目录内所有的文件,包括隐藏文件.DS_Store
和文件夹data
都被放进了一个列表当中。
这是在不指定目录的情况下,默认为当前工作目录,当然,我们还可以指定目录来获取那个目录下的内容:
这样,我们就获取到了我们希望查找的目录下的所有内容。我们继续:
os.mkdir(文件夹路径, 权限)
这个函数用来创建文件夹,其命令 和 Linux 中是一模一样,功能也是:
可以看到我们两次打印结果的对比,确实多了一个test
的目录。前一个参数很好理解,重点是后一个参数,什么是权限?
关于系统中的文件权限,我下面所讲的仅限Linux
系统,确切的说是unix
,因为包括 Mac 一样通用:
来,我们先进入目录打印出来看看:
我们主要来看一下data
目录的drwxr-xr-x
,分别来介绍一下:
第一个字母
d
代表的是一个目录,如果是-
呢,这表示这是一个文件前三位的
rwx
代表当前目录(文件)对所有人(u)的权限中间位置的
r-x
代表的所属组(g)的权限末尾的三位
r-x
代表的是其他人(o)的权限三个位置介绍完了我们来看字母所代表的意义:
r,w,x
代表不同的操作权限,其中:
r
就是可读,权限针对文件,表示可以查看文件内容,针对目录,表示可以ls
查看目录中存在的文件名称。w
就是可写,针对文件,表示可以更改文件的内容,针对目录,表示是否可以删除目录中的子文件或者子目录。x
是访问权限,针对文件,表示是否可以开启文件当中记录的程序,针对目录,这表示是否可以进入该目录。
那为什么是777
呢?那是因为r
代表是4
, w
代表是2
, x
代表的是1
, 那么7
就可以理解了,就是所有数值相加的结果。那么为什么是三个7
呢?因为这是在设置三个不同目标的权限,三个位数分别是所有人,所有组,其他。
不过,大家还要注意的一点是,无法使用 Python 去创建一个比自己这个进程权限还要高的文件。
mkdir()
方法是只能创建一个文件夹,无法递归创建文件夹,而当我们需要进行递归创建该怎么办呢?也就是说,我们不仅仅是想要创建test
文件夹,而是想创建/test/a/b/c/d/e
该怎么办?
os.makedirs()
可是进行递归创建文件夹。我们先看一下当前目录结构,直接 Finder 来看吧:
好,让我们执行一下代码:
再来看看目录结构:
这样就能最直观的看到执行这个函数之后的结果了。
在创建完一些无用目录之后,我当然想着是怎么删除它们。
os.rmdir*()
删除空文件夹,比如我们尝试着删除一下刚才我们创建的目录:
报错了,告知我们无法删除一个空目录,原因就是我们在test
里创建了好几层文件夹,那现在test
肯定不是空目录,那让我们从内层开始试试:
没有报错,应该是成功了,我们来看看:
确实,f
文件夹被删除了。不过太烦了,一个个删除到什么时候去了,还不如我到Finder
中直接手动删除呢。
os.removedirs()
就是一个递归删除空文件夹的函数,我们来试试:
居然又报错,告诉我们非空目录,这...
原来,removedirs
方法使用必须是从后往前递归的,也就是说,我们需要将需要删除的所有目录的层级关系给到这个方法,在执行过程中,向上递归,路径中的所有空目录都会被删除:
再执行一次,这回没问题了。从test
开始,下层的所有空目录都被删除了。
然后就是删除文件了
os.remove()
就是删除文件,为了测试这个方法,我在data
目录下创建了一个空文件test.txt
不过在删除之前,我们还是要先用一下这个文件,来看看如何改名:
os.rename()
: 用于修改文件或者文件夹的名字
好了,文件用完了,现在让我们删除吧:
顺利删掉了刚才创建的文件。
os.system()
执行操作系统中的命令
比如,我们刚才在命令行里执行过ls -al
的命令用于查看当前目录下的所有文件及目录,包括其相关权限,那么我们在 Python 里可以执行吗?来试试看就知道了:
执行效果如图:
这个方法实际上不止是让你在 Python 中执行系统命令用的,可以用于执行其他.py
, 也就是 Python 文件。比如我们创建了一个hello.py
文件,里面写了如下代码:
那么,我在其他 Python 文件中使用os.system
方法就可以执行这段代码,当然,前提是你得写对路径。
这样,你在其他文件内写的一个方法就被当前文件执行了。
os.path
路径模块
在 Python 创建的整个工程或者某一个函数里,路径操作也是经常要做的事情。比如:
os.path.abspath()
就是将相对路径转化为绝对路径吗,多数时候,我们是需要获取文件的绝对路径的,更多的是为了获取当前工作目录的绝对路径。
os.path.basename()
, 这个方法可以获取到路径后截取返回主体部分,来看代码,一看就明白了:
截取了路径中最末尾的文件名和扩展名,如果路径上最末尾的是一个文件夹不包含文件,那获取的就是那最后一个文件夹名称。
os.path.dirname()
, 返回路径中主体部分之前的内容
不过使用这个方法的时候需要注意,如果你填入的是一个相对路径,它并不能打印出绝对路径:
join()
链接多个路径,组成一个新的路径
实际上,它更像是一个字符串拼接,因为这个方法并不会去验证路径的有效性。
split()
这个方法和join()
正好相反,用于拆分路径,把路径拆分为路径和主体部分。然后返回一个元组。
splitext()
拆分路径,可以拆分文件后缀名
os.path.getsize()
获取文件的大小 , 单位是字节数
os.path.isdir()
会检测是否是一个文件夹,检测其是否存在,返回True
或者False
os.path.isfile()
会检测文件是否存在,一样是返回True
或者False
exists()
是一个通用函数,检测路径是否存在。和以上两个不同的是,也可以检测文件,也可以检测路径:
当我们有一个相对路径和一个绝对路径,而我们想看看两个路径是否指向一个目标位置的时候,是不是要先获取相对路径的绝对路径之后,再去对比呢?
其实没有那么麻烦, 只需要os.path.samefile(a, b)
就可以了。
使用这个方法,我们需要填入的两个值是真实存在的路径。
当然,官方的文档内还有更多的函数,我这里仅仅是列出了一些常用的。大家可以去官方文档内去看看。
shutil
高级操作模块
shutil
模块对文件和文件集合提供了许多的高级操作。其中就有支持文件复制和删除的一些功能。
要说,其实shutil
这个模块的很多方法和Unix
里的shell util
都一样,所以会用命令行的小伙伴,对这个模块应该是极其容易上手:
shutil.copy()
, 一看就明白是干什么的是吧?就是将文件拷贝到指定目录的。
报错了,咋回事?看提示,应该目录或文件不存在。嗯,这个方法要操作之前,目标路径中的目录是必须存在的,它无法自动创建目录。
让我们手动创建目录之后再试试, 还记得我们之前创建目录的命令怎么做吗?
有返回值了,那我们操作应该完成了。走,去目录里看看:
确实,目录和文件都存在了,这里注意我标注的两个文件,copy
这个命令指示拷贝了一个副本到目标目录中,原文件还是存在于原来的位置。但是注意到时间了吗?修改时间被更改了,改为了我们执行当前操作的时间。
copy2
是另外一个拷贝方法,它所有功能和copy
都一样,但是如果真是一模一样的方法,也就没必要多创建一个了对吧?这个方法最大的不同,就是保留了原文件的信息,包括操作时间和权限等。
再让我们操作一下试试:
除了这两个拷贝方法外,还有一个拷贝方法copyfile()
, 专门用于拷贝文件中的内容,写入到新的文件中去。让我们在./data/test/
中新建一个data2.txt
文件来接收写入内容:
我们来打开data2.txt
之后查看一下内容,确实写入了:
shutil.copytree('./a', './b')
方法看名字应该就猜到是干什么的,是将整个目录结构全部拷贝到指定目录中。使用的时候要多注意,指定的目标目录必须不存在同名目录。
shutil.rmtree()
,我们之前有用到带rm
的方法,那么看名字也就知道了,这个方法是删除整个文件夹,包括文件夹下的所有目录和文件,和之前我们使用的removedirs
不同,这个方法并不是从下往上递归,而是直接全部删除。让我再创建一次多级目录才测试一下:
目录创建完成后,来让我们将整个test
文件夹全部删除:
执行成功,test
整个目录及其内部文件全部删除了。
我们最后再来看一个用的非常多的方法shutil.move()
, 我们都知道,windows
中有剪切和复制两种菜单命令,然后到新的目标目录后进行粘贴,如果是剪切命令,这原目录中文件会在粘贴完成后删除,而如果是复制,不会执行删除。Linux
的逻辑稍微有些不同,是先进行拷贝,然后在目标目录之后决定是粘贴还是移动,如果是粘贴的话就保留原目录的文件,如果是移动,则会在粘贴完成之后在原位置删除文件。
虽然逻辑上有些许不同,但是不管是Windows
还是Linux
(包括 Mac),如果是移动某个文件的时候都遵循的是先复制一份到目标目录之后,再把原文件删除的先后顺序。
其实move()
命令也是一样的逻辑, 基于这个逻辑,move
实际上也可以用于修改文件夹或文件的名称。
然后我们去看一下:
可以看到,执行成功了。我们多注意一下就会发现,move
命令也有和copy2
相同的特性,将原文件的信息保留了下来。
zipfile
压缩模块
ZIP 文件格式基本是互联网上最通用的一种压缩格式,常见的存档和压缩标准。该模块提供了用于创建,读取,写入,附加和列出 ZIP 文件的工具。
在日常使用中,我们也会经常用到这个模块的相关功能。和之前介绍方法不同,我们这一部分按需求来介绍:
压缩文件
有没有发现,其方法和我们在对文件进行读写操作的时候很像,逻辑就是先创建一个压缩包文件,然后往里面扔入对应的文件。
解压缩文件
压缩之后,我们这次解压缩来看看压缩包内的文件是不是我们刚才扔进去的内容:
执行之后结果:
文件的确都是原来的文件。
批量压缩
不过之前压缩文件的时候也太麻烦了,文件一个一个的列出来扔进压缩包,那有没有办法将指定文件夹中的文件全部打包呢?
当然没问题,既然我们嫌弃手动一个个添加文件名太麻烦,那我们直接用机器添加不就好了,怎么做呢?
首先第一步当然是获取文件夹下所有的文件,应该还记得listdir()
这个方法吧?才学的。
获取列表之后,我们直接用代码一个个的扔到压缩包内就可以了,用for
循环吧:
通过将打印出来的arr
我们可以看到所有被成功扔进压缩包的内容。
其他压缩方法
zipfile
是 Python 中内置的专门用于压缩zip
格式压缩包的方法,但是其实,我们并不限于压缩成zip
格式。那是不是还有rarfile
, 7zfile
等模块呢?那真是想多了,我们刚才学过的shutil
方法,就可以进行压缩操作。不过不一样的是,虽然效果是一样的,但是我们这个方法是创建归档:
shutil.make_archive()
, 用于创建一个压缩文档。这个方法中有三个比较重要的参数,一个是创建的归档文件名称,第二个是指定的归档格式,第三个则是要归档的文件或文件夹路径。
成功完成tar
格式的归档。
除了以上的这些介绍的内置模块之外,我们平时应用中还会用到许多其他的模块,比如日历模块calendar
,时间模块time
等等。我上方讲解模块使用的同时,更多的是想向大家传递一个思想就是内置模块基本都很易上手,并且就算一时之间不太明白,可以多看看官方文档。从官方文档上学习是一个很好的习惯。我们可以从官方Python模块索引中去找到自己需要的模块。
好了,那这节课就先到这里了,下一节课中,我们利用日历和时间模块来做一个练习:万年历。大家要提前做预习,去官方文档好好学习一下其相关模块,包括calendar
、datetime
、time
等.
那小伙伴们,让我们下节课练习再见吧。
版权声明: 本文为 InfoQ 作者【茶桁】的原创文章。
原文链接:【http://xie.infoq.cn/article/0ff6d87c0122242cf1dc1ef12】。文章转载请联系作者。
评论