Python 从入门到深入¶
本文辑录了笔者在学习 Python 过程中的总结,归纳,比如针对同一问题不同场景的实现。它也很好的呈现了学习 Python 从编程到解决实际问题再到依据实际情况进行高效编程的阶梯过程。
放在这里方便笔者和他人学习和参考。文档中大部分示例使用 Python3.4 版本完成,当示例结果与系统平台相关时,通常会提供 Linux 和 Windows 两个版本的输出结果。 该文档还在不断增加完善中……,如果您对本文档有任何疑问,见解或者指导,请 Email 到 lli_njupt@163.com。
通过点击 在readthedocs中显示当前文档 可以浏览该文档的最新版本。
关于科学计算和机器学习进阶移步这里 数据科学和机器学习 。
Python 环境配置¶
Python 语言是一种动态的脚本语言,需要动态解析,当前分为主流的 Python2 和 Python3 版本,它们的语法是不安全兼容的,也即 Python3 通常无法直接执行 Python2 版本的 .py 文件。
所以在安装之前需要确定需要安装的 Python 版本。由于不同的应用软件需要不同的 Python 开发环境,可能需要同时安装多个 Python 版本,那么防止冲突,可以使用 virtualenv 实现安装环境的隔离。
另外注意:在 Linux 环境中安装软件时确保磁盘的文件系统支持软连接,不要使用共享的 Windows 磁盘安装,FAT32 和 NTFS 由于不支持软连接,可能导致安装时出现异常错误。
简单安装¶
这里以 Ubuntu 为例,安装命令异常简单:
0 1 | # sudo apt-get install python2.7
$ sudo apt-get install python3.6
|
安装后通过查看版本验证环境是否安装成功:
0 1 | $ python3.6 --version
Python 3.6.3
|
与此同时 Python 的软件包包管理器 pip 也被一同安装了:
0 1 | $ pip -V
pip 19.1.1 from /usr/local/lib/python3.6/dist-packages/pip (python 3.6)
|
通过 pip 安装的软件包将放置到 /usr/local/lib/python3.6/dist-packages 下。
直接使用 apt-get install 安装多个版本的 Python 可能导致 pip 包管理器混乱:此时会出现多个以 pip 开头的命名,例如 pip2,pip3。但是这并不意味着它们就分别对应 Python2 和 Python3,通过执行 pipx -V 查看它对应的 python 解释器版本,这样使用 pip install 就会安装到对应版本解释器的 dist-packages 文件夹下。
当然也可以通过源码直接安装,由于各种平台的软件管理工具已经非常成熟,通常不推荐这样做,例如:
0 1 2 3 4 5 | $ wget -c https://www.python.org/ftp/python/2.7.9/Python-2.7.9.tgz
$ tar -xzvf Python-2.7.9.tgz
$ cd Python-2.7.9/
$ LDFLAGS="-L/usr/lib/x86_64-linux-gnu" ./configure
$ make
$ sudo make install
|
Windows 平台有对应的 exe 或者 msi 安装文件,安装时选中安装环境变量即可,如果在命令行中无法运行 python,则需手动添加环境变量。
多版本安装¶
直接使用 apt-get install 安装 Python3,当安装多个 Python3.x 版本时,pip 就会冲突。我们需要手动解决这种冲突,方法就是重装 pip。
0 1 | $ curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py
$ python get-pip.py
|
首先下载 get-pip.py 脚本,通过它就可以为不同版本的python安装对应的 pip 软件包了:
0 1 2 3 4 5 6 | $ python2.7 get-pip.py
$ python3.6 get-pip.py
# 安装完毕后查看版本对应情况
$ pip -V
$ pip2 -V
$ pip3 -V
|
如果不对应需要将 pip 更名为对应版本,例如 pip3 对应 python3.6,更名为 pip3.6。
virtualenv¶
virtualenv 是一个强大的 python 安装环境管理工具,可以实现不同 python 运行环境的隔离。特别适用于某种特殊应用,例如 tensorflow 可能依赖于一些特定版本的第三方软件包,所以即便同样是 python3.x 版本,由于其他软件包的依赖问题,应该单独为它建立一个 tensorflow 的开发环境。否则一旦更新软件包,很可能导致已经安装好的环境无法使用,恢复起来也非常麻烦。
virtualenv 基于当前系统中已经安装的 Python 环境来创建隔离的运行环境。
0 | $ pip install virtualenv
|
这里为 tensorflow 创建一个独立的Python运行环境:
0 1 2 | # 为项目环境创建目录,所有安装文件均被防止在该目录下,而与外部其他Python环境无关
$ mkdir tfproj
$ cd tfproj/
|
接着使用 virtualenv 创建运行环境:
0 1 | # --no-site-packages 表示不可访问外部软件包
$ virtualenv --no-site-packages tfenv
|
virtualenv 默认使用系统中的 python 创建系统环境,使用 python -V 查看默认的 python 版本。
当然可以指定特定的 python 版本,例如:
0 | $ virtualenv -p /usr/bin/python3.6 --no-site-packages py36tfenv
|
创建完毕后,文件下将创建 tfenv 路径:
0 1 2 3 4 5 6 7 8 9 | $ ll
total 8
drwxrwxrwx 1 root root 144 Jul 13 17:58 ./
drwxrwxrwx 1 root root 8192 Jul 13 18:05 ../
drwxrwxrwx 1 root root 224 Jul 13 17:58 tfenv/
# 默认安装了包管理软件 pip
$ find . -name pip
./tfenv/bin/pip
./tfenv/lib/python3.6/site-packages/pip
|
使用 source 使能虚拟开发环境:
0 1 2 3 4 5 | # 进入虚拟环境,注意 shell 提示符出现的变化
$ source tfenv/bin/activate
(tfenv) hadoop@hadoop0:/home/red/sdd/tfproj$
(tfenv) hadoop@hadoop0:/home/red/sdd/tfproj$ env |grep PATH
PATH=/home/red/sdd/tfproj/tfenv/bin:...
|
默认安装了包管理软件 pip,和在主机上一样,使用它为这个独立环境安装第三方软件包。
若要退出当前的 tfenv 环境,使用 deactivate 命令。
virtualenv 在创建独立虚拟运行环境时把指定的 python 命令和它依赖的库文件复制一份到当前虚拟环境, 命令 source tfenv/bin/activate 会修改相关环境变量,此时交互 shell 中的 PATH 等环境变量指向了当前虚拟环境所在路径,所以 python 和 pip 也指向当前的虚拟环境。
安装编译环境¶
有些第三方安装包在安装前需要编译,如果没有安装编译环境,安装时将提示找不到 Python.h,例如:
0 1 2 3 4 | ...
# include <Python.h>
^
compilation terminated.
error: command 'i686-linux-gnu-gcc' failed with exit status 1
|
安装对应编译环境的头文件和库文件命令:
0 1 2 3 4 5 6 | $ sudo apt-get install python3.6-dev
Reading package lists... Done
Building dependency tree
Reading state information... Done
The following extra packages will be installed:
libpython3.6 libpython3.6-dev
...
|
安装时应指定对应的 Python 版本,例如 python3.6-dev 或 python2.7-dev。
pip 管理软件包¶
pip 安装软件包¶
pip 安装 python 软件包非常方便,首先通过 pip -V 查看是否是我们要安装的 python 环境,如果不是这要安装多版本方式重新安装,或者对 pip 重命名。如果所有用户都可以使用该开发环境,那么需要在安装命令前使用 sudo 安装。
0 1 2 | $ whereis pip
pip: /usr/local/bin/pip3.6 /usr/local/bin/pip2.7 \
/usr/local/bin/pip /usr/local/bin/pip3.4
|
whereis 命令查看 pip 命令,笔者环境中存在多个版本的 pip 管理器,此时 pip 通常是一个软连接,执行实际的某个版本的 pipx。
0 1 2 3 | $ pip install numpy
# 要用 pip 安装指定版本的 Python 包,只需通过 == 操作符指定
$ pip install robotframework==2.8.7
|
如果软件包比较大,而网络不稳定,可能导致安装失败,可以通过 wget 等方式下载 pip 要安装的软件包:
0 1 2 | $ pip install numpy
Collecting numpy
Downloading https://.../numpy-1.16.4-cp36-cp36m-manylinux1_i686.whl (14.8MB)
|
pip 安装时会打印出下载的软件包的路径,此时使用 wget 或者浏览器直接下载,下载的文件直接通过 pip install filename 安装即可,例如:
0 1 2 3 | $ pip install numpy-1.16.4-cp36-cp36m-manylinux1_i686.whl
Processing ./numpy-1.16.4-cp36-cp36m-manylinux1_i686.whl
Installing collected packages: numpy
Successfully installed numpy-1.16.4
|
如果我们想更新已有软件包,命令为:
0 1 | # 或者 pip install -U pip
$ pip install --upgrade pip
|
pip 查看软件包¶
查看所有通过当前 pip 安装的软件包:
0 1 2 3 4 5 | $ pip list
Package Version
----------------------------- ------------------
appdirs 1.4.3
atomicwrites 1.3.0
...
|
pip show 查看单个软件包信息,包含版本,官网,作者,发布协议,安装路径和对其他软件包的依赖关系:
0 1 2 3 4 5 6 7 8 9 10 | $ pip show urllib3
Name: urllib3
Version: 1.25.3
Summary: HTTP library with thread-safe connection pooling, file post, and more.
Home-page: https://urllib3.readthedocs.io/
Author: Andrey Petrov
Author-email: andrey.petrov@shazow.net
License: MIT
Location: /usr/local/lib/python3.6/dist-packages
Requires:
Required-by: requests, pyppeteer
|
查看 pip 帮助¶
普通的 linux 命令使用 man cmd 查看帮助信息,但是 pip 是 python 脚本,查看帮助信息方式为:
0 1 2 3 4 5 6 7 8 9 10 11 12 | # 或者 pip --help
$ pip -h
Usage:
pip <command> [options]
Commands:
install Install packages.
download Download packages.
uninstall Uninstall packages.
freeze Output installed packages in requirements format.
list List installed packages.
...
|
也可以针对单个命令字查看它支持的选项,例如:
0 1 2 3 4 5 6 7 8 | $ pip install -h
Usage:
pip install [options] <requirement specifier> [package-index-options] ...
pip install [options] -r <requirements file> [package-index-options] ...
pip install [options] [-e] <vcs project url> ...
pip install [options] [-e] <local project path> ...
pip install [options] <archive url/path> ...
...
|
打印输出和格式化¶
直接打印输出¶
print(value, ..., sep=' ', end='\n', file=sys.stdout, flush=False)
print()内建函数用于打印输出,默认打印到标准输出 sys.stdout。
0 1 2 3 4 5 | print("Hello %s: %d" % ("World", 100))
print("end")
>>>
Hello World: 100
end
|
print()函数会自动在输出行后添加换行符。
不换行打印输出¶
0 1 2 3 4 5 6 | strs = "123"
for i in strs:
print(i),
print("end")
>>>
1 2 3 end
|
通过在print()语句后添加”,” 可以不换行,但会自动在输出后添加一个空格。
print()函数支持end参数,默认为换行符,如果句尾有逗号,则默认为空格,可以指定end参数为空字符,来避免输出空格
0 1 2 3 4 5 | str0 = "123"
print(str0, end='')
print("end")
>>>
123end
|
使用 sys.stdout 模块中的 write() 函数可以实现直接输出。
0 1 2 3 4 5 6 7 | import sys
strs = "123"
for i in strs:
sys.stdout.write(i)
print("end")
>>>
123end
|
分隔符打印多个字符串¶
print()函数支持一次输入多个打印字符串,默认以空格分割,可以通过sep参数指定分割符号。
0 1 2 3 4 5 6 7 8 9 10 11 12 | str0 = "123"
str1 = "456%s" # 字符串中的%s不会被解释为格式化字符串
# 含有%s的格式化字符串只能有一个,且位于最后
print(str0,str1,"%s%s" %("end", "!"))
print(str0,str1,"%s%s" %("end", "!"), sep='*')
print("%s*%s*end!" % (str0, str1)) # 手动指定分隔符
>>>
123 456%s end!
123*456%s*end!
123*456%s*end!
|
格式化输出到变量¶
0 1 2 3 4 5 6 7 8 9 | tmpstr = ("Number is: %d" % 100)
print(tmpstr)
hexlist = [("%02x" % ord(x) )for x in tmpstr]
print(' '.join(hexlist))
print("end")
>>>
Number is: 100
4e 75 6d 62 65 72 20 69 73 3a 20 31 30 30
end
|
通过打印字符串的 ascii 码,可以看到换行符是 print()函数在打印时追加的,而并没有格式化到变量中。
长行打印输出¶
0 1 2 3 4 5 6 7 8 9 10 | def print_long_line():
print("The door bursts open. A MAN and WOMAN enter, drunk and giggling,\
horny as hell.No sooner is the door shut than they're all over each other,\
ripping at clothes,pawing at flesh, mouths locked together.")
print_long_line()
>>>
The door bursts open. A MAN and WOMAN enter, drunk and giggling,horny as
hell.No sooner is the door shut than they're all over each other, ripping
at clothes,pawing at flesh, mouths locked together.
|
如果 print() 函数要打印很长的数据,则可使用右斜杠将一行的语句分为多行进行编辑,编译器在执行时, 将它们作为一行解释,注意右斜杠后不可有空格,且其后的行必须顶格,否则头部空格将被打印。
0 1 2 3 | def print_long_line():
print("""The door bursts open. A MAN and WOMAN enter, drunk and giggling,
horny as hell.No sooner is the door shut than they're all over each other,
ripping at clothes,pawing at flesh, mouths locked together.""")
|
使用一对三引号和上述代码是等价的,以上写法每行字符必须顶格,否则对齐空格将作为字符串内容被打印,这影响了代码的美观。可以为每行添加引号来解决这个问题。
0 1 2 3 4 5 6 7 8 9 10 | def print_long_line():
print("The door bursts open. A MAN and WOMAN enter, drunk and giggling,"
"horny as hell.No sooner is the door shut than they're all over each other,"
"ripping at clothes,pawing at flesh, mouths locked together.")
print_long_line()
>>>
The door bursts open. A MAN and WOMAN enter, drunk and giggling,horny as
hell.No sooner is the door shut than they're all over each other, ripping
at clothes,pawing at flesh, mouths locked together.
|
打印含有引号的字符串¶
Python 使用单引号或者双引号来表示字符,那么当打印含有单双引号的行时如何处理呢?
0 1 2 3 4 5 6 7 | print("It's a dog!")
print('It is a "Gentleman" dog!')
print('''It's a "Gentleman" dog!''')
>>>
It's a dog!
It is a "Gentleman" dog!
It's a "Gentleman" dog!
|
打印输出到文件¶
print(value, …, sep=’ ‘, end=’n’, file=sys.stdout, flush=False)
print() 函数支持 file 参数来指定输出文件的描述符。默认值是标准输出sys.stdout,与此对应, 标准的错误输出是 sys.stderr,当然也可以指定普通文件描述符。
输出到磁盘文件时,为了保证实时性,根据实际情况可能需要把 flush 参数设置为 True。
0 1 | logf = open("logfile.log", "a+")
print("123", file=logf, flush=True)
|
对齐输出(左中右对齐)¶
通过print()函数可以直接实现左对齐输出。print() 函数不能动态指定对齐的字符数, 也不能指定其他填充字符,只能使用默认的空格进行填充。
0 1 2 3 4 5 6 7 | man = [["Name", "John"], ["Age", "25"], ["Address", "BeiJing China"]]
for i in man:
print("%-10s: %s" % (i[0], i[1]))
>>>
Name : John
Age : 25
Address : BeiJing China
|
Python中字符串处理函数 ljust(), rjust() 和 center() 提供了更强大的对齐输出功能。
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | print("123".ljust(5) == "123 ")
print("123".rjust(5) == " 123")
print("123".center(5) == " 123 ")
print("123".ljust(5, '~'))
print("123".rjust(5, '~'))
print("123".center(5, '~'))
>>>
True
True
True
123~~
~~123
~123~
|
左对齐 ljust() 示例,计算特征量的长度,决定动态偏移的字符数。
0 1 2 3 4 5 6 7 8 | len_list=[len(x[0]) for x in man]
offset = max(len_list) + 5 # 增加5个空白符号
for i in man:
print("%s: %s" % (i[0].ljust(offset), i[1]))
>>>
Name : John
Age : 25
Address : BeiJing China
|
左对齐 rjust() 示例:
0 1 2 3 4 5 6 7 8 | len_list=[len(x[0]) for x in man]
offset = max(len_list) + 5 # 增加5个空白符号
for i in man:
print("%s: %s" % (i[0].ljust(offset), i[1]))
>>>
Name : John
Age : 25
Address : BeiJing China
|
居中对齐示例,这里以字符‘~’填充。
0 1 2 3 4 5 6 7 8 | lines = ["GNU GENERAL PUBLIC LICENSE", "Version 3, 29 June 2007"]
len_list=[len(x) for x in lines]
center_num = max(len_list) + 30
for i in lines:
print(i.center(center_num, "~"))
>>>
~~~~~~~~~~~~~~~GNU GENERAL PUBLIC LICENSE~~~~~~~~~~~~~~~
~~~~~~~~~~~~~~~~Version 3, 29 June 2007~~~~~~~~~~~~~~~~~
|
格式化输出¶
更多格式化输出请参考 字符串格式化 。
百分号格式化¶
与 C 语言类似,python 支持百分号格式化,并且基本保持了一致。
数值格式化¶
整数格式化符号可以指定不同进制:
- %o —— oct 八进制
- %d —— dec 十进制
- %x —— hex 十六进制
- %X —— hex 十六进制大写
0 1 2 3 | print('%o %d %x %X' % (10, 10, 10, 10))
>>>
12 10 a A
|
浮点数可以指定保留的小数位数或使用科学计数法:
- %f —— 保留小数点后面 6 位有效数字,%.2f,保留 2 位小数位。
- %e —— 保留小数点后面 6 位有效数字,指数形式输出,%.2e,保留 2 位小数位,使用科学计数法。
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | print('%f' % 1.23) # 默认保留6位小数
>>>
1.230000
print('%0.2f' % 1.23456) # 保留 2 位小数
>>>
1.23
print('%e' % 1.23) # 默认6位小数,用科学计数法
>>>
1.230000e+00
print('%0.2e' % 1.23) # 保留 2 位小数,用科学计数法
>>>
1.23e+00
|
字符串格式化¶
- %s —— 格式化字符串
- %10s —— 右对齐,空格占位符 10 位
- %-10s —— 左对齐,空格占位符 10 位
- %.2s —— 截取 2 个字符串
- %10.2s —— 10 位占位符,截取两个字符
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | print('%s' % 'hello world') # 字符串输出
>>>
hello world
print('%20s' % 'hello world') # 右对齐,取 20 个字符,不够则空格补位
>>>
hello world
print('%-20s' % 'hello world') # 左对齐,取 20 个字符,不够则空格补位
>>>
hello world
print('%.2s' % 'hello world') # 取 2 个字符,默认左对齐
>>>
he
print('%10.2s' % 'hello world') # 右对齐,取 2 个字符
>>>
he
print('%-10.2s' % 'hello world') # 左对齐,取 2 个字符
>>>
he
|
format 格式化¶
format() 是字符串对象的内置函数,它提供了比百分号格式化更强大的功能,例如调整参数顺序,支持字典关键字等。它该函数把字符串当成一个模板,通过传入的参数进行格式化,并且使用大括号 ‘{}’ 作为特殊字符代替 ‘%’。
位置匹配¶
位置匹配有以下几种方式:
- 不带编号,即“{}”,此时按顺序匹配
- 带数字编号,可调换顺序,即 “{1}”、“{2}”,按编号匹配
- 带关键字,即“{name}”、“{name1}”,按字典键匹配
- 通过对象属性匹配,例如 obj.x
- 通过下标索引匹配,例如 a[0],a[1]
0 1 2 3 4 5 6 7 8 9 10 11 | >>> print('{} {}'.format('hello','world')) # 默认从左到右匹配
hello world
>>> print('{0} {1}'.format('hello','world')) # 按数字编号匹配
hello world
>>> print('{0} {1} {0}'.format('hello','world')) # 打乱顺序
hello world hello
>>> print('{1} {1} {0}'.format('hello','world'))
world world hello
>>> print('{wd} {ho}'.format(ho='hello',wd='world')) # 关键字匹配
world hello
|
通过对象属性匹配,可以方便地实现对象的 str 方法:
0 1 2 3 4 5 6 7 8 9 | class Point:
def __init__(self, x, y):
self.x, self.y = x, y
# 通过对象属性匹配
def __str__(self):
return 'Point({self.x}, {self.y})'.format(self=self)
>>>str(Point(5, 6))
'Point(4, 2)'
|
对于元组,列表,字典等支持索引的对象,支持使用索引匹配位置:
0 1 2 3 4 5 6 7 8 | >>> point = (0, 1)
>>> 'X: {0[0]}; Y: {0[1]}'.format(point)
'X: 0; Y: 1'
>>> a = {'a': 'val_a', 'b': 'val_b'}
# 注意这里的数字 0 代表引用的是 format 中的第一个对象
>>> b = a
>>> 'X: {0[a]}; Y: {1[b]}'.format(a, b)
'X: val_a; Y: val_b'
|
数值格式转换¶
- ‘b’ - 二进制。将数字以2为基数进行输出。
- ‘c’ - 字符。在打印之前将整数转换成对应的Unicode字符串。
- ‘d’ - 十进制整数。将数字以10为基数进行输出。
- ‘o’ - 八进制。将数字以8为基数进行输出。
- ‘x’ - 十六进制。将数字以16为基数进行输出,9以上的位数用小写字母。
- ‘e’ - 幂符号。用科学计数法打印数字。用’e’表示幂。
- ‘g’ - 一般格式。将数值以fixed-point格式输出。当数值特别大的时候,用幂形式打印。
- ‘n’ - 数字。当值为整数时和’d’相同,值为浮点数时和’g’相同。不同的是它会根据区域设置插入数字分隔符。
- ‘%’ - 百分数。将数值乘以100然后以fixed-point(‘f’)格式打印,值后面会有一个百分号。
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | # 整数格式化
>>> print('{0:b}'.format(3))
11
>>> print('{:c}'.format(97))
a
>>> print('{:d}'.format(20))
20
>>> print('{:o}'.format(20))
24
>>> print('{:x},{:X}'.format(0xab, 0xab))
ab,AB
# 浮点数格式化
>>> print('{:e}'.format(20))
2.000000e+01
>>> print('{:g}'.format(20.1))
20.1
>>> print('{:f}'.format(20))
20.000000
>>> print('{:n}'.format(20))
20
>>> print('{:%}'.format(20))
2000.000000%
|
各种进制转换:
0 1 2 3 4 5 6 7 | >>> # format also supports binary numbers
>>> "int: {0:d}; hex: {0:x}; oct: {0:o}; bin: {0:b}".format(42)
'int: 42; hex: 2a; oct: 52; bin: 101010'
>>> # with 0x, 0o, or 0b as prefix:
# 在前面加“#”,自动添加进制前缀
>>> "int: {0:d}; hex: {0:#x}; oct: {0:#o}; bin: {0:#b}".format(42)
'int: 42; hex: 0x2a; oct: 0o52; bin: 0b101010'
|
位数对齐和补全¶
- < (默认)左对齐、> 右对齐、^ 中间对齐、= (只用于数字)在小数点后进行补齐
- 取字符数或者位数“{:4s}”、”{:.2f}”等
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | >>> print('{} and {}'.format('hello','world')) # 默认左对齐
hello and world
>>> print('{:10s} and {:>10s}'.format('hello','world')) # 取10位左对齐,取10位右对齐
hello and world
>>> print('{:^10s} and {:^10s}'.format('hello','world')) # 取10位中间对齐
hello and world
>>> print('{} is {:.2f}'.format(1.123,1.123)) # 取2位小数
1.123 is 1.12
>>> print('{0} is {0:>10.2f}'.format(1.123)) # 取2位小数,右对齐,取10位
1.123 is 1.12
>>> '{:<30}'.format('left aligned') # 左对齐
'left aligned '
>>> '{:>30}'.format('right aligned') # 右对齐
' right aligned'
>>> '{:^30}'.format('centered') # 中间对齐
' centered '
>>> '{:*^30}'.format('centered') # 使用“*”填充
'***********centered***********'
>>>'{:0=30}'.format(11) # 还有“=”只能应用于数字,这种方法可用“>”代替
'000000000000000000000000000011'
|
正负号和百分显示¶
正负符号显示通过 %+f, %-f, 和 % f 实现:
0 1 2 3 4 5 | >>> '{:+f}; {:+f}'.format(3.14, -3.14) # 总是显示符号
'+3.140000; -3.140000'
>>> '{: f}; {: f}'.format(3.14, -3.14) # 若是+数,则在前面留空格
' 3.140000; -3.140000'
>>> '{:-f}; {:-f}'.format(3.14, -3.14) # -数时显示-,与'{:f}; {:f}'一致
'3.140000; -3.140000'
|
打印百分号,注意会自动计算百分数:
0 1 2 3 4 | >>> 'Correct answers: {:.2%}'.format(1/2.1)
'Correct answers: 47.62%'
# 以上代码等价于
>>> 'Correct answers: {:.2f}%'.format(1/2.1 * 100)
|
时间格式化¶
0 1 2 3 | >>> import datetime
>>> d = datetime.datetime(2018, 5, 4, 11, 15, 38)
>>> '{:%Y-%m-%d %H:%M:%S}'.format(d)
'2018-05-04 11:15:38'
|
占位符嵌套¶
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | >>> for align, text in zip('<^>', ['left', 'center', 'right']):
'{0:{fill}{align}16}'.format(text, fill=align, align=align)
'left<<<<<<<<<<<<'
'^^^^^center^^^^^'
'>>>>>>>>>>>right'
>>> width = 5
>>> for num in range(5,12):
for base in 'dXob':
print('{0:{width}{base}}'.format(num, base=base, width=width), end=' ')
print()
5 5 101
6 6 110
7 7 111
8 10 1000
9 11 1001
A 12 1010
B 13 1011
|
repr 和 str 占位符¶
0 1 2 3 4 5 6 7 8 | """
replacement_field ::= "{" [field_name] ["!" conversion] [":" format_spec] "}"
conversion ::= "r" | "s" | "a"
这里只有三个转换符号,用"!"开头。
"!r"对应 repr();"!s"对应 str(); "!a"对应ascii()。
"""
>>> "repr() shows quotes: {!r}; str() doesn't: {!s}".format('test1', 'test2')
"repr() shows quotes: 'test1'; str() doesn't: test2" # 输出结果是一个带引号,一个不带
|
format 缩写形式¶
可在格式化字符串前加 f 以达到格式化的目的,在 {} 里加入对象,这是 format 的缩写形式:
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | # a.format(b)
>>> "{0} {1}".format("hello","world")
'hello world'
>>> a = "hello"
>>> b = "world"
>>> f"{a} {b}"
'hello world'
name = 'Tom'
age = 18
sex = 'man'
job = "IT"
salary = 5000
print(f'My name is {name.capitalize()}.')
print(f'I am {age:*^10} years old.')
print(f'I am a {sex}')
print(f'My salary is {salary:10.2f}')
# 结果
My name is Tom.
I am ****18**** years old.
I am a man
My salary is 5000.00
|
调试和性能优化¶
定制调试信息¶
打印文件名和行号¶
借助sys.exc_info 模块自己捕获异常,来打印调用者信息,同时打印当前调试信息。
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | import sys
def xprint(msg=""):
try:
print("do try")
raise Exception
except:
f = sys.exc_info()[2].tb_frame.f_back
print('%s[%s]: %s' % (f.f_code.co_filename, str(f.f_lineno), msg))
def test_xprint():
xprint()
xprint("%d %s" %(10, "hello"))
test_xprint()
>>>
C:/Users/Red/.spyder/test/test.py[218]:
C:/Users/Red/.spyder/test.py[219]: 10 hello
|
异常时打印函数调用栈¶
异常发生时,Python默认处理方式将中断程序的运行,有时候我们希望程序继续运行。 可以通过 try 语句结合 sys.exc_info() 和 traceback模块抛出异常,并给出提示信息。
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | import traceback
import sys
def xtry(runstr):
ret, status = None, True
try:
ret = eval(runstr)
except:
info = traceback.format_exc()
try:
raise Exception
except:
f = sys.exc_info()[2].tb_frame.f_back
print('%s[%s]: %s' % (f.f_code.co_filename, str(f.f_lineno), info))
status = False
return status, ret
|
xtry()函数接受一个字符串作为表达式,与xprint()函数类似,在异常出现时打印出文件名和行号,并且借助traceback模块格式化调用栈信息。 同时返回是否出现异常和表达式的执行结果,status为True表示可以正常执行,否则出现异常。
下面是一个示例:
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | def divide(a, b):
return a / b
status, ret = xtry("divide0(100, 0)")
print(status, ret)
print("still running!!") # 继续执行
>>>
C:/Users/Red/.spyder/dolist/except.py[37]: Traceback (most recent call last):
File "C:/Users/Red/.spyder/dolist/except.py", line 22, in xtry
ret = eval(runstr)
File "<string>", line 1, in <module>
File "C:/Users/Red/.spyder/dolist/except.py", line 12, in divide0
try:
ZeroDivisionError: division by zero
False None
still running!!
|
断言和测试框架¶
assert 语句¶
断言语句 assert 在表达式为假时抛出断言异常 AssertionError 并终止程序的执行。 这在调试和测试代码时非常有用。
0 1 2 3 | assert 1 == 2
assert isinstance('str', str)
assert 0
assert False
|
0 和 False是等价的,1 和 True 是等价的,当表达式为假时,抛出如下的异常信息:
0 1 2 3 4 5 6 | assert 1 == 2
>>>
File "C:/Users/Red/.spyder/except.py", line 107, in <module>
assert 1 == 2
AssertionError
|
断言语句还支持一个格式化字符串参数,以逗号区分,用于提供更明确的断言信息。
0 1 2 3 4 5 6 7 | oct_num = -1
assert oct_num in range(10), "Oct number must be in (%d-%d)" % (0, 9)
>>>
File "C:/Users/Red/.spyder/except.py", line 107
assert oct_num in range(10), "Oct number must be in (%d-%d)" % (0, 9)
AssertionError: Oct number must be in (0-9)
|
专门的测试框架工具通常会对 Python 自带的断言功能进行扩展,以提供更强大的测试和诊断能力。
单元测试模块 unittest¶
单元测试主要针对最基础的代码可测单元进行测试,比如一个表达式,一个变量值的合法性,一个函数的入参和出参规格直至一个模块的功能。 著名的极限编程中的测试驱动开发(TDD:Test-Driven Development)就是以单元测试为基础的开发方式, 单元测试代码在编写功能代码时同时进行,每次对代码的增删和缺陷修复都要进行单元测试,以保证代码是符合预期的。 这很像在修路的同时,同时修筑了足够高的防护栏,而在赛车选手变换各类驾驶技巧时,不会冲出赛道。
可以这样说,只要单元测试没有漏洞,编码者就有底气说问题已经彻底修复了。
unittest 测试用例¶
Python 自带单元测试框架 unittest , 它将测试用例定义为 TestCase 类。 编写单元测试时,首先需要编写一个测试类,并继承 unittest.TestCase,类中的方法必须以 test 开头:
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | import unittest
class test_suit1(unittest.TestCase):
def test1(self):
'''test suit 1''' # 测试用例描述,同用例标题一并显示在测试报告里
self.assertEqual(1, 1)# 测试用例断言,期望 1 == 1,否则抛出异常
class test_suit2(unittest.TestCase):
def test2(self):
'''test suit 2'''
self.assertEqual(2, 0)
unittest.main() # 可以传入 verbosity=1 打印每一测试用例结果
>>>
.F
======================================================================
FAIL: test2 (unit.test_suit2)
test suit 2
----------------------------------------------------------------------
Traceback (most recent call last):
File "/home/red/sdc/lbooks/ml/unit.py", line 14, in test2
self.assertEqual(2, 0)
AssertionError: 2 != 0
----------------------------------------------------------------------
Ran 2 tests in 0.001s
|
测试结果的第一行给出所有测试用例的结果, . 和 F 分别表示测试通过或失败,每一个测试用例对应一个字符。
接着给出出错所用例的类名和描述,并附上对应出错代码的文件和行号等信息。最后给出总的运行用例数和测试耗时。
上面的示例直接通过 unittest.main() 函数运行,也可以在命令行中调用 unittest 模块,注意要注释掉 unittest.main()。
0 | python3.4 -m unittest unit_sample.py # 可以添加 -v 打印每一测试用例结果
|
unittest 目前支持如下断言函数:
名称 等价 版本 assertEqual(a, b) a == b assertNotEqual(a, b) a != b assertTrue(x) bool(x) is True assertFalse(x) bool(x) is False assertIs(a, b) a is b 3.1 assertIsNot(a, b) a is not b 3.1 assertIsNone(x) x is None 3.1 assertIsNotNone(x) x is not None 3.1 assertIn(a, b) a in b 3.1 assertNotIn(a, b) a not in b 3.1 assertIsInstance(a, b) isinstance(a, b) 3.2 assertNotIsInstance(a, b) not isinstance(a, b) 3.2
unittest 测试套件¶
上面的例子在实际的运行中,测试顺序并不是严格按照我们定义的测试用例顺序执行,有时我们的测试用例可能依赖执行顺序,比如打开文件,写文件等。 测试套件 TestSuite 可以解决该问题,同时它可以组织多个脚本文件的测试用例。
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | class TestMathFunc(unittest.TestCase):
def test_abs(self):
"""Test method abs()"""
self.assertEqual(1, abs(-1))
self.assertEqual(1, abs(1))
def test_max(self):
"""Test method max(x1,x2...)"""
self.assertEqual(2, max(1, 2))
def test_min(self):
"""Test method min(x1,x2...)"""
self.assertEqual(1, min(1, 2))
# 创建测试套件
suite = unittest.TestSuite()
# 1. 添加部分测试用例
tests = [TestMathFunc("test_max"), TestMathFunc("test_min")]
suite.addTests(tests)
# 2. 添加所有测试用例
suite.addTest(unittest.makeSuite(TestMathFunc))
runner = unittest.TextTestRunner(verbosity=1) # verbosity=0-2 调整输出
runner.run(suite)
>>>
.....
----------------------------------------------------------------------
Ran 5 tests in 0.000s
OK
|
示例中提供了两种向测试套件添加测试用例的方法,通过 addTests() 部分添加测试用例或通过 addTest() 导入套件中全部用例。测试结果中显示运行了5个用例。
测试结果输出到文件¶
0 1 2 3 4 5 | ......
suite.addTest(unittest.makeSuite(TestMathFunc))
with open('unittest_report.txt', 'w') as f:
runner = unittest.TextTestRunner(stream=f, verbosity=1)
runner.run(suite)
|
查看 unittest_report.txt 文件,可以发现与上面示例相同的输出。这里采用 unittest 自带的 TextTestRunner(),输出结果为普通文本文件。 verbosity 参数可以控制执行结果的输出:0 是简单报告,1 是一般报告,2 是详细报告。
可以借助 HTMLTestRunner 和 xmlrunner 模块生成 html 或者 xml 格式的报告文件。
测试环境的布置和清理¶
如果测试需要在每次执行之前准备环境,并且在每次执行完后需要进行测试环境的撤销,比如执行前创建临时文件夹,临时文件,测试用数据,连接数据库,创建并连接套接字等, 执行完成之后要删除临时文件夹,临时数据,断开连接。不可能为了每个测试用例都添加准备环境、清理环境的操作。
我们只要在测试类中,重写 unnitest 模块提供的 setUp() 和 tearDown() 两个方法即可。
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | class TestMathFunc(unittest.TestCase):
def setUp(self):
print("Prepare unittest environment.")
def tearDown(self):
print("Clean up unittest environment.")
......
>>>
Prepare unittest environment.
Clean up unittest environment.
.Prepare unittest environment.
Clean up unittest environment.
......
|
可以看到 setUp() 和 tearDown() 在每个测试用例执行前后都会执行一次。 也可以借助这一机制,来统计每个测试用例的运行时间。
如果想要在所有测试用例执行之前和结束之后,只执行一次准备和清理动作,可以用 setUpClass() 与 tearDownClass()。
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | class TestMathFunc(unittest.TestCase):
@classmethod
def setUpClass(cls):
print("Prepare unittest environment.")
@classmethod
def tearDownClass(cls):
print("Clean up unittest environment.")
......
>>>
Prepare unittest environment.
.....Clean up unittest environment.
----------------------------------------------------------------------
Ran 5 tests in 0.000s
OK
|
注意:必须使用 classmethod 修饰符来指明 setUpClass() 与 tearDownClass() 是类的方法,不需要实例化即可执行。
跳过特定测试用例¶
如果想要跳过某个测试用例不执行,可以在测试函数前使用 skip 修饰器。
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | class TestMathFunc(unittest.TestCase):
def test_abs(self):
"""Test method abs()"""
self.assertEqual(1, abs(-1))
self.assertEqual(1, abs(1))
@unittest.skip("Don't run it now!")
def test_max(self):
"""Test method max(x1,x2...)"""
self.assertEqual(2, max(1, 2))
......
>>>
s..s.
----------------------------------------------------------------------
Ran 5 tests in 0.001s
OK (skipped=2)
|
可以看到在 test_max() 用例在执行时标记为 s,表示跳过。
skip 修饰器一共有三种:
- unittest.skip(reason) 无条件跳过。
- unittest.skipIf(condition, reason) 当 condition 为 True 时跳过。
- unittest.skipUnless(condition, reason) 当 condition 为 False 时跳过。
参数化测试¶
如果要针对某个函数,或者类进行多种数据输入组合的测试,特别是完整性测试时,为每一种情况写一句断言,是非常麻烦的事情,借助参数化模块 parameterized,可以解决这一问题。
安装它的命令为 sudo pythonX.Y -m pip install parameterized,注意 Python 版本。
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | from parameterized import parameterized
class TestCanDrive(unittest.TestCase):
@parameterized.expand([
[True, True, False], # 也可以是 tuple
[True, False, True],
[False, False, False],
[False, False, False]
])
# 运行时遍历上述列表里的参数,把所有项执行一遍
def test_can_drive(self, with_license, drunk, expected):
status = can_drive(with_license, drunk)
self.assertEqual(status, expected)
suite.addTest(unittest.makeSuite(TestCanDrive))
runner = unittest.TextTestRunner(verbosity=1)
runner.run(suite)
>>>
....
----------------------------------------------------------------------
Ran 4 tests in 0.000s
OK
|
尽管我们只定义了一个测试函数,却运行了4个测试用例。如果测试用例数据非常多,我们可以把需要传递给 parameterized.expand() 函数的列表参数的数据放在一个文件里面,使用的时候读取。这样在修改维护测试数据上都将轻松很多。
批量执行测试用例¶
尽管通过 import 所有的测试模块脚本,并将它们加入到当前总的测试脚本中的测试套件中,以实现批量处理,但是每次 增删模块,都需要修改当前测试脚本,非常麻烦。
unittest 里的 defaultTestLoader.discover() 方法可以对指定路径下的测试脚本文件进行通配符匹配,直接返回测试套件。
0 1 2 3 4 | cases_suit = unittest.defaultTestLoader.discover("test_dir", \
pattern="test_*.py", top_level_dir=None)
runner = unittest.TextTestRunner(verbosity=1)
runner.run(cases_suit)
|
注意:defaultTestLoader.discover() 方法只处理包类型目录,也即目录下必须存在 __init__.py 文件,top_level_dir 指明包的顶层目录。
性能分析¶
在分析python代码执行效率时经常使用time包中的time.time()和time.clock()函数。但是两者是有区别的。
根据cpu的运行机制,cpu是多任务的,如在多进程的执行过程中,一段时间内会被多个进程或者线程占用。 一个进程从开始到结束其实是在这期间的一系列时间片(tick)上断断续续执行的。 此外,如果cpu是多核的或者超线程的,那么多线程的程序执行占用的cpu时间也可能多于真实世界流逝的时间。所以这就引出了程序执行的cpu时间(在这段系统时间内程序占用cpu运行的时间)和墙上时钟(wall time)。
绝对时间(absolute time):也即真实世界时间(real-world time),由time.time()返回。 它是从某个过去固定的时间点(比如 UNIX epoch为00:00:00 UTC on 01/01/1970)到当前时刻 真实世界经过的秒数。系统通过RTC(real-time clock)电路和纽扣电池来保持该时间。系统启动时读取 该秒数,在运行时,也可以通过NTP协议动态修改该秒数。系统基于该值,通过时区和夏令时转换显示为 便于理解的当地时间。UTC时区又被称为GMT或者Zulu时间。
real-world time或者real time在英文中还被称为墙上时钟(wall time或者wall-clock time),所以time.time() 两次返回的值的差就和墙上挂钟或者手表走过的时间是一样的。计算机中的RTC时钟系统是可以进行调整的, 这和真实世界中的钟表是一样的,任何人造计时装置都会走快走慢,RTC时钟系统也一样。
time和clock函数¶
- time.time()统计的是墙上时钟(wall time),也就是系统时钟的时间戳(1970纪元后经过的浮点秒数)。所以两次调用的时间差即为系统经过的总时间。
- time.clock()统计的是cpu时间,这在统计某一进程或者线程或函数的执行速度最为合适。两次调用time.clock()函数值即为程序运行占用的cpu时间。cpu时间又可细分为用户时间(User Time)和系统时间(System Time),分别表示进程/线程运行在用户态和内核态所占用的时间。
这两个函数均返回浮点数,单位秒。以下是两个函数的对比:
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | import time
def addto(n):
time.sleep(3) # 区分clock()和time()不同平台实现不同
return sum(range(n + 1))
start = time.time()
addto(10000000)
end = time.time()
print("time.time():\t\t%f" % (end - start))
start = time.clock()
addto(10000000)
end = time.clock()
print("time.clock():\t\t%f" % (end - start))
>>> # Windows 运行结果
time.time(): 3.573660
time.clock(): 3.552534
>>> # Linux 运行结果
time.time(): 4.246678
time.clock(): 1.216453
|
以上结果令人迷惑,在不同的平台上运行结果不一致。Linux上time.clock()要比time.time()短得多,而Windows平台相差不多。
如果去掉测试函数中的sleep(3),则两个函数在Linux平台上的输出也变成接近的。睡眠函数让程序让出cpu,显然在Linux平台上clock()函数统计的是程序实际消耗的cpu时间,而Windows平台返回的则是墙上时间。
通过查看系统clock()函数的底层调用,可以了解不同平台的区别。
0 1 2 3 4 5 6 7 | print(time.get_clock_info("clock"))
>>> # Windows 运行结果
namespace(adjustable=False, implementation='QueryPerformanceCounter()',
monotonic=True, resolution=3.9506172839506174e-07)
>>> # Linux 运行结果
namespace(adjustable=False, implementation='clock()', monotonic=True, resolution=1e-06)
|
Windows 平台底层调用 QueryPerformanceCounter() 函数,它实际上返回的就是墙上时间。
由以上测试用例,可以看出采用 time.clock() 来统计代码运行效率,具有平台不确定性,代码不可移植,该函数官方在Python 3.3版本已不再推荐使用。已被time.perf_counter()和time.process_time()取代。
详细说明请参考 PEP0418 。
高精度时间统计函数¶
高精度的时间间隔统计的实现基于cpu频率计数器,最高可以精确到cpu的工作频率。
time.perf_counter() 函数返回cpu时间,包括用户时间和系统时间,sleep 的时间,它包含了当前函数开始和结束间隔内被调度出的时间。
time.process_time() 函数返回本进程或者线程的cpu占用时间,包括用户时间和系统时间,不包含 sleep 时间。
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | start = time.perf_counter()
addto(10000000)
end = time.perf_counter()
print("time.perf_counter():\t%f" % (end - start))
start = time.process_time()
addto(10000000)
end = time.process_time()
print("time.process_time():\t%f" % (end - start))
>>> # Windows 运行结果
time.perf_counter(): 3.553474
time.process_time(): 0.578125
>>> # Linux 运行结果
time.perf_counter(): 4.178790
time.process_time(): 1.130667
|
通常情况下,使用这两个函数来对代码效率进行简单统计。
timeit性能分析模块¶
timeit模块默认使用 perf_counter() 时钟计时函数。
0 1 2 3 4 5 6 7 | import timeit
# 通过 timer 参数可以指定计时器
# print(timeit.timeit('x=1', timer=time.process_time))
print(timeit.default_timer)
>>>
<built-in function perf_counter>
|
timeit模块提供了 timeit() 和 repeat() 函数用于对代码片段或者函数进行重复测试。
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | #看执行1000000次x=1的时间:
print(timeit.timeit('x=1'))
#看x=1的执行时间,执行1次(number可以省略,默认值为1000000):
print(timeit.timeit('x=1', number=1))
#看一个列表生成器的执行时间,执行10000次:
t = timeit.timeit('[i for i in range(100) if i%2==0]', number=10000)
print(t)
t = timeit.timeit('addto(100000)', 'from __main__ import addto', number=1000)
print(t)
>>>
0.013765925919869915
3.950553946197033e-07
0.07114627161354292
2.6092641975410515
|
repeat和timeit用法相似,多了一个repeat参数,表示重复测试的次数(默认值为3), 返回值为一个时间的列表。
0 1 2 3 4 5 6 | t = timeit.repeat('addto(100000)', 'from __main__ import addto', number=100, repeat=3)
print(t)
print(min(t))
>>>
[0.2632288394961506, 0.25551288889255375, 0.2601240493822843]
0.25551288889255375
|
cProfile 和 profile¶
确定性性能分析((Deterministic Profiling))指的是反映所有的函数调用,返回,和异常事件的执行所用的时间,以及它们之间的时间间隔。相比之下,统计性性能分析指的是取样有效的程序指令,然后推导出所需要的时间,后者花费比较少的开销,但是给出的结果不够精确。
因Python是解释性语言,在执行程序的时候,需要解释器解析执行,这部分的执行是不需要进行性能分析的。Python自动为每一个事件提供一个hook,来定位需要分析的代码。除此之外,因为Python解释型语言的本质往往需要在执行程序的时候加入很多其它的开销,而确定性性能分析只会加入一点点处理开销。这样一来,确定性性能分析其实开销不大,还可以提供丰富的统计信息。
函数调用次数的统计能够被用于确定程序中的bug,比如一个不符合常理的次数,明显偏多之类的,还可以用来确定可能的内联函数。函数内部运行时间的统计可被用来确定”hot loops”,那些运行时间过长,需要优化的部分;累计时间的统计可被用来确定比较高层次的错误,比如算法选择上的错误。
cProfile 和 profile 均是标准库内建的确定性性能分析工具。
profile是原始的纯Python分析器。它提供的函数接口和调用方式与cProfile完全兼容。与cProfile相比,用户可以根据需要,在脚本层面扩展该模块。
cProfile是默认的分析工具,它基于lsprof,一个用C语言实现的扩展库, 底层调用 C 语言接口提供的动态库,在Unix系统上,它通常是位于Python共享库文件夹 /usr/lib/pythonx.y/lib-dynload/下的_lsprof.cpython-xxx-linux-gnu.so。所以相对于profile,它的运行效率要高。
使用 cProfile 进行性能分析有两种方式,可以使用命令行执行,也可以在脚本中导入函数。 使用命令行执行时,无需对脚本做任何改动。
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | # cprofile.py 中的测试代码
def addto0(n):
total = 1.0
for i in range(1,n+1):
total += i
return total
def addto1(n):
return addto0(n)
def test_add(n):
for i in range(2):
addto0(n)
for i in range(3):
addto1(n)
test_add(10000000)
# 直接命令行调用cProfile模块
# python -m cProfile -s cumulative cprofile.py
12 function calls in 5.643 seconds
Ordered by: cumulative time
ncalls tottime percall cumtime percall filename:lineno(function)
1 0.000 0.000 5.643 5.643 {built-in method exec}
1 0.000 0.000 5.643 5.643 cprofile.py:7(<module>)
1 0.000 0.000 5.643 5.643 cprofile.py:19(test_add)
5 5.643 1.129 5.643 1.129 cprofile.py:9(addto0)
3 0.000 0.000 3.385 1.128 cprofile.py:16(addto1)
1 0.000 0.000 0.000 0.000 {method 'disable' of '_lsprof.Profiler' objects}
|
-s 参数指明输出排序方式为 cumulative,也即按照函数累计耗时大小排序。这方便找到耗时最久的代码点。
第一行指明脚本中共执行了 12 次函数调用,一共耗时 5.642秒。
- ncalls:每个函数调用的次数,这里它们的和正好是12。
- tottime: 每个函数调用累计耗时,不含函数中子函数耗时。
- percall: 函数每次调用累计耗时的平均时间,等于 tottime / ncalls。
- cumtime: 每个函数调用累计耗时,含函数中子函数耗时。
- precall:函数每次调用累计耗时的平均时间,等于 cumtime / ncalls。
- filename:lineno(function):给出文件名,行号和函数名
test_add()函数被调用1次,其中调用了2次addto0() 和 3次addto1(),addto1()中调用了addto0(), 所以 addto0() 一共被调用了5次。
-s 参数还支持 ncalls,tottime,filename,line和module等排序方式。
代码中直接调用cProfile函数,这里用 ncalls排序:
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | import cProfile
# 直接把分析结果打印到控制台
cProfile.run("test_add(10000000)", sort="ncalls")
>>>
12 function calls in 5.693 seconds
Ordered by: call count
ncalls tottime percall cumtime percall filename:lineno(function)
5 5.693 1.139 5.693 1.139 cprofile.py:11(addto0)
3 0.000 0.000 3.319 1.106 cprofile.py:18(addto1)
1 0.000 0.000 0.000 0.000 {method 'disable' of '_lsprof.Profiler' objects}
1 0.000 0.000 5.693 5.693 {built-in method exec}
1 0.000 0.000 5.693 5.693 <string>:1(<module>)
1 0.000 0.000 5.693 5.693 cprofile.py:21(test_add)
|
pstats分析输出结果¶
cProfile 统计结果输出到文件:
0 1 2 | # python -m cProfile -o profile.stats cprofile.py # 命令方式
cProfile.run("addto(10000000)", filename="profile.stats")# 脚本调用函数方式
|
pstates 模块完成对文件 profile.stats 的分析。print_stats()输出跟之前一样的累计报告信息。
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | import pstats
p = pstats.Stats("profile.stats")
p.sort_stats("cumulative")
p.print_stats()
>>>
12 function calls in 5.631 seconds
Ordered by: cumulative time
ncalls tottime percall cumtime percall filename:lineno(function)
1 0.000 0.000 5.631 5.631 {built-in method exec}
1 0.000 0.000 5.631 5.631 cprofile.py:7(<module>)
1 0.000 0.000 5.631 5.631 cprofile.py:21(test_add)
5 5.631 1.126 5.631 1.126 cprofile.py:11(addto0)
3 0.000 0.000 3.378 1.126 cprofile.py:18(addto1)
1 0.000 0.000 0.000 0.000 {method 'disable' of '_lsprof.Profiler' objects}
|
为了追溯关心的函数,可以通过print_callers()打印调用者的信息。
0 1 2 3 4 5 6 7 8 9 10 11 12 13 | p.print_callers()
>>>
Ordered by: cumulative time
Function was called by...
ncalls tottime cumtime
{built-in method exec} <-
cprofile.py:7(<module>) <- 1 0.000 5.631 {built-in method exec}
cprofile.py:21(test_add) <- 1 0.000 5.631 cprofile.py:7(<module>)
cprofile.py:11(addto0) <- 3 3.378 3.378 cprofile.py:18(addto1)
2 2.252 2.252 cprofile.py:21(test_add)
cprofile.py:18(addto1) <- 3 0.000 3.378 cprofile.py:21(test_add)
{method 'disable' of '_lsprof.Profiler' objects} <-
|
反过来,还可以通过print_callees()打印被调用者的信息。
0 1 2 3 4 5 6 7 8 9 10 11 12 13 | p.print_callees()
>>>
Ordered by: cumulative time
Function called...
ncalls tottime cumtime
{built-in method exec} -> 1 0.000 5.631 cprofile.py:7(<module>)
cprofile.py:7(<module>) -> 1 0.000 5.631 cprofile.py:21(test_add)
cprofile.py:21(test_add) -> 2 2.252 2.252 cprofile.py:11(addto0)
3 0.000 3.378 cprofile.py:18(addto1)
cprofile.py:11(addto0) ->
cprofile.py:18(addto1) -> 3 3.378 3.378 cprofile.py:11(addto0)
{method 'disable' of '_lsprof.Profiler' objects} ->
|
查看前特定行的函数信息,如下所示:
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | # 打印指定的前多少行
p.print_stats(2)
# 以百分比,打印前50%的行数
p.print_stats(0.5)
>>>
...
ncalls tottime percall cumtime percall filename:lineno(function)
1 0.000 0.000 5.631 5.631 {built-in method exec}
1 0.000 0.000 5.631 5.631 cprofile.py:7(<module>)
...
ncalls tottime percall cumtime percall filename:lineno(function)
1 0.000 0.000 5.631 5.631 {built-in method exec}
1 0.000 0.000 5.631 5.631 cprofile.py:7(<module>)
1 0.000 0.000 5.631 5.631 cprofile.py:21(test_add)
|
查看给定的函数调用信息,比如想查看哪些函数调用了 addto0() 函数:
0 1 2 3 4 5 6 7 8 9 | p.print_callers("addto0")
>>>
Ordered by: cumulative time
List reduced from 6 to 1 due to restriction <'addto0'>
Function was called by...
ncalls tottime cumtime
cprofile.py:11(addto0) <- 3 3.378 3.378 cprofile.py:18(addto1)
2 2.252 2.252 cprofile.py:21(test_add)
|
同理,如果要查看test_add()函数调用了哪些函数:
0 1 2 3 4 5 6 7 8 9 | p.print_callees("test_add")
>>>
Ordered by: cumulative time
List reduced from 6 to 1 due to restriction <'test_add'>
Function called...
ncalls tottime cumtime
cprofile.py:21(test_add) -> 2 2.252 2.252 cprofile.py:11(addto0)
3 0.000 3.378 cprofile.py:18(addto1)
|
cProfile数据可视化¶
使用 gprof2dot 脚本和 dot命令可以根据cProfile统计信息生成函数调用流程图。
0 1 | # pip install gprof2dot
# gprof2dot -f pstats profile.stats | dot -Tpng -o output.png
|
逐行分析模块line_profiler¶
ipython¶
ipython 是基于 Python 的交互式 shell,比默认的 python shell 强大很多:支持变量自动补全,自动缩进,支持 bash shell 命令,内置了许多很有用的功能和函数。
ipython 安装非常简单,Windows 平台需额外安装 pyreadline 用于支持命令补全和颜色显示等功能:
0 1 | # pip install ipython
# pip install pyreadline
|
ipython 的交互提示符总是以 In [n]: 开始,其中 n 表示输入命令的编号:
0 1 2 3 4 5 6 | (py36) C:\Users\Red>ipython
Python 3.6.2 |Continuum Analytics, Inc.| (default, Jul 20 2017, 12:30:02)
[MSC v.1900 64 bit (AMD64)]
Type 'copyright', 'credits' or 'license' for more information
IPython 6.1.0 -- An enhanced Interactive Python. Type '?' for help.
In [1]:
|
ipython 功能简介¶
查看模块中所有函数,可以在 ‘.’ 之后按 Tab 键:
0 1 2 3 4 | In [11]: np.random.[Tab]
absolute_import bytes() division geometric() info
bench() chisquare() exponential() get_state() laplace()
beta() choice() f() gumbel() Lock()
binomial() dirichlet() gamma() hypergeometric() logistic()
|
tab 键可以自动补全命令或者函数,极大提高开发效率:
0 1 2 | In [4]: %hist[Tab]
%hist
%history
|
在变量的前面或后面添加问号(?)显示该对象的帮助信息,这就叫做对象的内省。这比 python 中的 help 命令方便多了:
0 1 2 3 4 5 6 7 | # 查看 ? 自身的帮助信息
In [16]: ?
......
? -> Introduction and overview of IPython's features (this screen).
object? -> Details about 'object'.
object?? -> More detailed, verbose information about 'object'.
%quickref -> Quick reference of all IPython specific syntax and magics.
help -> Access Python's own help system.
|
?? 可显示更多信息,例如函数源码。
0 1 2 3 4 5 6 7 8 9 10 | In [21]: def test(a):
...: print(a)
...:
In [22]: test??
Signature: test(a)
Source:
def test(a):
print(a)
File: c:\users\red\<ipython-input-21-e5cfd00ad69a>
Type: function
|
魔术命令是 ipython 的另一大特色:
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | %quickref 显示IPython的快速参考
%magic 显示所有魔术命令的详细文档
%debug 从最新的异常跟踪的底部进入交互式调试器
%hist 打印命令的输入(可选输出)历史
%pdb 在异常发生后自动进入调试器
%paste 执行剪贴板中的Python代码
%cpaste 打开一个特殊提示符以便手工粘贴待执行的Python代码
%reset 删除interactive命名空间中的全部变量/名称
%page OBJECT 通过分页器打印输出OBJECT
%run script.py 在IPython中执行一个Python脚本文件
%prun statement 通过cProfile执行statement,并打印分析器的输出结果
%time statement 报告statement的执行时间
%timeit statement 多次执行statement以计算系综平均执行时间。对那些执行时 间非常小的代码很有用
%who、%who_ls、%whos 显示interactive命名空间中定义的变量,信息级别/冗余度可变
%xdel variable 删除variable,并尝试清除其在IPython中的对象上的一切引用
|
%time 和 %timeit 是非常便利的测试代码执行时间的魔术命令,time 执行一次,而 timeit 执行多次取平均。
0 1 2 3 4 5 6 7 8 9 10 11 | In [31]: def sumto(n):
...: sum = 0
...: for i in range(n+1):
...: sum += i
...: return sum
In [32]: %time sumto(100000)
Wall time: 6.98 ms
Out[32]: 5000050000
In [33]: %timeit sumto(100000)
7.44 ms ± 361 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
|
更多命令请使用 %quickref 和 %magic 来显示 IPython 快速参考。
Jupyter Notebook¶
Jupyter Notebook 的前身被称为 IPython notebook,是一个交互式的 web 应用程序,显然 Notebook 是笔记本的意思,可以将命令和结果记录为一个扩展名为 .ipynb 的文件,通过它非常方便创建和共享程序文档,支持数学方程,内嵌图片和 markdown 语法。
Jupyter Notebook 建立在 IPython 基础之上,所以继承了 IPython 的强大扩展功能,是网页版的 IPython,所以访问它就要通过浏览器。
0 1 | $ pip install jupyter
$ jupyter notebook # 启动命令
|
启动 jupyter notebook 之后在浏览器中输入 http://localhost:8888 即可。
如果我们在 github 或者其他网站发现一个 .ipynb,或者 email 中发来嵌入一条链接,我们想在线浏览该文件,可以通过 https://nbviewer.jupyter.org/ 网站实现:只要在页面的地址框填入 .ipynb 超链接即可。
字符串处理¶
字符串(str)是 Python 中最常用的数据类型,可以使用单引号或双引号来创建字符串。注意:
- Python不支持单字符类型(对应 c 语言中的 char),单字符在 Python 中也是字符串类型。
- 字符串是不可变类型,即无法直接修改字符串的某一索引对应的字符,需要转换为列表处理。可以认为字符串是特殊的元组类型。
创建字符串变量¶
直接赋值创建字符串¶
Python 中通过各类引号(单双引号和三引号)标识字符串。
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | str0 = "Hello" # 双引号
str1 = 'world!' # 单引号
str2 = """123""" # 三引号
str3 = '''456'''
# 多行
str4 = "hello"\
" world"
str5 = """hello\
world"""
for i in range(6):
print(eval("str" + str(i)))
>>>
Hello
world!
123
456
hello world
hello world
|
由字符串组合生成新字符串¶
“+” 运算符实现字符串的拼接。
0 1 2 3 4 5 6 7 8 9 | str0 = "Name:"
str1 = "John"
str2 = str0 + " " + str1
str3 = "Age:" + " " + str(18)
print(str2)
print(str3)
>>>
Name: John
Age: 18
|
“*” 运算符实现字符串重复。
0 1 2 3 4 | str0 = "~" * 10
print(str0)
>>>
~~~~~~~~~~
|
操作符必须作用在 str 类型上,如果为其他类型必须使用 str() 函数进行 转换,例如这里的 str(18)。
新字符串也可以通过 字符串合并 ,转换,切片,分割和替换等方式得到。
复制字符串¶
复制字符串是处理字符串的常用操作,通过切片实现。
0 1 2 3 4 5 | str0 = ["0123456789"]
str1 = str0[:]
print(list1)
>>>
0123456789
|
其他类型转换为字符串¶
str() 内建函数可以将多种其他数据类型转化为字符串。
0 1 2 3 4 5 6 7 8 9 10 11 12 13 | print(str(1))
print(str(1.0))
print(str(1+1j))
print(str([12, "abc"]))
print(str((12, "abc")))
print(str({"Name":"John", "Age": "18"}))
>>>
1
1.0
(1+1j)
[12, 'abc']
(12, 'abc')
{'Name': 'John', 'Age': '18'}
|
可以注意到,将复杂数据类型转化为字符串时会保留其语法格式,若要进行更细致的操作 需要相应函数帮助,比如str.format(),它们提供了异常强大的转换功能。
特殊字符的转义处理¶
所谓特殊字符,有些字符是不显示的,比如回车换行符,有些是用来做特殊控制的,比如单引号在代码中表示字符的开始或者结束,这些字符必须用可见的字符来表示,但是可见字符已经表示了其自身,解决方案就是通过在普通字符前添加 ‘' 前缀,实现对普通字符的转义,\ 被称为转义符。
ASCII码值转义¶
0 1 2 3 4 5 6 7 8 | str0 = "A"
print(ord(str0[0]))
print("0x%02x" % ord(str0[0])) # 16进制输出
print("%o" % ord(str0[0])) # 8进制输出
>>>
65
0x41
101
|
“\yyy” 和 “\xyy” 分别表示三位八进制数和二位十六进制数 ascii 码对应的字符,8进制如果不够3位,前面必须补0,比如099。
0 1 2 3 4 5 6 7 | str1 = "\101"
str2 = "\x41"
print(str0 == str1)
print(str0 == str2)
>>>
True
True
|
其他可转义字符可以在 https://docs.python.org/2.0/ref/strings.html 找到。
0 1 2 3 4 5 6 7 8 9 10 | escape_characters = [["\'", "single quote"],
["\"", "double quote"],
["\\", "back slash"],
["\r", "carriage return"],
["\n", "new line"],
["\t", "table"],
["\a", "bell"],
["\b", "backspace"],
["\f", "form feed"],
["\v", "vertical tab"],
["\0", "null"]]
|
Unicode码值转义¶
在字符串中,我们可以使用 ‘\x0d’ 和 ‘\r’ 表示回车符号,那么对于中文字符来说,也同样有两种表示方式:
0 1 2 3 4 5 | str0 = '\u4f60'
str1 = '你'
print(str0, str1)
>>>
你 你
|
0x4f60 是中文字符 ‘你’ 的 Unicode 码值,可以采用 ‘\u’ 前缀加 Unicode 码值的方式表示一个中文字符,它和 ‘你’ 是等价的。 注意Unicode 码值和 UTF-8 编码的区别,参考 常见的编码方式 。
引号转义¶
对于字符串中的单双引号可以进行转义处理,也可以互斥使用单双引号:
0 1 2 3 4 5 6 7 8 9 10 | str0 = '123"456'
str1 = "123\"456"
str2 = """123'''456"""
print(str0)
print(str1)
print(str2)
>>>
123"456
123"456
123'''456
|
既有单引号又有双引号,可以使用转义,或者三引号处理。
0 1 2 3 4 5 6 7 | str0 = "123\"\'456"
str1 = """123'"456"""
print(str0)
print(str1)
>>>
123"'456
123'"456
|
可以指定多个字符串,字符串中间的空格被忽略。
0 1 2 3 4 | str0 = "spam " "eggs"
print(str0)
>>>
spam eggs
|
原生字符¶
使用原生(raw)字符串输入标志r或R可以免除大量转义,直接原样输出。
0 1 2 3 4 5 6 7 | str0 = r"123\"\'456"
str1 = R"123\"\'456"
print(str0)
print(str1)
>>>
123\"\'456
123\"\'456
|
注意:原生字符串必须保证代码行复合编码逻辑,也即起止标志(引号)必须配对,
比如字符串 r"123\"
是不能被解析的,右斜杠和引号同时存在令解析器认为字符串没有结束符”。
将提示 “SyntaxError: EOL while scanning string” 错误。
访问字符串中的值¶
要理解下标和切片访问方式,必须理解字符串的索引。和 C 语言类似,字符串中每一个字符都有一个唯一 的自然数字与它对应,从0开始。例如字符串 str0 = “Python”,其下标从0开始,则str0[0]对应字符’P’。 以此类推,str0[5]对应最后的字符’n’。
下标直接访问¶
0 1 2 3 4 | str0 = "Python"
print(str0[0], str0[-1])
>>>
P n
|
切片取子字符串¶
通过提供起止索引来访问子字符串的方式称为切片。下标超过最大索引,或者起始索引大于终止索引,返回空字符串。
切片操作支持指定步长,格式为 [start:stop:step],前两个索引和普通切片一样。
0 1 2 3 4 5 6 7 8 9 10 11 12 | str0 = "012456789"
print(str0[1:3])
print(str0[1::2]) # 2为步长
print(str0[3:])
print(str0[3:-1])
print(len(str0[100:])) # 返回空字符串
>>>
12
1468
456789
45678
0
|
切片操作的步长可以为负数,常用于翻转字符串,此时如果不提供默认值 start 和 stop 则默认为尾部和头部索引:
0 1 2 3 4 5 6 | str0 = 'abcde'
print(str0[::-1])
print(str0[3::-2]) # 从[3]元素开始,逆序隔元素取值
>>>
edcba
db
|
切片操作等价于:
0 1 2 3 4 5 6 7 8 9 10 | def doslice(instr, start, stop, step):
newstr = ''
for i in range(start, stop, step):
newstr += instr[i]
return newstr
print(doslice(str0, 3, 0, -2))
>>>
db
|
过滤特定的字符¶
filter(function or None, iterable) –> filter object
内建函数 filter() 可以对迭代数据类型执行特定的过滤操作。返回迭代对象。
取数字组成的字符串中的偶数字符,并得到新字符串。
0 1 2 3 4 5 | str0 = "0123456789"
iterable0 = filter(lambda i: int(i)%2 == 0, str0)
print("".join(iterable0))
>>>
02468
|
更新字符串中的值¶
字符串不允许直接修改,只能转换为其他类型更新后在转换回字符串。
转换为列表再转回¶
字符串转换为列表后,每一个字符串称为列表的一个元素,此时通过索引就可以更新每一个字符, 然后再通过 join() 函数转回字符串。
0 1 2 3 4 5 6 7 8 9 10 11 12 13 | str0 = "0123456"
list0 = list(str0)
print(list0)
list0[0] = 'a'
print(list0)
str0 = ''.join(list0)
print(str0)
>>>
['0', '1', '2', '3', '4', '5', '6']
['a', '1', '2', '3', '4', '5', '6']
a123456
|
切片更新字符串¶
0 1 2 3 4 5 | str0 = "abcdef"
str1 = str0[:3] + 'D' + str0[3:]
print(str1)
>>>
abcDdef
|
如果只是在头部或者尾部追加,可以直接使用”+”拼接运算符实现。 更复杂的操作需要通过替换函数等实现。
字符串格式化¶
更多格式化输出请参考 格式化输出 。
常用格式化符号¶
符号 描述 %c 格式化字符及其ASCII码 %s 格式化字符串 %d 格式化整数 %u 格式化无符号整型 %o 格式化无符号八进制数 %x 格式化无符号十六进制数 %X 格式化无符号十六进制数(大写) %f 格式化浮点数字,可指定小数点后的精度 %e 用科学计数法格式化浮点数 %E 作用同%e,用科学计数法格式化浮点数 %g %d 和 %e 的简写 %G %d 和 %E 的简写 % 直接输出 %
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | print("%c" % 'a')
print("%s" % "string")
print("%s" % 123) # 自动调用str()转换为str
print("%d" % 100.0) # 自动调用int()转换为int
print("%u" % 100)
print("%o" % 100)
print("0x%x" % 100)
print("0X%X" % 100)
print("%f" % 100)
print("%e" % 100)
print("%E" % 100)
print("%g" % 100.0)
print("%G" % 100.0)
print("%") # 直接输出 %,无需 %%
>>>
a
string
123
100
100
144
0x64
0X64
100.000000
1.000000e+02
1.000000E+02
100
100
%
|
转换符格式化¶
转换符格式化 (conversion specifier) 可以引用字典变量。 转换符的格式为 %(mapping_key)flags,mapping_key指明引用变量的名称,flags指明转换格式。
0 1 2 3 | print('%(language)s has %(number)01d quote types.' % {'language': "Python", "number": 2})
>>>
Python has 2 quote types.
|
更复杂的格式化使用str.format()更便捷。
format函数格式化¶
Python2.6 开始,新增了一种格式化字符串的函数 str.format(),它增强了字符串格式化的功能。 基本语法是通过 {} 和 : 来代替以前的 %。format 函数可以接受不限个参数,位置可以不按顺序。
为格式化参数指定顺序¶
0 1 2 3 4 5 6 7 | print("{} {}".format("abc", "123")) # 不指定位置,按默认顺序
print("{0} {1}".format("abc", "123")) # 设置指定位置
print("{1} {0} {1}".format("abc", "123")) # 设置指定位置
>>>
abc 123
abc 123
123 abc 123
|
通过名称或索引指定参数¶
直接通过名称引用,或者可以通过字典和列表传递参数。
0 1 2 3 4 5 6 7 8 9 10 11 12 13 | print("name: {name}, age: {age}".format(name="John", age="25"))
# 通过字典设置参数
man = {"name": "John", "age": "25"}
print("name: {name}, age: {age}".format(**man))
# 通过列表索引设置参数
man_list = ['John', '25']
print("name: {0[0]}, age: {0[1]}".format(man_list)) # "0" 是必须的
>>>
name: John, age: 25
name: John, age: 25
name: John, age: 25
|
直接传递对象¶
0 1 2 3 4 5 6 7 8 | class testobj(object):
def __init__(self, value):
self.value = value
testval = testobj(100)
print('value: {0.value}'.format(testval)) # 只有一个对象,此时 "0" 是可选的
>>>
100
|
数字格式化¶
str.format() 提供了强大的数字格式化方法。
数字 格式 输出 描述 3.1415926 {:.2f} 3.14 保留小数点后两位 3.1415926 {:+.2f} +3.14 带符号保留小数点后两位 -1 {:+.2f} -1.00 带符号保留小数点后两位 2.71828 {:.0f} 3 不带小数, <=0.5舍,>0.5入 5 {:0>2d} 05 数字补零 (填充左边, 宽度为2) 5 {:x<4d} 5xxx 数字补x (填充右边, 宽度为4) 10 {:x<4d} 10xx 数字补x (填充右边, 宽度为4) 1000000 {:,} 1,000,000 以逗号分隔的数字格式 0.25 {:.2%} 25.00% 百分比格式 1000000000 {:.2e} 1.00e+09 指数记法 13 {:10d} 13 右对齐 (默认, 宽度为10) 13 {:<10d} 13 左对齐 (宽度为10) 13 {:^10d} 13 中间对齐 (宽度为10)
针对不同的进制,str.format() 提供了以下格式化方法:
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | print('{:b}'.format(11)) # 二进制
print('0b{:0>5b}'.format(11))# 二进制,数字补零 (填充左边, 宽度为5
print('{:d}'.format(11)) # 十进制
print('{:o}'.format(11)) # 八进制
print('{:x}'.format(11)) # 16进制
print('{:#x}'.format(11)) # 16进制带0x前缀
print('{:#X}'.format(11)) # 16进制带0X前缀
>>>
1011
0b01011
11
13
b
0xb
0XB
|
^, <, > 分别是居中、左对齐、右对齐,后面带宽度。
: 号后面带填充的字符,只能是一个字符,不指定则默认是用空格填充。
- 表示在正数前显示 +,负数前显示 -; (空格)表示在正数前加空格。
b、d、o、x/X 分别是二进制、十进制、八进制、十六进制。
转义大括号¶
由于大括号在format()函数作为参数引用的特殊用途,如果要在字符串中输出大括号,则 使用大括号 {} 来转义大括号。
0 1 2 3 4 5 | print("{0} is {{0}}".format("value"))
print("{0} is ".format("value") + "{0}") # 连接字符串
>>>
value is {0}
value is {0}
|
应采用字符串连接的方式来组合含有大括号的字符串,这样更清晰。
字符串查找和统计¶
判断子字符串存在¶
使用 in 和 not in 判定子串自否存在。
0 1 2 3 4 5 6 7 8 9 | str0 = "0123456789"
print("123" in str0)
print("123" not in str0)
if "123" in str0:
print("123 in {0}".format(str0))
>>>
True
False
123 in 0123456789
|
指定范围查找字符串¶
本小结主要针对 find(),index()和rindex()函数的使用。
S.find(sub [,start [,end]]) -> int
find() 在 [start, end) 索引范围内查找 sub 字符串,如果存在返回第一个的索引,否则返回-1。
0 1 2 3 4 5 6 7 8 | str0 = "0123456789"
print(str0.find("78"))
print(str0.find("ab"))
print(str0.find("78", 1, 5))
>>>
7
-1
-1
|
S.index(sub [,start [,end]]) -> int
index()函数与find()函数非常类似,找不到时会抛出 ValueError 异常。
S.rindex(sub [,start [,end]]) -> int
rindex() 与 index() 作用类似,从右侧开始查找。
字符是否以子串开始或结束¶
S.endswith(suffix[, start[, end]]) -> bool
endswith() 函数返回True或者False,start和end指定查找范围,可选。
0 1 2 3 4 5 | print(str0.endswith("89"))
print(str0.endswith("89", 0, 9))
>>>
True
False
|
S.startswith(prefix[, start[, end]]) -> bool
与 endswith()类似,判断字符串是否以 prefix 子串开始。
字符串最大最小值¶
max() 和 min() 用于获取字符串中的最大ascii码和最小ascii码的字符。
0 1 2 3 4 | str0 = "0123456789"
print(max(str0), min(str0))
>>>
9 0
|
统计字符串出现次数¶
S.count(sub[, start[, end]]) -> int
count() 返回 str 在 string 里面出现的次数, 如果 start 或者 end 指定则返回指定范围内 str 出现的次数。
0 1 2 3 4 5 6 7 8 | str0 = "0123456789"
print(str0.count("0", 0, 9))
print(str0.count("0", 1, 9))
print(str0.count("abc"))
>>>
1
0
0
|
统计字符串不同字符数¶
set() 函数可以对字符串进行归一化处理。需要注意,set()函数返回的字符集合是无序的。
0 1 2 3 4 5 6 7 8 9 10 11 | str0 = "00112233"
print(set(str0))
for i in set(str0):
print("%s count %d" % (i, str0.count(i)))
>>>
set(['1', '0', '3', '2'])
1 count 2
0 count 2
3 count 2
2 count 2
|
字符串大小写转换¶
首字符转化为大写¶
S.capitalize() -> string
capitalize()将字符串的第一个字母变成大写,其他字母变小写。
0 1 2 3 4 5 6 7 | print("a, B".capitalize())
print(" a,B".capitalize()) # 第一个字符是空格,a不大写
print("a,BC D".capitalize())
>>>
A, b
a,b
A,bc d
|
转为大写或小写¶
S.upper() -> string
S.lower() -> string
upper()和lower()分别对字符串进行大写和小写转换。
0 1 2 3 4 5 6 | str0 = "Hello World!"
print(str0.upper())
print(str0.lower())
>>>
HELLO WORLD!
hello world!
|
大小写反转¶
S.swapcase() -> string
小写变大写,大写变小写,非字符字符保持不变。
0 1 2 3 4 | str0 = "Hello World!"
print(str0.swapcase())
>>>
hELLO wORLD!
|
标题化字符串¶
S.title() -> string
title() 返回”标题化”的字符串,即所有单词都是以大写开始,其余字母均为小写。
0 1 2 3 | str0 = "HI world!"
print(str0.title())
>>>
Hi World!
|
字符串对齐和填充¶
关于字符串对齐参考 打印输出到文件。
S.zfill(width) -> string
zfill() 函数返回指定长度的字符串,原字符串右对齐,前面填充字符0。
0 1 2 3 4 5 6 7 8 | str0 = "Hello world"
print(str0.zfill(20))
print(str0.zfill(30))
print(str0.rjust(30, '0'))
>>>
000000000Hello world
0000000000000000000Hello world
0000000000000000000Hello world
|
由示例可以得知 zfill(width) 和 rjust(width, ‘0’)是等价的。
字符串strip和分割¶
strip字符串¶
S.strip([chars]) -> string or unicode
strip()函数默认将字符串头部和尾部的空白符(whitespace)移除。 注意:该函数不删除中间部分的空白符。
也可以通过chars参数指定要出去的字符集。
POSIX标准给出空白符包括如下几种:
符号 描述 space 空格 \f 换页 (form feed) \n 换行 (newline) \r 回车 (carriage return) \t 水平制表符 (horizontal tab) \v 垂直制表符 (vertical tab)
0 1 2 3 4 5 6 7 | str0 = " hello \r\n\t\v\f"
str1 = "00000hell10o10000"
print(str0.strip()) # 去除首尾空白符号
print(str1.strip("01")) # 去除首尾字符 1和0
>>>
hello
hell10o
|
如果参数 chars 为 unicode 类型,则首先将要处理的字符串 S 转换为 unicode类型。
S.lstrip([chars]) -> string or unicode
S.rstrip([chars]) -> string or unicode
lstrip() 和 rstrip() 方法与strip()类似,只是只去除头部或者尾部的空白符,或指定的字符集 中的字符。
单次分割¶
S.partition(sep) -> (head, sep, tail)
partition()方法用来根据指定的分隔符将字符串进行分割,sep 若包含多个字符,则作为一个整体分割。
rpartition()方法从右侧开始分割。
如果字符串包含指定的分隔符,则返回一个3元的元组, 第一个为分隔符左边的子串,第二个为分隔符本身,第三个为分隔符右边的子串。
0 1 2 3 4 5 6 7 8 9 10 | str0 = "www.google.com"
print(str0.partition("."))
print(str0.rpartition("."))
print(str0.partition("1")) # 如果没有则元组前两个元素为空字符
print(str0.partition("google"))# "google" 作为整体充当分割符
>>>
('www', '.', 'google.com')
('www.google', '.', 'com')
('', '', 'www.google.com')
('www.', 'google', '.com')
|
字符串切片¶
S.split([sep [,maxsplit]]) -> list of strings
split() 通过指定分隔符对字符串进行切片,默认使用所有空白符,如果参数 maxsplit 有指定值,则仅分隔 maxsplit 个子字符串。
注意:split() 返回的是一个字符串列表。
0 1 2 3 4 5 6 7 8 9 10 | str0 = "abcdef \n12345 \nxyz";
print(str0.split())
print(str0.split(' ', 1))
print(str0.split('A')) # 如果没有,返回的列表只包含原字符串
print(str0.split('15')) # "15" 作为整体分隔符,而不是分别用1和5做分隔符
>>>
['abcdef', '12345', 'xyz']
['abcdef', '\n12345 \nxyz']
['abcdef \n12345 \nxyz']
['abcdef \n12345 \nxyz']
|
按换行符切割¶
S.splitlines(keepends=False) -> list of strings
splitlines() 按照换行符(‘\r’, ‘\r\n’, \n’)分隔, 返回一个包含各行作为元素的列表,如果参数 keepends 为 False, 不包含换行符,如果为 True,则保留换行符。
0 1 2 3 4 5 6 | str0 = 'str1\n\nstr2\n\rstr3\rstr4\r\n\r\n'
print(str0.splitlines())
print(str0.splitlines(True))
>>>
['str1', '', 'str2', '', 'str3', 'str4', '']
['str1\n', '\n', 'str2\n', '\r', 'str3\r', 'str4\r\n', '\r\n']
|
字符串替换¶
制表符替换为空格¶
S.expandtabs([tabsize]) -> string
expandtabs() 方法把字符串中的水平制表符(‘\t’)转为空格,默认的空格数是 8。
0 1 2 3 4 5 6 7 8 | str0="s\te"
print(str0)
print(str0.expandtabs())
print(str0.expandtabs(4))
>>>
s e
s e
s e
|
新子串替换旧子串¶
S.replace(old, new[, count]) -> string
replace() 方法把字符串中的旧字符串 old 替换成新字符串 new,如果指定第三个参数 count,则替换不超过 count 次。
0 1 2 3 4 5 6 | str0 = "old old old old"
print(str0.replace("old", "new"))
print(str0.replace("old", "new", 3))
>>>
new new new new
new new new old
|
replace() 方法只能把参数作为一个整体进行替换,如果我们要替换字符串中的多个字符,可以借助 re 正则表达式模块。
0 1 2 3 4 5 6 | import re
str0 = '\r\nhello 1213 \nworld'
print(re.sub('[\r\n\t23]', '', str0))
>>>
hello 11 world
|
字符映射替换¶
S.translate(table [,deletechars]) -> string [Python2.x]
S.translate(table) -> str [Python3.x]
translate() 函数根据参数 table 给出的转换表(它是一个长度为 256 的字符串)转换字符串的单个字符,要过滤掉的字符通过 deletechars 参数传入。
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | # 引用 maketrans 函数生成转换表
from string import maketrans
# intab 和 outtab 长度必须相同
intab = "aeiou+"
outtab = "12345-"
trantab = maketrans(intab, outtab)
str0 = "aeiou+r1m"
print(str0.translate(trantab, "rm"))
# Python 3.x 版本不支持 deletechars
print(str0.translate(trantab))
>>>
12345-1
12345-r1m
|
translate() 只限于单个字符的映射替换。
字符串映射替换¶
为了解决 translate() 方法单字符映射的限制,使用 re 功能可以无副作用的替换多个字符串。
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | # count 表示替换的次数,默认替换所有
def replace_strs(instr, map_dict, count=0):
import re
# escape all key strings
re_dict = dict((re.escape(i), j) for i, j in map_dict.items())
pattern = re.compile('|'.join(re_dict.keys()))
return pattern.sub(lambda x: re_dict[re.escape(x.group(0))], instr, count)
str0 = "This and That."
map_dict = {'This' : 'That', 'That' : 'This'}
print(replace_strs(str0, map_dict))
# 注意重复调用 replace() 方法带来的副作用
newstr = str0.replace('This', 'That').replace('That', 'This')
print(newstr)
>>>
That and This.
This and This.
|
字符串排序¶
对一个字符串排序,通常有两种方式:
- 转换为列表,使用列表的 sort() 方法。
- 使用内建函数 sorted(),它可以为任何迭代对象进行排序,还可以指定 key 来忽略大小写排序。参考 sorted 。
这两种方式都需要把排序后的列表转换回字符串。
0 1 2 3 4 5 6 7 8 9 | str0 = "hello"
strlist = list(str0)
strlist.sort()
print(''.join(strlist))
print(''.join(sorted(str0)))
>>>
ehllo
ehllo
|
字符串合并¶
如同 int 类一样,字符串类重载了 + 运算符,使用 + 运算符合并两个子串是最简洁的。
S.join(iterable) -> str
join() 方法用于将可迭代类型中的元素以指定的字符连接生成一个新的字符串。
0 1 2 3 4 5 6 7 8 9 10 11 12 13 | str0 = "ABC"
tuple0 = ("a", "b", "c")
list0 = ["1", "2", "3"]
print("--".join(str0))
print("--".join(tuple0))
print("--".join(list0))
print("".join(list0))
>>>
a--b--c
1--2--3
A--B--C
ABC
|
使用 join() 函数打印乘法表:
0 1 2 3 4 5 6 7 8 9 10 11 12 | print('\n'.join([' '.join(['%sx%s=%-2s' \
% (y,x,x*y) for y in range(1,x+1)]) for x in range(1,10)]))
>>>
1x1=1
1x2=2 2x2=4
1x3=3 2x3=6 3x3=9
1x4=4 2x4=8 3x4=12 4x4=16
1x5=5 2x5=10 3x5=15 4x5=20 5x5=25
1x6=6 2x6=12 3x6=18 4x6=24 5x6=30 6x6=36
1x7=7 2x7=14 3x7=21 4x7=28 5x7=35 6x7=42 7x7=49
1x8=8 2x8=16 3x8=24 4x8=32 5x8=40 6x8=48 7x8=56 8x8=64
1x9=9 2x9=18 3x9=27 4x9=36 5x9=45 6x9=54 7x9=63 8x9=72 9x9=81
|
字符串特征类型判定¶
判定函数均返回 布尔值。非空表示如果字符为空字符则返回 False。
为何更好的理解字符类型特征,请参考 Unicode的字符集分类 。
方法 描述 string.isalnum()
- 非空,全为字母或者数字 (alphanumeric character)
- 等价于 isalpha() or isdecimal() or isdigit() or isnumeric()
string.isalpha()
- 非空,全为字母 (alphabetic character)
- Unicode “Letter”字符集,包含 Lm, Lt, Lu, Ll 和 Lo
string.isdigit()
- 非空,全为十进制数字 (0-9)
- Unicode “Decimal, Digit”字符集 Nd
string.isspace()
- 非空,全为空白符(whitespace), 参考 strip字符串
- Unicode “Separator” 字符集,包含 ZI, Zp 和 Zs
string.istitle()
- 非空,是否为标题化字符串,至少包含一个区分大小写的字符
- 区分大小写的字符称为 cased characters
- 包含 Lu (Letter, uppercase) Ll (Letter, lowercase)和Lt (Letter, titlecase).
string.isupper() 非空,至少包含一个区分大小写的字符,且全为大写 string.lower() 非空,至少包含一个区分大小写的字符,且全为小写 ustring.isnumeric()
- 非空,全为数字,只存在于unicode对象
- Unicode “Digit, Decimal, Numeric”字符集, 包含 Nd, Ni 和 No
ustring.isdecimal() 非空,全为数字,只存在于unicode对象,包含 Nd string.isascii() 空,或者全为ASCII码字符,U+0000-U+007F 3.7版本引入 string.isprintable()
- 空,或全为可打印字符
- 不可打印字符 Unicode “Separator” 字符集,包含 ZI, Zp 和 Zs
- 例外:空格是可打印字符
对于 isdecimal() 和 isnumeric() 的区别做如下测试。
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | list0 = [u"1234", u"0123", u"٠١٢٣٤٥٦٧٨٩", u"¼½", u"一二三四",
u"〇零参百万千亿", u"廿卅卌", u"ⅠⅡⅢⅣ", u"①"]
for i in list0:
print("%s\t\t: %s %s %s" % (i, str(i.isdecimal()), str(i.isnumeric()),\
str(i.isdigit())))
>>>
1234 : True True True
0123 : True True True
٠١٢٣٤٥٦٧٨٩ : True True True
¼½ : False True False
一二三四 : False True False
〇零参百万千亿 : False True False
廿卅卌 : False True False
ⅠⅡⅢⅣ : False True False
① : False True True
|
isnumeric() 在unicode空间上要宽泛得多。
字符编解码¶
世界上存在这各种各样的符号,有数学符号,有语言符号,为了在计算机中统一表达,制定了 统一编码规范,被称为Unicode编码。它让计算机具有了跨语言、跨平台的文本和符号的处理能力。
细说编码 和 彻底理解字符编码 是两篇了解字符编码比较深入浅出的文章。 这里只做简单的总结性介绍。
常见的编码方式¶
- ASCII编码:美国制定,单字节编码,只用了8位的后7位,第一位总是0,一共128个字符
- ISO-8859-1(Latin1):ISO组织制定,单字节编码,扩展了Ascii编码的最高位,一共256个字符
- GB2312:分区编码,针对简体中文,2字节编码,高字节表示区,低字节表示位,共收录6763个中文字符
- BIG5(cp950):针对繁体中文,2字节编码,共收录13060个中文字符
- GBK(cp936):“国标”、“扩展”汉语拼音的第一个字母缩写,2字节编码。扩展了GB2312编码,完全兼容GB2312,包含繁体字符,但是不兼容BIG5 (所以BIG5编码的文档采用GBK打开是乱码,GB2312采用GBK打开可以正常浏览)
- Unicode(统一码/万国码/单一码):全球通用的单一字符集,包含人类迄今使用的所有字符,但只规定了符号的编码值,没有规定计算机如何编码和存储,针对Unicode有两种编码方案。
Unicode编码方案主要有两条主线:UCS和UTF。
- UCS(Universal Character Set):由ISO/IEC标准组织维护管理,包含两种编码方案
- UCS-2:2字节编码,BOM(LE: FF FE;BE: FE FF)
- UCS-4: 4字节编码,BOM(LE: FF FE 00 00;BE: 00 00 FE FF)
- UTF(Unicode Transformation Format):由 Unicode Consortium 进行维护管理,目前有三种编码方案
- UTF-8:1-4字节变长编码,压缩了存储空间,加快了数据传输速度,无需BOM机制。
- UTF-16:2或者4字节编码,BOM(LE: FF FE;BE: FE FF)
- UTF-32: 4字节编码, BOM(LE: FF FE 00 00;BE: 00 00 FE FF)
目前最普遍使用的是UTF-8编码。
编码方式 UTF-8 UTF-16 UTF-32 UCS-2 UCS-4 编码空间 0-10FFFF 0-10FFFF 0-10FFFF 0-FFFF 0-7FFFFFFF 最少字节数 1 2 4 2 4 最多字节数 4 4 4 2 4 BOM 依赖 n y y y y
编码码值和字符转换¶
Python3 中,ord() 和 chr() 可以实现 Unicode 字符和码值之间的转换。
ASCII码值字符转换¶
ord() 和 chr() 方法实现了字符和编码值的转换。它们不仅支持0-127的 ASCII 码,还支持 ISO-8859-1 的扩充部分,也即 0-255 的码值。
0 1 2 3 4 5 6 7 | print(ord('a')) # 返回单个字符的ascii码,整型
print(chr(65)) # 返回ascii码对应的字符,参数范围:0-255
print(ord(chr(255)))
>>>
97
A
255
|
Unicode码值字符转换¶
ord() 还接受传入 1 个宽字符,并返回它的 Unicode 码值。
此时与 ord() 相对应的函数是 unichr(),它也支持 ASCII 码,因为 ASCII 码是 Unicode 的子集。
0 1 2 3 4 5 6 7 8 9 10 | # python3 中等同 ord('一')
print("Oct: %d Hex: u%04x" % (ord(u'一'), ord(u"一")))
print('\u4e00')
print(unichr(19968))
print(unichr(65))
>>>
Oct: 19968 Hex: u4e00
一
一
A
|
Python3中取消了 unichr() 函数,chr() 将参数范围扩展到了所有 Unicode 码值范围。
在Python2版本中,存在 str 和 unicode 两种字符串类型,Python3 中只有一种即 unicode 字符串类型。
注意 Unicode 码值和 utf-8 编码之间的关系,Unicode 提供的是码值,utf-8 将码值根据规则变换成可以存储的编码。
0 1 2 3 4 5 | print(hex(ord('一'))) # 16进制的Unicode码值
print('一'.encode('utf-8'))# 3字节的 UTF-8 编码
>>>
0x4e00
b'\xe4\xb8\x80'
|
字节序列 Bytes¶
Python3 中明确区分字符串类型 (str) 和 字节序列类型 (bytes),也称为字节流。内存,磁盘中均是以字节流的形式保存数据,它由一个一个的字节 (byte,8bit)顺序构成,然而人们并不习惯直接使用字节,既读不懂,操作起来也很麻烦,人们容易看懂的是字符串。
所以字符串和字节流需要进行转化,字节流转换为人们可以读懂的过程叫做解码,与此相反,将字符串转换为字节流的过程叫做编码。我们已经了解对于值在 0-128 之间的字符,可以使用 ASCII 编码,而对于多字节的中文,则需要 GBK 或者 utf-8 编码。
当我们读取文件时,如果打开模式时文本模式,就会自动进行解码的转换,当我们写出字符串时,也会进行编码转换,不需要显式的进行编码,如果要进行网络传输,那么就要手动进行编解码。
Python 对 bytes 类型的数据用带 b 前缀加字符串(如果字节值在 ascii 码值内则显示对应的 ascii 字符,否则显示 \x 表示的16进制字节值)表示。
我们可以通过 b’xxx’ 显式定义一个 bytes 类型对象,例如:
0 1 2 3 4 | bytes0 = b'abc'
print(type(bytes0).__name__)
>>>
bytes
|
同样可以使用字符串对象的 encode 函数得到 bytes 对象:
0 1 2 3 4 5 | # 默认编码方式即为 utf-8
bytes0 = 'abc'.encode('utf-8')
print(bytes0)
>>>
b'abc'
|
也可以通过 bytes 类名把字符串类型转换为 bytes 对象:
0 1 2 3 4 5 6 7 8 | bytes0 = bytes('abc', 'utf-8')
print(type(bytes0).__name__, bytes0)
bytes0 = bytes('abc', 'ascii')
print(type(bytes0).__name__, bytes0)
>>>
bytes b'abc'
bytes b'abc'
|
显然对于 ‘abc’ 英文字母构成的字符串使用 ‘utf-8’ 和 ‘ascii’ 方式所得到的字节流是一样的。而对于扩展的单字符,比如值为 255 的字符 ‘ÿ’,使用 ‘ascii’ 编码则会报错:
0 1 2 3 4 5 | ch = chr(255)
bytes0 = bytes('abc' + ch, 'ascii')
>>>
UnicodeEncodeError: 'ascii' codec can't encode character
'\xff' in position 3: ordinal not in range(128)
|
此时使用扩展编码 latin1 就不会报错了:
0 1 2 3 4 5 | ch = chr(255)
bytes0 = bytes('abc' + ch, 'latin1')
print(type(bytes0).__name__, bytes0)
>>>
bytes b'abc\xff'
|
utf-8 是一种适用于全世界各种语言文字符号进行编码的编码格式。默认在编写 Python 代码时也使用该编码格式。可以看到一个中文字符,在 utf-8 中通常使用 3 个字节进行编码:
0 1 2 3 4 | bytes0 = bytes('中文', 'utf-8') # 等价于 '中文'.encode('utf-8')
print(type(bytes0), bytes0)
>>>
<class 'bytes'> b'\xe4\xb8\xad\xe6\x96\x87'
|
字节流类型具有只读属性,字节流中的每个字节都是 int 型,可以通过下标访问,但不可更改字节值:
0 1 2 3 4 5 6 7 | print(bytes0[0])
print(type(bytes0[0]))
bytes0[0] = 0
>>>
228 # 也即 0xe4
<class 'int'>
TypeError: 'bytes' object does not support item assignment
|
bytes¶
bytes() 类支持以下参数来实例化一个字节流对象:
- 不提供参数,生成一个空对象,值为 b’‘。
- 字符串参数,必须提供编码参数 encoding,此时可以传入 errors=’ignore’ 忽略错误的字节。
- 正整数 n,返回含 n 个 \x00 字节的对象。
- bytearray 对象,将可读写的 bytearray 对象转换为只读的 bytes 对象,参考 bytearray 。
- 整数型可迭代对象,比如 [1, 2, 3],每个元素值必须在 [0-255] 之间。
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | print(bytes()) # 等价于 bytes(0)
# 以下两种方式是等价的,对字符串进行编码得到 bytes 对象
# print('string'.encode('utf-8'))
print(bytes('string', encoding='utf-8'))
print(bytes(3))
ba = bytearray([1, 2])
print(bytes(ba))
print(bytes([1, 2, 3]))
>>>
b''
b'string'
b'\x00\x00\x00'
b'\x01\x02'
b'\x01\x02\x03'
|
bytes 对象具有 decode() 方法,将字节流转换回字符串:
0 1 2 3 4 | bytes0 = bytes('中文', 'utf-8')
print(bytes0.decode('utf-8'))
>>>
中文
|
bytearray¶
bytearray() 创建一个可读写的字节流对象。其他参数和属性与 bytes() 一致。
0 1 2 3 4 5 6 7 8 | ba = bytearray(3)
print(type(ba), ba)
ba[0] = 1 # 修改字节值
print(ba)
>>>
<class 'bytearray'> bytearray(b'\x00\x00\x00')
bytearray(b'\x01\x00\x00')
|
hex¶
bytes.hex() 方法可以以 16 进制字符串方式显示字节流对象:
0 1 2 3 4 5 6 7 | bytes0 = bytes('中文', 'utf-8')
print(bytes0)
print(bytes0.hex())
>>>
b'\xe4\xb8\xad\xe6\x96\x87'
e4b8ade69687
|
当然使用 bytes.fromhex() 方法也可以将一个 16 进制字符串转换为 bytes 对象:
0 1 2 3 4 5 6 7 8 | int0 = 0xefefefef
hexstr = hex(int0) # 转字符串 0xefefefef
print(hexstr)
bytes0 = bytes.fromhex(hexstr[2:]) # 去除 0x 前缀
print(bytes0)
>>>
0xefefefef
b'\xef\xef\xef\xef'
|
bytes 和字符串转换¶
我们已经知道,str 对象编码后变为 bytes 对象,bytes 对象解码后对应 str 对象。
0 1 2 3 4 5 6 7 8 9 10 11 | bytes0 = bytes('中文', 'utf-8')
bytes1 = '中文'.encode('utf-8')
print(bytes0, bytes1)
# 以下两种方式是等价的:对 bytes 对象解码得到字符串
# str1 = str(bytes1, 'utf-8')
str0 = bytes0.decode('utf-8')
print(str0, str1)
>>>
b'\xe4\xb8\xad\xe6\x96\x87' b'\xe4\xb8\xad\xe6\x96\x87'
中文
|
bytes() 中的编码意为对参数进行编码得到 bytes 对象,str() 中的编码参数意为第一个参数是 bytes 类型的 buffer,编码参数指定了它的解码,在 str() 函数内部使用它对 buffer 进行解码得到 str 对象。
类字符串操作¶
字节流对象类似 str,所以定义了很多类似字符串的方法,比如转换其中 ascii 字符的大小写,查找等,例如:
0 1 2 3 4 5 6 | bytes0 = bytes('string', encoding='utf-8')
print(bytes0.find(b't')) # 参数必须也是 bytes 类型
print(bytes0.swapcase())
>>>
1
b'STRING'
|
正则表达式¶
我们常常需要判断一个给定字符串的合法性,比如一串数字是否是电话号码;一串字符是否是合法的 URL,Email 地址;用户输入的密码是否满足复杂度要求等等。
如果我们为每一种格式都定义一个判定函数,首先这种定义可能很复杂,比如电话号码可以为座机时表示为 010-12345678 ,也可以表示为 0510-12345678, 还可以是手机号 13800000000。这样代码的逻辑复杂度就线性增加。其次我们定义的函数功能很难重用,匹配 A 的不能匹配 B。能否有一个万能的函数,只要我们传入特定的参数就能实现我们特定的字符匹配需求呢?答案是肯定的。
在 字符串映射替换 中我们曾经使用过 re.sub 函数来替换多个字符串。这个问题看似简单,直接可以想到使用多次 replace 替换,但是会带来副作用,因为前一次被替换的字符串可能被再次替换掉,比如后面的替换字符串是前一个的子串,或者已经替换的字符串和前后字符正好形成了后来要替换的字符串。
一个可行的解决方案是使用第一个被替换字符串把字符串分割成多个子串,然后用第二个被替换字符串再次分割每一子串,依次类推,直至最后一个被替换字符分割完毕,再依次使用被替换字符进行合并逆操作。这种方案实现起来比较复杂,使用 re.sub 就简单多了。
正则表达式(Regular Expression)描述了一种字符串匹配的模式(Pattern),re 模块名就是正则表达式的缩写,它提供强大的字符匹配替换统计等操作,且适用于 Unicode 字符串。
正则表达式¶
这里简要总结正则表达式的语法,不做深入扩展。
正则表达式中有两个概念,一个字符串包含若干个字符,每个字符在内存中都有对应的二进制编码,以及字符先后关系构成的位置,比如字符串开始位置和结束位置如图所示表示为 ps 和 pe。包含 N 个字符的字符串有 N+1 个位置,位置不占用内存,仅用于匹配定位。
正则表达式使用一些特殊字符(通常以 \ 开头)来表示特定的一类字符集(比如数字0-9)和字符位置(比如字符串开始位置)。它们被称为元字符(metacharacter)。元字符和其他控制字符构成的表达式被称为匹配模式(pattern)。
匹配过程中有一个位置指针,开始总是指向位置 ps,根据匹配模式每匹配一次,就将指针移动到匹配字符的后序位置,并尝试在每一个位置上进行模式匹配,直至尝试过 pe 位置后匹配过程结束。
\ 是转义字符,和其他语言中的转义字符作用类似,‘.’ 在正则表达式中表示匹配除换行符 \n 外的所有字符,如果要匹配 ‘.’ 自身,就要使用 ‘\ .’ 的形式。
由于 Python 字符串本身也采用 \ 作为转义符,所以正则表达式字符串前要加 r ,表示原始输入,以防转义冲突。
匹配字符的元字符¶
元字符 | 字符集 | 非集 | 字符集 |
---|---|---|---|
. | 匹配除换行符 \n 外的所有字符 | \n | 换行符 \n |
\d | 匹配数字 0-9 | \D | 非数字 |
\s | 空白符: [<空格>trnfv] | \S | 非空白符 |
\w | 匹配单词字符 | \W | 非单词字符 |
- d 是 digit numbers,s 是 sapce characters,w 是 word 的缩写。
- 元字符的非集也是元字符。
- 单词字符也即构成英文单词的字符,包括 [A-Za-z0-9_],对于中文来说,还包括 unicode 中的非特殊中文字符(比如中文标点符号)。
[…] 用于直接指定字符集,表示匹配其中任意一个:
- 可以直接给出,比如 [abc]
- 可以给定范围,比如 [a-c]
- 可以在开始位置添加 ^,表示取反,比如 [^a-c],表示 abc 以外的所有字符集。
- 如果要在 [] 中指定特殊字符,比如 ^,需要转义。
匹配位置的元字符¶
元字符 | 字符集 | 非集 | 字符集 |
---|---|---|---|
^ | 匹配字符串起始位置 ps | $ | 匹配字符串末尾位置 pe |
\b | 匹配 \w 和 \W 之间位置,ps,p2,p3,pe | \B | \w 和 \W 之外位置,如图p1,p4,p5,p6 |
\A | 等同 ^ | \Z | 等同 $ |
- ^ 和 $ 在多行模式下支持每行的起始和末尾位置匹配。\A 和 \Z 不支持多行模式。
- ^ 在数学中被称为 hat ,帽子总是戴在头上,匹配字符起始位置,而 $ 很像蛇的尾巴,匹配字符结尾。
- A 和 Z 分别是字母表的首尾字母,分别匹配字符起止位置。
- b 表示 between,是 \w 和 \W 单词字符和非单词字符之间的位置。
findall 和 finditer¶
findall(pattern, string, flags=0)
Return a list of all non-overlapping matches in the string.
findall() 方法返回匹配的所有子串,并把它们作为一个列表返回。匹配从左到右有序返回子串。如果无匹配,返回空列表。
使用 findall() 来验证上述元字符的功能是一个好方法。
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | import re
instr = "Hi\nJohn"
cpattern_list = [r'.', r'\n', r'\d', r'\D', r'\s', r'\S', r'\w', r'\W']
for i in cpattern_list:
print('\'Hi\\nJohn\' -> %02s ->' % i, re.findall(i, instr))
>>>
'Hi\nJohn' -> . -> ['H', 'i', 'J', 'o', 'h', 'n']
'Hi\nJohn' -> \n -> ['\n']
'Hi\nJohn' -> \d -> []
'Hi\nJohn' -> \D -> ['H', 'i', '\n', 'J', 'o', 'h', 'n']
'Hi\nJohn' -> \s -> ['\n']
'Hi\nJohn' -> \S -> ['H', 'i', 'J', 'o', 'h', 'n']
'Hi\nJohn' -> \w -> ['H', 'i', 'J', 'o', 'h', 'n']
'Hi\nJohn' -> \W -> ['\n']
|
字符集元字符返回的均是匹配的字符列表,而位置元字符返回的是位置,所以均是空字符,其中:
- ^ 和 $ 匹配位置 ps 和 pe。
- \b 匹配到位置 ps,p2,p3 和 pe。
- \B 匹配到位置 p1,p4,p5 和 p6。
0 1 2 3 4 5 6 7 8 9 10 11 | instr = "Hi\nJohn"
ppattern_list = [r'^', r'$', r'\A', r'\Z', r'\b', r'\B']
for i in ppattern_list:
print('\'Hi\\nJohn\' -> %02s ->' % i, re.findall(i, instr))
>>>
'Hi\nJohn' -> ^ -> ['']
'Hi\nJohn' -> $ -> ['']
'Hi\nJohn' -> \A -> ['']
'Hi\nJohn' -> \Z -> ['']
'Hi\nJohn' -> \b -> ['', '', '', '']
'Hi\nJohn' -> \B -> ['', '', '', '']
|
为了展示 \b 和 \B 确实匹配了相应位置,我们尝试匹配这个位置的下一个字符,由于 . 不能匹配 \n ,所以要指定选择分支 (.|n)。
0 1 2 3 4 5 6 | instr = "Hi\nJohn"
print(re.findall(r'\b(.|\n)', instr))
print(re.findall(r'\B.', instr))
>>>
['H', '\n', 'J']
['i', 'o', 'h', 'n']
|
finditer(pattern, string, flags=0)
Return an iterator over all non-overlapping matches in the
string. For each match, the iterator returns a match object.
finditer() 方法与 findall() 唯一不同在于返回的不是列表,而是一个返回 match 对象的迭代器,无匹配,则返回内容为空迭代器。
0 1 2 3 4 5 6 7 8 9 10 | instr = "test1 test2"
print(re.findall(r'(?<=test).', instr))
it = re.finditer(r'(?<=test).', instr)
print(type(it))
for i in it:
print(i.group(), end=' ')
>>>
['1', '2']
<class 'callable_iterator'>
1 2
|
重复字符¶
有了元字符,只能够匹配特定的单个字符或者位置,有了重复字符的参与,就可以生成更加复杂的模式,比如我们要匹配 8 个数字,不用写 8个 \d,而直接用 \d{8}。
重复字符又称为数量符,常用的重复字符表如下:
数量符 | 描述 |
---|---|
* | 重复 >=0 次 |
+ | 重复 >=1 次 |
? | 重复 0 或 1 次 |
{m} | 重复 m 次 |
{m,n} | 重复 m 到 n 次 |
(,n) | 重复 0 到 n 次 |
(m,) | 重复 m 到无限次 |
- 重复字符用在匹配字符的元字符之后,也可以用在分组后,参考 或逻辑和分组 。不可单独使用,功能作用在前一个元字符或者分组上。
- 以上重复模式默认为贪婪模式,总是选择尽量多匹配的分支,比如 {m, n} 就尽量选择靠近 n 的分支,可以在其后加 ‘?’ 变成非贪婪模式,比如 *?,{m,n}?。
继续借助 findall() 方法来验证以上重复字符的功能:
0 1 2 3 4 5 6 7 8 9 10 11 12 | instr = "HHH"
pattern_list = [r'H*', r'H+', r'H?', r'H{2}', r'H{2,3}', r'H{2,}', r'H{,3}']
for i in pattern_list:
print('\'HHH\' -> %06s' % i, re.findall(i, instr))
>>>
'HHH' -> H* ['HHH', '']
'HHH' -> H+ ['HHH']
'HHH' -> H? ['H', 'H', 'H', '']
'HHH' -> H{2} ['HH']
'HHH' -> H{2,3} ['HHH']
'HHH' -> H{2,} ['HHH']
'HHH' -> H{,3} ['HHH', '']
|
这里以 ‘H*’ 简述匹配过程:
- 指针 p 指向 ps,尝试尽量多的匹配, 匹配到 ‘HHH’,p 指向 pe。
- 指针指向 pe 匹配到 0 次,也即 ‘’。
所以以上结果中含有 ‘’ 的情况均是因为在 pe 处匹配 0 次出现的。
非贪婪模式¶
0 1 2 3 4 5 6 7 8 9 10 11 12 | instr = "HHH"
pattern_list = [r'H*', r'H+', r'H?', r'H{2}', r'H{2,3}', r'H{2,}', r'H{,3}']
for i in pattern_list:
print('\'HHH\' -> %07s' % (i + r'?'), re.findall(i + r'?', instr))
>>>
'HHH' -> H*? ['', '', '', '']
'HHH' -> H+? ['H', 'H', 'H']
'HHH' -> H?? ['', '', '', '']
'HHH' -> H{2}? ['HH']
'HHH' -> H{2,3}? ['HH']
'HHH' -> H{2,}? ['HH']
'HHH' -> H{,3}? ['', '', '', '']
|
这里以 ‘H*’ 简述非贪婪模式匹配过程:
- 指针 p 指向 ps,尝试尽量少的 0 次匹配, 匹配到 ‘’,p 指向 p1。
- 依次采用尽量少的 0 次匹配,直至指向 pe 再次匹配到 ‘’。
所以 ‘H*’ 最后匹配的 ‘’ 个数是 H 的个数 3 加 1。
或逻辑和分组¶
前文提到电话号码可以有不同的表示形式,比如区号分 3 位和 4 位,手机号总是 13 位。这就用到了或逻辑运算符 |。
- 它用在多个表达式式中间,表示匹配其中任何一个,比如 A | B | C,它总是先尝试匹配左边的表达式,一旦成功匹配则跳过右边的表达式。
- 如果 | 没有包含在 () 中,则它的范围是整个表达式。
0 1 2 3 4 | instr = "color colour"
print(re.findall(r'color|colour', instr))
>>>
['color', 'colour']
|
使用 () 括起来的表达式,被称为分组(Group)。重复字符可以加在分组之后。
0 1 2 3 4 | instr = "color colour"
print(re.findall(r'(colo)?', instr))
>>>
['colo', '', '', 'colo', '', '', '']
|
表达式中的每个分组从左至右被自动从 1 编号,可以在表达式中引用编号。也可以为分组指定名字。
分组操作 | 描述 |
---|---|
(exp) | 匹配exp,并自动编号 |
<id> | 引用编号为<id>的分组匹配到的字符串,例如 (d)abc1 |
(?P<name>exp) | 为分组命名,例如 (?P<id>ab){2},匹配 abab |
(?P=name) | 引用命名为<name>的分组匹配到的字符串,例如 (?P<name>d)abc(?P=name) |
(?:exp) | 匹配exp,但跳过匹配字符,且不为该分组编号 |
(?#comment) | 正则表达式注释,不影响正则表达式的处理 |
0 1 2 3 4 5 6 7 8 | instr = "1abc1 2abc2"
print(re.findall(r'(\d)abc\1', instr))
instr = "1abc1 2abc2"
print(re.findall(r'(?P<name>\d)abc(?P=name)', instr))
>>>
['1', '2']
['1', '2']
|
分组操作还支持以下语法,用于匹配特定位置:
分组位置操作 | 描述 |
---|---|
(?=exp) | 匹配exp字符串前的位置 |
(?<=exp) | 匹配exp字符串后的位置 |
(?!exp) | 不匹配exp字符串前的位置 |
(?<!exp) | 不匹配exp字符串后的位置 |
0 1 2 3 4 5 6 7 8 9 10 | instr = "0abc1"
print(re.findall(r'(?=abc).', instr))
print(re.findall(r'(?<=abc).', instr))
print(re.findall(r'(?!abc).', instr))
print(re.findall(r'(?<!abc).', instr))
>>>
['a']
['1']
['0', 'b', 'c', '1']
['0', 'a', 'b', 'c']
|
位置匹配可以对匹配字符进行条件选择,例如匹配三个连续的数字,且其后不能再跟数字:
0 1 2 3 4 | instr = "111a1222"
print(re.findall(r'\d{3}(?!\d)', instr))
>>>
['111', '222']
|
匹配模式选项¶
re 模块定义了 6 种模式选项:
- re.I (re.IGNORECASE): 匹配时忽略大小写。
- re.M (re.MULTILINE): 多行模式,改变’^’和’$’的行为,可以匹配任意一行的行首和行尾。
- re.S (re.DOTALL): 点任意匹配模式,此时’.’ 匹配任意字符,包含 \n。
- re.L (re.LOCALE): 使预定字符类 w W b B s S 取决于当前区域设定。
- re.U (re.UNICODE): 使预定字符类 w W b B s S d D 取决于 unicode 定义的字符属性。
- re.X (re.VERBOSE): 详细模式。此模式下正则表达式可以写成多行,忽略空白字符,并可以加入注释。
以下两个表达式是等价的:
0 1 2 | instr = "Hi\nJohn"
print(re.findall(r'\b(.|\n)', instr))
print(re.findall(r'\b(.)', instr, re.S))
|
以下两个正则表达式也是等价的:
0 1 2 3 | pattern = re.compile(r'''\d + # the integral part
\. # the decimal point
\d * # some fractional digits''', re.X)
pattern = re.compile(r"\d+\.\d*")
|
compile¶
compile(pattern, flags=0)
Compile a regular expression pattern, returning a pattern object.
compile() 方法将字符串形式的表达式编译成匹配模式对象。 第二个参数 flag 指定匹配模式类型,可以按位或运算符 ‘|’ 生效多种模式类型,比如re.I | re.M。另外,也可以在表达式字符串中指定模式,以下两个表达式是等价的:
0 1 | re.compile(r'abc', re.I | re.M)
re.compile('(?im)abc')
|
将表达式编译成匹配模式对象后,可以重复使用该对象,无需每次都传入表达式。
0 1 2 3 4 5 6 | pattern = re.compile(r'(?i)hi')
print(pattern.findall("Hi\nJohn"))
print(pattern.findall("hi\nJohn"))
>>>
['Hi']
['hi']
|
pattern 对象提供了几个可读属性用于查看表达式的相关信息:
- pattern: 匹配模式对应的表达式字符串。
- flags: 编译时用的匹配模式选项,数字形式。
- groups: 表达式中分组的数量。
- groupindex: 表达式中有别名的分组的别名为键、以组编号为值的字典,不含无别名的分组。
0 1 2 3 4 5 6 7 8 9 10 11 12 13 | def print_pattern_obj(p):
print("p.pattern\t:", p.pattern)
print("p.flags\t\t:", p.flags)
print("p.groups\t:", p.groups)
print("p.groupindex\t:", p.groupindex)
p = re.compile(r'(key\d{1} *)(: *val\d{1})(?P<comma> *,)', re.I)
print_pattern_obj(p)
>>>
p.pattern : (key\d{1} *)(: *val\d{1})(?P<comma> *,)
p.flags : 34
p.groups : 3
p.groupindex : {'comma': 3}
|
match 和 search¶
match(pattern, string, flags=0)
Try to apply the pattern at the start of the string, returning
a match object, or None if no match was found.
match() 方法从字符段头部开始判断是否匹配,一旦匹配成功,返回一个 Match 对象,否则返回 None。Match 对象保存了首次匹配的结果。
match() 方法与字符串方法 startswith() 很像,只是它使用正则表达式来判断字符头部是否满足条件。
0 1 2 3 4 | m = re.match(r'\d{3}', 'a123')
print(m)
>>>
None
|
由于字符串 ‘a123’ 不是以 3 个数字开头的字符串,所以返回 None。再看一个更复杂的例子:
0 1 2 3 4 5 | pattern = re.compile(r'(key\d{1} *)(: *val\d{1})(?P<comma> *,)')
m = pattern.match('key0 : val0, key1 : val1')
print(type(m))
>>>
<class '_sre.SRE_Match'>
|
search(pattern, string, flags=0)
Scan through string looking for a match to the pattern, returning
a match object, or None if no match was found.
search() 搜索整个字符串,查找匹配的字符,找到后返回一个 match 对象,否则返回 None。
0 1 2 3 4 5 | pattern = re.compile(r'(key\d{1} *)(: *val\d{1})(?P<comma> *,)')
m = pattern.search('key: val, key0 : val0, key1 : val1')
print(m)
>>>
<_sre.SRE_Match object; span=(10, 22), match='key0 : val0,'>
|
示例尝试匹配 key 和 val 后有一数字的键值对,如果使用 match() 则会返回 None。
match 对象¶
match 对象保存一次匹配成功的信息,有很多方法会返回该对象,这里对它包含的属性进行介绍。使用上例中的匹配对象,将属性打印如下:
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | def print_match_obj(m):
print("m.re\t\t:", m.re)
print("m.string\t:", m.string)
print("m.pos\t\t:", m.pos)
print("m.endpos\t:", m.endpos)
print("m.lastindex\t:", m.lastindex)
print("m.lastgroup\t:", m.lastgroup)
print("m.group(1,2)\t:", m.group(1, 2))
print("m.groups()\t:", m.groups())
print("m.groupdict()\t:", m.groupdict())
print("m.start(2)\t\t:", m.start(2))
print("m.end(2)\t\t:", m.end(2))
print("m.span(2)\t\t:", m.span(2))
print("m.expand(r'\\1-\\2\\3')\t\t:", m.expand(r'\1-\2\3'))
print("m.expand(r'\\1-\\2g<3>')\t\t:", m.expand(r'\1-\2\g<3>'))
print("m.expand(r'\\1-\\2g<comma>')\t:", m.expand(r'\1-\2\g<comma>'))
print_match_obj(m)
>>>
m.re : re.compile('(key\\d{1} *)(: *val\\d{1})(?P<comma> *,)')
m.string : key0 : val0, key1 : val1
m.pos : 0
m.endpos : 24
m.lastindex : 3
m.lastgroup : comma
m.group(1,2) : ('key0 ', ': val0')
m.groups() : ('key0 ', ': val0', ',')
m.groupdict() : {'comma': ','}
m.start(2) : 5
m.end(2) : 11
m.span(2) : (5, 11)
m.expand(r'\1-\2g<comma>') : key0 -: val0,
|
- re:匹配时使用的模式
- string:要进行匹配操作的字符串
- pos 和 endpos:分别表示开始和结束搜索的位置索引,pos 等于 ps,也即 0 位置;这里的 endpos 为 24,等于 ps,是字符 val1 后的位置,也即 string 的长度。
- lastindex:最后一个匹配的分组编号,我们的模式中有 3 个分组,第 3 个分组用于匹配一个 ‘,’。
- lastgroup:最后一个匹配的分组的别名,如果没有别名,则为 None。
- group():group() 方法使用编号后者别名获取分组,参考 match.group 。
- groups():groups() 方法等价于 group(1,2,…last),返回所有分组匹配的子串,是一个元组。
- groupdict():groupdict() 方法返回分组中有别名的分组子串,是一个字典,例如 {‘comma’: ‘,’}。
- start() 和 end() :分别返回指定分组匹配的字符串的起止字符在 string 上的位置索引值,支持编号和别名。
- span(group):等价于 (start(group), end(group)),返回元组类型。
- expand(template):将匹配到的分组代入 template 中然后返回,参考 match.expand 。
match.group¶
group() 方法获取一个或多个分组匹配的字符串:
- 不提供参数,等同于 group(0),编号 0 代表返回整个匹配的子串。
- 指定多个编号参数时将返回一个元组。
- 可以使用编号也可以使用别名;
- 没有匹配字符串的分组返回 None,匹配了多次的组返回最后一次匹配的子串。
0 1 2 3 4 5 6 7 8 9 10 | pattern = re.compile(r'(key\d{1} *)(: *val\d{1})(?P<comma> *,)')
m = pattern.match('key0 : val0, key1 : val1')
print(m.group())
print(m.group(1, 2))
print(m.group(1, 2, 'comma'))
>>>
key0 : val0,
('key0 ', ': val0')
('key0 ', ': val0', ',')
|
match.expand¶
expand(template) 方法将匹配到的分组代入 template 中然后返回。template 中支持两种方式引用分组:
- 可以使用 id 或 g<id> 引用分组编号,例如 1 和 g<1> 是等价的,编号从 1 开始。
- g<name> 通过别名引用分组,例如 g<comma>。
以下三种方式是等价的。
0 1 2 3 4 5 6 7 8 9 10 | pattern = re.compile(r'(key\d{1} *)(: *val\d{1})(?P<comma> *,)')
m = pattern.match('key0 : val0, key1 : val1')
print("m.expand(r'\\1-\\2\\3')\t\t:", m.expand(r'\1-\2\3'))
print("m.expand(r'\\1-\\2g<3>')\t\t:", m.expand(r'\1-\2\g<3>'))
print("m.expand(r'\\1-\\2g<comma>')\t:", m.expand(r'\1-\2\g<comma>'))
>>>
m.expand(r'\1-\2\3') : key0 -: val0,
m.expand(r'\1-\2g<3>') : key0 -: val0,
m.expand(r'\1-\2g<comma>') : key0 -: val0,
|
split¶
split(pattern, string, maxsplit=0, flags=0)
Split the source string by the occurrences of the pattern,
returning a list containing the resulting substrings.
split() 方法按照匹配的子串将 string 分割后返回列表。maxsplit 用于指定最大分割次数,不指定将全部分割。
0 1 2 3 4 | p = re.compile(r'[, \-\*]')
print(p.split('1,2 3-4*5'))
>>>
['1', '2', '3', '4', '5']
|
sub 和 subn¶
sub(pattern, repl, string, count=0, flags=0)
Return the string obtained by replacing the leftmost
non-overlapping occurrences of the pattern in string by the
replacement repl.
sub() 方法使用 repl 替换 string 中每一个匹配的子串后返回替换后的字符串。repl 接受两种类型的参数:
- 当 repl 是一个字符串时,可以使用 id 或 g<id>,g<name> 引用分组,id 编号从 1 开始。
- 当 repl 是一个函数时,它只接受一个match对象作为参数,并返回一个用于替换的字符串(返回的字符串中不可再引用分组)。
count用于指定最多替换次数,不指定时全部替换。
0 1 2 3 4 5 6 7 8 9 10 11 | p = re.compile(r'(\S+) (\S+)')
instr = '1970-01-01 00:00:00'
print(p.sub(r'\2 \1', instr))
def func(m):
return ' '.join([m.group(2), m.group(1)])
print(p.sub(func, instr))
>>>
00:00:00 1970-01-01
00:00:00 1970-01-01
|
示例用于互换年月日和时分秒位置。
subn(pattern, repl, string, count=0, flags=0)
Return a 2-tuple containing (new_string, number).
subn() 方法参数与 sub() 一致,但是它返回一个元组,元组的格式为 (sub(…), 替换次数)。 例如:
0 1 2 3 4 5 6 7 8 9 10 11 | p = re.compile(r'(\S+) (\S+)')
instr = '1970-01-01 00:00:00'
print(p.subn(r'\2 \1', instr))
def func(m):
return ' '.join([m.group(2), m.group(1)])
print(p.subn(func, instr))
>>>
('00:00:00 1970-01-01', 1)
('00:00:00 1970-01-01', 1)
|
escape¶
escape(pattern)
Escape all the characters in pattern except ASCII letters, numbers and '_'.
escape() 方法对表达式中所有可能被解释为正则运算符的字符进行转义。如果字符串很长且包含很多特殊技字符,而又不想输入一大堆反斜杠,或者字符串来自于用户,且要用作正则表达式的一部分的时候,需要使用这个函数。
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | instr = "* and ?."
map_dict = {'?' : '*', '*' : '?'}
def replace_strs(instr, map_dict, count=0):
import re
re_dict = dict((re.escape(i), j) for i, j in map_dict.items())
print(re_dict)
pattern = re.compile('|'.join(re_dict.keys()))
return pattern.sub(lambda x: re_dict[re.escape(x.group(0))], instr, count)
print(replace_strs(instr, map_dict))
>>>
{'\\?': '*', '\\*': '?'}
? and *.
|
如果我们在编译 pattern 时,直接提供表达式字符串参数,可以在字符串前加 r,如果表达式存储在其他格式的变量中,就需要 escape() 处理。
列表处理¶
序列(sequence)是 Python 中最基本的数据结构之一。序列中的每个元素都分配一个整数数字来唯一确定它的位置, 称为索引,第一个索引总是从0开始,第二个索引是1,依此类推。
Python有多个序列的内置类型,但最常见的是列表和元组,很多函数接受序列作为参数,比如 str.join()。
序列支持的操作包括索引,切片,加,乘,成员检查。此外,Python已经内置确定序列的长度以及确定最大和最小的元素的方法。
列表(list)作为序列类型之一是很常用的 Python 数据类型,它使用方括号包裹元素成员,成员间用逗号分隔,例如 [1, “abc”]。
列表的数据项不需要具有相同的类型。
创建列表变量¶
直接创建列表¶
注意:列表的数据项不需要具有相同的类型。
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | # 每个值可以取相同类型的数据
list0 = [0, 1, 2, 3, 4]
list1 = [1.0, 2.0, 3.0]
list2 = ["12", "ab", "he"]
# 也可以取不同类型的数据,每个元素可以为任意数据类型
list3 = ["123", 1, 3.0, [1, 2], {"key": "val"}]
list4 = [list0, list1]
for i in range(5):
print(eval("list" + str(i)))
>>>
[0, 1, 2, 3, 4]
[1.0, 2.0, 3.0]
['12', 'ab', 'he']
['123', 1, 3.0, [1, 2], {'key': 'val'}]
[[0, 1, 2, 3, 4], [1.0, 2.0, 3.0]]
|
由列表组合生成新列表¶
“+” 运算符实现列表的拼接。
0 1 2 3 4 5 | list0 = [0, 1, 2, 3, 4]
list1 = ['a', "bc", 1] + list0
print(list1)
>>>
['a', 'bc', 1, 0, 1, 2, 3, 4]
|
“*” 运算符实现列表的重复。
0 1 2 3 4 5 6 7 | list0 = ['*'] * 5
list1 = [1, 2] * 5
print(list0)
print(list1)
>>>
['*', '*', '*', '*', '*']
[1, 2, 1, 2, 1, 2, 1, 2, 1, 2]
|
列表推导¶
迭代语句方便对单个列表元素进行处理得到新列表,被称为列表推导(list comprehension),它是一种简化代码的方法。
0 1 2 3 4 5 6 7 8 9 10 11 12 | lista = [1, 2]
listb = ["ab", "cd"]
list0 = [x * 2 for x in lista]
list1 = [x[1] for x in listb]
list2 = [x + ".txt" for x in listb]
for i in range(3):
print(eval("list" + str(i)))
>>>
[2, 4]
['b', 'd']
['ab.txt', 'cd.txt']
|
列表推导等价于如下过程:
0 1 2 3 4 5 6 | listb = ["ab", "cd"]
list2 = [x + ".txt" for x in listb]
# 等价于
list2 = []
for x in listb:
list2.append(x + ".txt")
|
其他类型转换列表¶
list()内建函数实现其他类型向列表的转换。
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | # 将字符串转换为每个字符组成额list
list0 = list("abcdef")
# 将元组转化为list类型
list1 = list((1, 2, 3))
# 将字典转换为 list,默认转换key到list,以下方式是等同的
dic0 = {"key0": "val0", "key1": "val1"}
list2 = list(dic0)
list3 = list(dic0.keys())
# 将字典的值转换为list
list4 = list(dic0.values())
for i in range(5):
print(eval("list" + str(i)))
>>>
['a', 'b', 'c', 'd', 'e', 'f']
[1, 2, 3]
['key0', 'key1']
['key0', 'key1']
['val0', 'val1']
|
zip 合并¶
zip() 方法结合类型转换,可以巧妙的把两个或者多个链表中的元素一一对应成元组类型,生成新的元组列表。
0 1 2 3 4 | list0 = list(zip(['one', 'two', 'three'], [1, 2, 3]))
print(list0)
>>>
[('one', 1), ('two', 2), ('three', 3)]
|
更复杂的操作参考 zip_longest 。
复制列表¶
与字符串类似,列表作为序列类型,支持切片复制。
0 1 2 3 4 5 6 7 8 9 10 11 | list0 = ["ab", "cd"]
list1 = list0[:]
print(list1)
list1[0] = "AB" # 改变 list1 不会影响 list0
print(list0)
print(list1)
>>>
['ab', 'cd']
['ab', 'cd']
['AB', 'cd']
|
列表的 L.copy() 方法与切片复制都是浅拷贝,只复制父对象一级,子对象不复制,还是引用。 如果要完全复制,需要借助 copy 模块进行深拷贝。
0 1 2 3 4 5 6 7 8 9 10 11 12 13 | list0 = [1, 2, [1, 2]]
list1 = list0.copy() # 浅拷贝,与切片复制等同
import copy # 深拷贝,完全复制
list2 = copy.deepcopy(list0)
list0[2][0] = "a" # 不会改变深拷贝 list2
for i in range(3):
print("list%d:\t%s" % (i, eval("list" + str(i))))
>>>
list0: [1, 2, ['a', 2]]
list1: [1, 2, ['a', 2]]
list2: [1, 2, [1, 2]]
|
深浅拷贝¶
int, float 和 complex 称为数值型类型,str 是字符类型,它们是 Python 中的基本数据类型,而 list 等其他数据类型为复合型数据类型,它们的元素可以嵌套,所以对这些复合类型分为深浅复制。
对于基本类型来说,一个值只有一个存储地址,所有拥有相同值的变量(名字)都指向它。
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | import copy
def with_sameid(inlist):
idlist = map(lambda x:id(x), inlist)
return len(set(idlist)) == 1
int0 = 1
int1 = int0
intlist = [int0, int1, copy.copy(int0), copy.deepcopy(int0)]
print(with_sameid(intlist))
float0 = 3.14
float1 = float0
floatlist = [float0, float1, copy.copy(float0), copy.deepcopy(float0)]
print(with_sameid(floatlist))
comp0 = 1j
comp1 = comp0
complist = [comp0, comp1, copy.copy(comp0), copy.deepcopy(comp0)]
print(with_sameid(complist))
str0 = 'abc'
str1 = str0
strlist = [str0, str1, copy.copy(str0), copy.deepcopy(str0)]
print(with_sameid(strlist))
>>>
True
True
True
True
|
对于list、tuple、dict 等复合类型类型,直接赋值给一个变量相当于引用,指向同一内存地址。浅拷贝,只拷贝一层,不拷贝父对象内部的子对象,列表的 L.copy() 方法,copy.copy() 和切片复制都是浅拷贝。深拷贝,递归拷贝所有子对象,使用 copy.deepcopy() 方法。
0 1 2 3 4 5 6 7 8 9 10 | list0 = [1, 2, [3.0, 3.1]]
list1 = list0
listlist = [list0, list1, list.copy(list0), copy.deepcopy(list0)]
for i in listlist:
print(id(i), id(i[2]))
>>>
2101860280712 2101860020040
2101860280712 2101860020040 # 赋值相当于引用不变
2101860281096 2101860020040 # 二级元素地址,浅拷贝不变
2101859999240 2101860019656 # 深拷贝,所有地址改变
|
访问列表中的值¶
下标直接访问¶
通过下标直接取列表的单个元素,返回元素原来对应的类型。
0 1 2 3 4 5 6 7 8 | list0 = [1, 2, 3, [4, 5]]
print(list0[0]) # 1
print(list0[-1]) # 4
print(type(list0[-1]))
>>>
1
[4, 5]
<class 'list'>
|
切片取子列表¶
切片操作,取部分连续元素,返回列表类型,即便只取到一个元素。
0 1 2 3 4 5 6 7 8 9 10 11 | list0 = [1, 2, 3, 4]
print(list0[0:1]) # [1]
print(type(list0[0:1]))
print(list0[0:-1]) # 去掉尾巴元素的列表
print(list0[1:]) # 去掉头元素的列表
>>>
[1]
<class 'list'>
[1, 2, 3]
[2, 3, 4]
|
更详细切片操作参考 切片取子字符串。
过滤特定的元素¶
通过filter()函数提取特定元素生成新列表。
0 1 2 3 4 5 6 | # 提取长度大于3的字符串元素
listc = ["abc", 123, "defg", 456]
list0 = list(filter(lambda s:isinstance(s, str) and len(s) > 3, listc))
print(list0)
>>>
['defg']
|
枚举访问列表¶
enumerate()方法可以将列表转化为枚举对象,这样就很容易获得序列的编号。
0 1 2 3 4 5 6 7 8 9 10 | enumerate_obj = enumerate(['item0', 'item1', 'item2'])
for i, value in enumerate_obj:
print(i, value)
print(type(enumerate_obj))
>>>
0 item0
1 item1
2 item2
<class 'enumerate'>
|
实际上,enumerate()方法可以将任意可迭代类型转化为枚举对象。
索引访问和循环¶
字符串可以使用索引直接访问,列表也可以,所有的序列类型均可以使用索引访问,索引访问的本质是对象实现了 __getitem__() 方法。
这里实现一个可读写的字符串类型来分析通过下标进行读写的本质。
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | class RWStr():
## 一个可读写的字符串类
def __init__(self, instr=''):
self.instr = instr
self.len = len(instr)
def __setitem__(self, index, instr): # 实现写操作,支持字符和字符串插入
if index > self.len:
raise IndexError("list index out of range")
tmpstr = self.instr[:index] + instr + self.instr[index:]
self.instr = tmpstr
self.len = len(tmpstr)
print(self.len)
def __getitem__(self, index): # 读操作,支持索引和切片
if isinstance(index, int):
return self.instr[index]
elif isinstance(index, slice):
return self.instr[index]
else:
raise TypeError('Index must be int, not {}'.format(type(index).__name__))
rwstr = RWStr("hello")
print(rwstr[0])
print(rwstr[1:5])
rwstr[5] = " world!"
for i in rwstr:
print(i, end=' ')
>>>
h
ello
12
h e l l o w o r l d !
|
通过示例,也可以看出,只要实现了 __getitem__() 方法,就可以通过循环语句进行迭代读取处理。__setitem__() 方法对应写操作。
列表统计¶
统计元素个数¶
0 1 2 3 4 | list0 = [1, 1, 2, [2, 3]]
print(len(list0))
>>>
4
|
统计元素出现次数¶
0 1 2 3 4 | list0 = [1, 1, 2, [2, 3]] # 注意 [2, 3] 是一个列表元素
print(list0.count(2))
>>>
1
|
统计列表不同元素数¶
通过集合 set() 方法求交集。
注意:元素不能为复杂数据类型,比如列表,字典等。
0 1 2 3 4 5 | list0 = [1, 1, 2, "abc"]
set0 = set(list0)
print(list(set0))
>>>
[1, 2, 'abc']
|
统计最大最小值¶
max() 和 min() 方法可以得到列表中的最大和最小值。
注意:列表中元素必须均为数值,否则需要先转换为数值。
0 1 2 3 4 5 6 | list0 = [1, 1, 2, 3.0]
print(max(list0))
print(min(list0))
>>>
3.0
1
|
列表排序和反向¶
列表排序¶
L.sort(key=None, reverse=False) -> None -- stable sort *IN PLACE*
sort()函数直接对列表执行排序,无返回。注意:列表中元素类型必须相同。
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | # 正序排列,直接对list操作
list0 = ['c1', 'b2', 'a0', 'd3']
list0.sort(reverse=False)
print(list0)
>>>
['a0', 'b2', 'c1', 'd3']
# 逆序排列
list0.sort(reverse=True)
print(list0)
>>>
['d3', 'c1', 'b2', 'a0']
# 可以指定key函数进行更复杂的排序
list0.sort(key=lambda x:x[1])
print(list0)
>>>
['a0', 'c1', 'b2', 'd3']
|
列表反向¶
L.reverse() -- reverse *IN PLACE*
reverse()方法反向列表,元素颠倒。
0 1 2 3 4 5 | list0 = [1, 2, 3, 4, ['a', 'b']]
list0.reverse()
print(list0)
>>>
['a', 'b'], 4, 3, 2, 1]
|
字符串可以借助列表反向函数,实现反向。
0 1 2 3 4 5 | list0 = list("0123456")
list0.reverse()
print(''.join(list0))
>>>
6543210
|
列表元素插入和扩展¶
索引位置插入¶
L.insert(index, object) ->None -- insert object before index
在指定索引位置插入对象,其余元素后移,直接操作无返回。
0 1 2 3 4 5 6 7 8 9 10 | list0 = [0, 1, 2, 3]
list0.insert(2, 88)
print(list0)
# 如果索引超出list长度,则直接插入结尾
list0.insert(len(list0) + 1, [100, 101])
print(list0)
>>>
[0, 1, 88, 2, 3]
[0, 1, 88, 2, 3, [100, 101]]
|
尾部追加¶
尽管通过 list.insert(len(list), object) 实现尾部追加,为了高效处理,Python 提供了专门的尾部追加函数 append()
L.append(object) -> None -- append object to end
append() 方法在列表尾部追加,直接操作无返回,参数作为整体插入为1个元素。
0 1 2 3 4 5 | list0 = [0, 1, 2, 3]
list0.append([99,100])
print(list0)
>>>
[0, 1, 2, 3, [99, 100]]
|
L.extend(iterable) -> None -- extend list by appending elements from the iterable
列表的extend()方法可以接受一个迭代对象,并把所有对象逐个追加到列表尾部。
0 1 2 3 4 5 6 7 8 9 | list0 = [0, 1, 2, 3]
list0.extend(["a", "b"])
print(list0)
list0.extend("123")
print(list0)
>>>
[0, 1, 2, 3, 'a', 'b']
[0, 1, 2, 3, 'a', 'b', '1', '2', '3']
|
列表元素的删除¶
根据索引删除¶
del()函数根据索引删除元素,支持切片操作,直接作用在列表上,无返回。
0 1 2 3 4 5 6 7 8 9 | list0 = [1, 2, 2, 3, 4]
del(list0[0]) # 直接作用在list上
print(list0)
del(list0[0:3]) # 支持切片移除
print(list0)
>>>
[2, 2, 3, 4]
[4]
|
根据索引删除并返回元素¶
L.pop([index]) -> item -- remove and return item at index (default last).
Raises IndexError if list is empty or index is out of range.
list的pop()方法删除指定索引元素并返回它,与append()配合可以实现队列或者堆栈。 如果索引超出范围,抛出 IndexError 异常。
0 1 2 3 4 5 6 7 8 | list0 = [[1, 2], 2, 3, 4]
print(list0.pop()) # 默认参数index=-1,也即移除最后一个元素
print(list0.pop(0))
print(list0)
>>>
4
[1, 2]
[2, 3]
|
根据元素值删除元素¶
L.remove(value) -> None -- remove first occurrence of value.
Raises ValueError if the value is not present.
remove()函数移除第一个匹配value值的元素,无返回。如果元素不存在,抛出 ValueError 异常。
0 1 2 3 4 5 | list0 = [1, 2, 2, 3, 4]
list0.remove(2)
print(list0)
>>>
[1, 2, 3, 4]
|
元素索引和存在判定¶
获取元素索引¶
L.index(value, [start, [stop]]) -> integer -- return first index of value.
Raises ValueError if the value is not present.
index()方法可以在指定范围获取第一个匹配值的索引。如果值不存在则抛出 ValueError 异常。
0 1 2 3 4 5 6 | list0 = [1, 2, 2, 4]
print(list0.index(2)) # 返回第一个匹配值2的元素索引
print(list0.index(2, 2, 3))# 在list[2:3 + 1]中查找第一个匹配2的元素索引
>>>
1
2
|
判断元素是否存在¶
判断某元素是否存在使用 in 运算符;not in 运算符判断不存在,语句结果是布尔量。
0 1 2 3 4 5 6 7 8 9 10 | list0 = [1, 2, 3, 5]
if 5 in list0:
print(list0.index(5))
print(5 in list0)
print(5 not in list0)
>>>
3
True
False
|
也可以通过 index() 方法捕获异常的方式判定元素是否存在。
列表比较¶
直接使用比较运算符¶
比较运算费,又称为关系运算符,远算结果为布尔值。包括以下几种:
运算符 描述 实例 == 等于 - 比较对象是否相等 (a == b) 返回 False != 不等于 - 比较两个对象是否不相等 (a != b) 返回 true > 大于 - 返回a是否大于b (a > b) 返回 true < 小于 - 返回a是否小于b (a < b) 返回 true >= 大于等于 - 返回a是否大于等于b (a >= b) 返回 False <= 小于等于 - 返回a是否小于等于b (a <= b) 返回 true
- == 和 != 运算符比较对象可以为任何不同的类型。
- 含有 > 和 < 的运算符,比较对象类型必须相同。
0 1 2 3 4 5 6 7 8 9 10 11 | list0, list1 = [123, 'xyz'], [123, 'abc']
print(list0 > list1)
print(list0 == list0)
print(list0 == "123xyz")
print(list0 != 123)
print(list0 >= 123) # '>='不支持不同类型对象的比较
>>>
True
True
False
True
|
使用用cmp()函数¶
注意:cmp()函数返回值为整型,已在3.0版本移除,它等价于 (a > b) - (a < b)。
0 1 2 3 4 5 6 7 | print cmp(list0, list1)
print cmp(list1, list0)
print cmp(list1, list1)
>>>
1
-1
0
|
使用 operator模块¶
operator模块提供的比较函数是运算符的另一种表达形式,它们之间是等价的。 比如 operator.lt() 函数与 a < b 是等价的。
0 1 2 3 4 5 | operator.lt(a, b) # '<'
operator.le(a, b) # '<='
operator.eq(a, b) # '=='
operator.ne(a, b) # '!='
operator.ge(a, b) # '>'
operator.gt(a, b) # '>='
|
注意:还有一组带有下划线的函数,比如 operator.__lt__() 它们是为了向前兼容才保留的。
0 1 2 3 4 5 | import operator
print(operator.eq(list0, list0))
print(operator.lt(list1, 0)) # '<'不支持不同类型对象的比较
>>>
True
|
元组¶
元组(Tuple)与列表类似,它使用() 表示,元组兼容列表中大部分操作,比如索引,切片等。唯一不同在于元素只读,不可更改。
元组的运算¶
如果元组只有一个元素,那么一定要加上逗号,由于() 本身是一个运算符,将直接返回括号内的内容。
0 1 2 3 4 5 | print(type((1)))
print(type((1,)))
>>>
<class 'int'>
<class 'tuple'>
|
我们可以定义空元组,也可以追加元组到当前元组,所以只读是其中元素不可被删除或者更改,而不是元组自身不可更改。
0 1 2 3 4 5 6 7 8 9 | tuple0 = ()
tuple0 += (1, 2, 3)
print(tuple0[0])
print(tuple0[0:2])
print(len(tuple0))
>>>
1
(1, 2)
3
|
可以将元组对象转换为其他类型:
0 1 2 3 4 5 6 | # 类型转换
print(str(tuple0))
print(list(tuple0))
>>>
(1, 2, 3)
[1, 2, 3]
|
同样元组支持重复运算和拼接运算:
0 1 2 3 4 5 6 7 8 9 | tuple0 *= 2 # 重复
print(tuple0)
tuple1 = (4, 5)
tuple0 += tuple1
print(tuple0) # 拼接
>>>
(1, 2, 3, 1, 2, 3)
(1, 2, 3, 1, 2, 3, 4, 5)
|
元组是可迭代对象,支持 for in 操作,也可以作为函数的可迭代对象实参:
0 1 2 3 4 | for i in tuple0:
print(i)
# 作为可迭代对象实参
print(sum(tuple0))
|
不可更新元组元素的值:
0 1 2 3 4 5 6 7 8 9 10 | # 不支持的操作
tuple0[4] = 0
tuple0[0] = 5
tuple0 += (4) # 注意与 tuple0 += (4,) 的区别
# 指向新对象
tuple0 = tuple1
print(tuple0)
>>>
(4, 5)
|
Python 内置模块 collections 扩展了普通元组,参考 namedtuple 。
字典¶
字典是 Python 中内建的一种具有弹性储存能力的数据结构,可存储任意类型对象,与序列使用整数索引不同,它使用键(key)进行索引。
通常任何不变类型的对象均可作为索引,比如数字,字符串和元组,列表可以被修改,不可作为键。由于键作为索引使用,所以它必须是唯一的。
字典的每个键都有对应的值 (value),键值对用冒号 “:” 分割,每个键值对之间用逗号 “,” 分割,整个字典包括在花括号 {} 中。
0 | dict0 = {key0 : val0, key1 : value1}
|
创建和访问字典¶
直接创建字典¶
0 1 2 3 4 5 6 7 8 9 10 11 12 13 | dict_empty = {} # 可以创建空字典
key = 'abc'
dict0 = {1: None,'abc': 1, (1, 2): "tuple key", key: "replaced"}
print (dict0)
print (dict0[1])
print (dict0[key])
print (dict0[(1,2)])
>>>
{1: None, 'abc': 'replaced', (1, 2): 'tuple key'}
None
replaced
tuple key
|
可以看到如果,出现重复的键,比如这里的 ‘abc’ ,则最后的一个键值会替换前面的,键对应的值可以是任意数据类型, 不同的键可以对应相同的值。
访问字典¶
字典以键为索引访问对应的值,如果键不存在,抛出 KeyError :
0 1 2 3 4 5 6 | print(dict0[2])
>>>
File "C:/Users/Red/.spyder/dictest.py", line 16, in <module>
print (dict0[2])
KeyError: 2
|
D.get(k[,d]) -> D[k] if k in D, else d. d defaults to None.
字典方法 D.get() 方法可以在键不存在时返回指定的值,如果不指定则默认返回 None 。
0 1 2 3 4 5 | print(dict0[2])
print(dict0.get(2, "hello"))
>>>
None
hello
|
间接创建字典¶
包含键值对的( key-value)序列,使用 dict() 进行类型转换。
0 1 2 3 4 5 | list0 = [('key0', 1), ('key2', None), (3, ['1', '2'])] # 可以是元组类型
dict0 = dict(list0)
print(dict0)
>>>
{'key0': 1, 'key2': None, 3: ['1', '2']}
|
通过参数对序列,也可以创建字典,但是关键字必须是字符串:
0 1 2 3 4 | dict0 = dict(key0=1, key1="abc")
print(dict0)
>>>
{'key0': 1, 'key1': 'abc'}
|
字典推导¶
类似列表推导,字典推导(dict comprehension),可以简化代码。
0 1 2 3 4 5 6 7 | dict0 = {x: x**2 for x in [1, 2, 3]}
dict1 = {x: "/home/" + x + '.jpg' for x in ("pic0", "pic1")}
print(dict0)
print(dict1)
>>>
{1: 1, 2: 4, 3: 9}
{'pic0': '/home/pic0.jpg', 'pic1': '/home/pic1.jpg'}
|
zip 合并¶
zip()函数名副其实,它的作用很像拉链,将两个列表合并成一个 zip 对象,dict() 可以把它转化为字典。
0 1 2 3 4 | dict0 = dict(zip(['one', 'two', 'three'], [1, 2, 3]))
print(dict0)
>>>
{'one': 1, 'two': 2, 'three': 3}
|
由列表生成定值字典¶
D.fromkeys(iterable, value=None, /) method of builtins.type instance
Returns a new dict with keys from iterable and values equal to value.
D.fromkeys() 方法支持从迭代对象取键,并可指定值的字典。通常使用列表或者元组作为参数。
0 1 2 3 4 5 6 7 8 9 10 11 12 13 | seq = ['key0', 'key1'] # 也可为元组,字符串等可迭代对象
dict0 = dict.fromkeys(seq) # 字典所有值均为 None
dict1 = dict.fromkeys(seq, 10) # 字典所有值均为 10
dict2 = dict.fromkeys(seq, [1, 2]) # 字典所有值均为 [1, 2]
dict3 = dict.fromkeys('123', 10) # 一次从字符串中取一个字符作为键
for i in range(4):
print("dict%d:\t%s" % (i, eval("dict" + str(i))))
>>>
dict0: {'key0': None, 'key1': None}
dict1: {'key0': 10, 'key1': 10}
dict2: {'key0': [1, 2], 'key1': [1, 2]}
dict3: {'1': 10, '2': 10, '3': 10}
|
如果序列中出现重复成员,在生成的字典中它作为键只会出现一次。
字典操作¶
键值添加和更新¶
0 1 2 3 4 5 6 7 8 9 | dict0 = {}
dict0['key0'] = "val0" # 添加键值对
print(dict0)
dict0['key0'] = 123 # 更新键的值
print(dict0)
>>>
{'key0': 'val0'}
{'key0': 123}
|
键值不存在时更新¶
D.setdefault(k[,d]) -> D.get(k,d), also set D[k]=d if k not in D
与直接对键赋值不同,D.setdefault() 方法可以在键存在时不做操作,而在键不存在时更新键值对。
0 1 2 3 4 5 6 7 8 9 10 11 12 | dict0 = {'key0': 'val0'}
dict0['key1'] = 'val1' # 直接赋值
print(dict0)
dict0.setdefault('key1', "newval") # key1 存在,不做操作
print(dict0)
dict0.setdefault('key2', "newval") # key2 不存在,插入
print(dict0)
>>>
{'key0': 'val0', 'key1': 'val1'}
{'key0': 'val0', 'key1': 'val1'}
{'key0': 'val0', 'key1': 'val1', 'key2': 'newval'}
|
更新键值对¶
D.update() 方法把一个迭代对象(通常为字典)中的键值对更新到当前字典中,如果键存在则覆盖。
0 1 2 3 4 5 6 7 8 9 10 | dict0 = {'key0': 'val0'}
dict1 = {'key0': 0, "key1" : [1, 2]}
dict0.update(dict1)
dict0.update([("name", "value")]) # 其他含键值对的可迭代对象
# 即便释放 dict1 不影响 dict0 值,是完全复制
del(dict1)
print(dict0)
>>>
{'key0': 0, 'key1': [1, 2], 'name': 'value'}
|
删除键值和清空字典¶
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | dict0 = {"key0" : "val0", "key1" : "val1"}
del dict0['key0'] # 删除键值
print(dict0)
dict0.clear() # 清空字典
print(dict0)
del(dict0) # 删除dict0变量,释放资源
print(dict0) # NameError 找不到 dict0 变量
>>>
{'key1': 'val1'}
{}
......
NameError: name 'dict0' is not defined
|
del() 函数删除dict0变量,不可再被使用。D.clear() 方法只清空字典,字典可以被访问。
按键访问并删除¶
D.pop(k[,d]) -> v, remove specified key and return the corresponding value.
If key is not found, d is returned if given, otherwise KeyError is raised
D.pop() 方法删除字典给定键 key 所对应的成员,并返回它对应的值,如果键不存在返回参数指定的默认值。
0 1 2 3 4 5 6 7 8 | dict0 = {'key0': 0, 'key1': [1, 2]}
print(dict0.pop('key0', "default"))
print(dict0)
print(dict0.pop('key5', "default"))
>>>
0
{'key1': [1, 2]}
default
|
随机遍历访问¶
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | dict0 = {"key0" : "val0", "key1" : "val1"}
for i in dict0: # 默认迭代字典键序列
print(i, end=' ')
print("\n")
for i in dict0.values(): # 迭代字典值序列
print(i, end=' ')
print("\n")
for i in dict0.items(): # 迭代字典键值对
print(i)
>>>
key0 key1
val0 val1
('key0', 'val0')
('key1', 'val1')
|
使用字典内建的 D.values() 方法和 D.items()方法可以方便循环处理每一个成员。
遍历删除¶
D.popitem() 内建方法随机返回并删除字典中的一对键和值,为元组类型。字典不可为空,否则会报错。
0 1 2 3 4 5 6 7 8 9 10 | dict0 = {'key0': 0, 'key1': [1, 2], 'name': 'value'}
for i in range(len(dict0)):
item = dict0.popitem()
print(item)
print(dict0) # 空字典
>>>
('name', 'value')
('key1', [1, 2])
('key0', 0)
{}
|
示例中可以看出字典是无序的,并没有按照成员赋值的顺序,而是按照键的 ASCII 码排序。
字典复制¶
类似列表,字典也支持深浅拷贝,字典自带的 D.copy() 方法是浅拷贝,借助 copy 模块实现深拷贝。
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | dict0 = {'key0': 'val0', 'list' : [1, 2, 3]}
dict1 = dict0 # 引用对象
dict2 = dict1.copy() # 浅拷贝:只复制父对象一级,子对象不复制,还是引用
import copy
dict3 = copy.deepcopy(dict0) #深拷贝,完全复制
dict1['key0'] = 'newval'
del dict1['key0'] # 删除会影响引用 dict0
dict0['list'][0] = 'a' # 改变子对象值,影响浅拷贝 dict2,不影响深拷贝 dict3
for i in range(4):
print("dict%d:\t%s" % (i, eval("dict" + str(i))))
>>>
dict0: {'list': ['a', 2, 3]}
dict1: {'list': ['a', 2, 3]}
dict2: {'key0': 'val0', 'list': ['a', 2, 3]}
dict3: {'key0': 'val0', 'list': [1, 2, 3]}
|
字典和字符串转换¶
通过 str() 类型转化方法可以把字典转换位字符串:
0 1 2 3 4 5 | dict0 = {'key0': 'val0', 'list' : [1, 2, 3]}
str0 = str(dict0)
print(str0)
>>>
{'key0': 'val0', 'list': [1, 2, 3]}
|
字符串转字典通常有两种方式,eval() 方法和 json 模块提供的 json.loads() 方法。
0 1 2 3 4 5 6 7 8 9 | dict0 = eval(str0) # eval 方法
print(dict0)
import json # 使用 json 模块
dict1 = json.loads("\"" + str0 + "\"") # 或 repr(str0)
print(dict1)
>>>
{'key0': 'val0', 'list': [1, 2, 3]}
{'key0': 'val0', 'list': [1, 2, 3]}
|
注意,采用 json 模块时字符串前后必须添加引号,简单的方式为 repr(str0)
。
字典相等比较¶
Python2.x 版本使用 cmp() 方法比较字典,Python3 取消了该方法,直接使用比较运算符。
0 1 2 3 4 5 6 7 8 | dict0 = {'key0': 0, 'key1': [1, 2]}
dict1 = {'key0': 0, 'key2': [1, 2]}
print(dict0 == dict1)
print(dict0 != dict1)
>>>
False
True
|
字典不可以比较大小,只可以比较是否相等,相等即指字典所有的键值对完全相同。
字典排序¶
由于字典本身是无需的,所以需要转换为有序的对象,例如列表。字典对象的 items() 方法可以转换为可迭代对象,迭代对象的元素为元组,形式为 (‘key’, value),然后使用 sorted 函数通过参数 key 指定排序所用的关键字。
0 1 2 3 4 5 6 7 8 9 10 11 12 | # 按 key 排序
In [58]: scores = {'John': 15, 'Bill': 18, 'Kent': 12}
...: new_scores = sorted(scores.items(), key=lambda x:x[1], reverse=False)
...: print(new_scores)
...:
[('Kent', 12), ('John', 15), ('Bill', 18)]
# 按 value 排序
In [59]: scores = {'John': 15, 'Bill': 18, 'Kent': 12}
...: new_scores = sorted(scores.items(), key=lambda x:x[0], reverse=False)
...: print(new_scores)
...:
[('Bill', 18), ('John', 15), ('Kent', 12)]
|
统计和存在判定¶
统计字典元素个数¶
0 1 2 3 4 | dict0 = {'key0': 'val0', 'key1' : "val1"}
print(len(dict0))
>>>
2
|
键存在判定¶
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | dict0 = {'key0': 'val0', 'key1' : "val1"}
#print(dict0.has_key('a')) # False
#print(dict0.has_key('key0')) # True
# Python3.x 不再支持 has_key() 方法,被 __contains__(key) 替代
print(dict0.__contains__('a'))
print(dict0.__contains__('key0'))
# 或者使用 key in 判断,not in 执行反操作
print('a' in dict0)
print('a' not in dict0)
>>>
False
True
False
True
|
通常使用 in
或者 not in
成员运算符。
集合¶
可散列对象¶
如果一个对象实现了 __hash__ 方法和 __eq__ 方法,那么这个对象就是可散列的(Hashable),参考 __hash__ 。
Python 提供的基本数据类型都是可散列的,比如数值型,str 和 bytes 。当元组包含的所有元素都是可散列类型时,元组也是可散列的。 对于列表和字典,由于它们的值是可以动态改变的,所以无法提供一个唯一的散列值来代表这个对象,所以是不可散列的。
可散列对象要求对象在整个生命周期,调用 __hash__() 方法都必须返回一致的散列值,散列值可以通过内置函数 hash(obj) 获取。
可散列特性让一个对象可以作为字典的 key(字典实际使用一个键的散列值作为 key),或者一个集合(set)的成员。
注意
如果一个类型没有定义 __eq__(),那么它也不应该定义__hash__(),集合操作对成员进行重复比较时,首先查看 __hash_() 值是否相等,然后再分别使用两个对象的 __eq__() 比较,只有全部为真时,才认为是重复元素,如果没有实现对应方法则报错。
以下示例可已看出由于 a 和 b 的散列值都是 1,最终键 a 的值被键 b 的值覆盖。
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | class HashableCls():
def __eq__(self, b):
return True
def __hash__(self):
return 1
a = HashableCls()
b = HashableCls()
print(hash(a), hash(b))
dict0 = {a : 1, b : 2}
print(dict0[a], dict0[hash(a)])
>>>
1 1
2 2
|
为了方便,我们那可以定义以下函数来判断一个对象是否为可散列的:
0 1 2 3 4 5 | def ishashable(obj):
try:
if obj.__hash__ and obj.__eq__:
return True
except:
return False
|
集合特性¶
集合(set)是一个无序且不重复的可散列的元素集合。具有以下特性:
- 集合成员可以做字典中的键。
- 和列表,元组,字典类型一样,集合支持用 in 和 not in 操作符检查成员。
- 由 len() 内建函数得到集合的基数(大小)。
- 支持 for 循环迭代集合的成员。但因集合本身是无序的,不可以进行索引或执行切片操作,也没有键(key)可用来获取集合中元素的值。
set 非常类似 dict,只是没有 value,相当于 dict 的 key 集合。
集合操作¶
创建集合¶
set() -> new empty set object
set(iterable) -> new set object
注意
在创建空集合的时候只能使用 s = set(),s = {} 将创建空字典。
0 1 2 3 4 5 6 7 | s0 = set() # 创建空集合
s1 = {1, 2, 3, 4} # 创建非空集合
print(type(s0).__name__)
print(s0, s1)
>>>
set
set() {1, 2, 3, 4}
|
可以看到,打印集合输出的形式和打印字典一致的。空集合用 set() 表示,以防和空字典冲突。set() 方法可以接受一个可迭代对象,会自动去除重复元素:
0 1 2 3 4 5 6 7 8 9 10 | set0 = set('abbc')
set1 = set([1, 2, 2, 3])
set2 = set((1, 2, 3))
set3 = set({"key0":'val0','key1':'val1'})
print(set0, set1)
print(set2, set3)
>>>
{'b', 'a', 'c'} {1, 2, 3}
{1, 2, 3} {'key0', 'key1'}
|
添加和移除元素¶
添加指定元素¶
add(obj) 方法用于添加新元素,一次只能添加一个,如果该元素已存在,则忽略。
0 1 2 3 4 5 6 7 | set0 = set()
set0.add('abc')
set0.add('abc') # 忽略,不会报错
print(set0)
>>>
{'abc'}
|
删除指定元素¶
remove(obj) 删除一个指定元素,如果不存在报错。
0 1 2 3 4 | set0 = {'a', 'b'}
print(set0.remove(1))
>>>
KeyError: 1
|
discard(obj) 删除一个指定元素,如果不存在,则忽略。
0 1 2 3 4 5 6 7 | set0 = {'a'}
set0.discard('a')
set0.discard('a') # 忽略,不会报错
print(set0)
>>>
set()
|
随机删除¶
pop() 方法随机删除一个元素并返回,更新原集合。
0 1 2 3 4 5 6 | s0 = {'a', 'b'}
print(set0.pop())
print(set0)
>>>
b
{'a'}
|
取差集¶
差集表示 set0 中存在,set1 中不存在的集合。
difference(...)
Return the difference of two or more sets as a new set.
set0.difference(set1) 可以对两个或多个集合取差集,不影响原集合,返回一个新集合。
0 1 2 3 4 5 6 | set0 = {1, 2, 3}
set1 = {3, 4, 5}
diff = set0.difference(set1, {1})
print(diff)
>>>
{2}
|
difference_update() 与 difference() 唯一不同在于取差集后,更新原集合,无返回。
0 1 2 3 4 5 6 | set0 = {1, 2, 3}
set1 = {3, 4, 5}
set0.difference_update(set1, {1})
print(set0)
>>>
{2}
|
合并不同项¶
合并不同项又称为对称差集,指两个集合中不重复的元素集合,会移除两个集合中都存在的元素。
set0.symmetric_difference(set1) 合并 set0 和 set1 中的不同元素,返回新集合。
0 1 2 3 4 5 | set0 = {'a', 'b'}
set1 = set0.symmetric_difference({'b', 'c'})
print(set1)
>>>
{'a', 'c'}
|
symmetric_difference_update() 更新原集合,无返回。
0 1 2 3 4 5 | set0 = {'a', 'b'}
set0.symmetric_difference_update({'b', 'c'})
print(set0)
>>>
{'a', 'c'}
|
取并集¶
set0.union(set1,set2…) 取两个或多个集合的并集,返回新集合,不更新原集合。
0 1 2 3 4 5 | set0 = {'a', 'b'}
set1 = set0.union({1}, {2})
print(set1)
>>>
{2, 1, 'b', 'a'}
|
set0.update(set1,set2…) 取并集,更新原集合,无返回。
0 1 2 3 4 5 | set0 = {'a', 'b'}
set0.update({1}, {2})
print(set0)
>>>
{2, 1, 'b', 'a'}
|
取交集¶
intersection(...)
Return the intersection of two sets as a new set.
intersection() 方法取两个或多个集合的交集,返回一个新的集合,不影响原集合。
0 1 2 3 4 5 6 | set0 = {1, 2, 3}
set1 = {1, 4, 5}
sect = set0.intersection(set1, {1})
print(sect)
>>>
{1}
|
intersection_update() 取交集,更新原集合,无返回。
0 1 2 3 4 5 6 | set0 = {1, 2, 3}
set1 = {1, 4, 5}
set0.intersection_update(set1, {1})
print(set0)
>>>
{1}
|
交集判定¶
set0.isdisjoint(set1) 判定两个集合是否有交集,有返回 Flase,无则返回 True。
0 1 2 3 4 5 6 7 | set0 = {1, 2, 3}
set1 = {1, 4, 5}
print(set0.isdisjoint(set1))
print(set0.isdisjoint({0}))
>>>
False
True
|
子集超集判定¶
子集判定¶
set0.issubset(set1),判定 set0 是否为 set1 的子集,是返回 True,否则返回 False。
任何集合都是自身的子集,空集是任何集合的子集。
0 1 2 3 4 5 6 | set0 = {1}
print(set0.issubset({1, 2}))
print(set().issubset({})) # 空集是任何集合的子集
>>>
True
True
|
set0.issuperset(set1),判定 set0 是否为 set1 的超集,是返回 True,否则返回 False。
0 1 2 3 4 5 6 | set0 = {1}
print(set0.issuperset({1, 2}))
print(set().issuperset({}))
>>>
False
True
|
frozenset¶
frozenset 是指冻结的集合,它的值是不可变的,一旦创建便不能更改,没有 add,remove 方法,支持集合的其他不更新自身的交并集操作。
普通集合是可变的,不是可散列的,冻结集合是可散列的,它可以作为字典的 key,也可以作为其它集合的元素。
0 1 2 3 4 5 6 | fset = frozenset('abc')
print(type(fset).__name__)
print(fset)
>>>
frozenset
frozenset({'b', 'a', 'c'})
|
集合操作符¶
集合类型提供了一系列函数方法用于集合运算,它同时借用了一些操作符,比如位操作符来简化集合操作:
操作符 示例 说明 len(s) len({1, 2}) =>2 集合元素数 x in s 1 in {1,2} =>True 成员判定 x not in s 1 not in {1,2} =>False 成员判定 set <= other {1,2} <= {1,2} => True 子集判定,等价于 {1,2}.issubset({1,2}) set < other {1,2} < {1,2} => False 真子集判定 set >= other {1,2} >= {1,2} => True 超集判定,等价于 {1,2}.issuperset({1,2}) set > other {1,2} > {1,2} => False 真超集判定 set | other |… {1,2} | {2,3} => {1,2,3} 并集,等价于 {1,2}.union({2,3}) set |= other|… set |= {2,3} => {1,2,3} 并集,等价于 {1,2}.update({2,3}) set & other &… {1,2} & {2,3} => {2} 交集,等价于 {1,2}.intersection({2,3}) set &= other &… set &= {2,3} 交集,等价于 {1,2}.intersection_update({2,3}) set - other -… {1,2} - {2,3} => {1} 差集,等价于 {1,2}.difference({2,3}) set -= other|… set -= {2,3} 差集,等价于 {1,2}.difference_update({2,3}) set ^ other {1,2} ^ {2,3} {1,3} 合并不同项,等价于 {1,2}.symmetric_difference({2,3}) set ^= other set ^= {2,3} 等价于 {1,2}.symmetric_difference_update({2,3})
含有等于号 = 的表达式表示将结果更新到集合中,无返回。
文件操作¶
文件操作是编程语言提供的最基本的功能,如果数据不能以文件形式存储在存储介质上,比如硬盘,那么信息就不能长期存储,也无法共享和传输。
Python 提供了强大的文件操作方法。操作系统不允许用户程序直接访问磁盘,而是要通过文件系统调用,Python 通过 C 函数库访问系统调用,进行文件读写。对文件读写操作需要通过文件描述符进行。
文件打开和关闭¶
文件打开模式¶
打开文件获取文件描述符,打开文件时需要指定对文件的读写模式,和写追加模式。常见的文件模式如下:
模式 描述 ‘r’ 以只读方式打开文件(默认),不存在报错 FileNotFoundError ‘w’ 以写方式打开文件,如果文件存在,首先清空原内容,如果不存在先创建文件 ‘x’ 以写方式打开文件,如果文件存在,则报错 FileExistsError ‘a’ 以写方式打开文件,并追加内容到文件结尾,如果文件不能存在则先创建文件 ‘+’ 可同时读写
‘+’ 模式可以和其他模式组合,以同时支持读写,常用组合和特性如下:
打开模式 r r+ w w+ a a+ 可读
可写
创建
覆盖
指针在开始
指针在结尾
数据流在读写时又分为两种转换模式:
模式 描述 ‘b’ 二进制模式(不转换) ‘t’ 文本模式(转换,默认)
二进制模式将内存中的数据信息直接写入到文件中,不做任何转换。如内存中的数据 0x01 以二进制写入就是直接写入 0x01 到文件中。
而文本模式是为解决操作系统兼容性引入的。
在文本中,有一些字符不是是用来显示的,而是用来控制的,称为控制符,比如告诉编辑器一行结束就是字符 ‘\n’, 对应 ASCII 码 0x0d,但是不同的操作系统使用的控制符并不一致,这导致在A平台下编辑的文件到B平台下可能就无法正常显示。
类 Unix 操作系统采用单个 ‘\n’ 表示行结束符。Windows 使用两个字符 “\r\n” 表示。而 Mac 系统使用 ‘\r’ 表示。
所以当指明使用文本模式读取时,如果是在 Winodows 系统,那么当读取到 “\r\n” 或者 ‘\r’ 时就会转换为 “\r\n” ,写的时候会将 ‘n’ 转换为 “\r\n”。 如果是进行文本编辑,那么应该使用默认的文本打开模式,如果是多媒体等格式的文件就要使用二进制模式,以保证读取和写入的数据是一致的。 参见 Windows 上的 fopen 手册。
在类 Unix 系统上这两种模式是等价的,也即均为 ‘b’ 模式,遵循 POSIX 标准的 C 库函数不会对数据进行任何转换,而把这些处理交给用户空间的编辑器。
open(file, mode='r', buffering=-1, encoding=None, errors=None,
newline=None, closefd=True, opener=None)
Open file and return a stream. Raise IOError upon failure.
在 Python 中,和 c 接口中的意义有所不同,只有 ‘b’ 模式,且并不作为转换模式使用,而是指定参数的类型:指定了 ‘b’ 模式,那么写入参数必须是 bytes 类型,同样读取返回的也是 bytes 类型:
0 1 2 3 4 5 6 7 8 | # 'b' 模式中,write 参数必须是 bytes 类型的
with open('test.txt', 'wb') as fw:
fw.write('一abc')
# 正确写入方式为
# fw.write(bytes('一abc', encoding='utf-8'))
>>>
TypeError: a bytes-like object is required, not 'str'
|
同样读取时如果指定了 ‘b’ 模式,则返回 bytes 类型:
0 1 2 3 4 | with open('wb.txt', 'rb') as fr:
print(fr.read())
>>>
b'\xe4\xb8\x80abc'
|
通常对文件操作时应该明确指定编码格式,例如:
0 1 2 3 4 5 6 7 8 9 10 11 | def test_file_encode():
with open('wb.txt', 'w', encoding='utf-8') as fw:
fw.write('一abc') # 非 'b' 模式可以直接写入字符串
# 如果此处打开模式为 'rb' 则 fr.read() 返回 bytes 类型
with open('wb.txt', 'r', encoding='utf-8') as fr:
print(fr.read()) # 自动使用 encoding 参数进行解码
test_file_encode()
>>>
一abc
|
可以通过 hexdump 查看写出的文件内容:
0 1 2 | # hexdump 查看写出文件,可以发现前三个字节为 '一' 的 unicode 码值的 utf-8 编码
$ hexdump -C wb.txt
00000000 e4 b8 80 61 62 63 |...abc|
|
指定文件编码¶
open() 函数实现文件的打开,它返回一个文件描述符,在 Python 里它是一个文件对象。
0 1 2 3 4 5 6 7 8 | f = open("test.txt", 'r')
print(f)
>>>
# Windows 运行结果
<_io.TextIOWrapper name='test.txt' mode='r' encoding='cp936'>
# Linux 运行结果
<_io.TextIOWrapper name='test.txt' mode='r' encoding='UTF-8'>
|
这里之所以打印文件对象,是要查看编码,发现在不同的平台上它的值是不同的,该参数可以在打开文件时指定,如果不指定,则使用 locale.getpreferredencoding() 获取。 它用于文本模式时如何解码读取的文件数据,或者如何编码写入到文件。关于编码格式参考 常见的编码方式 。
0 1 2 3 | print(locale.getpreferredencoding(False))
>>>
cp936
|
cp936 编码,也即 GBK 编码,所以在 Windows 上默认读写文件使用该编码方式,在 Linux 上则是 UTF-8。那么相同的文件,由于解码不同,就会读取出错,写入也一样。
为了能够正确读取文件,应该指明要读写的文件的编码方式,通常我们使用 UTF-8 编码来保存中文文档。Python3.7 版本后,locale.getpreferredencoding() 总是返回 UTF-8,以和 Python 默认编码保持一致,不再依赖于系统环境。
0 1 2 3 4 | f = open("test.txt", 'r', encoding='UTF-8')
print(f)
>>>
<_io.TextIOWrapper name='test.txt' mode='r' encoding='UTF-8'>
|
当完成读写操作后,应关闭文件描述符,以将缓存数据写入磁盘,并释放系统资源,这非常简单:
0 | f.close()
|
文件描述符的属性¶
在 Python 中,文件描述符就是一个文件对象,它具有如下属性:
属性 描述 file.closed 返回布尔值,True 已被关闭。 file.mode 返回被打开文件的访问模式。 file.name 返回文件的名称。
0 1 2 3 4 5 6 7 | f = open("test.txt", 'r', encoding="UTF-8")
print ("name: %s, closed: %s, mode:'%s'" % (f.name, f.closed, f.mode))
f.close()
print ("name: %s, closed: %s, mode:'%s'" % (f.name, f.closed, f.mode))
>>>
name: test.txt, closed: False, mode:'r'
name: test.txt, closed: True, mode:'r'
|
文件对象内建方法¶
按功能划分文件对象内建方法:
- 关闭
- file.close() 关闭文件。
- 读取
- file.read([size=-1]) 从文件读取指定的字节数,如未指定或为负则读取所有,返回读取数据,无数据时返回空字符串 ‘’。
- file.readline([size=-1]) 读取一行含换行符,如指定正整数,则最多返回 size 个字符。
- file.readlines([hint=-1]) 读取所有行以列表返回,如指定整数,至少读取 hint 个字符,确保最后读取的行是完整的。
- 写入和截断
- file.write(str) 将字符串写入文件,返回写入的字符长度。
- file.writelines(sequence) 向文件写入字符串序列(必须是字符串序列),比如列表,元组,如需要换行需自行加入换行符。
- file.flush() 刷新缓冲区数据到文件。
- file.truncate([size]) 截断文件,截断文件指针偏移处之后数据,如指定正整数,则把文件截断为 size 字节,不影响指针偏移。
- 文件指针偏移
- file.seek(offset[, whence]) 移动文件指针到指定偏移位置。如指定参数 whence,则移动偏移相对于 0 文件开始, 1 当前位置, 2 文件末尾。
- file.tell() 返回文件指针偏移位置。
- 文件描述符
- file.fileno() 返回整型文件描述符,用于 os 模块方法。
- 判定
- file.isatty() 如果文件连接到一个终端设备返回 True。
注意
任何对文件的读取和写入动作,都会自动改变文件的指针偏移位置。
0 1 2 3 4 5 6 | with open("sample.py", 'r') as f:
data = f.read()
data = f.read()
print(repr(data))
>>>
''
|
文件或目录常用操作¶
文件或目录创建删除¶
创建删除文件¶
普通文件使用 open() 写模式即可创建。对应的删除方法为 os.remove()。
0 1 2 3 4 5 | fname = "test.txt"
with open(fname, 'w'):
pass
os.remove(fname) # 删除当前目录下 test.txt 文件
os.unlink(fname)
|
os.unlink() 行为与 os.remove() 一致,函数无返回,如果文件不存在,报错 FileNotFoundError。
创建删除目录¶
在当前文件夹下创建单级目录使用 os.mkdir(dirname),创建多级目录使用 os.makedirs(dirpath)。
0 1 | print(os.mkdir("folder"))
print(os.makedirs("parent/folder"))
|
函数无返回,如果文件夹存在则抛出 FileExistsError 错误。
0 1 2 3 4 5 6 | import os,shutil
os.rmdir("parent/folder") # 一级目录删除
os.removedirs("parent/folder")# 递归删除
shutil.rmtree("parent") # 强制删除 parent 文件夹
shutil.rmtree("parent/folder")# 强制删除 folder 文件夹
|
与创建函数类似,以上函数均无返回值,如果删除目录不存在,不会报错。
- os.rmdir() 只删除最后一级目录 folder,并且 folder 必须为空,否则报错目录非空的 OSError。
- os.removedirs() 与 os.rmdir() 类似,文件夹必须为空,首先删除 folder,然后再删除 parent。
- shutil.rmtree() 无论目录是否非空,强制删除整个文件夹,应慎用。
临时文件和目录¶
手动创建和删除临时文件。
0 1 2 3 4 5 6 7 8 9 10 11 | filename = '/tmp/tmp_file_%s.txt' % os.getpid()
try:
f = open(filename, 'w')
except:
pass
else:
print(f.name)
f.close()
os.remove(f.name)
>>>
/tmp/tmp_file_10973.txt
|
手动创建临时文件有几个缺点,首先需要获得一个唯一的临时文件名称,其次其他程序也可以访问该文件,这为信息安全留下隐患。
使用 tempfile 模块创建临时文件是最好的选择。其他程序无法找到或打开该文件,因为它并没有引用文件系统表,同时用这个函数创建的临时文件,关闭后会自动删除。
0 1 2 3 4 5 6 7 8 9 10 11 12 13 | import tempfile
try:
tempf = tempfile.TemporaryFile()
except:
pass
else:
print(tempf)
print(tempf.name)
tempf.close() # 关闭时自动删除
>>>
<_io.BufferedRandom name=4>
4
|
使用 tempfile 模块创建临时文件无需指定文件名。需要注意的是默认使用 w+b 权限创建文件,文本模式请使用参数 ‘w+t’ 生成临时文件。
如果需要和其他程序共享临时文件,需要生成具名的临时文件:
0 1 2 3 4 5 6 7 8 9 10 11 | try:
tempf = tempfile.NamedTemporaryFile('w+t')
except:
pass
else:
print(tempf)
print(tempf.name)
tempf.close() # 关闭时自动删除
>>>
<tempfile._TemporaryFileWrapper object at 0xb71d7a2c>
/tmp/tmpas7rymlm
|
这里使用权限 ‘w+t’ 生成的临时文件将使用文本模式读写,它在关闭后也会被自动删除。
0 1 2 3 4 5 6 | tempdir = tempfile.mkdtemp()
print(tempdir)
os.removedirs(tempdir) # 手动删除临时文件夹
>>>
/tmp/tmpffpyahtn
|
注意
tempfile.mkdtemp() 生成的临时文件夹,必须手动删除。
文件和目录重命名¶
0 1 2 | try:
os.rename('fname0', 'fname1')
......
|
文件和文件夹均使用 os.rename() 方法更名,成功无返回,如果文件不存在,则报错 FileNotFoundError。
注意
当目标文件或文件夹存在时,os.rename() 不会报错,而是直接覆盖。
获取文件或文件夹大小¶
os.path.getsize(fname) 返回文件大小,如果文件不存在报错 FileNotFoundError:
0 1 2 3 4 5 6 | fname = "test.txt"
# os.stat(fname).st_size ,实际上 getsize() 方法与此等价
fsize = os.path.getsize(fname)
print(fsize)
>>>
30
|
注意,如果参数指定文件夹,并不会报错,而是返回文件夹节点占用的物理存储空间大小,而不是整个文件夹内容的大小。 获取文件夹大小需要 os.walk() 遍历函数实现。
0 1 2 3 4 5 6 7 8 9 10 11 12 13 | def get_folder_size(path):
total_size = 0
for item in os.walk(path):
for file in item[2]:
try:
total_size += os.path.getsize(os.path.join(item[0], file))
except Exception as e:
print("error with file: " + os.path.join(item[0], file))
return total_size
print(get_folder_size('.'))
>>>
51171
|
复制或移动文件和目录¶
文件复制¶
shutil.copyfile(src, dst, *, follow_symlinks=True)
Copy data from src to dst.
shutile 模块的 copyfile() 方法将 src 指定的文件复制为 dst 文件,注意:
- src 和 dst 必须都是文件路径,不可以是文件夹。
- 如果 src 不存在,报错 FileNotFoundError。
- 如果 dst 文件已存在,那么会覆盖。
- follow_symlinks 为 True ,则 src 如果为软连接,则复制后 dst 也是软连接。
- 复制成功,返回新文件的路径。
0 1 2 | try:
shutil.copyfile("oldfile", "newfile")
.....
|
目录复制¶
shutile 模块的 copytree() 方法用于复制目录,symlinks 参数指明是复制软连接还是复制文件。
copytree(srcdir, dstdir, symlinks=False, ignore=None, copy_function=<function copy2>,
ignore_dangling_symlinks=False)
Recursively copy a directory tree.
0 1 2 | try:
shutil.copytree("srcdir", "dstdir")
......
|
注意 srcdir 和 newdir 都只能是目录,且 newdir 必须不存在,否则报 FileExistsError。成功返回目标目录路径。
移动文件和目录¶
move(src, dst, copy_function=<function copy2>)
Recursively move a file or directory to another location. This is
similar to the Unix "mv" command. Return the file or directory's
destination.
移动文件和目录均使用 shutil.move()函数,类似于 Unix 下的 mv 命令。成功返回目标文件或目录。
0 1 2 | try:
shutil.move("src", "dst")
......
|
需要注意以下几点:
- src 文件或者目录必须存在,否则报错 FileNotFoundError
- dst 如果存在并且是目录,则把 src 复制到 dst/ 下
- dst 如果不存在,则直接把 src 复制为 dst。
文件和目录属性判定¶
有时候,我们需要判断特定路径的属性,比如是文件还是目录,如果路径不存在,这类函数不会触发异常,而是返回 False,常见判定操作如下:
判定方法 描述 os.path.isabs() 判断是否为绝对路径,以 “/” 开始的路径均为 True os.path.isfile() 判断路径是否为文件,支持软连接 os.path.isdir() 判断路径是否为目录,支持软连接 os.path.islink(path) 判断路径是否为链接 os.path.ismount(path) 判断路径是否为挂载点 os.path.exists(path) 路径存在则返回 True, 否则返回 False
以下函数,如果参数不合法,则会报相应的异常错误:
判定方法 描述 os.path.samefile(src, dst) 两个路径名是否指向同个文件后者文件夹 os.path.sameopenfile(fp1, fp2) 判断 fp1 和 fp2 文件描述符是否指向同一文件 os.path.samestat(stat1, stat2) 判断文件状态 stat1 和 stat2 是否指向同一个文件
文件名和路径操作¶
文件名和路径分割¶
下列方法不关心目录或者文件是否真实存在,它们只进行字符层面的处理,不会触发异常错误。
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | abspath = os.path.abspath("tmp.txt") # 返回绝对路径
basename = os.path.basename(abspath) # 返回文件名
dirname = os.path.dirname(abspath) # 返回文件路径
print(abspath)
print(basename)
print(dirname)
print(os.path.split(abspath)) # 分割路径和文件名,返回元组类型
print(os.path.splitext(abspath)) # 分割扩展名,返回元组类型
>>>
/home/red/sdc/lbooks/ml/tmp.txt
tmp.txt
/home/red/sdc/lbooks/ml
('/home/red/sdc/lbooks/ml/tmp', '.txt')
('/home/red/sdc/lbooks/ml', 'tmp.txt')
|
还有 os.path.splitdrive(path) 方法一般用于 Windows 平台,返回驱动器名和路径组成的元组。
最长路径¶
0 1 2 3 4 5 6 7 8 9 10 | path_list0 = ["/home/red/", "/home/john", "/home/lily"]
path_list1 = ["/home/VIPred/", "/home/VIPjohn", "/home/VIPlily"]
commonpath0 = os.path.commonprefix(path_list0)
commonpath1 = os.path.commonprefix(path_list1)
print(commonpath0)
print(commonpath1)
>>>
/home/
/home/VIP
|
os.path.commonprefix() 方法返回所有 path 共有的最长的路径,示例可以看出,它只是在字符层面进行匹配,它返回的不一定是路径,而只是最长匹配的字符串。
路径合成¶
0 1 2 3 4 5 6 | path = os.path.join("123", "456", "tmp") #把目录和文件名合成一个路径
print(path)
>>>
123/456/tmp # Linux 平台
123\456\tmp # Windows 平台
|
os.path.join() 方法只进行字符层面的拼接,不同的平台拼接字符可能不一致,这与 '/'.join()
不同。
路径转换和规范化¶
绝对路径和相对路径¶
os.path.relpath(path, start=os.curdir) Return a relative version of a path
os.path.relpath() 方法支持设定参考路径,默认为 Python 当前工作路径。
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | path = "/tmp/text.txt"
realpath = os.path.realpath(path) # 返回 path 的真实路径,将忽略任何软连接
relpath = os.path.relpath(path)
print(realpath)
print(relpath)
>>>
# Linux 平台
C:\tmp\text.txt
..\..\..\..\tmp\text.txt
# Windows 平台
/tmp/text.txt
../../../../../tmp/text.txt
|
路径相关的方法是以来于系统平台的,不同平台有不同的路径表示方法,注意区别。
路径规范化¶
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | path = "/tmp/../text.txt"
normcase = os.path.normcase(path)
normpath = os.path.normpath(path)
print(normcase)
print(normpath)
>>>
# Linux 平台
\tmp\..\text.txt
\text.txt
# Winodws 平台
/tmp/../text.txt
/text.txt
|
- os.path.normcase(path) 通常用于路径大小写不敏感的文件系统,路径转化为小写,在Unix 和 Mac 不对路径做任何改变,Winodws 上会把 “/” 转化为 “”。
- os.path.normpath(path) 标准化路径,消除路径冗余,比如将 A//B, A/B/, A/./B and A/foo/../B 转化为 A/B。
文件相关的时间¶
时间戳分类¶
每一个文件或者目录都有记录相应操作的时间戳,通常将它们分为以下几类:
- 最后的访问时间(access time)标记为 atime。通常访问文件,比如读取时会更新访问时间。但是由于文件的时间戳是要记录在磁盘上的,如果每次访问都要写磁盘,将严重影响I/O效率,所以通常使用 relative atime 策略,也即当访问时,发现文件的 ctime 或者 mtime 比 atime 新时才更新。
- 最后的修改时间(modify time)标记为 mtime。当文件内容发生改变时更新该时间。
- 最后的更改时间(change time)标记为 ctime。Linux上,当文件内容,或者属性(所有者,操作权限,所在目录)改变时更新该时间。
- 文件的创建时间(create time)被标记为 Birth,依赖文件系统格式 Linux 上目前不支持获取该时间,在 Winodes 上它被标记为 ctime。
在 Linux 上获取时间戳的命令是 stat :
0 1 2 3 4 5 6 7 8 | # stat tmp.txt
File: ‘test.txt’
Size: 0 Blocks: 0 IO Block: 4096 regular empty file
Device: 821h/2081d Inode: 334232 Links: 1
Access: (0777/-rwxrwxrwx) Uid: ( 0/ root) Gid: ( 0/ root)
Access: 2017-08-02 12:08:03.475780700 +0800
Modify: 2017-08-02 12:08:03.475780700 +0800
Change: 2017-08-02 12:08:03.475780700 +0800
Birth: -
|
获取时间戳¶
os.path 模块提供了三种方法来获取时间戳,其中 getctime() 在 Linux 上表示为 change time,而在 Windows 上获取的则是文件的创建时间。 这三个函数均是返回从 epoch (1970.1.1 00:00:00) 到当前时刻的秒数,浮点表示,参数也可以为路径,如果文件或目录不存在,报错 FileNotFoundError。
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | fname = "test.txt"
try:
atime = os.path.getatime(fname)
mtime = os.path.getmtime(fname)
ctime = os.path.getctime(fname)
except:
pass
else:
print(atime)
print(mtime)
print(ctime)
>>>
1501646883.4757807
1501646892.1091917
1501646892.1091917
|
文件的时间戳常被用于数据同步,编译等操作。更详细的描述请参考 Unix 文件系统中的时间戳。
更新访问和修改时间¶
utime(path, times=None, *, ns=None, dir_fd=None, follow_symlinks=True)
Set the access and modified time of path.
os.utime() 方法可以更新文件或者目录的访问和修改时间,如果不提供 times 参数,则使用当前时间,否则 times 参数应该是形为 (atime, mtime) 的元组。它们是相对于 epoch 以来的秒数。
注意路径必须存在,否则报错 FileNotFoundError。
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | fname = "test.txt"
try:
#os.utime(fname),更新为当前时间
os.utime(fname, (1530714880.9, 1530714592))
except:
pass
else:
print(os.path.getatime(fname))
print(os.path.getmtime(fname))
>>>
1530714880.9
1530714592.0
1543464892.8930445
|
返回指定目录下的所有文件和目录名:os.listdir()
文件属性和权限¶
stat(path, *, dir_fd=None, follow_symlinks=True)
Perform a stat system call on the given path.
os.stat() 方法获取文件各类属性,主要包括文件权限,文件所属,链接状态,大小和时间戳。 os.path.getsize() 方法底层就是调用该方法获取文件大小的。
0 1 2 3 4 5 | print(os.stat("test.txt"))
>>>
os.stat_result(st_mode=33206, st_ino=1125899907973557, st_dev=2028413472,
st_nlink=1, st_uid=0, st_gid=0, st_size=30, st_atime=1543471075,
st_mtime=1543471075, st_ctime=1543236918)
|
修改文件权限,文件所属操作请参考 os.chmod() 和 os.chown() 手册。
文件路径遍历¶
遍历当前目录¶
os.listdir(path) 返回 path 指定的文件夹包含的文件或文件夹的名字的列表。参数必须是文件夹,否则报错 NotADirectoryError。
0 1 2 3 4 5 6 7 8 | try:
paths = os.listdir('.')
except:
pass
else:
print(paths)
>>>
['text.py', 'fileopt.py', ......]
|
遍历所有子目录¶
os.walk(top, topdown=True, onerror=None, followlinks=False)
Directory tree generator.
os.walk() 中的 top 参数指定遍历文件夹, topdown 指定遍历顺序,默认从上层到子文件夹。 可以通过 os.walk() 统计文件夹大小,对每个文件进行特定处理等。
os.walk() 返回目录树迭代对象,对象成员是一个三元组,形式为 (root, dirs, files),分别对应目录,目录中文件夹和目录中文件。
0 1 2 3 4 5 6 7 8 9 10 11 12 | import os
from os.path import join, getsize
for root, dirs, files in os.walk('.'):
print(root, "consumes", end="")
print(sum([getsize(join(root, name)) for name in files]), end="")
print("bytes in", len(files), "non-directory files")
if '.git' in dirs:
dirs.remove('.git') # don't visit .git directories
>>>
. consumes 44843bytes in 31 non-directory files
./folder consumes 0bytes in 0 non-directory files
......
|
以上示例,统计每个文件夹中文件所占大小,并忽略 .git 文件夹。 os.walk() 非常适合对文件进行信息统计和批处理操作,以下是一个用于把文件夹下所有文件扩展名改为小写的函数实现:
0 1 2 3 4 5 6 7 8 9 10 11 12 13 | import os
def lower_file_extname(path):
for root, dirs, files in os.walk(path):
for i in files:
oldpath = os.path.join(root, i)
splits = os.path.splitext(oldpath)
if len(splits) != 2:
continue
newpath = splits[0] + splits[1].lower()
try:
os.rename(oldpath, newpath)
except Exception as e:
print(e)
|
文件操作模块¶
os 模块提供一些可移植的功能函数,它们的底层依赖于操作系统。 其中 os.path 模块封装了路径相关的方法。
tempfile 模块主要提供临时文件的创建和使用。shutil 模块提供更高层的文件和目录操作方法。
其他常用的文件操作模块如下:
模块 | 描述 |
---|---|
base64 | 提供二进制字符串和文本字符串间的编码/解码操作 |
binascii | 提供二进制和 ASCII 编码的二进制字符串间的编码/解码操作 |
bz2a | 访问 BZ2 格式的压缩文件 |
csv | 访问 csv 文件(逗号分隔文件) |
filecmpb | 用于比较目录和文件 |
fileinput | 提供多个文本文件的行迭代器 |
getopt/optparsea | 提供了命令行参数的解析/处理 |
glob/fnmatch | 提供 Unix 样式的通配符匹配的功能 |
gzip/zlib | 读写 GNU zip( gzip) 文件(压缩需要 zlib 模块) |
shutil | 提供高级文件访问功能 |
c/StringIO | 对字符串对象提供类文件接口 |
tarfilea | 读写 TAR 归档文件, 支持压缩文件 |
tempfile | 创建一个临时文件(名) |
uu | 格式的编码和解码 |
zipfilec | 用于读取 ZIP 归档文件的工具 |
时间处理¶
Python 提供了三个与时间操作相关的模块。
- time 模块侧重于底层时间操作,给其他高层模块提供接口,侧重点在时分秒处理。
- datetime 模块主要是用来表示日期的,提供获取并操作时间中的年月日时分秒信息的能力。
- calendar 模块主要是用来表示年月日,星期几等日历信息。
为了深入理解时间模块提供的各种方法函数,首先了解几个与时间相关的概念。
时间相关的概念¶
- epoch
- 本意为纪元,也即一个时刻的开始。它在计算机计时系统中就是指时间基准点,在Unix系统中,这个基准点就是 UTC 时间的1970年1月1日0时0分0秒整那个时间点。只有有了参考点,计时系统才能工作,否则经过了10s,现在的时刻还是无法确定,有了基准点,只要计数了经过的秒数,就能计算出现在的年月日时分秒。
- GMT, UTC
GMT 是格林尼治时间(Greenwich Mean Time)的缩写,还叫做世界协调时 UTC(Coordinated Universal Time)。历史上,先有GMT,后有UTC。 UTC 是现在使用的时间标准,它根据原子钟来计算时间,而GMT根据地球的自转和公转来计算时间,GMT是老的时间计量标准。 可以认为UTC是真正的基准时间,GMT相对UTC的偏差为0。
计算机中有一个称为实时时钟(RTC,Real-Time Clock)的硬件模块,它会实时记录UTC时间,该模块有单独的电池供电,即使关机也不影响。
- 时区,TZ/TZONE
尽管有了时间基准,和精确计数时间的原子钟,已经可以精确地表示一个时间,但是很多情况下,还是要根据地区实际情况对时间进行一个调整,由于世界各国家与地区经度不同,地方时也有所不同,因此会划分为不同的时区。全球一共划分24个时区,其中 GMT 时间被定义为 0 时区,其他时区根据东西方向偏移,计算出本地时区的时间。
北京时间CST(China Standard Time)为东八区,也即 GMT+8,当前表示为 UTC+8。
- 夏令时,DST
- DST 全称是Daylight Saving Time,为了充分利用日光,减少用电,人为地对时间做出一个调整,这取决于不同国家和地区的政策法规。比如说,夏天天亮的很早,如果还是像冬天一样规定从早上9:00到5:00上班,就不能充分利用日照,这有两种做法,就是每个企业都规定一套自己的夏季上下班时间,这比较麻烦,那么统一由授时中心在进入夏天的某个时刻,通常为凌晨,人为将时间提前一小时,这样原来的早上9:00上班的规定没变,但是实际上已经是8:00上班了。等夏季过去,夏令时结束,再在某个时间点把时间推后一个小时。
- NTP
- NTP 是网络时间协议(Network Time Protocol)的缩写,尽管每台计算机都有自己的 RTC 系统,但它们的精确度是不一致的,会存在走时误差,为了协调时间的同步,操作系统在启用了 NTP 之后,会自动在计算机联网之后与时间服务器同步时间,并更新RTC中的时间。
时间的表示方式¶
在 Python 中,通常用这三种方式来表示时间:
- 时间戳(timestamp)通常来说,时间戳表示的是从 epoch 开始按秒计算的偏移量,在 Python 中是一个浮点数。
- 格式化的时间字符串 ,比如 Thu Nov 29 20:50:50 CST 2018,用于人的解读。
- 元组格式,对应 C 语言中的 struct_time 数据结构,一共有 9 个元素。
元组中的 9 个元素名称和意义表示如下:
序号 属性 意义 0 tm_year (年)2008 1 tm_mon (月)1 到 12 2 tm_mday (日)1 到 31 3 tm_hour (时)0 到 23 4 tm_min (分)0 到 59 5 tm_sec (秒)0 到 61 (60或61是闰秒) 6 tm_wday (周)0 到 6 (0是周一) 7 tm_yday (年)1 到 366 (儒略历) 8 tm_isdst (夏令时)0 未启用,1 启用, -1 未知
获取各类格式的时间¶
time.time() 方法返回浮点型时间戳,单位秒。
0 1 2 3 4 5 | import time
timestamp = time.time()
print(timestamp)
>>>
1543497999.9993412
|
time.gmtime() 可以获取 UTC 9 元组。
0 1 2 3 4 5 6 | gmt = time.gmtime()
print(gmt)
>>>
time.struct_time(tm_year=2018, tm_mon=11,
tm_mday=29, tm_hour=13, tm_min=31, tm_sec=1,
tm_wday=3, tm_yday=333, tm_isdst=0)
|
time.localtime() 可以获取本地时区 9 元组。
0 1 2 3 4 5 6 | ltime = time.localtime()
print(ltime)
>>>
time.struct_time(tm_year=2018, tm_mon=11,
tm_mday=29, tm_hour=21, tm_min=34, tm_sec=18,
tm_wday=3, tm_yday=333, tm_isdst=0)
|
可以发现本地时区北京时间的 tm_hour 为 UTC 时区 tm_hour + 8。
time.gmtime() 和 time.localtime() 可以接受一个时间戳为参数,如果不提供,默认使用当前的时间戳。
0 1 2 3 4 5 6 7 8 9 10 | gmt0 = time.gmtime()
gmt1 = time.gmtime(time.time())
print(gmt0 == gmt1)
ltime0 = time.localtime()
ltime1 = time.localtime(time.time())
print(ltime0 == ltime1)
>>>
True
True
|
所以如果传入参数 0,那么就可以看到 epoch 时刻对应的 9 元组表示形式。
0 1 2 3 4 5 | print(time.gmtime(0))
>>>
time.struct_time(tm_year=1970, tm_mon=1,
tm_mday=1, tm_hour=0, tm_min=0, tm_sec=0,
tm_wday=3, tm_yday=1, tm_isdst=0)
|
0 1 2 3 4 5 6 7 8 9 | # 以下两种方式是等价的
print(time.ctime())
print(time.asctime(time.localtime()))
print(time.ctime(0))
>>>
Thu Nov 29 21:44:48 2018
Thu Nov 29 21:44:48 2018
Thu Jan 1 08:00:00 1970
|
time.ctime() 默认使用当前的时间戳,也可以传入时间戳,比如0,生成本地时区的时间字符串。
时间格式化¶
time.ctime() 默认的格式化方式如不能满足需求,可以指定格式化字符串,输出特定的时间字符串。
Python 中常用的时间格式化符号如下所示:
符号 意义 %y 两位数的年份表示(00-99) %Y 四位数的年份表示(0000-9999) %m 月份(01-12) %d 月内中的一天(0-31) %H 24小时制小时数(0-23) %I 12小时制小时数(01-12) %M 分钟数(00=59) %S 秒(00-59) %a 本地简化星期名称,例如 Thu %A 本地完整星期名称,例如 Thursday %b 本地简化的月份名称 %B 本地完整的月份名称 %c 本地相应的日期表示和时间表示,例如 Thu Nov 29 21:56:29 2018 %j 年内的一天(001-366) %p 本地A.M.或P.M.的等价符,例如 AM 和 PM %U 一年中的星期数(00-53)星期天为星期的开始 %w 星期(0-6),星期天为星期的开始 %W 一年中的星期数(00-53)星期一为星期的开始 %x 本地相应的日期表示 例如 11/29/18 %X 本地相应的时间表示,例如 21:55:07 %z 当前时区偏移值,例如 +0800 %Z 当前时区的名称,例如 CST %% %号本身
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | # 1970-01-01 00:00:00
timestr = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
print(timestr)
# Thu Jan 1 00:00:00 1970
timestr = time.strftime("%a %b %d %H:%M:%S %Y", time.localtime())
print(timestr)
# 另一种更便捷的方式
print(time.asctime(time.localtime()))
>>>
2018-11-29 21:59:13
Thu Nov 29 21:59:13 2018
Thu Nov 29 21:59:13 2018
|
时间格式转换¶
在 Python 中,有三种时间表示格式,几种格式间相互转换使用的方法如下:
From To 方法 时间戳 UTC 9元组 time.gmtime() 时间戳 本地时区 9元组 time.localtime() 时间戳 时间字符串 time.ctime() UTC 9元组 时间戳 calendar.timegm() 本地时区 9元组 时间戳 time.mktime() 9 元组 时间字符串 time.asctime() 或 time.strftime() 时间字符串 9元组 time.strptime()
时间字符串无法直接转换为时间戳,需要先转换为9元组。下图以更清晰的方式展现了它们之间的转换关系。
时间戳转时间元组¶
时间戳转时间元组,如果不提供参数,默认为当前时间时间戳,即 time.time()。
0 1 2 3 4 5 6 7 8 9 | gmt0 = time.gmtime(0) # 转 UTC 9元组
ltime0 = time.localtime(0) # 转本地时区 9元组
print(gmt0)
print(ltime0)
>>>
time.struct_time(tm_year=1970, tm_mon=1, tm_mday=1, tm_hour=0,
tm_min=0, tm_sec=0, tm_wday=3, tm_yday=1, tm_isdst=0)
time.struct_time(tm_year=1970, tm_mon=1, tm_mday=1, tm_hour=8,
tm_min=0, tm_sec=0, tm_wday=3, tm_yday=1, tm_isdst=0)
|
时间元组转字符串¶
时间元组转字符串有两种方式,time.asctime()提供了固定格式,如果不提供参数,默认为当前时间时间戳。time.strftime()可以任意定制时间字符串。
0 1 2 3 4 5 6 7 8 9 10 | ctstr = time.asctime(time.localtime(0))
ftstr0 = time.strftime("%a %b %d %H:%M:%S %Y", time.localtime(0))
ftstr1 = time.strftime("%a %b %d %H:%M:%S %Y", time.gmtime(0))
print(ctstr)
print(ftstr0)
print(ftstr1)
>>>
Thu Jan 1 08:00:00 1970
Thu Jan 01 08:00:00 1970
Thu Jan 01 00:00:00 1970
|
时间戳转时间字符串¶
time.ctime(),默认为当前时间,获取本地时区的时间字符串。
0 1 2 3 | print(time.ctime(0))
>>>
Thu Jan 1 08:00:00 1970
|
时间字符串转时间元组¶
0 1 2 3 4 | print(time.strptime('2017-9-30 11:32:23', '%Y-%m-%d %H:%M:%S'))
>>>
time.struct_time(tm_year=2017, tm_mon=9, tm_mday=30, tm_hour=11,
tm_min=32, tm_sec=23, tm_wday=5, tm_yday=273, tm_isdst=-1)
|
时间元组转时间戳¶
UTC 9元组转时间戳使用 calendar.timegm(),本地时区 9元组转时间戳使用 time.mktime()。
0 1 2 3 4 5 6 7 8 9 10 | old_gmt = time.gmtime()
old_ltime = time.localtime()
print(time.time()) # 使用的当前时间戳
print(calendar.timegm(old_gmt)) # UTC 转回的时间戳
print(time.mktime(old_ltime)) # 本地时区转回的时间戳
>>>
1543549663.177866
1543549663
1543549663.0
|
时间字符串转时间戳¶
时间字符串无法直接转换为时间戳,需要先转换为9元组。然后根据时间字符串是否为本地时间,再选择时间元组转时间戳的时间函数。
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | # inpput timestr as "Thu Jan 1 08:00:00 1970"
def timestr2stamp(timestr, isutc=False):
try:
tuple_time = time.strptime(timestr,"%a %b %d %H:%M:%S %Y")
time_stamp = calendar.timegm(tuple_time) if isutc else time.mktime(tuple_time)
except Exception as e:
print(e)
return False,0
return True,time_stamp
# 字符串表示的是本地时间
status, time_stamp = timestr2stamp("Thu Jan 1 08:00:00 1970")
if status:
print(time_stamp)
print(time.ctime(time_stamp))
# 字符串表示的是UTC时间
status, time_stamp = timestr2stamp("Thu Jan 1 00:00:00 1970", True)
if status:
print(time_stamp)
print(time.ctime(time_stamp))
>>>
0.0
Thu Jan 1 08:00:00 1970
0
Thu Jan 1 08:00:00 1970
|
时间格式转换类¶
尝试记住时间各类格式之间的转换函数是一件头疼的事,这里按照通常的命名方法来实现一个时间格式转换类,并直接定义为类方法。
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 | import time,calendar
class TimeFmtConverter():
# stamp to utc 9tuple
@classmethod
def stamp2utuple(cls, stamp=None):
if stamp == None:
stamp = time.time()
return time.gmtime(stamp)
# stamp to local tz 9tuple
@classmethod
def stamp2ltuple(cls, stamp=None):
if stamp == None:
stamp = time.time()
return time.localtime(stamp)
# tuple to time string with time.asctime()
@classmethod
def tuple2str(cls, intuple=None):
if intuple == None:
intuple = time.localtime()
return time.asctime(intuple)
# tuple to time string with time.strftime()
@classmethod
def tuple_fmt2str(cls, fmt="%a %b %d %H:%M:%S %Y", intuple=None):
if intuple == None:
intuple = time.localtime()
return time.strftime(fmt, intuple)
# stamp to utc time string
@classmethod
def stamp2ustr(cls, stamp=None):
if stamp == None:
stamp = time.time()
return cls.tuple2str(time.gmtime(stamp))
# stamp to local tz string
@classmethod
def stamp2lstr(cls, stamp=None):
if stamp == None:
stamp = time.time()
return time.ctime(stamp)
# local tz 9tuple to stamp
@classmethod
def ltuple2stamp(cls, intuple=None):
if intuple == None:
intuple = time.localtime()
return time.mktime(intuple)
# utc tuple to stamp
@classmethod
def utuple2stamp(cls, intuple=None):
if intuple == None:
intuple = time.gmtime()
return calendar.timegm(intuple)
# utc time string to time stamp
# inpput timestr style as "Thu Jan 1 00:00:00 1970"
@classmethod
def ustr2stamp(cls, timestr=None):
if timestr == None:
return cls.utuple2stamp()
tuple_time = time.strptime(timestr, "%a %b %d %H:%M:%S %Y")
return calendar.timegm(tuple_time)
# local tz time string to time stamp
# inpput timestr style as "Thu Jan 1 08:00:00 1970"
@classmethod
def lstr2stamp(cls, timestr=None):
if timestr == None:
return cls.ltuple2stamp()
tuple_time = time.strptime(timestr, "%a %b %d %H:%M:%S %Y")
return time.mktime(tuple_time)
@classmethod
def test(cls):
print(cls.stamp2utuple())
print(cls.stamp2ltuple())
print(cls.tuple2str())
print(cls.tuple_fmt2str())
print(cls.stamp2lstr())
print(cls.stamp2ustr())
print(cls.stamp2lstr(cls.ltuple2stamp()))
print(cls.stamp2ustr(cls.ustr2stamp("Thu Jan 1 00:00:00 1970")))
print(cls.stamp2lstr(cls.lstr2stamp("Thu Jan 1 08:00:00 1970")))
TimeFmtConverter.test()
>>>
time.struct_time(tm_year=2018, tm_mon=12, tm_mday=1, tm_hour=14,
tm_min=24, tm_sec=0, tm_wday=5, tm_yday=335, tm_isdst=0)
time.struct_time(tm_year=2018, tm_mon=12, tm_mday=1, tm_hour=22,
tm_min=24, tm_sec=0, tm_wday=5, tm_yday=335, tm_isdst=0)
Sat Dec 1 22:24:00 2018
Sat Dec 01 22:24:00 2018
Sat Dec 1 22:24:00 2018
Sat Dec 1 14:24:00 2018
Sat Dec 1 22:24:00 2018
Thu Jan 1 00:00:00 1970
Thu Jan 1 08:00:00 1970
|
datetime 模块¶
datetime 模块提供了强大的时间相关的运算,比如偏移,统计等方法。
错误和异常处理¶
语法错误¶
解释器在运行代码之前,首先进行代码的语法错误检查,有些代码编辑器,也可以给出语法错误的提示。
0 1 2 3 4 5 6 7 | if True
print("OK")
>>>
File "C:/Users/Red/.spyder/exception.py", line 10
if True
^
SyntaxError: invalid syntax
|
语法错误信息中第一行给出出错的文件和行号,第二行给出出错的语句,第三行由一个向上的小箭头指出语法错误的位置。 最后一行给出错误类型为 SyntaxError,也即语法错误,并给出一个简短的说明。
以上几类信息帮助快速定位和解决语法错误。
标准异常类型¶
代码运行时出现导致解释器无法继续执行的错误被称为异常。 异常在Python中被表示为一个异常对象。当异常发生时,如果不捕获处理它,则会导致程序终止执行。
0 1 2 3 4 5 6 | 1/0
>>>
File "C:/Users/Red/.spyder/except.py", line 42, in <module>
1/0
ZeroDivisionError: division by zero
|
与语法错误的提示类似,给出文件和行号等,不同的是,由于异常是语句运行时导致的错误,无法用小箭头指出位置。 上面是一个除0异常(ZeroDivisionError)。
常见的异常类型如下:
异常名称 类型描述 BaseException 所有异常的基类 SystemExit 解释器请求退出 KeyboardInterrupt 用户中断执行(通常是输入^C) Exception 常规错误的基类 StopIteration 迭代器没有更多的值 GeneratorExit 生成器(generator)发生异常来通知退出 StandardError 所有的内建标准异常的基类 ArithmeticError 所有数值计算错误的基类 FloatingPointError 浮点计算错误 OverflowError 数值运算超出最大限制 ZeroDivisionError 除(或取模)零 AssertionError 断言语句失败 AttributeError 对象没有这个属性 EOFError 没有内建输入,到达EOF EnvironmentError 操作系统错误的基类 IOError 输入/输出操作失败 OSError 操作系统错误 WindowsError 系统调用失败 ImportError 导入模块/对象失败 LookupError 无效数据查询的基类 IndexError 序列中没有此索引(index) KeyError 映射中没有这个键 MemoryError 内存溢出错误(对于Python NameError 未声明/初始化对象 UnboundLocalError 访问未初始化的本地变量 ReferenceError 弱引用(Weak RuntimeError 一般的运行时错误 NotImplementedError 尚未实现的方法 SyntaxError Python IndentationError 缩进错误 TabError Tab SystemError 一般的解释器系统错误 TypeError 对类型无效的操作 ValueError 传入无效的参数 UnicodeError Unicode UnicodeDecodeError Unicode UnicodeEncodeError Unicode UnicodeTranslateError Unicode Warning 警告的基类 DeprecationWarning 关于被弃用的特征的警告 FutureWarning 关于构造将来语义会有改变的警告 OverflowWarning 旧的关于自动提升为长整型(long)的警告 PendingDeprecationWarning 关于特性将会被废弃的警告 RuntimeWarning 可疑的运行时行为(runtime SyntaxWarning 可疑的语法的警告 UserWarning 用户代码生成的警告
Python中异常类的层次关系,详见 Exception hierarchy 。
捕获异常¶
Python 使用 try/except 或 try/except/else 语句用来捕获异常。
try/except语句¶
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | def divide0():
return 1 / 0
def divide1():
return divide0()
try:
divide1()
print("can't reach here!")
except ArithmeticError:
print("ArithmeticError")
except ZeroDivisionError:
print("ZeroDivisionError")
except:
print("Wildcard Error")
print("still running...")
>>>
ArithmeticError
still running...
|
由以上实例,可以得出以下结论:
- try 后放置我们需要捕获异常的语句,即便是子函数中的异常也会被捕获。
- try 中一旦某一条语句异常发生,后面的语句不再被执行,而是执行 except 分支语句。
- try 后可以跟多条 except 语句,用于处理每一种异常,也可以不指明异常类型,这样会匹配所有异常。
- except 语句只能匹配其中的一条,子类异常可以匹配基类的异常类型,比如这里的 ZeroDivisionError 继承自 ArithmeticError,所以除0错误会首先匹配第一个异常分支。 所以需要将最希望匹配的异常类型放在前面。
- 异常在捕获后,程序不会退出,而会继续执行。
except 语句可以同时处理多种异常,避免为每一种异常书写一条处理代码。
0 1 2 3 4 5 6 7 8 | try:
a = b + 1
except (RuntimeError, TypeError, NameError):
pass
print("still running...")
>>>
still running...
|
try/except/else 语句¶
try/except/else 是最常用的异常捕获语句,这允许在没有异常发生时得以做特定的处理。
0 1 2 3 4 5 6 7 8 | try:
a = 1 + 2
except:
print("except")
else:
print("Everything is OK!")
>>>
Everything is OK!
|
try/finally 语句¶
finally 语句无论异常是否发生都会被执行,它常用来做清理动作,比如关闭文件描述符或者网络套接字。
0 1 2 3 4 5 6 7 | try:
f.write("something...")
except:
print("except")
esle:
print("write ok!")
finally:
f.close()
|
注意:try/finally 语句中可以使用 else 分支。
Python 的 with 语句,可以更好的实现资源清理功能。如下所示,系统都将自动关闭文件描述符。
0 1 | with open('fname', 'r') as f:
data = f.read()
|
打印异常信息¶
在 except 语句中的异常名(或多个异常名)后可以以 as VAR 的形式添加一个变量, 该变量会返回异常的一个实例,异常的详细信息存储在该实例的 args 成员中。 它通常是一个对当前异常的说明信息。
0 1 2 3 4 5 6 7 8 9 10 11 12 | try:
1 / 0
except Exception as inst:
print(type(inst)) # the exception instance
print(type(inst.args))
print(inst.args) # arguments stored in .args
print(inst)
>>>
<class 'ZeroDivisionError'>
<class 'tuple'>
('division by zero',)
division by zero
|
异常类中的 __str__() 方法让 print() 函数可以直接打印异常的说明信息。 异常的 args 成员是一个元组 (tuple) 类型。
主动触发异常¶
raise 语句用于主动在程序中触发异常。
0 1 2 3 4 5 6 7 8 9 10 11 | try:
raise ValueError
except Exception as e:
print(e)
try:
raise ValueError('Invalid value')
except Exception as e:
print(e)
>>>
Invalid value
|
第一个示例抛出不带参数的异常,raise ValueError 是 raise ValueError() 的简写。此时 print(e) 只会打印一个空行。
也可以给异常传递多个参数,实际上它可以接受任意多个任意类型的参数,在异常处理中可以单独处理这些以元组类型返回的参数。 当然把这些信息统一到一个用户自定的类型中是一个更明智的选择。
0 1 2 3 4 5 6 7 8 | try:
raise ValueError('string', 1, ['abc', 123])
except Exception as e:
print(e.args[0])
print(e)
>>>
string
('string', 1, ['abc', 123])
|
用户自定义异常¶
自定义的异常类 Networkerror 继承了运行时异常,与内建的异常类不同,它不能接受任意多个参数, 参数的多少由 __init__() 初始化函数决定。
0 1 2 3 4 5 6 7 8 9 10 | class Networkerror(RuntimeError):
def __init__(self, arg):
self.args = (arg,)
try:
raise Networkerror("Bad hostname")
except Networkerror as e:
print(e)
>>>
Bad hostname
|
在一个用户模块中,可能需要定义一系列私有的异常,它通常继承自名为 Error 的自定义类,它继承异常的基类 Exception, 没有任何方法,是为了以后的扩展考虑。
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | class Error(Exception):
"""Base class for exceptions in this module."""
pass
class InputError(Error):
"""Exception raised for errors in the input.
Attributes:
expression -- input expression in which the error occurred
message -- explanation of the error
"""
def __init__(self, expression, message):
self.expression = expression
self.message = message
|
模块和包¶
模块和包是 Python 组织代码的形式。这如同 C 语言中的 .c 和 .a 文件一样。
模块和模块的导入¶
为了编写可维护可重用的代码,通常把代码按功能分类, 分别放在不同的文件里,这样每个文件中的代码就相对较少,且功能统一。 在Python中,一个 .py 脚本源码文件就称之为一个模块 (module)。
使用模块还可以避免函数名和变量名冲突。每一个模块都有自己的全局符号表,包含所有可以被其他模块使用的变量,函数等, 同名函数和变量可以同时存在不同的模块中,因此在编写模块时, 不必考虑名字会与其他模块冲突,这在多人协同编程时至关重要。
import 导入模块¶
在当前目录下创建三个文件 module0.py,module1.py 和 test_module.py,也即创建了三个模块, 它们的名字和文件名一致,分别为 module0,module1 和 test_module。
我们在 test_module 模块中引用 module0 和 module1 中的函数:
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | # module0.py
module_name = 'module0'
def module_info():
print("func in module0")
# module1.py
module_name = 'module1'
def module_info():
print("func in module1")
# test_module.py
import module0, module1 # 导入模块
print(module0.module_name)
print(module1.module_name)
module0.module_info()
module1.module_info()
>>>
module0
module1
func in module0
func in module1
|
import 语句是导入模块的一种方式,它具有以下特点:
- 一条 import 语句可以同时导入多个模块,以逗号分隔
- import 将整个模块的全局符号表导入到当前模块的符号表中,可以使用模块中所有全局变量,类和函数
- import 导入的模块,对其中的变量和函数访问时要加上模块名
- 导入的模块中相同函数名和变量名不会冲突
import 语句通常放在文件开头,这不是必须的,它也可以放在靠近引用模块的代码前。 重复导入相同的模块不会产生错误,解释器只在最早出现的 import 该模块的语句处导入。
导入模块时,解释器按照 sys.path 给出的路径顺序查找要导入的模块,以下是 Linux环境上的一个实例:
0 1 2 3 4 5 6 | import sys
print(sys.path)
>>>
['/home/red/sdc/lbooks/ml', '/usr/lib/python3.4',
'/usr/lib/python3.4/plat-i386-linux-gnu', '/usr/lib/python3.4/lib-dynload',
'/usr/local/lib/python3.4/dist-packages', '/usr/lib/python3/dist-packages']
|
当前路径具有最高优先级,所以模块命名不可与 Python 自带模块或第三方模块命名冲突,也不可和类,全局变量,内建函数命名冲突。sys.path 是一个路径的列表变量, 可以添加指定的路径:
0 | sys.path.append('/sdc/lib/python')
|
重命名模块¶
使用 import 导入时可以为模块重新命名,将长命名模块进行缩写,可以让代码更简洁:
0 1 2 3 4 5 6 | import module0 as m0
import module1 as m1
print(m0.module_name)
print(m1.module_name)
m0.module_info()
m1.module_info()
|
模块部分导入¶
与 import 语句不同,from 语句可以选择导入模块的某些变量或者函数,把它们直接加入到当前脚本的全局符号表中,引用时无需添加模块名前缀。
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | from module0 import module_info
from module1 import module_info
from module1 import module_info as mi
module_info()
mi()
print(module_name)
>>>
func in module1
func in module1
Traceback (most recent call last):
File "test_module.py", line 21, in <module>
print(module1.module_name)
NameError: name 'module_name' is not defined
|
由以上示例,可以得出如下结论:
- from 只把语句中指定的变量或函数从模块导入
- from 语句也支持重命名,此时重命名的是特定变量或者函数
- from 导入和变量或者函数不需要加模块名,直接访问,这导致在出现同名冲突时,后导入模块覆盖先导入模块,比如这里 module_info() 输出的是 func in module1
- 未导入变量或函数不能访问,比如这里提示 module_name 未定义
from 也可以把模块的全局符号表均导入到当前脚本全局符号表中:
0 | from module0 import * # 导入所有符号表到当前脚本
|
这种方法不该被过多使用,或者说大多数情况,都不要使用这种方法,因为它引入的符号命名很可能覆盖已有的定义,这也是上面示例所要强调的。
模块内代码应该高内聚,模块间应该低耦合,这是规范编码的基本要求,使用 import as 语句是推荐的做法。
包和包模块的导入¶
在实际的编码环境中,已经存在成千上万的模块,并且新模块还在不停被创建,此外多人协同编码时,不同的人编写的模块名也可能相同。基于这样的事实, 为了避免模块名冲突,Python 又引入了按目录来组织模块的方法,称为包(Package)。
简单说包就是一个文件夹,这个文件夹包含一个 __init__.py 文件,它可以是一个空文件。
引入了包以后,只要顶层的包名不冲突,那么所有的模块都不会冲突。
import 导入包模块¶
0 1 2 3 4 5 6 | ├── package0
│ ├── __init__.py
│ └── module0.py
├── package1
│ ├── __init__.py
│ └── module0.py
└── test_package.py
|
以上是示例的目录结构,由于包是一个目录,那么导入包中的模块时,就要指明模块所在包中的路径,Python 将路径“圆点化”表示,称为包路径:
0 1 2 3 4 5 6 | # test_package.py
import package0.module0
import package1.module0 as p1_m0
package0.module0.module_info()
p1_m0.module_info()
|
import 参数的最后部分必须是模块名(文件名),而不能是包名(目录名),比如 import package0
,那么访问 package0.module0 会提示找不到 module0 的错误:
0 1 2 3 | Traceback (most recent call last):
File "test_package.py", line 15, in <module>
package0.module0.module_info()
AttributeError: 'module' object has no attribute 'module0'
|
import 语句也可以为包或其中的模块重命名,这使得代码更简洁。导入包与直接导入模块不同的是:
- 导入包指定的是“圆点化”路径,比如例子中的
package0.module0
,引用时需要完整的包路径。 - __init__.py 脚本会在第一次导入包中模块时被执行,它常用来做包的预处理。
- 使用包中模块时,必须带上完整的包路径(包名),比如这里的
package0.module0.module_info()
,重命名可使代码更简洁。
部分导入包内模块¶
0 1 2 3 4 5 6 7 8 9 10 11 | # from package0 import * # 可以导入 package0 包中所有模块
from package0 import module0
from package0.module0 import module_info
module0.module_info()
module_info()
from package1 import module0 as p1_m0
from package1.module0 import module_info as m0
p1_m0.module_info()
m0()
|
from 后的参数可以为包路径,也可以为模块名路径,并且可以精确指定 import 模块中各成员。
尽管可以通过 from package0 import *
导入包中的所有模块或子包,但这种方法不要使用,很容易造成命名冲突,代码不清晰,导入应该明确要导入的模块名。
面向对象编程¶
在海滩上一群小孩子在堆沙堡,他们先把沙子装进不同的塑料模具里,然后再把成型的沙子倒出来,堆成一个个漂亮的沙堡。一个制造沙堡柱子的塑料模具可以制造出无数个带有相同花纹的沙柱,而小孩子的脚丫踩在沙滩上就留下一串相同的脚印。在工业制造上,使用模具来铸造机壳和零件更是司空见惯。所以在计算机编程语言中引入“模具”来制造用来编程的“零件”也就顺其自然了。
Python 是面向对象编程的语言,这一概念是相对于面向过程编程语言来讲的。在面向过程程序设计中,问题被看作一系列需要完成的任务,函数则用于完成这些任务,解决问题的焦点集中于函数,C 浯言是最常见的面向过程的编程语言。
面向对象编程(Object Oriented Programming,OOP)是一种程序设计思想。它把对象作为程序的基本单元,一个对象包含了数据和操作数据的函数,相同属性和操作方法的对象被抽象为类。类(Class)就类似上面所说的模具,而对象(Object)就是使用模具生产出的零件,对象就是类的实例(Instance)。
继承和多态、封装是面向对象编程的三个基本特征。
基本概念¶
为了更深入的理解 Python 语言是如何支持面向对象机制的,先要厘清一些基本概念。在介绍这些概念之前,先看一个示例:
0 1 2 3 4 5 6 7 8 9 | print(isinstance(object, type))
print(isinstance(type, object))
print(isinstance(type, type))
print(isinstance(object, object))
>>>
True
True
True
True
|
如果你已经可以解释清楚上面的执行结果,那么这一小结就可以跳过了。概念层面和实现层面往往有着令人不可思议的隔阂,就像古人无法理解原子一样。比如在 Python 的解释器 CPython 中没有对类(Class)的实现,只有对对象(Object)的实现,也即一切皆对象,Python 中的类更准确的应该被称为类型(Type)。
对象(Object)和类型(Type)是 Python 中两个最最基本的概念,它们是构筑 Python 语言大厦的基石,就像现实世界中的质子和电子一样。 这一章节主要参考 Python 数据模型 和 Python 类 。
对象(Object)¶
所有的数据类型,值,变量,函数,类,实例等等一切可操作的基本单元在 Python 都使用对象(Object)表示。每个对象有三个基本属性:ID,类型和值,也即有一块内存中存储了一个对象,这块内存中一定存有这三个属性。
0 1 2 3 4 5 6 7 8 9 | a = 1
print(id(a), type(a), a)
print(id(int), type(int), int)
print(id(type), type(type), type)
>>>
1384836208 <class 'int'> 1
1837755504 <class 'int'> 1
1837581680 <class 'type'> <class 'int'>
1837610960 <class 'type'> <class 'type'>
|
id()内建方法获取对象的唯一编号,它是一个整数,通常就是对象的内存地址。type() 内置方法获取对象的类型(Type),尽管这里冠以了 class 开头的说明,但是它就是指对象的数据类型,在 Python2.x 版本均是 <type ‘xxx’>。
a 是一个对象,它的数据类型是 int,它的值是 1。int 和 type 也是对象,它们的数据类型均是 type。
一个对象也可能有一个或者多个基类(Bases),当一个对象表示数据类型时,比如 int 对象,它就具有了 __bases__ 属性。
0 1 2 3 4 5 6 7 | print(int.__bases__)
print(type.__bases__)
print(a.__bases__)
>>>
(<class 'object'>,)
(<class 'object'>,)
AttributeError: 'int' object has no attribute '__bases__'
|
type 和 bases 定义了该对象与其他对象间的关系,实际上对象内的 type 和 bases 均指向其他对象,是对其他对象的地址引用。
一个对象可以使用名字引用它,也可以没有,比如整数 1,一个不具名的对象我们无法同时测试它的三个属性,因为没有名字,第二次测试时无法保证还是原来的对象,但是依然可以测试匿名对象具有这三个属性。
0 1 2 3 4 5 6 7 8 | print(int.__name__)
print(id(1), type(1), 1)
print((1).__name__)
>>>
int
1384836208 <class 'int'> 1
AttributeError: 'int' object has no attribute '__name__'
|
类型(Type)¶
一个对象必有 Type 属性,同样 Type 是不能脱离开对象存在的。一个对象的类型定义了这个对象支持的行为以及它承载的值的类型,比如取名字,算数运算,求长度等等,一个 int 类型的对象只接受整型的数值。
type() 内置方法获取对象的类型。我们也可以使用类名加 “.__class__ ”来获取对象的类型,它们是等价的。
0 1 2 3 4 5 6 | a = 1
print(type(a))
print(a.__class__)
>>>
<class 'int'>
<class 'int'>
|
类(Class)¶
在大多数编程语言中,类就是一组用来描述如何生成一个对象的代码段,由于 Python 是动态语言,类是动态生成的,它和传统意义上的类的意义不同。在 Python 中定义一个新类(Class)等于创建了一个新类型(Type)的对象(Object),解释器中一切对象均存储在 PyObject 结构中。
通过 class 关键字我们可以定义一个新的类型(New User-defined Type)。
0 1 2 3 4 5 6 7 8 9 | class Base():
pass
b = Base()
print(id(Base), type(Base), Base)
print(id(b), type(b), b)
>>>
2978875124248 <class 'type'> <class '__main__.Base'>
2978906227544 <class '__main__.Base'> <__main__.Base object at 0x000002B594A5C358>
|
示例中,我们定义了一个自己的类型 Base,b 是它的实例(Instance),它的类型是 Base。
Class 和 Type 均是指类型(Type),Class 通常用于普通用户使用 class 自定义的类型。Type 通常指 Python 的解释器 CPython 内置的类型。
CPython 提供内置方法 type() 而没有定义 class(),因为它们本质是一样的,只是不同的语境产生的不同说法。
Python 中支持类的多重继承,被继承的类称为当前类的基类(Base Classes)或者超类(Super Classes),也叫做父类。当前类被称为被继承类的子类(Subclasse),issubclass(class, class) 内置方法用于判断子类。
0 1 2 3 4 5 6 7 8 9 10 | class A:
pass
class B(A):
pass
print(issubclass(B, A))
print(issubclass(B, object))
>>>
True
True
|
无论是自定义类,还是内置类型,它们均具有 __bases__ 属性,由于支持多继承,它是一个元组类型,指明了类型对象继承了哪些类。
0 1 2 3 4 5 6 7 8 9 10 | class A():
pass
class B():
pass
class C(A, B):
pass
print(C.__bases__)
>>>
(<class '__main__.A'>, <class '__main__.B'>)
|
类拥有创建对象的能力,而这就是为什么它是一个类的原因。但是它的本质仍然是一个对象,于是可以对它进行如下的操作:
- 可以将它赋值给一个变量
- 可以复制它
- 可以为它增加属性
- 可以将它作为函数参数进行传递
- 可以作为函数的返回值
- 可以动态的创建一个类
type() 内置方法不仅可以获取对象类型,还可以动态创建一个类:
0 1 2 3 4 5 6 7 8 9 10 | class A(object):
name = 'A'
def print_name(self):
print(self.name)
# 等价的类定义
def print_name(self):
print(self.name)
A = type('A', (object,), {'name': 'A', "print_name" : print_name})
|
实例(Instance)¶
实例(Instance)和对象(Object)也是不同的语境产生的不同说法。
“1 是一个 int 类型的实例” 和 “1 是 int 类型的对象” 是等价的。
如果把上句中的“类型”替换为“类”,就成了我们熟悉的面向对象编程中的说法:“1 是一个 int 类的实例” 和 “1 是 int 类的对象”。
当创建某个对象或强调某个对象的类型时,常常说这个对象是某某类的实例,当强调对象自身时,我们只说某某对象。
当一个对象是某个类的实例时,它也是这个类的基类的实例。内置方法 isinstance(obj, class) 用来判断一个对象是否是某个类的实例。
0 1 2 3 4 5 6 7 8 9 | class Base():
pass
b = Base()
print(isinstance(b, Base))
print(isinstance(b, object))
>>>
True
True
|
对象和类型的关系¶
厘清了上述概念,开始分析 Python 中对象和类型的关系。
Python 中的对象之间存在两种关系:
- 父子关系或继承关系(Subclass-Superclass 或 Object-Oriented),如“老鼠”类继承自“哺乳动物”类,我们说“老鼠是一种哺乳动物”(Mouse is a kind of mammal)。对象的 __bases__ 属性记录这种关系,使用 issubclass() 判断。
- 类型实例关系(Type-Instance),如“米老鼠是一只老鼠”(Mickey is an instance of mouse),这里的米老鼠不再是抽象的类型,而是实实在在的一只老鼠。对象的 __class__ 属性记录这种关系,使用 isinstance() 判断。
Python 把对象分为两类,类型对象(Type)和非类型对象(Non-type)。
- int, type, list 等均是类型对象,可以被继承,也可以被实例化。
- 1, [1] 等均是非类型对象,它们不可再被继承和实例化,对象间可以根据所属类型进行各类操作,比如算数运算。
object 和 type 是 CPython 解释器内建对象,它们的地位非常特殊,是 Python 语言的顶层元素:
- object 是所有其他对象的基类,object 自身没有基类,它的数据类型被定义为 type。
- type 继承了 object,所有类型对象都是它的实例,包括它自身。判断一个对象是否为类型对象,就看它是否是 type 的实例。
现在回到开篇的问题,isinstance() 内置方法本质是在判断对象的数据类型,它会向基类回溯,直至回溯到 object,在 CPython 中最终调用如下函数:
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | static int
type_is_subtype_base_chain(PyTypeObject *a, PyTypeObject *b)
{
do {
if (a == b)
return 1;
a = a->tp_base;
} while (a != NULL);
return (b == &PyBaseObject_Type);
}
print(isinstance(object, type)) # 1
print(isinstance(type, object)) # 2
print(isinstance(type, type)) # 3
print(isinstance(object, object))# 4
print(object.__class__) # <class 'type'>
print(type.__class__) # <class 'type'>
|
object 和 type 在 CPython 中分别对应 PyTypeObject(对 PyObject 的封装)类型的 PyBaseObject_Type 和 PyType_Type 变量,其中用于表示类型的成员 ob_type 是一个指针,均指向 PyType_Type。所以 object 和 type 对象类型均为 type。
- object 的类型被定义为 type,所以为 true,这是人为通过指针定义的(ob_type 指针指向了 PyType_Type)。
- type 的类型还是 type,它是自身的实例,type 继承自 object,所以也是 object 的实例,所以为 true。
- 勿用解释。
- object.__class__ 为 type,type 继承了 object,所以为 true。
Python 中还定义了一些常量,比如 True,False。其中有两个常量 None 和 NotImplemented 比较特殊,通过 type() 可以获取它们的类型为 NoneType 和 NotImplementedType,这两个类型不对外开放,也即普通用户无法继承它们,它们只存在于 CPython 解释器中。
类和对象¶
类和对象是面向对象编程中封装特征的体现,类将一类属性和围绕属性操作的方法封装在一起,相同功能代码内聚成类,不同功能的类之间相互隔离。
类属性¶
类自身可以具有自己的属性,被称为类属性,或者类成员变量。类属性可以直接通过类名访问,也可以通过实例访问。
0 1 2 3 4 5 6 7 8 9 10 11 12 | class Employee():
class_version = "v1.0" # 类属性
def __init__(self, id, name):
self.id = id
self.name = name
print(Employee.class_version) # 类名直接访问类属性
worker0 = Employee(0, "John")
print(worker0.class_version) # 实例访问类属性
>>>
v1.0
v1.0
|
如果要改变类属性,直接进行赋值操作是最简单的方法。对类属性赋予新的值,它的所有实例的类属性也会更新。
0 1 2 3 4 5 6 7 8 9 10 | worker1 = Employee(1, "Bill")
Employee.class_version = "v1.1"
print(Employee.class_version)
print(worker0.class_version)
print(worker1.class_version)
>>>
v1.1
v1.1
v1.1
|
实际上,类属性在创建实例时并不会被单独创建,都是引用的类的属性,它们在内存中只有一份。同样我们可以通过实例来改变类属性,此时将进行拷贝动作,该实例的类属性将脱离类的属性,实现了属性的解绑定,把原来类属性覆盖了,该属性成为了实例的私有属性,其他实例不会受影响。
0 1 2 3 4 5 6 7 8 9 | worker0.class_version = "v1.2" # 类属性被复制,不影响类和其他实例
print(worker0.class_version)
print(Employee.class_version)
print(worker1.class_version)
>>>
v1.2
v1.1
v1.1
|
在实际的使用时,类属性应该只用于类相关的描述,类示例可以访问和使用它们,但不应该更改它们。
类私有属性和类方法¶
类的私有属性以两个下划线 “__” 开头,类似于 Java 中的 private 关键字,私有属性不能通过类名或者类实例来直接访问,只能通过类方法访问。
0 1 2 3 4 5 6 7 8 9 | class Employee():
__class_version = "v1.0" # 类的私有属性
def __init__(self, id, name):
self.id = id
self.name = name
print(Employee.__class_version)
>>>
AttributeError: type object 'Employee' has no attribute '__class_version'
|
类方法的第一个参数总是 cls,它指类自身,在调用时被隐式传递,声明类方法必须加上 @classmethod 装饰器说明符,参考 内置装饰器。
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | class Employee():
__class_version = "v1.0"
def __init__(self, id, name):
self.id = id
self.name = name
@classmethod
def cls_ver_get(cls):
return cls.__class_version
@classmethod
def cls_ver_set(cls, new_version):
cls.__class_version = new_version
print(Employee.cls_ver_get())
Employee.cls_ver_set("v1.1")
print(Employee.cls_ver_get())
>>>
v1.0
v1.1
|
类的私有属性也可以被类的实例通过类方法访问或者修改类,但是修改过程不会出现拷贝,类私有属性在内存中永远只有一份,所有修改会影响到类和类的所有实例。
0 1 2 3 4 5 6 7 8 9 10 11 12 | worker0 = Employee(0, "John")
worker1 = Employee(1, "Bill")
worker0.cls_ver_set("v1.2")
print(worker0.cls_ver_get())
print(Employee.cls_ver_get())
print(worker1.cls_ver_get())
>>>
v1.2
v1.2
v1.2
|
总结,类私有属性是类的私有“财产”,在内存中永远只有一份,只能通过类方法访问,实例可以通过类方法修改类的私有属性,修改会被“广播”到所有示例和类自身。
如果要定义只读属性,不要定义赋值动作的函数即可,比如这里的 cls_ver_set()。
类方法是访问类私有属性的接口,对私有属性提供了保护,在类方法中可以对设置的参数进行有效性检查。在使用类属性时应该明确用途,遵循先私有再公用的原则,以保证代码的高内聚和低耦合。不适当的对类属性的暴露将导致难于追溯的复杂问题,比如对象属性声明了与类属性同名的变量,将覆盖类的属性,类属性被解绑定了,这可能并不是我们所期待的。
类的静态方法¶
使用 @staticmethod 装饰器说明符可以定义类的静态方法,参考 内置装饰器。与类方法唯一的不同在于没有第一个隐式传递的参数 cls, 通常用于对一类函数进行封装。
0 1 2 3 4 5 6 7 8 9 | class Employee():
......
@staticmethod
def static_get():
print("This is a class static method")
Employee.static_get()
>>>
This is a class static method
|
类的静态方法无法访问类属性。另外需注意,无论是类方法还是类的静态方法都只能通过类名加 ‘.’ 的方式调用,不能间接调用它们,例如:
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | class Employee():
__class_version = "v1.0"
def __init__(self, id, name):
self.id = id
self.name = name
@classmethod
def cls_ver_get(cls):
return cls.__class_version
func_map = {'cls_ver_get':cls_ver_get}
def call_func_map(self):
self.func_map['cls_ver_get']()
worker0 = Employee(0, "John")
worker0.call_func_map()
>>>
TypeError: 'classmethod' object is not callable
|
查看类属性和方法¶
dir(...)
dir([object]) -> list of strings
dir() 内建函数用于获取任意对象的所有属性和方法,在 Python 中一切都是基于对象实现的,在本质上类也是一个对象,可以使用 dir() 方法获取类的属性和方法。
0 1 2 3 4 5 | print(dir(Employee))
>>>
(<class '__main__.Employee'>, <class 'object'>)
['_Employee__class_version', '__class__', '__delattr__', '__dict__', '__dir__', '__doc__',
......, 'cls_ver_get', 'cls_ver_set']
|
由于所有类默认都会继承内建对象的基类 object,这里看到的很多方法和属性均来自 object 对象,但是 _Employee__class_version 是我们自定义的属性,它自动被解释器添加了 _Employee前缀,而方法的名字则被保留。
0 1 2 3 4 5 6 7 | print(type(Employee.__init__))
print(type(Employee.static_get))
print(type(Employee.cls_ver_get))
>>>
<class 'function'>
<class 'function'>
<class 'method'>
|
注意,类中没有明确声明为类方法的函数类型均为 function,只有类方法的类型为 method。
动态绑定类属性和方法¶
我们可以为已定义的类动态的绑定新的属性和方法。
0 1 2 3 4 5 6 7 8 9 10 | Employee.__class_version = "v1.3"
dir(Employee)
print(Employee.__class_version)
print(Employee.cls_ver_get())
>>>
['_Employee__class_version', '__class__', '__class_version',
......, 'cls_ver_get', 'cls_ver_set']
v1.3
v1.2
|
我们曾在类定义中定义了私有属性 __class_version,令人费解的是为何直接访问 Employee.__class_version 会报错没有这个属性,而赋值操作却不会出错。
在上一小结,我们知道,类属性名在解释器中会被自动添加 _classname 的前缀,形成了类定义时的命名空间,如果我们在动态绑定类属性和方法,指定了同名的类属性,这个属性并不会被覆盖,而实际上会创建一个新的属性。通过 dir() 查看属性定义,发现多了 __class_version 属性,而类定义时的属性被命名为 _Employee__class_version。
所以尽管我们在赋值后可以 Employee.__class_version,实际上已经不是同一个属性,我们使用类的私有函数 cls_ver_get() 获取值依然没有改变。
同样,如果绑定一个同名的类方法,那么由于同名的方法没有被重命名,则会被覆盖。
0 1 2 3 4 5 6 7 8 9 10 | def new_init(self, id, name, age):
print("new_init")
self.id = id
self.name = name
self.age = age
Employee.__init__ = new_init
worker1 = Employee(1, "Bill")
>>>
TypeError: new_init() missing 1 required positional argument: 'age'
|
我们这里定义新的初始化方法,并绑定到 Employee 类,现在如果还是用原来的参数来初始化对象,则会报参数错误。
为类动态创建的属性和方法,实例化的对象可以访问吗?我们看一个新的示例:
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | class Employee():
__class_version = "v1.0"
def __init__(self, id, name):
self.id = id
self.name = name
@classmethod
def cls_ver_get(cls):
return cls.__class_version
worker0 = Employee(0, "John")
# worker0.say_name() 动态方法未绑定报错
# print(worker0.class_version) 动态属性未绑定会报错
def say_name(self):
print("My name is %s" % self.name)
Employee.say_name = say_name
Employee.class_version = "v1.0"
worker0.say_name()
print(worker0.class_version)
>>>
My name is John
v1.0
|
可以看到在动态属性后者方法绑定后,即便是已经实例化的对象,也可以调用它们。我们不能通过动态绑定的方式定义类的私有属性。
尽管经过以上的分析,我们可以做一些“诡异”的动作,来尝试对一个只读属性赋值,也可以动态绑定一个私有属性,Python 不会阻止你干这样的“坏事”,但编码时不要这样做,这些均是基于解释器对定义时命名空间的行为,这一行为很可能随着版本改变,如果需要增加私有属性,请在类定义中添加。
0 1 2 3 4 5 6 7 | Employee._Employee__class_version = "v1.3"
print(Employee.cls_ver_get())
Employee._Employee__private_arg = "private"
print(Employee._Employee__private_arg)
>>>
v1.3
private
|
对象的属性和方法¶
对象是类的实例化,我们可以认为在创建对象时,复制了类的一份内存空间,并在内存空间中填入了实例的参数值。类的私有属性和类方法不可被实例化,它们还是指向原来的类。
对象和类一样,同样可以定义私有属性和方法,这些属性和方法必须以 “__” 开头,不可以通过对象名直接引用。
同样可以对一个对象进行动态添加属性和方法,只是它们均为这个对象所私有,不会影响类的其他实例对象。如果对象方法已经存在,则被覆盖。
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | # 为对象添加新属性
worker0.age = 25
def say_age(self):
print("My id is %d" % self.age)
# 为对象添加新方法
from types import MethodType
worker0.say_age = MethodType(say_age, worker0)
print(type(worker0.say_age))
print(type(say_age))
worker0.say_age()
>>>
<class 'method'>
<class 'function'>
My id is 25
|
对象中的函数被称为 method 方法类型,普通函数类型为 function,这里需要借助 types 模块中的 MethodType() 将一个函数转化为一个对象的方法。
在类中定义的函数除非指明是类方法,它们的类型默认为 function,所以类可以通过赋值来动态绑定新的方法。对象是类的实例化,实例化过程会将 function 类型转换为 method 类型,所以类对象动态添加方法不能直接赋值。
访问对象属性¶
如同类属性的访问一样,有两种方式访问对象的属性,直接使用对象名访问或者通过对象方法访问。类中所有第一个参数为 self 的函数都是对象的方法。
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | class Employee():
def __init__(self, id, name):
self.id = id
self.name = name
def get_name(self):
return self.name
def set_name(self, newname):
self.name = newname
worker0 = Employee(0, "John")
print(worker0.name)
print(worker0.get_name())
worker0.name = "Lee"
print(worker0.get_name())
worker0.set_name("John")
print(worker0.get_name())
print(Employee.get_name(worker0)) # 通过类访问对象
>>>
John
John
Lee
John
John
|
示例中还给出了通过类来访问实例的方法,这可以加深对类定义中实例方法 self 参数的理解。
经过直接使用对象属性很方便,但是这未反了面向对象的思想,任何对外暴露对象属性的编码都暗喻着混乱的到来。比如我们如果要在更改姓名时做一些功能扩展,比如记录日志,那么使用类方法的维护成本要低得多,不仅如此,对参数合法性的检查也是对象方法的一大职能。
是否能既兼顾直接访问对象属性的方便,又不违反面向对象的编程思想呢?答案是使用 @property 装饰器,参考 实例方法属性化。
限制属性和方法¶
我们可以动态的为对象添加属性和方法,这为程序设计提供了强大的灵活性,面向对象的一大优点在于封装,这种不恰当的扩展会破坏封装,类的特殊变量 __slots__ 可以给这种灵活性加以限制。
0 1 2 3 4 5 6 7 8 9 | class Employee():
__slots__ = ('id')
def __init__(self, id, name):
self.id = id
self.name = name
worker0 = Employee(0, "John")
>>>
AttributeError: 'Employee' object has no attribute 'name'
|
__slots__ 是一个名字字符串的元组,如果我们定义了它,所有的属性名都必须在其中声明,否则将提示错误。
0 1 2 3 4 5 6 7 8 9 10 11 12 13 | class Employee():
__slots__ = ('id', 'name')
def __init__(self, id, name):
self.id = id
self.name = name
def get_name(self):
return self.name
worker0 = Employee(0, "John")
worker0.age = 30
>>>
AttributeError: 'Employee' object has no attribute 'age'
|
这种限制同样对动态绑定起作用。同时方法也成为只读的,不能删除,也不能动态覆盖旧的方法,同时不能添加新方法。
0 1 2 3 4 5 6 7 8 9 10 11 12 | def get_id(self):
return self.id
worker0 = Employee(0, "John")
del (worker0.get_name)
worker0.get_name = None
worker0.get_id = get_id
>>>
AttributeError: 'Employee' object attribute 'get_name' is read-only
AttributeError: 'Employee' object attribute 'get_name' is read-only
AttributeError: 'Employee' object has no attribute 'get_id'
|
但是这种限制只作用在类对象上,并不限制类自身,但是这个动态绑定的属性对于类对象是只读的,可以访问它,但是不能改写它,它是一个可以只可访问的类私有属性。
0 1 2 3 4 5 6 7 | Employee.age = 25
print(worker0.age)
worker0.age = 30
>>>
25
AttributeError: 'Employee' object attribute 'age' is read-only
|
如果子类也定义了 __slots__ 属性,则它会继承父类的 __slots__ 属性,否则不受父类的限制。
0 1 2 3 4 5 6 | class Programmer(Employee):
def __init__(self, id, name, lang):
self.language = lang
super().__init__(id, name)
engineer = Programmer(0, "John", "Python")
engineer.age = 30
|
以上继承不受 Employee 中 __slots__ 属性的影响,一旦子类中也定义了 __slots__,language 就不再是实例合法的属性了。
0 1 2 3 4 5 6 7 8 9 | class Programmer(Employee):
__slots__= () # 添加 'language' 使其变成合法属性
def __init__(self, id, name, lang):
self.language = lang
super().__init__(id, name)
engineer = Programmer(0, "John", "Python")
>>>
AttributeError: 'Programmer' object has no attribute 'language'
|
合理的运用类私有成员和 __slots__ 属性定义可以帮助我们编写具有高内聚,易扩展,可维护的代码,遵守规范能够获益的不仅仅是养成一个好习惯,它最大的挽救了我们的时间。
基类和继承¶
object和类特殊方法¶
object 是所有类的基类(Base Class,也被称为超类(Super Class)或父类),如果一个类在定义中没有明确定义继承的基类,那么默认就会继承 object。
0 1 2 3 4 5 6 7 8 | class Employee():
# 等价于
class Employee(object):
print(Employee.__mro__) # 打印类的继承关系
>>>
(<class '__main__.Employee'>, <class 'object'>)
|
__mro__ 属性记录类继承的关系,它是一个元组类型,从结果可以看出 Employee 继承自 object 基类。
object 自带一些属性和方法。对某些方法的了解有利于加深对类实例化过程的理解。
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | dict0 = dir(object)
for i in dict0:
print("%-20s:%s" % (i, type(eval("object." + i))))
>>>
__delattr__ :<class 'wrapper_descriptor'>
__dir__ :<class 'method_descriptor'>
__doc__ :<class 'str'>
__format__ :<class 'method_descriptor'>
__getattribute__ :<class 'wrapper_descriptor'>
__hash__ :<class 'wrapper_descriptor'>
__init__ :<class 'wrapper_descriptor'>
__init_subclass__ :<class 'builtin_function_or_method'>
......
__repr__ :<class 'wrapper_descriptor'>
__setattr__ :<class 'wrapper_descriptor'>
__sizeof__ :<class 'method_descriptor'>
__str__ :<class 'wrapper_descriptor'>
|
__dir__¶
__dir__() 方法用于类的所有属性和方法名,它是一个字典,内置函数 dir() 就是对它的调用。
__doc__¶
__doc__ 属性指向当前类的描述字符串。描述字符串是放在类定义中第一个未被赋值的字符串,它不会被继承。
0 1 2 3 4 5 6 7 8 9 | class C():
"A Sample Class C" # 对类的描述,如果没有则为""
pass
print(C.__doc__)
print(object.__doc__)
>>>
A Sample Class C
The most base type
|
__str__¶
__str__ 方法用于 str() 函数转换中,默认使用 print() 方法打印一个对象时,就是对它的调用,我们可以重写这个函数还实现自定义类向字符串的转换。
0 1 2 3 4 5 6 7 8 9 10 11 12 13 | class C():
pass
print(C) # 调用 object 的 __str__ 方法
class C():
def __str__(self):
return "C Class"
print(C()) # 调用类对象的 __str__ 方法
>>>
<__main__.C object at 0x0000027A98BE2828>
C Class
|
__repr__¶
repr() 函数调用对象中的 __repr__() 方法,返回一个 Python 表达式,通常可以在 eval() 中运行它。
__call__¶
还有一些特殊方法没有在基类中实现,但是它们具有非常特殊的功能,比如 __call__() 可以将一个对象名函数化。实现了 __call__() 函数的类,其实例就是可调用的(Callable)。 可以像使用一个函数一样调用它。
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | class Employee():
def __init__(self, id, name):
self.id = id
self.name = name
def __call__(self, *args):
print(*args)
print('Printed from __call__')
worker0 = Employee(0, "John")
worker0("arg0", "arg1")
>>>
arg0 arg1
Printed from __call__
|
装饰器类 就是基于 __call__() 方法来实现的。注意 __call__() 只能通过位置参数来传递可变参数,不支持关键字参数,除非函数明确定义形参。
可以使用 callable() 来判断一个对象是否可被调用,也即对象能否使用()括号的方法调用。
0 1 2 3 4 | # 如果 Employee 类不实现 __call__,则返回 False
callable(worker0)
>>>
True
|
属性方法¶
在基类中提供了3个与属性操作相关的方法:
- __delattr__,用于 del 语句,删除类或者对象的某个属性
- __setattr__,用于动态绑定属性
- __getattribute__,在获取类属性时调用,无论属性是否存在
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | class C():
def __init__(self):
self.hello = "123"
def __delattr__(self, name):
print("delattr %s" % name)
super().__delattr__(name) # 调用 object 的 __delattr__
def __setattr__(self, attr, value):
print("setattr %s" % attr)
super().__setattr__(attr, value)# 调用 object 的 __setattr__
c = C()
del c.hello # 调用类对象的 __delattr__
print(c.hello) # 报错 hello 属性不存在
c.newarg = "100" # 调用类对象的 __setattr__
>>>
delattr hello
setattr newarg
|
为了使打印输出更清晰,这里单独来验证 __getattribute__ 方法,可以看到无论属性是否存在均会调用自定义的 __getattribute__ 方法。
0 1 2 3 4 5 6 7 8 9 10 11 12 13 | class C():
def __init__(self):
self.hello = "123"
def __getattribute__(self, name):
print("getattribute %s" % name)
return super().__getattribute__(name)
print(C().hello)
print(C().ARG) # 报错没有 ARG 属性
>>>
getattribute hello
getattribute ARG
|
这里之所以单独把属性相关的方法放在一起总结,在于 Python 提供了三个内置方法 getattr(),setattr() 和 hasattr(),它们均是基于类的属性方法来实现的。
__hash__¶
如果一个对象具有 __hash__() 方法,那么它就是可散列的(Hashable,严格来说需要同时实现 __eq__(),基类默认实现了该函数)。
基类 object 默认实现了 __hash__ 方法,使用对象的 id 作为散列值,所以用户定义的类的实例都是可散列的,且彼此不相等,如果明确不可散列,需要做如下处理:
0 1 2 3 4 5 | def UnHashableCls():
......
# 最直接的方式:__hash__ = None
def __hash__(self):
msg = "unhashable type: '{0}'".format(self.__class__.__name__)
raise TypeError(msg)
|
如果用户类重新实现了 __eq__,则必须同时实现 __hash__,否则 __hash__ 会隐式的设置为 None,其实例是不可散列的。
0 1 2 3 4 5 6 7 8 9 10 11 12 | class A():
# 可以引用基类的 hash 函数来使对象可散列
# __hash__ = object.__hash__
def __eq__(self, b):
return True
a = A()
print(a.__hash__)
>>>
None
|
集合元素必须是可散列的,参考 集合 。
__sizeof__¶
__sizeof__() 方法在 sys.getsizeof(obj) 中被调用,获取对象占用内存大小。
__enter__ 和 __exit__¶
Python 的 with as 语句提供自动回收资源的魔法。实际上就是调用的对象的这两个方法完成的。
with as 语句最常用来进行文件句柄的自动关闭,这样我们就不用再担心忘记关闭文件描述符了,例如:
0 1 2 | with open('tmp.txt', 'r') as f:
data = file.read()
.....
|
为了阐述 with as 语句和 __enter__, __exit__ 之间的关系,我们看下面的示例:
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | class WithAs():
def __enter__(self):
print('call __enter__')
return 'abc'
def __exit__(self, exc_type, exc_value, trace):
print("call __exit__\n", exc_type, exc_value, trace)
obj = WithAs()
with obj as i:
print(i)
print('--------------')
>>>
call __enter__
abc
call __exit__
None None None
--------------
|
在 obj as i 时执行 __enter__(),并把它的返回值赋值给 i,我们可以在语句块中使用它。在 with 语句块结束时自动调用 __exit__() 方法。
with as 语句的真正强大之处在于它可以处理异常,我们注意到 __exit__ 方法还有 exc_type 等三个参数,它们就是用来协助处理异常的,正常情况下这三个参数均设置为 None,可以使用 exc_type 是否为 None 判断 with 语句块是否出现异常。
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | class WithAs():
def div0(self):
1/0
def __enter__(self):
return self
def __exit__(self, exc_type, exc_value, trace):
print(exc_type)
print(exc_value)
print(trace)
if exc_type is not None:
# rollback something
else:
# clean something
with WithAs() as obj:
obj.div0()
>>>
call __exit__
<class 'ZeroDivisionError'>
division by zero
<traceback object at 0x000001C18B79A0C8>
......
ZeroDivisionError: division by zero
|
这里返回了一个匿名函数,在 with 语句块中调用它发生了除 0 错误,一旦出现了异常,就会调用 __exit__ 方法,并把异常类型,异常值和 traceback 对象传递给各个参数。with 语句块中出现任何没有显式(try)处理的异常时都会调用 __exit__ 方法,并传递相应的异常参数,例如:
0 1 2 3 4 5 6 7 8 | with WithAs() as obj:
1/0
>>>
<class 'ZeroDivisionError'>
division by zero
<traceback object at 0x000001C18B83A2C8>
......
ZeroDivisionError: division by zero
|
attr 内置方法¶
Python 提供了三个内置方法 getattr(),setattr() 和 hasattr() ,分别用于获取,设置和判定对象的属性。既然我们已经可以通过对象名直接访问它们,为何还要使用这些函数呢?通过它们我们可以对任意一个我们不熟悉的对象进行尝试性访问,而不会导致程序出错。
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | class C():
def __init__(self):
self.hello = "123"
c = C()
if hasattr(c, "hello"):
print(c.hello)
if not hasattr(c, "ARG"):
setattr(c, "ARG", "ARGVal")
print(getattr(c, "ARG"))
print(getattr(c, "ARG0", "default value"))
>>>
123
ARGVal
default value
|
getattr() 方法最大的用途在于如果对象没有相应属性,可以不报错 AttributeError,可以为它指定一个默认值。
内置方法和对应操作¶
算术运算¶
运算符 | 对象方法 | 描述 |
---|---|---|
+ | __add__(n) | obj + n |
- | __sub__(n) | obj - n |
+ | __pos__() | +obj |
- | __neg__() | -obj |
* | __mul__(n) | obj * n |
/ | __truediv__(n) | obj / n |
% | __mod__(n) | obj % n 取余 |
** | __pow__(n) | pow(obj, n) 或 obj ** n |
// | __floordiv__(n) | obj // n 取整除,返回商的整数部分 |
divmod | __divmod__(n) | divmod(obj, n) 返回一元组(obj // n, obj % n) |
abs | __abs__() | abs(obj) 取绝对值 |
比较运算¶
比较运算用于判断两个对象的值的关系。
运算符 | 对象方法 | 描述 |
---|---|---|
== | __eq__(n) | obj == n |
!= | __ne__(n) | obj != n |
<> | __ne__(n) | obj != n 等同 != |
> | __gt__(n) | obj > n |
>= | __ge__(n) | obj >= n |
< | __lt__(n) | obj < n |
<= | __le__(n) | obj <= n |
赋值运算¶
运算符 | 对象方法 | 描述 |
---|---|---|
= | 内部定义 | |
+= | __iadd__(n) | obj += n |
-= | __isub__(n) | obj -= n |
*= | __imul__(n) | obj *= n |
/= | __itruediv__(n) | obj /= n |
%= | __imod__(n) | obj %= n |
**= | __ipow__(n) | obj **= n |
//= | __ifloordiv__(n) | obj //= n |
<<= | __ilshift__(n) | obj <<= n |
>>= | __irshift__(n) | obj >>= n |
@= | __imatmul__(n) | obj @= n |
|= | __ior__(n) | obj |= n |
^= | __ixor__(n) | obj ^= n |
&= | __iand__(n) | obj &= n |
@ | __matmul__(n) | obj @ b |
位运算¶
运算符 | 对象方法 | 描述 |
---|---|---|
& | __and__(n) | obj & n |
| | __or__(n) | obj | n |
^ | __xor__(n) | obj ^ n |
~ | __inv__() 或 __invert__() | ~obj |
<< | __lshift__(n) | obj << n |
>> | __rshift__(n) | obj >> n |
逻辑运算¶
注意 and,or,not 和位运算符 &,|,~ 的区别。逻辑运算使用对象的布尔值判断。
运算符 | 对象方法 | 描述 |
---|---|---|
and | 内部定义 | 由 __bool__() 和 __len__() 决定 |
or | 同上 | 同上 |
not | 同上 | 同上 |
成员运算¶
成员运算通常用于列表,元组,字典和集合等符合数据类型,类型实现 __contains__() 函数。
运算符 | 对象方法 | 描述 |
---|---|---|
in | __contains__(n) | obj in n |
not in | !__contains__(n) | obj not in n |
身份运算¶
身份运算使用 id() 函数判断两个对象地址是否相同。注意与比较运算的区别。
运算符 | 对象方法 | 描述 |
---|---|---|
is | 内部定义 | id(obj) == id(n) |
is not | 同上 | id(obj) != id(n) |
is 用于判断两个变量是否引用同一个, 会对比其中两个变量的地址。
其他对象方法¶
对象方法 | 操作 | 描述 |
---|---|---|
__dir__() | dir(obj) | 获取属性字典 |
__bool__() | bool(obj) if obj: | 转换为布尔值 |
__str__() | str(obj) | 转换为字符串 |
__repr__() | repr(obj) | 表达式 |
__delattr__(attr) | del obj.attr | 删除对象属性 |
__setattr__(attr,val) | obj.attr = val | 设置对象属性 |
__getattr__(attr) | getattr(obj, attr) | 获取对象属性 |
__delitem__(index) | del obj[index] | 删除 index 索引元素 |
__getitem__(index) | obj[index] slice | 获取索引值 |
__setitem__(index, v) | obj[index] = v | 根据索引设置值 |
__index__() | index(obj) | 获取索引 |
__call__() | obj() | 可调用 callable 对象 |
__iter__() | iter(obj) | 可迭代 iterable 对象 |
__next__() | next(obj) | 迭代器 iterator |
__hash__() | hash(obj) | 求hash值,如同时实现 __eq__ 则是可散列对象 |
__sizeof__() | sys.getsizeof(obj) | 获取对象占用内存大小 |
__enter__ __exit__() | with as | 自动清理 |
实例所属类的判定¶
判断一个对象是否是某个类的实例,可以用 isinstance() 内置方法判断。
0 1 2 3 4 5 6 7 8 9 10 11 12 13 | print(isinstance([], list))
print(isinstance("123", str))
print(isinstance("123", object))
print(isinstance(engineer, Employee))
print(isinstance(engineer, Employee))
print(isinstance(Programmer, Employee))
>>>
True
True
True
True
True
False
|
在继承关系中,如果一个对象的是某个类的子类的实例,这个对象也被认为是这个类的实例。所有对象都是 object 基类的实例。
多重继承的顺序¶
继承是面向对象编程的一大特征,继承可以使得子类具有父类的属性和方法,并可对属性和方法进行扩展。Python 中继承的最大特点是支持多重继承,也即一个类可以同时继承多个类。
我们可以在新类中使用父类定义的方法,也可以定义同名方法,覆盖父类的方法,还可以在自定义的方法中使用 super() 调用父类的同名方法。那么如果从多个类继承,多个类中又实现了同名的方法,如何确定它们的继承顺序呢?
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | class A(object):
def f0(self):
print('A f0')
def f1(self):
print('A f1')
class B(object):
def f0(self):
print('B f0')
def f1(self):
print('B f1')
class C(B):
def f0(self):
print('C f0')
class D(A, B):
def f1(self):
print('D f1')
class E(D, B):
pass
class F(E, C, B):
pass
print(F.__mro__)
f = F()
f.f0()
f.f1()
|
这种继承顺序被称为方法解释顺序(MRO,Method Resolution Order)。Python2.3 版本后采用 C3 线性序列算法来计算 MRO。
类之间的继承关系可以用有向无环图(DAG, Directed Acyclic Graph)来描述,每个顶点代表一个类,顶点之间的有向边代表类之间的继承关系。C3算法对所有顶点进行线性排序。
生成线性顶点序列的步骤如下:
- 找到入度为0的点 F,把 F 拿出来,把 F 相关的边剪掉,线性序列为 {F}
- 现在有两个入度为0的点(E, C),按照上一步被剪掉的类 F 中声明顺序优先原则,取 E,剪掉 E 相关的边,这时候的排序是{F, E}
- 现在有两个入度为0的点(D, C),按照上一步被剪掉的类 E 中声明顺序优先原则,剪掉 D 相关的边,这时候排序是{F, E, D}
- 现在有两个入度为0的点(C, A),按照上一步被剪掉的类 D 中声明顺序优先原则,剪掉 A 相关的边,这时候的排序是{F, E, D, A}
- 这时入度为0的点只有 C,取 C,剪掉 C 相关的边,这时候的排序是{F, E, D, A, C}
- 这时入度为0的点只有 B,取 B,剪掉 B 相关的边,这时候的排序是{F, E, D, A, C,B},此时只剩下基类 object
- 所以最后的排序是{F, E, D, A, C, B, object}
最终按照上述排序查找 f0() 和 f1() 的定义,上例中的结果为:
0 1 2 3 4 | >>>
(<class '__main__.F'>, <class '__main__.E'>, <class '__main__.D'>,
<class '__main__.A'>, <class '__main__.C'>, <class '__main__.B'>, <class 'object'>)
A f0
D f1
|
注意,Python2.3 以前的版本,不继承任何父类的类被称为经典类(Classic Class),使用的是深度优先排序算法。在 Python3 任何类都会默认继承 object,被称为新式类(New-style Class)。
我们可以使用 PyCharm 生成类的继承图,在任意源码文件右击,选择 Diagrams->Show Diagram…,在显示的继承图界面右击选择 Layout,可以用多种方式显示继承关系。
枚举类¶
Python 基本数据类型没有支持枚举,但是提供了 enum 模块。它实现了 Enum 类,用来定义唯一只读的序列集。 枚举类型的行为类似 namedtuple。
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | from enum import Enum
Animal = Enum('Animal', 'ANT BEE CAT DOG')
print(Animal.__name__)
print(Animal.ANT)
print(Animal.ANT.value)
for i in Animal:
print("%s: %s->%d" % (i, i.name, i.value))
>>>
Animal
Animal.ANT
1
Animal.ANT: ANT->1
Animal.BEE: BEE->2
Animal.CAT: CAT->3
Animal.DOG: DOG->4
|
Enum() 返回一个枚举类,第一个参数为枚举类名称,第二个参数使用空格分割,是枚举类的属性,从 1 开始被自动编号。注意这些属性均是只读的。上例中 Animal 类等价于下面的类定义:
0 1 2 3 4 | class Animal(Enum):
ANT = 1
BEE = 2
CAT = 3
DOG = 4
|
unique 装饰器可以自动检查定义中是否有重复值。
0 1 2 3 4 5 6 | from enum import unique
@unique
class Animal(Enum):
ANT = 1
BEE = 2
CAT = 3
DOG = 3
|
元类 metaclass¶
“元类就是深度的魔法,99%的用户应该根本不必为此操心。如果你想搞清楚究竟是否需要用到元类,那么你就不需要它。那些实际用到元类的人都非常清楚地知道他们需要做什么,而且根本不需要解释为什么要用元类。” —— Python界的领袖 Tim Peters
就笔者看来,元类是 Python 附送的彩蛋,如果你需要动态创建类,请使用 type() 内建方法。
生成器和迭代器¶
生成器¶
列表作为一个容器,其所占内存大小和元素个数成正比,也即每增加一个元素,那么内存中就要分配一块区域来存储它。我们看一个例子:
0 1 2 3 4 5 6 7 8 9 10 | import sys
list0 = [1] * 100
list1 = [1] * 1000000
print(sys.getsizeof(list0))
print(sys.getsizeof(list1))
>>>
864
8000064
|
list1 列表元素个数是 list0 元素个数的 10000 倍,所占内存也大约大 10000 倍。
如果我们要处理更多元素,那么所占内存就呈线性增大,所以受到内存限制,列表容量是有限的。通常我们并不会一次处理所有元素,而只是集中在其中的某些相邻的元素上。所以如果列表元素可以用某种算法用已知量推导出来,就不必一次创建所有的元素。这种边循环边计算的机制,称为生成器(generator),生成器是用时间换空间的典型实例。
生成器通常由两种方式生成,用小括号()表示的生成器表达式(generator expression)和生成器函数(generator function)。
生成器表达式¶
在生成列表和字典时,可以通过推导表达式完成。只要把推导表达式中的中括号换成小括号就成了生成器表达式。
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | list0 = [x * x for x in range(5)]
print(list0)
list_generator0 = (x * x for x in range(5))
print(list_generator0)
list_generator1 = (x * x for x in range(5000000))
print(sys.getsizeof(list_generator0))
print(sys.getsizeof(list_generator1))
>>>
[0, 1, 4, 9, 16]
<generator object <genexpr> at 0x000002C7B9955B48>
88
88
|
显然生成器对象的大小不会因为生成元素的上限个数而增大,此外不能够像列表被打印出来,那么如何获取通过生成器获取每一个元素呢? 借助 Python 内建方法 next():
0 1 2 3 4 5 6 7 8 9 10 | list_generator0 = (x * x for x in range(3))
print(next(list_generator0))
print(next(list_generator0))
print(next(list_generator0))
print(next(list_generator0)) # 触发 StopIteration 异常
>>>
0
1
4
StopIteration
|
每次调用 next() 方法时,总是根据最后的值和生成器给出的生成方法来计算下一个值。直到最后一个元素,抛出 StopIteration 异常。generator 也是可迭代对象,通常不会使用 next() 来逐个获取元素,而是使用 for in,它自动在遇到 StopIteration 异常时结束循环。
0 1 2 3 4 5 6 7 8 9 | list_generator0 = (x * x for x in range(3))
print(isinstance(list_generator0, Iterable))
for i in list_generator0:
print(i)
>>>
True
0
1
4
|
生成器函数¶
通过生成器表达式来生成 generator 是有局限的,比如斐波那契数列用表达式写不出来,复杂的处理需要生成器函数完成。
0 1 2 3 4 5 6 7 8 9 10 | def fibonacci(n):
i, j = 0, 1
while(i < n):
print(i, end=' ')
i, j = j, i + j
fibonacci(5)
print(type(fibonacci))
>>>
0 1 1 2 3 <class 'function'>
|
很容易写出打印斐波那契数列的函数,参数表示生成的元素个数。有时候我们不需要打结果一个个打印出来,而是要把这种推导算法封装起来,把 fibonacci() 函数变成一个生成器函数。只需要把 print 这一行替换为 yielb i
即可。
如果一个函数定义中包含 yield 表达式,那么这个函数就不再是一个普通函数,而是一个生成器函数。yield 语句类似 return 会返回一个值,但它会记住这个返回的位置,下次 next() 迭代就从这个位置下一行继续执行。
0 1 2 3 4 5 6 7 8 9 10 11 12 | def fib_generator(n):
i, j = 0, 1
while(i < n):
yield i
i, j = j, i + j
print(type(fib_generator))
print(type(fib_generator(5)))
>>>
<class 'function'>
<class 'generator'>
|
生成器函数并不是生成器,它运行返回后的结果才是生成器。
0 1 2 3 4 5 | generator = fib_generator(5)
for i in generator:
print(i, end=' ')
>>>
0 1 1 2 3
|
生成器的本质¶
任何一个生成器都会定义一个名为 __next__ 的方法,这个方法要在最后一个元素之后需抛出 StopIteration 异常。next() 函数的本质就是调用对象的 __next__()。这个方法要么返回迭代的下一项,要么引起结束迭代的异常 StopIteration,下面的示例揭示了生成器的本质。
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | class FibGenerator():
def __init__(self, n):
self.__n = n
self.__s0 = 0
self.__s1 = 1
self.__count = 0
def __next__(self): # 用于内建函数 next()
if self.__count < self.__n:
ret = self.__s0
self.__s0, self.__s1 = self.__s1, (self.__s0 + self.__s1)
self.__count += 1
return ret
else:
raise StopIteration
def __iter__(self): # 用于 for 循环语句
return self
fg = FibGenerator(5)
print(type(fg))
print(isinstance(fg, Iterable))
for i in fg:
print(i, end=' ')
>>>
<class '__main__.FibGenerator'>
True
0 1 1 2 3
|
示例中如果没有定义 __iter__() 方法则只能使用 next() 函数进行迭代,当它定义后,就可以使用 for 和 in 语句访问了,同时定义了这两种方法的对象称为迭代器(Iterator)。
迭代和迭代对象¶
在 Python 中通过 for in 对对象进行遍历的操作被称为迭代(Iteration),可以进行迭代操作的对象被称为可迭代 (Iterable) 对象,例如字符串,列表和元组。如何判断一个对象是否可迭代呢?
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | from collections import Iterable
print(isinstance(1, Iterable))
print(isinstance('abc', Iterable))
print(isinstance([1, 2, 3], Iterable))
print(isinstance({'name': 'val'}, Iterable))
print(isinstance(range(10), Iterable))
print(type(range(10)))
>>>
False
True
True
True
True
<class 'range'>
|
除了常见的基本数据类型是可迭代对象外,文件对象,管道对象以及更复杂的生成器等也是可迭代对象。
迭代器¶
可迭代对象和迭代器¶
如 list、tuple、dict、set、str、range、enumerate 等这些可以直接用于 for 循环的对象称为可迭代(Iterable)对象,也即它们是可迭代的。但是生成器不但可以作用于 for 和 in 语句,还可以被 next() 函数不断调用并返回下一个值,直到最后抛出 StopIteration 错误,它是一个迭代器(Iterator)。
- 可迭代对象,需要提供 __iter__()方法,否则不能被 for 语句处理。
- 迭代器必须同时实现 __iter__() 和 __next__()方法,__next__() 方法包含了用户自定义的推导算法,这是迭代器对象的本质。生成器表达式和生成器函数产生生成器时,会自动生成名为 __iter__ 和 __next__ 的方法。参考 Python 迭代对象。
0 1 2 3 4 5 6 7 8 9 10 11 12 | list_generator0 = (x * x for x in range(3))
print('__iter__' in dir(list_generator0))
print('__next__' in dir(list_generator0))
fg = fib_generator(5)
print('__iter__' in dir(fg))
print('__next__' in dir(fg))
>>>
True
True
True
True
|
如同判断对象是否可迭代一样,可以使用isinstance()判断一个对象是否是 Iterator 对象:
0 1 2 3 4 5 6 | list_generator0 = (x * x for x in range(3))
isinstance(list_generator0, Iterator)
isinstance(list_generator0, Iterable)
>>>
True
True
|
迭代对象类型判断¶
另外一种方式是通过 iter() 函数来判断,这种方法是最准确的(可迭代对象并不一定是 Iterable 或者 Iterator 类的实例),后面会详解 iter()内建函数的作用。
0 1 2 3 4 5 6 7 8 9 10 11 12 | # 判断可迭代对象
def is_iterable(obj):
status = True
try:
iter(obj)
except TypeError:
status = False
return status
# 判断迭代器对象
def is_iterator(obj):
return is_iterable(obj) and obj is iter(obj)
|
由上述分析可知,只要一个对象是迭代器,那么它一定是可迭代对象,反过来不成立。
生成迭代器¶
iter() 内建方法可以把list、dict、str等可迭代对象转换成迭代器。
0 1 2 3 4 5 6 | list0 = [0, 1, 2]
iter0 = iter(list0)
print(type(iter0))
>>>
<class 'list_iterator'>
|
除字典外,一个对象只要实现了 __getitem__() 方法,就认为它是序列类型,序列类型总是可迭代的,循环作用在序列类型上的本质参考 索引访问和循环。
对于序列类型,字典,还有更复杂的可迭代类型如 range,Python 内建了对应的迭代器对它们进行迭代操作,它们无需实现 __next__() 方法,iter() 函数会返回对应的内建迭代器。
0 1 2 3 4 5 6 7 8 9 10 11 12 13 | print(type(iter("")))
print(type(iter([])))
print(type(iter({})))
print(type(iter({}.values())))
print(type(iter(range(5))))
>>>
<class 'str_iterator'>
<class 'list_iterator'>
<class 'dict_keyiterator'>
<class 'dict_valueiterator'>
<class 'range_iterator'>
|
需要强调,可迭代对象并不一定是 Iterable 或者 Iterator 类的实例,可以参考 索引访问和循环。
0 1 2 3 4 5 6 7 8 9 10 11 12 | ......
print(isinstance(rwstr, Iterator))
print(isinstance(rwstr, Iterable))
print(is_iterable(rwstr))
print(is_iterator(rwstr))
>>>
False
False
True
False
|
而对于迭代器类型来说,iter() 函数直接执行对象中的 __iter__()函数并返回,循环操作的实质如下所示:
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | for element in iterable:
# do something with element
# 等价于如下操作
# create an iterator object from that iterable
iter_obj = iter(iterable)
# infinite loop
while True:
try:
# get the next item
element = next(iter_obj)
# do something with element
except StopIteration:
# if StopIteration is raised, break from loop
break
|
iter(iterable) -> iterator
iter(callable, sentinel) -> iterator
查看 iter() 函数定义,它的第二种用法比较特殊,如果提供了第二个参数 sentinel,那么第一个参数必须是一个可调用对象,比如函数。下面示例用于实现读取到指定的行:
0 1 2 3 4 5 6 7 8 | # 读取到 SystemExit\n 的前一行
with open('test.txt', 'r', encoding="utf-8") as fp:
for line in iter(fp.readline, 'SystemExit\n'):
print(line)
# 只处理 1 和 2
list0 = [1, 2, 3]
for i in iter(list0.pop, 3):
print(i)
|
无限迭代器¶
所谓无限迭代器,也即是没有限制,永不抛出 StopIteration 异常。下面是一个奇数生成器,可以无限制地生成奇数。
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | class OddIter:
def __iter__(self):
self.num = 1
return self
def __next__(self):
num = self.num
self.num += 2
return num
for i in OddIter():
if i > 5: # 处理无限生成器需要退出分支
break
print(i, end=' ')
>>>
1 3 5
|
惰性计算¶
惰性计算又称为惰性求值(Lazy Evaluation),是一个计算机编程中的概念,它的目的是要最小化计算机要做的工作,尽可能延迟表达式求值。延迟求值特别用于函数式编程语言中。在使用延迟求值的时候,表达式不在它被绑定到变量之后就立即求值,而是在该值被取用的时候求值。惰性计算的最重要的好处是它可以构造一个无限的数据类型。
具有惰性计算特点的序列称为惰性序列,Python 中的迭代器就是一个惰性序列,调用 iter() 返回一个 iterator 并赋值给一个变量后不会立即进行求值,而是当你用到其中某些元素的时候才去求某元素的值。
惰性计算还可以在大规模数据处理中平滑处理时间,提高内存使用率。当处理大规模数据时,一次性进行处理往往是不方便的。
很多语言对惰性计算提供了支持,比如 Java,Scala,当然 Python 也不例外,它是函数式编程语言的一大特点。
函数和装饰器¶
函数特性¶
函数是大部分高级编程语言的构成基础,本小结主要总结在 Python 中函数的一些特性和高阶函数。
定义和调用顺序¶
由于 Python 是一种解释执行的脚本语言,解释器在执行到某行语句时,当遇到一个符号调用时,比如一个函数,会根据函数名去对应的作用域去回溯查找,直至内建函数,如果找不到,则报错未定义,所以如果函数定义在执行语句之后,是不允许的。
0 1 2 3 4 5 6 | a()
def a():
print("function a")
>>>
NameError: name 'a' is not defined
|
但是如果是在定义一个对象时,比如一个函数 a ,引用另一个未定义函数 b,而在执行语句调用 a 函数之前,b 已经定义了,则不会报错,因为解释器遇到 b 的定义时会加入到当前的作用域,然后再执行到该语句时,作用域中已经存在 b 的定义了。
0 1 2 3 4 5 6 7 8 9 | def a():
b()
def b():
print("function b")
a()
>>>
function b
|
匿名函数¶
如果一个函数使用简单的表达式就可以实现所需功能,那么就无需显式定义一个函数,lamdba 表达式可以返回一个没有名字的函数,也即匿名函数。
比如一些函数需要函数作为参数,那么直接使用 lambda 就很简便。
0 1 2 3 4 5 6 | homo_add = lambda x, y : x + y # 定义匿名函数
print(type(homo_add))
print(homo_add(1, 2))
>>>
<class 'function'>
3
|
上面的匿名函数定义等价于:
0 1 | def homo_add(x, y):
return x + y
|
把匿名函数赋值给变量 homo,所以它的类型是 function。通常在需要定义匿名函数的地方,直接使用 lambda 表达式即可,无需再给它一个名字:
0 1 2 3 | print(list(map(lambda x: x * x * x, [1, 2, 3, 4])))
>>>
[1, 8, 27, 64]
|
函数参数类型¶
Python 中的函数参数类型一共有五种,参考 inspect 模块 ,分别是:
- POSITIONAL_ONLY 位置参数,内置函数或模块使用,用户无法自定义一个只支持位置参数的函数。
- POSITIONAL_OR_KEYWORD 位置或关键字参数,参数同时支持位置或者关键字传递给函数。
- VAR_POSITIONAL 可变长参数,任意多个位置参数通过元组传递给函数。
- KEYWORD_ONLY 关键字参数,也被称为命名参数,通过指定的键值对传递给函数。
- VAR_KEYWORD 可变关键字参数,任意多个键值对参数通过字典传递给函数。
位置或关键字参数¶
首先看一下只(ONLY)支持通过参数位置来传递给函数的位置参数。它们没有名字,不能通过键值对传递。只有内置函数或者模块使用,用户无法自定义一个只支持位置参数的函数。
0 1 2 3 4 5 6 7 8 | def foo(n):
print(n)
foo(1)
foo(n = 2)
>>>
1
2
|
我们看到自定义的函数 foo(),不仅可以通过第一个参数位置来传递实参 1,还可以通过名称 n 来传递参数 2。这里的 n 就是一个位置或关键字参数。它是最常用的参数传递方式。
而有一些内置函数,无法通过名称来传递,否则会报不支持关键参数的错误,比如内置函数 oct(x),ord(c),divmod(x, y)等等。它们的函数手册里一般就使用一个字母来表示一个参数,常用的比如 x,y,c。
ord(c, /)
Return the Unicode code point for a one-character string.
0 1 2 3 4 5 | ord(c='1')
>>>
ord(c='1')
TypeError: ord() takes no keyword arguments
|
可变参数¶
可变参数用一个 * 号来声明,它把所有接收到的,未被位置或关键字参数处理的参数放入一个元组。
0 1 2 3 4 5 6 7 8 | def variable_args(name="default", *args):
print("name: %s" % name)
print(args)
variable_args("John", "Teacher", {"Level": 1})
>>>
name: john
('Teacher', {'Level': 1})
|
可以看到,”John” 均通过参数位置传递给了形参 name,后边多余的参数全部传递给了 *args
,它是一个元组。注意键值对参数不能被它处理。
关键字参数¶
0 1 2 3 4 5 6 7 8 | def keyword_only_args(name="default", *args, age):
print("name: %s, age: %d" % (name, age))
print(args)
keyword_only_args("John", "Teacher", {"Level": 1}, age=30)
>>>
name: John, age: 30
('Teacher', {'Level': 1})
|
由于 age 形参位于可变参数之后,那么它的位置是不明确的,此时只能指定关键字 age,以键值对的方式传递它,被称为关键字参数。此时 args 元组中不会处理它。
可变关键字参数¶
可变关键字参数通过前缀 ** 来声明,这种参数类型可以接收 0 个或多个键值对参数,并存入一个字典。
0 1 2 3 4 5 6 7 8 9 10 11 | def keyword_variable_args(name="default", *args, age, **kwargs):
print("name: %s, age: %d" % (name, age))
print(args)
print(kwargs)
keyword_variable_args("John", "Teacher", {"Level": 1}, id="332211",
city="New York", age=30)
>>>
name: John, age: 30
('Teacher', {'Level': 1})
{'id': '332211', 'city': 'New York'}
|
通过以上的示例,我们看到参数处理是有优先级的,首先通过位置匹配,然后进行关键字匹配,最后剩下的所有参数按照是否提供参数名来对应到可变参数或可变关键字参数。
可变参数函数¶
在了解了 Python 参数类型之后,我们可以定义一个可以处理任意类型任意参数数目的函数。
0 1 2 3 4 5 6 7 8 | def test_args(*args, **kwargs):
print(args)
print(kwargs)
test_args(1, 2, {"key0": "val0"}, name="name", age=18)
>>>
(1, 2, {'key0': 'val0'})
{'name': 'name', 'age': 18}
|
test_args() 是一个可以接受任意多个参数的函数。由于参数处理是有优先级的,kwargs 和 args 顺序不可颠倒。
函数参数传递形式¶
在介绍了 Python 参数类型后,我们可以通过两种形式为形参提供实参。
0 1 2 3 4 5 6 7 8 | def test_input_args(list0, num0, name="Tom"):
print("list:%s, num:%d, name:%s" % (str(list0), num0, name))
test_input_args([1], 2, name="John")
test_input_args(*([1], 2), **{"name": "John"})
>>>
list:[1], num:2, name:John
list:[1], num:2, name:John
|
可以通过常用位置和关键字传递,也可以使用可变参数和可变关键字参数传递,它们是等价的。有了第二种参数传递形式,就可以在一个函数中调用不同的函数了,这一特性对于实现装饰器函数非常重要。
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | def func0(n):
print("from %s, %d" %(func0.__name__, n))
def func1(m, n):
print("from %s, %d" %(func0.__name__, m + n))
def test_call_func(func, *args, **kwargs):
func(*args, **kwargs)
test_call_func(func0, 1)
test_call_func(func1, 1, 2)
>>>
from func0, 1
from func0, 3
|
高阶函数¶
functools 模块提供了一系列的重量级函数,这些函数有一个特点,函数调用其他函数完成复杂功能,或把一个函数作为返回值,这类函数被称为高阶(Higher-order)函数。 由于历史原因,多数高阶函数从内置函数中封装进 functools 模块,有些函数还没有,比如 map()。
Python3.x 中对这些函数进行了功能扩展,它们可以处理可迭代对象,并返回可迭代对象,具有惰性计算的特点,参考 惰性计算 。
map¶
map(func, *iterables) --> map object
Make an iterator that computes the function using arguments from
each of the iterables. Stops when the shortest iterable is exhausted.
map() 根据传入的函数对指定迭代对象做迭代处理,这一行为很像数学概念中的映射。
0 1 2 3 4 5 6 7 8 9 | mapobj = map(str, [1, 2, 3])
print(type(mapobj))
print(mapobj is iter(mapobj))
print(list(mapobj))
>>>
<class 'map'>
True
['1', '2', '3']
|
Python2.x 返回列表,Python3.x 则返回 map 对象,它是一个迭代器。这个改进具有重大的意义,可以用来处理无限序列。
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | def uint_creater():
i = 0
while(True):
yield i
i += 1
cube = map(lambda x: x * x * x, uint_creater())
for i in cube:
if i < 10000000000:
continue
if i > 10099999999:
break
print(i)
>>>
10007873875
10021812416
10035763893
|
上面的示例用于查看特定范围内可以用来表示立方数的数,在范围是上百亿级别也和普通小数一样处理。可以应用在数论研究领域,比如进行质数的稀疏度分析。 由于第二个参数可以是多个迭代对象,我们还可以对数据进行并行操作:
0 1 2 3 4 5 6 7 8 | funcs = [lambda x: x * x, lambda x: x * x * x]
map_func = lambda f: f(i)
for i in range(4):
print(list(map(map_func, funcs)))
>>>
[0, 0]
[1, 1]
[4, 8]
|
如果的函数列表中的函数具有多个参数如何处理呢? 只要改写传入函数的参数个数即可,这里计算列表中每个成对的元素的差与和:
0 1 2 3 4 5 6 7 8 9 | funcs = [lambda x, y: abs(x - y), lambda x, y: y + x]
map_func = lambda f: f(i[0], i[1])
for i in [[1, 2], [3, 4]]:
value = map(map_func, funcs)
print(list(value))
>>>
[1, 3]
[1, 7]
|
如果传入的函数有多个参数,如何处理呢?根据函数参数个数,来传递多个参数序列。例如依次求 pow(2, 2),pow(3, 3) 和 pow(4, 4) 的值:
0 1 2 3 | print(list(map(pow, [2, 3, 4], [2, 3, 4])))
>>>
[4, 27, 256]
|
map() 函数的本质等同于如下函数:
0 1 2 3 4 5 | def homo_map(func, seq):
result = []
for x in seq:
result.append(func(x))
return result
|
reduce¶
reduce() 函数有两个参数,它把 function 计算结果结果继续和序列的下一个元素做累积计算。
reduce(function, sequence[, initial]) -> value
Apply a function of two arguments cumulatively to the items of a sequence,
from left to right, so as to reduce the sequence to a single value.
reduce() 的行为等价于:
0 1 2 3 4 | def homo_reduce(func, seq):
result = seq[0]
for next in seq[1:]:
result = func(result, next)
return result
|
以下示例计算列表中所有数值的乘积。
0 1 2 3 4 5 | from functools import reduce
total = reduce((lambda x, y: x * y), [1, 2, 3, 4])
print(total)
>>>
24
|
filter¶
filter(function or None, iterable) --> filter object
Return an iterator yielding those items of iterable for which function(item)
is true. If function is None, return the items that are true.
filter() 方法与 map() 类似,和 map()不同的是,filter() 把传入的函数依次作用于每个元素,然后根据返回值的真假决定保留还是过滤掉该元素。
0 1 2 3 4 5 | def homo_filter(func, seq):
result = []
for x in seq:
if func(x)
result.append(x)
return result
|
下面的示例用于过滤空字符串:
0 1 2 3 4 5 6 7 8 9 | strs = ['hello', ' ', 'world']
ret = filter(lambda x : not x.isspace(), strs)
print(type(ret))
print(ret == iter(ret))
print(list(ret))
>>>
<class 'filter'>
True
['hello', 'world']
|
filter() 返回值是一个 filter 对象,它也是一个迭代器。filter() 还可以用于求交集:
0 1 2 3 4 5 | a = [4, 0, 3, 5, 7]
b = [1, 5, 6, 7, 8]
print(list(filter(lambda x: x in a, b)))
>>>
[5, 7]
|
sorted¶
sorted(iterable, *, key=None, reverse=False) --> new sorted list
Return a new list containing all items from the iterable in ascending order.
sorted() 相对于列表自带的排序函数 L.sort() 具有以下特点:
- 将功能扩展到所有的可迭代对象。
- L.sort 直接作用在列表上,无返回,sortd() 则返回新的排序列表。
- sortd() 是稳定排序,且经过优化,排序速度更快。
排序的本质在于对两个需要排序的元素进行大小的比较,来决定位置的先后,对于数字和字符串类型比较好判断。
0 1 2 3 4 5 6 7 8 9 10 | print(sorted([5, 2, 3, 1, 4]))
print(sorted((5, 2, 3, 1, 4)))
print(sorted({1: 'D', 2: 'B', 3: 'B', 4: 'E', 5: 'A'})) # 字典默认使用键名排序
# sorted() 返回列表类型,用它对字符串排序,注意类型转换
print(''.join(sorted("hello")))
>>>
[1, 2, 3, 4, 5]
[1, 2, 3, 4, 5]
[1, 2, 3, 4, 5]
ehllo
|
为 key 指定函数参数,该函数只能接受一个参数,它的返回值作为比较的关键字,比如忽略大小写排序:
0 1 2 3 4 | sorted_list = sorted("This is a test string from Andrew".split(), key=str.lower)
print(sorted_list)
>>>
['a', 'Andrew', 'from', 'is', 'string', 'test', 'This']
|
对于复杂对象,我们可以把元素中的部分成员作为排序关键字:
0 1 2 3 4 5 | scores = {'John': 15, 'Bill': 18, 'Kent': 12}
new_scores = sorted(scores.items(), key=lambda x:x[1], reverse=True)
print(new_scores)
>>>
[('Bill', 18), ('John', 15), ('Kent', 12)]
|
由于字典默认以 key 来迭代,对字典进行排序时,第一个参数要使用 dict.items() 来转化为 dict_items 对象。
如果要对自定义的类对象排序,可以选择某个对象成员,下面的示例使用年龄对学生进行排序:
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | class Student():
def __init__(self, name, grade, age):
self.name = name
self.grade = grade
self.age = age
def __repr__(self):
return repr((self.name, self.grade, self.age))
student_objects = [
Student('john', 'A', 15),
Student('jane', 'B', 12),
Student('dave', 'B', 10),
]
print(sorted(student_objects, key=lambda student: student.age))
>>>
[('dave', 'B', 10), ('jane', 'B', 12), ('john', 'A', 15)]
|
key 参数还可以指定 operator 模块提供的 itemgetter 和 attrgetter 方法。
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | student_tuples = [ ('john', 'A', 15),
('jane', 'B', 12),
('dave', 'B', 10),]
print(sorted(student_tuples, key=lambda student: student[2])) # age 排序
from operator import itemgetter, attrgetter
print(sorted(student_tuples, key=itemgetter(2))) # age 排序
print(sorted(student_objects, key=attrgetter('age')))
print(sorted(student_tuples, key=itemgetter(1,2))) # 先以 grade 排序,再以 age 排序
print(sorted(student_objects, key=attrgetter('grade', 'age')))
>>>
[('dave', 'B', 10), ('jane', 'B', 12), ('john', 'A', 15)]
[('dave', 'B', 10), ('jane', 'B', 12), ('john', 'A', 15)]
[('dave', 'B', 10), ('jane', 'B', 12), ('john', 'A', 15)]
[('john', 'A', 15), ('dave', 'B', 10), ('jane', 'B', 12)]
[('john', 'A', 15), ('dave', 'B', 10), ('jane', 'B', 12)]
|
reverse 参数默认以升序排序,如果为 True 则以降序排序。更详细的介绍参考 Python howto sorting 。
partial¶
partial(func, *args, **keywords) - new function with partial application
of the given arguments and keywords.
一些函数提供多种参数,有时我们只需要改变其中的一些参数,而另一些参数只需要固定的值,那么每次都要把所有参数都补全是件繁琐的事情。 partial() 方法可以将一个函数的参数固定,并返回一个新的函数。
int()函数可以把字符串转换为整数,当仅传入字符串时,int()函数默认按十进制转换,其中有一个 base 参数可以指定转换的进制。
0 1 2 3 | print(int('123'))
print(int('123', base=8))
print(int('a', base=16))
print(int('101', base=2))
|
如果要转换大量的十六进制字符串,每次都传入 base = 16 就很繁琐,为了简便可以想到定义一个 hexstr2int() 的函数,默认把 base = 16 传进去:
0 1 2 3 4 5 6 | def hexstr2int(x):
return int(x, base=16)
print(hexstr2int('a'))
>>>
10
|
functools.partial() 方法可以直接创建一个这样的函数,而不需要自己定义 hexstr2int():
0 1 2 3 4 5 6 7 | from functools import partial
hexstr2int = partial(int, base=16)
print(hexstr2int('a'))
print(type(hexstr2int))
>>>
10
<class 'functools.partial'>
|
注意到它返回的是一个 functools.partial 类型,而不是一个普通的函数,它等价于定义了一个如下的函数:
0 1 2 3 4 | def hexstr2int(x):
args = (x)
kwargs = {'base': 16}
return int(*args, **kwargs)
|
如果我们不使用关键字参数,而是直接使用值,那么将作为位置参数传递给 int(),例如:
0 1 2 3 4 5 6 7 | hexstr2int = partial(int, 'a')
#等价于
def hexstr2int(x):
args = ('a')
kwargs = {'base': x}
return int(*args, **kwargs)
|
如果一个函数有多个参数,那么就要区分这种参数的传递关系,我们看一个示例:
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | def func(a, b, c, d):
print("a %d, b:%d c:%d, d:%d" %(a, b, c, d))
return a * 4 + b * 3 + c * 2 + d
part_func = partial(func, 1, d=4)
part_func(2, 3)
part_func = partial(func, b=1, d=4)
part_func(2, c=3)
part_func0 = partial(part_func, c=3) # 嵌套
part_func0(2)
>>>
a 1, b:2 c:3, d:4
a 2, b:1 c:3, d:4
a 2, b:1 c:3, d:4
|
有些内置函数只有位置参数,没有关键字参数,如何实现定制函数呢?以 divmod() 为例,如果我们固定第一个参数,这很容易。
0 | tendivmode = partial(divmod, 10)
|
如果要固定第二个参数,就需要把 divmod() 内置方法的位置参数重定义为支持关键字传入的参数。例如:
0 1 2 3 | def homo_divmod(a, b):
return divmod(a, b)
divmod10 = partial(homo_divmod, b=10)
|
使用 partial() 的目的是为简化代码,让代码简洁清晰,但也要注意到它的副作用,由于它返回 functools.partial 类型,隐藏了某些逻辑,比如新函数没有函数名,让跟踪更困难。
作用域和闭包¶
在程序设计中变量所能作用的范围被称为作用域(scope),在作用域内,该变量是有效的,可以被访问和使用。
在介绍 Python 的作用域之前,先看一个名为 globals() 的内建函数。它返回当前运行程序的所有全局变量,类型为字典。
0 1 2 3 4 5 6 7 | print(type(globals()))
print(globals())
>>>
<class 'dict'>
{'__loader__': <_frozen_importlib.SourceFileLoader object at 0xb72acbac>,
'__name__': '__main__', '__package__': None, '__builtins__': <module 'builtins' (built-in)>,
'__file__': './scope.py', '__spec__': None, 'dict0': {...}, '__doc__': None, '__cached__': None}
|
块作用域¶
在代码块中定义的变量,它的作用域通常只在代码块中,这里测试下 Python 是否支持块作用域。
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | dict0 = globals()
print(len(dict0))
print(dict0.keys())
while True: # 在代码块中定义 block_para
block_var = "012345"
break
print(block_var)
dict0 = globals()
print(len(dict0))
print(dict0.keys())
>>>
012345
9
dict_keys(['__file__', '__spec__', '__builtins__', '__package__',
'__cached__', 'dict0', '__name__', '__loader__', '__doc__'])
10
dict_keys(['__file__', '__spec__', '__builtins__', '__package__', '__cached__',
'dict0', 'block_var', '__name__', '__loader__', '__doc__'])
|
从示例中,可以看出在 Python 中,在代码块结束后依然可以访问块中定义的变量,块作用域是不存在。代码块中的定义的变量的作用域就是代码块所在的作用域。默认就是全局作用域。在 globals() 的返回值中可以看到在代码块执行后,全局变量中出现了 block_var,为简便起见,这里只打印了全部变量名。
局部作用域¶
0 1 2 3 4 5 6 7 8 9 | def foo():
local_var = 0
foo()
print('local_var' in globals())
print(local_var)
>>>
False
NameError: name 'local_var' is not defined
|
即便执行了函数 foo(),local_var 实际上也分配过内存,执行依然报错,所以 local_var 的作用域也只是在函数内部,函数结束时,局部变量所占的资源就被释放了,外部无法再访问。
实际上,Python 中只有模块(module),类(class)以及函数(def、lambda)才会引入新的作用域,其它的代码块(如 if/elif/else/、try/except、for/while等)不会引入新的作用域。
作用域链¶
是否可以在函数中定义新的子函数,并调用子函数中呢?事实上,在 Python 中函数作为对象存在,函数可以作为另一个函数的参数或返回值,也可以在函数中嵌套定义函数。
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | def outer():
var0, var1 = "ABC", "DEF"
def inner():
var0 = "abc"
local_var = "123"
print(var0)
print(var1)
print(local_var)
print(var0)
inner()
outer()
# inner() 这里调用 inner()将报未定义错误
>>>
ABC
abc
DEF
123
|
内部函数只可以在包含它的外部函数中使用,也即它是局部的,相对于外部函数来说,内部函数是嵌入进来的,所以又被称为内嵌函数。从运行结果,可以得知:
- 内嵌函数中定义的变量只可在内嵌函数内使用
- 内嵌函数中可以访问外部函数定义的变量,如果内嵌函数中定义的变量与外部函数中变量重名,那么内嵌函数的作用域优先级最高。
变量的查找过程就像一条单向链一样,逐层向上,要么找到变量的定义,要么报错未定义。这种作用域机制称为作用域链。
函数作为返回值¶
函数名实际上就是一个变量,它指向了一个函数对象,所以可以有多个变量指向一个函数对象,并引用它。
0 1 2 3 4 5 6 7 | def foo():
return abs
myabs = foo()
print(myabs(-1))
>>>
1
|
以上示例直接把系统内建函数 abs() 作为返回值赋值给 myabs 变量,所以 myabs() 等价于 abs()。为了深入理解 Python 是如何处理函数作为返回值的,再看一个更复杂的例子。
0 1 2 3 4 5 6 7 8 9 10 11 12 | flist = []
for i in range(3):
def foo(x):
print(x + i)
flist.append(foo)
for f in flist:
f(1)
>>>
3
3
3
|
按照预期,程序应该输出 1 2 3,然而却得到 3 3 3,这是因为以下两点:
- Python 中没有块作用域,当循环结束以后,循环体中的临时变量 i 作为全局变量不会销毁,它的值是 2。
- Python 在把函数作为返回值时,并不会把函数体中的全局变量替换为实际的值,而是原封不动的保留该变量。
flist 列表中的函数等价于如下的函数实现:
0 1 2 | def flist_foo(x):
global i
print(x + i)
|
如果我们想要得到预期的效果,那么就要让全部变量变成函数内部的局部变量,把 i 作为参数传递给函数可以完成这一转换。
0 1 2 3 4 5 6 7 8 9 10 11 12 | flist = []
for i in range(3):
def foo(x, y = i):
print(x + y)
flist.append(foo)
for f in flist:
f(1)
>>>
1
2
3
|
闭包函数¶
闭包(closure)在 Python 中可以这样解释:如果在一个内部函数中,对定义它的外部函数的作用域中的变量(甚至是外层之外,只要不是全局变量,也即内嵌函数中还可以嵌套定义内嵌函数)进行了引用,那么这个子函数就被认为是闭包。所以我们上面例子中的 inner() 函数就是一个闭包函数,简称为闭包。
闭包具有以下两个显著特点,可以认为闭包 = 内嵌函数 + 内嵌函数引用的变量环境:
- 它是函数内部定义的内嵌函数。
- 它引用了它作用域之外的变量,但非全局变量。
如果我们将闭包作为外部函数的返回值,然后在外部调用这个闭包函数会怎样呢?
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | def offset(n):
base = n
def step(i):
return base + i
return step
offset0 = offset(0)
offset100 = offset(100)
print(offset0(1))
print(offset100(1))
>>>
1
101
|
按照常规分析,第一次调用 offset(0) 时,base 的值是 0,第二次调用 offset(100)后,base 的值应该变为 100,但是执行结束后,base 作为局部变量应该被释放了,也即不能再被访问了,然而结果却并非如此。
实际上在 Python 中,当内嵌函数作为返回值传递给外部变量时,将会把定义它时涉及到的引用环境和函数体自身复制后打包成一个整体返回,这个整体就像一个封闭的包裹,不能再被打开修改,所以称为闭包很形象。
对于上例中的 offset0 来说,它的引用环境就是变量 base = 0
,以及建立在引用环境上函数体 `` base + i `` 。 引用 offset0() 和执行下面的函数是等价的。
0 1 2 3 | def offset0(i):
base = 0
return base + i
|
四种作用域¶
Python 的作用域一共有4种,分别是:
- L (Locals)局部作用域,或作当前作用域。
- E (Enclosing)闭包函数外的函数中
- G (Globals)全局作用域
- B (Built-ins)内建作用域
Python 解释器查找变量时按照 L –> E –> G –>B 作用域顺序查找,如果在局部作用域中找不到该变量,就会去局部的上一层的局部找(例如在闭包函数中),还找不到就会去全局找,再者去内建作用域中查找。
上面的示例已经涉及到前三种作用域,下面的示例对内建作用域进行验证。
0 1 2 3 4 5 6 | def globals():
return "from local globals()"
print(globals())
>>>
from local globals()
|
系统内建的函数 globals() 被我们自定义的同名函数“拦截”,显然如果我们没有在全局作用域中定义此处的 globals(),则会去内建作用域中查找。
作用域同名互斥性¶
所谓作用域的同名互斥性,是指在不同的两个作用域中,若定义了同名变量,那么高优先级的作用域中不能同时访问这两个变量,只能访问其中之一。
0 1 2 3 4 5 6 7 8 9 10 11 | var = 0
def foo():
var = 1 # 定义了局部变量 var
print(var)
global var
print(var)
>>>
global var
^
SyntaxError: name 'var' is used prior to global declaration
|
global 声明 var 是全局变量,也即 global 可以修改作用域链,当访问 var 变量时而直接跳转到全局作用域查找, 错误提示在本语句前变量名 var 已经被占用了。所以函数体内的局部作用域内,要么只使用局部变量 var,要么在使用 var 前就声明是全局变量 var。
与以上示例类似,在内嵌函数中,也具有同样的特性,以下代码是在 Python 中使用闭包时一段经典的错误代码。
0 1 2 3 4 5 6 7 8 9 10 11 12 13 | def foo():
a = 0
def bar():
a = a + 1 # 或 a += 1
return a
return bar
c = foo()
print(c())
>>>
a = a + 1
UnboundLocalError: local variable 'a' referenced before assignment
|
以上代码并未如预期打印出来数字 1。根据闭包函数的机制进行分析,c 变量对应的闭包包含两部分,变量环境 a = 0
和函数体 a = a + 1
。
问题出在,函数体中的变量 a 和变量环境中的 a 不是同一个。
Python 语言规则指定,所有在赋值语句左边的变量名如果是第一次出现在当前作用域中,都将被定义为当前作用域的变量。由于在闭包 bar() 中,变量 a 在赋值符号 “=” 的左边,被 Python 认为是 bar() 中的局部变量。再接下来执行 c() 时,程序运行至 a = a + 1 时,因为先前已经把 a 定义为 bar() 中的局部变量,由于作用域同名互斥性,右边 a + 1 中的 a 只能是局部变量 a,但是它并没有定义,所以会报错。
引用 c() 和执行下面的函数是等价的。
0 1 2 3 4 | def c():
a = 0
local_a = local_a + 1
return local_a
|
nonlocal 声明¶
与 global 声明类似,nonlocal 声明可以在闭包中声明使用上一级作用域中的变量。
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | def foo():
a = 0
def bar():
nonlocal a
a += 1
return a
return bar
c = foo()
print(c())
print(c())
>>>
1
2
|
使用 nonlocal 声明 a 为上一级作用域中的变量 a,就解决了该问题,可以实现累加了。注意 nonlocal 关键字只能用于内嵌函数中,并且外层函数中定义了相应的局部变量,否则报错。
由闭包到装饰器¶
闭包和变量¶
尽管闭包函数可以引用外层函数中的变量,但是这个变量不能被动态改变。
在 函数作为返回值 一节中,已经看到 Python 在把函数作为返回值时,并不会把函数体中的全局变量替换为实际的值,而是原封不动的保留该变量。那么当这种情况出现在闭包中会怎样呢?
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | def fun():
flist = []
for i in range(3):
def foo(x):
print(x + i, end=' ')
flist.append(foo)
return flist
flist = fun()
for f in flist:
f(1)
>>>
3 3 3
|
结果是一样的,如果一个变量已被闭包函数引用,那么就要保证这个变量不会再被改变,否则闭包函数的行为将难以预知。除了 for 循环以外,while 循环也会导致相同问题,改进方法也一样,不再赘述。
装饰器的引入¶
在 Python 中,闭包函数最多的应用就是装饰器(Decorator)。 一个简单的日志生成的例子:
0 1 | def func(n):
print("from func(), n is %d!" % (n), flush=True)
|
已经存在了函数 func(),现在有一个新的需求,希望可以记录下函数的执行日志,我们可以在函数中添加一行记录日志的代码,但是如果有很多函数,这样做会费时费力,且代码重复冗长。一个容易想到的办法是重新定义一个日志函数,在调用完函数后,记录日志。
0 1 2 3 4 5 6 7 8 | def log(func):
func(0)
logging.debug('%s is called' % func.__name__)
log(func)
>>>
from func(), n is 0!
DEBUG:root:func is called
|
然而这样并不能彻底解决问题,对需要记录日志的函数的每一处调用都要调用新函数 log(),如果要取消日记记录,就要重新做一遍代码撤销的工作。这里就引入了装饰器。
装饰器¶
从装饰的实现方式上可以分为装饰器函数和装饰器类,也即分别使用函数或者类对其他对象(通常是函数或者类)进行封装(装饰)。
装饰器函数¶
无参装饰器¶
使用函数作为装饰器的方法如下:
0 1 2 3 4 5 6 7 8 9 10 11 12 | def log(func):
def wrapper(*args, **kwargs):
ret = func(*args, **kwargs)
logging.debug('%s is called' % func.__name__)
return ret
return wrapper
func = log(func)
func(0)
>>>
from func(), n is 0!
DEBUG:root:func is called
|
上面代码中的 wrapper() 是一个闭包,它的接受一个函数作为参数,并返回一个新的闭包函数,这个函数对传入的函数进行了封装,也即起到了装饰的作用,所以包含了闭包的函数 log() 被称为装饰器。运用装饰器可以在函数进入和退出时,执行特定的操作,比如插入日志,性能测试,缓存,权限校验等场景。有了装饰器,就可以抽离出大量与函数功能无关的重复代码。
上面的写法还是不够简便,Python 为装饰器专门提供了语法糖 @ 符号。无需在调用处修改函数时候,只需要在定义前一行加上装饰器。
0 1 2 3 4 5 6 7 8 | @log # 添加装饰器 log()
def func2(n):
print("from func2(), n is %d!" % (n), flush=True)
func2(0)
>>>
from func2(), n is 0!
DEBUG:root:func2 is called
|
以上语句相当于执行了如下操作:
0 1 | func2 = log(func2)
func2(0)
|
关于装饰器是如何把参数传递给不同函数的,请参考 函数参数传递形式 小结。
含参装饰器¶
为了让装饰器可以带参数,需要在原装饰器外部再封装一层,最外层出入装饰器参数,内存传入函数的引用。
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | def log(level='debug'):
def decorator(func):
def wrapper(*args, **kwargs):
ret = func(*args, **kwargs)
if level == 'warning':
logging.warning("{} is called".format(func.__name__))
else:
logging.debug("{} is called".format(func.__name__))
return ret
return wrapper
return decorator
@log(level="warning") # 添加带参数的装饰器 log()
def func(n):
print("from func(), n is %d!" % (n), flush=True)
func(0)
>>>
from func(), n is 0!
WARNING:root:func is called
|
以上语句相当于执行了如下操作:
0 1 | func = log('warning')(func)
func()
|
由于装饰器 log() 已经设置了默认参数,所以如果不需要传递参数给装饰器,那么直接使用 @log
即可。
类方法装饰器¶
类方法的函数装饰器和函数的函数装饰器类似。对于类方法来说,都有一个默认的形数 self,所以在装饰器的内部函数 wrapper 中也要传入该参数,其他的用法和函数装饰器相同。
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | import time
def decorator(func):
def wrapper(self, *args, **kwargs):
start_time = time.time()
ret = func(self, *args, **kwargs)
end_time = time.time()
print("%s.%s() cost %f second!" % (self.__class__,
func.__name__, end_time - start_time))
return ret
return wrapper
class TestDecorator():
@decorator
def mysleep(self, n):
time.sleep(n)
obj = TestDecorator()
obj.mysleep(1)
>>>
<class '__main__.TestDecorator'>.mysleep() cost 1.000091 second!
|
类方法装饰如要需要传入参数,请参考含参装饰器,只要再封装一层即可。
装饰器类¶
无参装饰器类¶
以上介绍了函数作为装饰器去装饰其他的函数或者类方法,那么可不可以让一个类发挥装饰器的作用呢?答案是肯定的。 而且,相比装饰器函数,装饰器类具有更大灵活性,高内聚,封装性特点。
装饰器类必须定义 __call__() 方法,它将一个类实例变成一个用于装饰器的方法。
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | class Tracer():
def __init__(self, func):
self.func = func
self.calls = 0
def __call__(self, *args, **kwargs):
self.calls += 1
print("call %s() %d times" % (self.func.__name__, self.calls))
return self.func(*args, **kwargs)
@Tracer
def test_tracer(val, name="default"):
print("func() name:%s, val: %d" % (name, val))
for i in range(2):
test_tracer(i, name=("name" + str(i)))
>>>
call test_tracer() 1 times
func() name:name0, val: 0
call test_tracer() 2 times
func() name:name1, val: 1
|
装饰器类不能用于装饰类的方法,因为 __call__() 的第一个参数必须传递装饰器类 Tracer 的实例。
带参数装饰器类¶
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | class Tracer():
def __init__(self, arg0): # 可支持任意参数
self.arg0 = arg0
self.calls = 0
def __call__(self, func):
def wrapper(*args, **kwargs):
self.calls += 1
print("arg0:%d call %s() %d times" % (self.arg0, func.__name__, self.calls))
return func(*args, **kwargs)
return wrapper
@Tracer(arg0=0)
def test_tracer(val, name="default"):
print("func() name:%s, val: %d" % (name, val))
for i in range(2):
test_tracer(i, name=("name" + str(i)))
>>>
arg0:0 call test_tracer() 1 times
func() name:name0, val: 0
arg0:0 call test_tracer() 2 times
func() name:name1, val: 1
|
装饰器类的参数需要通过类方法 __init__() 传递,所以被装饰的函数就只能在 __call__() 方法中传入,为了把函数的参数传入,必须在 __call__() 方法中再封装一层。
类装饰器¶
所谓类装饰器,就是对类进行装饰的函数或者类。从装饰器的本质,我们知道,一个对函数进行装饰的装饰器函数,它的语法糖被解释的时候,默认转换为如下形式:
0 1 2 3 4 5 | @decorator
def func():
......
func = decorator(func)
func()
|
如果使用装饰器类,则进行如下转换:
0 1 2 3 4 5 6 7 8 9 | class decorator():
.....
@decorator
def func():
......
instance = decorator(func)
func = instance.__call_()
func()
|
所以装饰一个函数,就是对函数进行封装,就要把被装饰的函数传递给装饰器,如果要装饰一个类,那么就要把类传递给装饰器。
使用函数装饰类¶
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | class DotClass():
pass
def class_add_method(Class):
Class.x, Class.y = 0, 0
def move(self, a, b):
self.x += a
self.y += b
print("Dot moves to (%d, %d)" % (self.x, self.y))
Class.move = move
return Class
DotClass = class_add_method(DotClass)
dot = DotClass()
dot.move(1, 2)
>>>
Dot moves to (1, 2)
|
DotClass 类原本是一个空类,既没有成员变量也没有方法,我们使用函数动态的为它添加类成员 x 和 y,以及类方法 move(),唯一要注意的是 move() 方法第一个参数一定是 self,在类对象调用它时,它对应实例自身。
可以看到上面的行为很像装饰器的过程,我们使用语法糖 @ 来测试下,是否如预期一样:
0 1 2 3 4 5 6 7 8 | @class_add_method
class DotClass():
pass
dot = DotClass()
dot.move(1, 2)
>>>
Dot moves to (1, 2)
|
以上示例我们只是为类安装了参数和方法,返回原来的类,我们也可以定义一个新类,并返回它。
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | def class_add_method_new(Class): # @语句处调用
class Wrapper():
def __init__(self, *args): # 创建实例时调用
self.wrapped = Class(*args) # 调用 DotClass.__init__
def move(self, a, b):
self.wrapped.x += a
self.wrapped.y += b
print("Dot moves to (%d, %d)" % (self.wrapped.x, self.wrapped.y))
def __getattr__(self, name): # 对象获取属性时调用
return getattr(self.wrapped, name)
return Wrapper
@class_add_method_new
class DotClass(): # DotClass = class_add_method_new(DotClass)
def __init__(self): # 在 Wrapper.__init__ 中调用
self.x, self.y = 0, 0
dot = DotClass() # dot = Wrapper()
dot.move(1, 2)
print(dot.x) # 调用 Wrapper.__getattr__
>>>
Dot moves to (1, 2)
1
|
示例中,我们返回了一个新的类,要注意的是,新的初始化函数封装了对原来类的实例化调用,并在新增的方法中引用原来类中成员,此外由于新类并不感知被装饰类的成员,所以必须实现 __getattr__() 方法。
使用带参函数装饰类¶
原理与带参数的函数装饰器装饰函数一样,只需要再封装一层即可,不再赘述。
0 1 2 3 4 5 6 7 8 9 10 11 12 13 | def decorator(arg0=0):
def class_add_method_new(Class):
class Wrapper():
......
return Wrapper
return class_add_method_new
@decorator(arg0=2)
class DotClass():
# @语句等价于
decorator = decorator(2)
DotClass = decorator(DotClass)
|
使用类装饰类¶
参考 无参装饰器类 和 带参数装饰器类 的实现,原理是一样的,这里不再赘述。无参类装饰器:
0 1 2 3 4 5 6 7 8 9 10 11 12 13 | class Tracer():
def __init__(self, Class): # @语句处调用
self.Class = Class
def __call__(self, *args, **kwargs): # 创建实例时调用
self.wrapped = self.Class(*args, **kwargs)
return self
def __getattr__(self, name): # 获取属性时调用
return getattr(self.wrapped, name)
@Tracer()
class C():
......
|
支持参数的类装饰器:
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | class TracerP():
def __init__(self, arg0): # @语句处调用
self.arg0 = arg0
def __call__(self, Class):
self.Class = Class
def wrapper(*args, **kwargs): # 创建实例时调用
self.wrapped = self.Class(*args, **kwargs)
return self
return wrapper
def __getattr__(self, name): # 获取属性时调用
return getattr(self.wrapped, name)
@TracerP(arg0=1)
class C():
......
|
注意使用装饰器的前提是为了更简便的实现功能,而不要为用而用,装饰器和被装饰的函数或类应该是各自功能内聚,没有耦合关系。否则应该考虑其他方式,比如类继承。 在选择装饰器时,也应遵循先易后繁的原则,在装饰器函数不能满足需求时,才使用装饰器类。
装饰器嵌套¶
如果我们需要对一个函数既要统计运行时间,又要记录运行日志,如何使用装饰器呢?Python 函数或类也可以被多个装饰器修饰,也即装饰器嵌套(Decorator Nesting)。要是有多个装饰器时,这些装饰器的执行顺序是怎么样的呢?
0 1 2 3 4 5 6 7 8 9 10 11 12 | def markbold(f):
return lambda: '<b>' + f() + '</b>'
def markitalic(f):
return lambda: '<i>' + f() + '</i>'
@markbold
@markitalic
def markstr():
return "Python"
>>>
<b><i>Python</i></b>
|
可以看到按照 markbold(markitalic(markstr()))
的顺序执行,多个装饰器按照靠近被修饰函数或者类的距离,由近及远依次执行的。
装饰器副作用¶
装饰器极大地复用了代码,但是一个缺点就是原函数的元信息不见了,比如函数的 docstring,__name__,参数列表。 这是一个严重的问题,当进行函数跟踪,调试时,或者根据函数名进行判断的代码就不能正确执行,这些信息非常重要。
0 1 2 3 4 5 6 7 8 9 10 | def markitalic(f):
return lambda: '<i>' + f() + '</i>'
@markitalic
def markstr():
return "Python"
print(markstr.__name__)
>>>
<lambda>
|
functools 模块中的 wraps 可以帮助保留这些信息。functools.wraps 本身也是一个装饰器,它把被修饰的函数元信息复制到装饰器函数中,这就保留了原函数的信息。
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | from functools import wraps
def markitalic(f):
@wraps(f)
def wrapper():
return '<i>' + f() + '</i>'
return wrapper
@markitalic
def markstr():
return "Python"
print(markstr.__name__)
>>>
markstr
|
其实 functools.wraps 并没有彻底恢复所有函数信息,具体请参考第三方模块 wrapt。
内置装饰器¶
定义类静态方法¶
@staticmethod
装饰器将类中的方法装饰为静态方法,不需要创建类的实例,可以通过类名直接引用。实现函数功能与实例解绑。
静态方法不会隐式传入参数,不需要传入 self ,类似一个普通函数,只是可以通过类名或者类对象来调用。
0 1 2 3 4 5 6 7 8 9 10 11 12 | class C():
@staticmethod
def static_method():
print("This is a static method!")
C.static_method() # 类名直接调用
c = C()
c.static_method() # 类对象调用
>>>
This is a static method!
This is a static method!
|
定义类方法¶
@classmethod
装饰器用于定义类方法,类方法和类的静态方法非常相似,只是会隐式传入一个类参数
。类方法被哪个类调用,就传入哪个类作为第一个参数进行操作。
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | class C():
@classmethod
def class_method(cls):
print("This is ", cls)
class B(C):
pass
C.class_method() # 类名直接调用
c = C()
c.class_method() # 类对象调用
B.class_method() # 继承类调用
>>>
This is <class '__main__.C'>
This is <class '__main__.C'>
This is <class '__main__.B'>
|
实例方法属性化¶
property(fget=None, fset=None, fdel=None, doc=None) -> property attribute
内置方法 property() 可以将类中定义的实例方法(对象方法)属性化,可以直接为成员赋值和读取,也可以定义只读属性。
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | class C():
def __init__(self):
self.__arg = 0
def getarg(self):
return self.__arg
def setarg(self, value):
self.__arg = value
def delarg(self):
del self.__arg
arg = property(fget=getarg, fset=setarg, fdel=delarg, doc="'arg' property.")
c = C()
c.arg = 10 # 调用 setarg
print(c.arg) # 调用 getarg
c.setarg(20) # 调用 setarg
print(c.getarg()) # 调用 getarg
del c.arg # 调用 delarg
|
如果不提供 fset 参数,则属性就变成只读的了。@property
装饰器以更简单的方式实现了相同功能。
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | class C():
def __init__(self):
self.__arg = 0
@property
def argopt(self):
return self.__arg
@argopt.setter
def argopt(self, value):
self.__arg = value
@argopt.deleter
def argopt(self):
del self.__arg
c = C()
c.arg = 10
print(c.arg)
del c.arg
|
注意三个方法的命名必须相同,getter(prorperty() 中名为 fget)对应的方法总是用 “@property” 修饰,其他两个为方法名加上 “.setter” 和 “.deleter”,如果定义只读属性,不定义 setter 方法即可。
内建模块¶
inspect¶
inspect 模块用于收集 Python 对象的信息,可以获取类或函数的参数的信息,源码,解析堆栈,对对象进行类型检查等。
我们使用 sample.py 作为测试模块,源码如下:
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | # -*- coding: utf-8 -*-
"""
Created on Wed Dec 12 17:16:47 2017
@author: Red
"""
sample_str = "sample module"
sample_list = [1, 2, 3]
# This is a function of sample
def sample_func(arg0, args1="name", *args, **kwargs):
"""This is a sample module function."""
f_var = arg0 + 1
return f_var
class A():
"""Definition for A class."""
def __init__(self, name):
self.name = name
def get_name(self):
"Returns the name of the instance."
return self.name
obj_a = A('A Class instance')
class B(A):
"""B class, inherit A class. """
# This method is not part of A class.
def cls_func(self):
"""Anything can be done here."""
def get_name(self):
"Overrides method from X"
return 'B(' + self.name + ')'
obj_b = B('B Class instance')
|
获取模块信息¶
getmodulename(path) 方法获取文件名,ismodule(obj) 判断对象是否为模块。
0 1 2 3 4 5 6 7 8 | import sample
print(inspect.getmodulename("./sample.py"))
print(inspect.ismodule(sample))
print(inspect.ismodule(1))
>>>
sample
True
False
|
我们也可以使用 getmembers() 获取更多的模块信息,关于 getmembers() 方法的详细使用请参考下一小节:
0 1 2 3 4 5 6 7 8 9 10 11 12 13 | for name, data in inspect.getmembers(sample):
if name == '__builtins__':
continue
if name.startswith('__'):
print(name, repr(data))
>>>
__cached__ 'Z:\\sdc\\lbooks\\ml\\__pycache__\\sample.cpython-36.pyc'
__doc__ '\nCreated on Wed Dec 12 17:16:47 2017\n@author: Red\n'
__file__ 'Z:\\sdc\\lbooks\\ml\\sample.py'
__loader__ <_frozen_importlib_external.SourceFileLoader object .....>
__name__ 'sample'
__package__ ''
__spec__ ModuleSpec(name='sample', loader=......
|
getmembers¶
getmembers(object, predicate=None)
Return all members of an object as (name, value) pairs sorted by name.
Optionally, only return members that satisfy a given predicate.
getmembers() 方法非常强大,它可以获取模块,对象成员属性。predicate 用于过滤特定属性的成员。 它返回一个列表,列表中的每个元素是一个形如 (name, value) 的元组。
0 1 2 3 4 | print(inspect.getmembers(sample))
>>>
[('A', <class 'sample.A'>), ('B', <class 'sample.B'>), ('__builtins__',
......
|
由于模块默认继承很多内建属性,它会打印很多信息,内建属性通常以 __ 开头,我们可以进行如下过滤:
0 1 2 3 4 5 6 7 8 9 10 11 12 | for name,type in inspect.getmembers(sample):
if name.startswith('__'):
continue
print(name, type)
>>>
A <class 'sample.A'>
B <class 'sample.B'>
obj_a <sample.A object at 0x000002B5960E9128>
obj_b <sample.B object at 0x000002B5960E99E8>
sample_func <function sample_func at 0x000002B5960732F0>
sample_list [1, 2, 3]
sample_str sample module
|
通过 predicate 参数指定 inspect 自带的判定函数,可以获取类,函数等任何特定的信息。
查看模块中的类¶
0 1 2 3 4 5 | for name,type in inspect.getmembers(sample, inspect.isclass):
print(name, type)
>>>
A <class 'sample.A'>
B <class 'sample.B'>
|
查看模块中函数¶
0 1 2 3 4 | for name,type in inspect.getmembers(sample, inspect.isfunction):
print(name, type)
>>>
sample_func <function sample_func at 0x000002B5961F8840>
|
查看类属性¶
查看类函数:
0 1 2 3 4 5 | for name, type in inspect.getmembers(sample.A, inspect.isfunction):
print(name, type)
>>>
__init__ <function A.__init__ at 0x000002B5961F8D08>
get_name <function A.get_name at 0x000002B5961F80D0>
|
查看对象属性¶
查看对象方法:
0 1 2 3 4 5 6 7 8 9 10 11 12 | for name, type in inspect.getmembers(sample.obj_a, inspect.ismethod):
print(name, type)
print()
for name, type in inspect.getmembers(sample.obj_b, inspect.ismethod):
print(name, type)
>>>
__init__ <bound method A.__init__ of <sample.A object at 0x000002B5961BAA90>>
get_name <bound method A.get_name of <sample.A object at 0x000002B5961BAA90>>
__init__ <bound method A.__init__ of <sample.B object at 0x000002B596117278>>
cls_func <bound method B.cls_func of <sample.B object at 0x000002B596117278>>
get_name <bound method B.get_name of <sample.B object at 0x000002B596117278>>
|
getdoc 和 getcomments¶
getdoc(object) 可以获取任一对象的 __doc__ 属性。
0 1 2 3 4 5 6 7 8 9 10 11 | print('A.__doc__:')
print(sample.A.__doc__)
print()
print('getdoc(A):')
print(inspect.getdoc(sample.A))
>>>
A.__doc__:
Definition for A class.
getdoc(A):
Definition for A class.
|
getcomments() 方法获取模块,函数或者类定义前的注释行,注释必须以 # 开头。
0 1 2 3 4 5 6 | print(inspect.getcomments(sample))
print(inspect.getcomments(sample.sample_func))
>>>
# -*- coding: utf-8 -*-
# This is a function of sample
|
getsource¶
getsource(object) 可以获模块,函数或者类,类方法的源代码。
0 1 2 3 4 5 6 7 8 9 10 11 | print(inspect.getsource(sample.sample_func))
print(inspect.getsource(sample.B.get_name))
>>>
def sample_func(arg0, arg1="name", *args, **kwargs):
"""This is a sample module function."""
f_var = arg0 + 1
return f_var
def get_name(self):
"Overrides method from X"
return 'B(' + self.name + ')'
|
getsourcelines(object) 返回一个元组,元组第一项为对象源代码行的列表,第二项是第一行源代码的行号。
0 1 2 3 | print(inspect.getsourcelines(sample.sample_func))
>>>
(['def sample_func(arg0, *args, **kwargs):\n',...... return f_var\n'], 10)
|
函数参数相关¶
signature() 返回函数的参数列表,常被 IDE 用来做代码提示:
0 1 2 3 4 5 | print(inspect.signature(sample.sample_func))
print(inspect.signature(sample.B.get_name))
>>>
(arg0, *args, **kwargs)
(self)
|
getfullargspec() 将函数参数按不同类型返回。
0 1 2 3 4 5 6 7 8 9 10 | arg_spec = inspect.getfullargspec(sample.sample_func)
print('namedkey:', arg_spec[0])
print('* :', arg_spec[1])
print('** :', arg_spec[2])
print('defaults:', arg_spec[3])
>>>
namedkey: ['arg0', 'args1']
* : args
** : kwargs
defaults: ('name',)
|
getcallargs() 方法将函数形参与实参绑定,返回一个字典:
0 1 2 3 4 5 6 7 8 | def f(a, b=1, *pos, **named):
pass
print(getcallargs(f, 1, 2, 3) == {'a': 1, 'named': {}, 'b': 2, 'pos': (3,)})
print(getcallargs(f, a=2, x=4) == {'a': 2, 'named': {'x': 4}, 'b': 1, 'pos': ()})
>>>
True
True
|
getmro¶
获取继承序列,与类对象的 __mro__ 属性对应:
0 1 2 3 4 5 | print(B.__mro__)
print(inspect.getmro(B))
>>>
(<class '__main__.B'>, <class '__main__.A'>, <class 'object'>)
(<class '__main__.B'>, <class '__main__.A'>, <class 'object'>)
|
获取调用栈¶
获取调用栈信息的系列方法均支持 context 参数,默认值为1,可以传入整数值 n 来获取调用栈的上线文的 n 行源码。
stack 和 getframeinfo¶
类似于 C 语言,Python 解释器也使用栈帧(Stack frame)机制来管理函数调用。
stack() 方法获取当前的所有栈帧信息,它是一个 list。getframeinfo() 打印栈帧信息。
0 1 2 3 4 5 6 7 8 9 10 11 | def dump_stack(stack):
for i in stack:
frame,filename,lineno,funcname,lines,index = i
print(inspect.getframeinfo(frame))
print(filename,lineno,funcname,lines,index)
dump_stack(inspect.stack())
>>>
Traceback(filename='tmp.py', lineno=29, function='<module>',
code_context=['dump_stack(inspect.stack())\n'], index=0)
('tmp.py', 29, '<module>', ['dump_stack(inspect.stack())\n'], 0)
|
可以看到一个栈帧是一个元组,包含文件名,行号,函数名(如果是在函数外调用,则显示模块名),调用 stack() 处的代码和上下文索引 6 个元素。
所谓上下文索引,即调用 stack() 所在语句在源码上下文的编号。如果要获取栈帧信息的更多源码,可以给传入 context 参数,默认为 1。
0 1 2 3 4 5 6 7 8 9 10 | # before line 2
# before line 1
dump_stack(inspect.stack(3))
# after line 1
>>>
Traceback(filename='tmp.py', lineno=29, function='<module>',
code_context=['dump_stack(inspect.stack(3))\n'], index=0)
('tmp.py', 29, '<module>', ['# before line 1\n', 'dump_stack(inspect.stack(3))\n',
'# after line 1\n'], 1)
|
trace¶
trace() 返回异常时的栈帧信息,如果没有异常发生,trace() 返回空列表。
0 1 2 3 4 5 6 7 8 9 10 11 12 | def call():
try:
1/0
except:
dump_stack(inspect.trace())
call()
>>>
Traceback(filename='tmp.py', lineno=31, function='call',
code_context=[' dump_stack(inspect.trace())\n'], index=0)
('tmp.py', 29, 'call', [' 1/0\n'], 0) # lines 返回触发异常时的代码
|
这里与 stack() 做一对比,显然 stack() 返回所有栈帧信息,顶层栈帧记录的不是触发异常的代码行,而是调用 stack() 的代码行。
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | def call():
try:
1/0
except:
dump_stack(inspect.stack())
call()
>>>
Traceback(filename='tmp.py', lineno=31, function='call',
code_context=[' dump_stack(inspect.stack())\n'], index=0)
('tmp.py', 31, 'call', [' dump_stack(inspect.stack())\n'], 0)
Traceback(filename='tmp.py', lineno=33, function='<module>',
code_context=['call()\n'], index=0)
('tmp.py', 33, '<module>', ['call()\n'], 0)
|
currentframe¶
获取当前正在运行的代码行所在的栈帧,也即当前栈帧。
0 1 2 3 4 5 6 7 | def dump_frame(frame):
print(getframeinfo(frame))
dump_frame(inspect.currentframe())
>>>
Traceback(filename='tmp.py', lineno=31, function='<module>',
code_context=['dump_frame(inspect.current)\n'], index=0)
|
getouterframes¶
getouterframes(frame) 返回从 frame 到栈底的所有栈帧,对于 frame 来说,从它到栈底的帧都被称为外部帧。
0 1 2 3 | def current_frame():
return inspect.currentframe()
stack = inspect.getouterframes(current_frame())
|
上述代码返回含当前栈帧的所有帧,等同于 stack()。
getinnerframes¶
getinnerframes(traceback) 用于获取一个 traceback 对象中的栈帧。
0 1 2 3 4 5 6 7 8 9 10 11 | import sys
try:
1/0
except:
prev_cls, prev, tb = sys.exc_info()
frames = inspect.getinnerframes(tb)
dump_stack(frames)
>>>
Traceback(filename='tmp.py', lineno=42, function='<module>',
code_context=[' dump_stack(frames)\n'], index=0)
tmp.py 38 <module> [' 1/0\n'] 0
|
collections¶
Python 提供了多种内置数据类型,比如数值型 int、float 和 complex,字符串 str 以及复合数据类型 list、tuple 和 dict。 collections 模块基于这些基本数据类型,封装了其他复杂的数据容器类型。
namedtuple¶
命名元组(namedtuple)使用 namedtuple() 工厂函数(Factory Function)返回一个命名元组类。 这个类继承自 tuple,用来创建元组类似的对象,对象拥有只读属性,这些属性有对应的名字,可以通过名字访问属性。
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | from collections import namedtuple
# Point 是一个类,第一个参数定义类名
Point = namedtuple('PointClass', ['x', 'y'])
print(Point.__name__)
# 实例化对象 p
p = Point(1, 2)
print(p)
# 使用索引访问属性
print(p[0], p[0] + p[1])
# 使用属性名访问
print(p.x, p.x + p.y)
>>>
PointClass
PointClass(x=1, y=2)
1 3
1 3
|
对象拥有只读属性,不可更改属性值。
0 1 2 3 | p.x = 5
>>>
AttributeError: can't set attribute
|
namedtuple() 的第一个参数定义类名,列表参数定义类的属性。 它返回的是一个类,我们可以继承它,来扩展对属性的操作。
0 1 2 3 4 5 6 7 8 9 10 11 12 13 | class Point(namedtuple('PointClass', ['x', 'y'])):
__slots__ = () # 禁止动态添加属性
@property # 只读属性,求勾股数
def hypot(self):
return (self.x ** 2 + self.y ** 2) ** 0.5
def __str__(self):
return 'Point: x=%6.3f y=%6.3f hypot=%6.3f' % (self.x, self.y, self.hypot)
for p in Point(3, 4), Point(14, 5/7):
print(p)
>>>
Point: x= 3.000 y= 4.000 hypot= 5.000
Point: x=14.000 y= 0.714 hypot=14.018
|
我们要定义一个新类,只是在已有类上添加一些参数,那么定义一个子类就太复杂了,一个简单的方法可以调用类属性 Point._fields,它是一个元组类型。比如扩展 Point 类到三维空间:
0 1 2 3 4 5 6 | print(Point._fields)
print(type(Point._fields))
Point3D = namedtuple('Point3D', Point._fields + ('z',))
>>>
('x', 'y')
<class 'tuple'>
|
命名元组的类方法 _make() 可以接受一个序列对象,方便批量把数据转化为命名元组对象。
0 1 2 3 4 5 6 7 8 | Point3D = namedtuple("Point3D", Point._fields + ('z',))
datum = [[1,2,3], [4,5,6], [7,8,9]]
for i in map(Point3D._make, datum):
print(i)
>>>
Point3D(x=1, y=2, z=3)
Point3D(x=4, y=5, z=6)
Point3D(x=7, y=8, z=9)
|
Counter¶
计数器 Counter 用于统计元素的个数,并以字典形式返回,格式为 {元素:元素个数}。
Counter 类继承了 dict ,它的帮助信息中提供了很多示例应用,这里引用如下:
生成计数器¶
Counter() 类接受如下参数生成一个计数器对象:
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | c = Counter() # 空计数器
print(c.most_common())
c = Counter('gallahad') # 可迭代对象
print(c.most_common())
c = Counter({'a': 4, 'b': 2}) # 字典
print(c.most_common())
c = Counter(a=4, b=2) # 关键字参数指定
print(c.most_common())
>>>
[]
[('a', 3), ('l', 2), ('g', 1), ('h', 1), ('d', 1)]
[('a', 4), ('b', 2)]
[('a', 4), ('b', 2)]
|
统计字符¶
0 1 2 3 4 5 6 7 8 | from collections import Counter
c = Counter('abcdeabcdabcaba')
print(c.most_common())
print(c.most_common(3))
>>>
[('a', 5), ('b', 4), ('c', 3), ('d', 2), ('e', 1)]
[('a', 5), ('b', 4), ('c', 3)]
|
示例中统计字符串中各个字符出现的次数,most_common(n=None) 参数 n 指定返回出现最多的字符数,不指定则返回全部。
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | c['a'] # 获取单个字符的个数
>>>
5
sorted(c) # 列出所有不重复的字符
>>>
['a', 'b', 'c', 'd', 'e']
for i in c.elements(): # 访问计数器中的元素
print(i, end=' ')
>>>
a a a a a b b b b c c c d d e
for i in c.values(): # 访问计数器中的计数值
print(i, end=' ')
>>>
5 4 3 2 1
c['a'] += 1 # 增加 'a' 的计数值
del c['b'] # 从计数器中删除 'b' 的计数信息
d = c.copy() # 复制计数器
c.clear() # 清空计数器
|
- elements():返回一个迭代器,元素被重复多少次,在迭代器中就包含多少个此元素,所有元素按字母序排列,个数<= 0 的不罗列。
- values():返回计数器的统计值,元组类型。
计数器加减¶
除了以上给出的操作,计时器还可以从其他计数器添加计数信息:
0 1 2 3 4 5 | d = Counter('simsalabim') # 构造计数器 b
c.update(d) # 将 b 的计数信息添加到 c 中
c['a']
>>>
9
|
update() 支持传入可迭代对象和字典,与添加对应,还可以将统计数相减,例如:
0 1 2 3 4 5 6 7 8 | c = Counter('which')
c.subtract('witch')
print(c.most_common())
c.subtract('watch')
print(c.most_common())
>>>
[('h', 1), ('w', 0), ('i', 0), ('c', 0), ('t', -1)]
[('h', 0), ('i', 0), ('w', -1), ('c', -1), ('a', -1), ('t', -2)]
|
subtract() 方法可接受一个可迭代对象或者一个计数器对象。如果没有对应的字符,则计数值为负值。
defaultdict¶
使用 dict 时,如果 Key 不存在,就会抛出错误 KeyError。如果希望 key 不存在时,返回一个默认值,可以使用 defaultdict。
0 1 2 3 4 5 6 7 | ddict = defaultdict(lambda:'Default')
print(ddict['key'])
ddict['key'] = 'val'
print(ddict['key'])
>>>
Default
val
|
defaultdict 的默认值通过传入的函数返回,这个函数不需要传入参数。下面的示例默认返回一个空列表:
0 1 2 3 4 | ddict = defaultdict(list)
print(ddict['key'])
>>>
[]
|
OrderedDict¶
dict 的 key 是无序的,OrderedDict 对 dict 进行了扩展,构造一个有序字典。OrderedDict 的 Key 按照插入的顺序排列,后加入的元素追加到字典尾部。
from collections import OrderedDict
0 1 2 3 4 5 6 7 | od = OrderedDict()
od['z'] = 1
od['y'] = 2
od['x'] = 3
print(od.keys())
>>>
odict_keys(['z', 'y', 'x'])
|
popitem¶
OrderedDict 继承了 dict 类,可以像 dict 一样进行插入删除操作。 popitem() 会返回最后一次追加的元素,这种行为就构成了一个字典堆栈。
0 1 2 3 | print(od.popitem())
>>>
('x', 3)
|
OrderedDict 把字典的 popitem() 方法扩展为 popitem(last=True),如果 last 为 False, 则总是从头部删除元素,例如:
0 1 2 3 | print(od.popitem(False))
>>>
('z', 1)
|
移动元素¶
move_to_end(key, last=True) 方法可以将指定 key 的元素移动到尾部或者头部。
0 1 2 3 4 5 6 7 8 9 10 11 12 13 | od = OrderedDict()
od['z'] = 1
od['y'] = 2
od['x'] = 3
print(od.keys())
od.move_to_end('y') # 'y' 移动到尾部
print(od)
od.move_to_end('y', False) # 'y' 移动到头部
print(od)
>>>
OrderedDict([('z', 1), ('x', 3), ('y', 2)])
OrderedDict([('y', 2), ('z', 1), ('x', 3)])
|
根据索引 popitem¶
这里对有序字典进行扩展, 可以通过 popitem(index=None) 删除指定位置的元素:
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | class IndexOrderedDict(OrderedDict):
def __init__(self, *args, **kwds):
super().__init__(*args, **kwds)
def popitem(self, index=None):
if index is None:
return super().popitem()
if not isinstance(index, int) or index < 0:
raise ValueError('Invalid index')
if index >= len(self.keys()):
return super().popitem()
key = list(self.keys())[index]
value = super().pop(key)
return key,value
iodict = IndexOrderedDict({'a': 1, 'b' : 2})
iodict['c'] = 3
print(iodict)
item = iodict.popitem(1)
print(item)
print(iodict)
>>>
IndexOrderedDict([('a', 1), ('b', 2), ('c', 3)])
('b', 2)
IndexOrderedDict([('a', 1), ('c', 3)])
|
deque¶
使用 list 存储数据时,按索引访问元素很快,但是由于 list 是单向链表,插入和删除元素就很慢了,数据量大的时候,插入和删除效率就会很低。
deque 为了提高插入和删除效率,实现了双向列表,允许两端操作元素,适合用于队列和栈。
appendleft 和 popleft¶
0 1 2 3 4 5 6 7 | from collections import deque
dq = deque(['center'])
dq.append('right')
dq.appendleft('left')
print(dq)
>>>
deque(['left', 'center', 'right'])
|
appendleft() 方法从头部扩展,与此对应 popleft() 方法从头部删除:
0 1 2 3 4 5 | print(dq.pop())
print(dq.popleft())
>>>
right
left
|
extendleft¶
extend() 方法从尾部扩展,extendleft() 从头部扩展。
0 1 2 3 4 5 6 | dq = deque(['center'])
dq.extend(['right0', 'right1'])
dq.extendleft(['left0', 'left1'])
print(dq)
>>>
deque(['left1', 'left0', 'center', 'right0', 'right1'])
|
rotate¶
rotate(n) 方法对元素进行旋转操作,n < 0 向左旋转 n 次,n > 0 向右旋转 n 次,1 次移动一个元素:
0 1 2 3 4 5 6 7 8 | dq = deque('abcdef')
dq.rotate(-2)
print(dq)
dq.rotate(2)
print(dq)
>>>
deque(['c', 'd', 'e', 'f', 'a', 'b'])
deque(['a', 'b', 'c', 'd', 'e', 'f'])
|
ChainMap¶
ChainMap 将多个字典串联起来,按照参数顺序搜索键,找到即返回。
0 1 2 3 4 5 6 7 8 9 10 11 12 13 | from collections import ChainMap
first = {'x': 1, 'y': 1}
second = {'y': 2, 'z': 2}
cmap = ChainMap(first, second)
print(cmap)
for i, j in cmap.items():
print(i, '=', j)
>>>
y = 1
z = 2
x = 1
|
ChainMap 并不对字典进行拷贝,而是指向原字典。
0 1 2 3 | print(id(first['x']), id(cmap['x']))
>>>
1531505776 1531505776
|
如果修改键值将作用在第一个字典上,即便其他字典存在该键,依然会作用在第一个字典上。
0 1 2 3 4 5 6 7 | cmap['a'] = 1
cmap['z'] = 3 # second 存在 'z',依然会添加到 first
print(cmap.maps)
print(first)
>>>
[{'x': 1, 'y': 1, 'a': 1, 'z': 3}, {'y': 2, 'z': 2}]
{'x': 1, 'y': 1, 'a': 1, 'z': 3}
|
maps¶
maps 属性以元组形式返回所有字典:
0 1 2 3 4 5 6 7 | first = {'x': 1, 'y': 1}
second = {'y': 2, 'z': 2}
cmap = ChainMap(first, second)
print(cmap.maps)
>>>
[{'x': 1, 'y': 1}, {'y': 2, 'z': 2}]
|
new_child¶
new_child() 方法基于已经存在的 ChainMap 对象返回新创建的 ChainMap 对象,可以传入需要新加入的字典,如果不提供参数,则加入空字典 {}。
注意新加入的字典优先级最高。
0 1 2 3 4 5 6 7 | three = {'a' : 0}
new_cmap = cmap.new_child(three)
print(new_cmap.maps)
print(id(cmap.maps[0]), id(new_cmap.maps[1]))
>>>
[{'a': 0}, {'x': 1, 'y': 1}, {'y': 2, 'z': 2}]
2101860273536 2101860273536
|
struct¶
struct 模块用来实现 bytes 和其他数据类型的转换,本质上类似 C 语言中的结构体,尝试把多个数据类型打包(格式化)到一个结构体中,这个结构体用 bytes 字节序列表示。
在 C 语言中我们可以通过指针类型转换,轻易的访问任何合法地址的任何一个字节,在 Python 中不支持对地址访问,所以也不能轻易访问到一个对象的任意字节。
Python 尽管提供的了 bytes 类,参考 bytes ,可以把字符串,可迭代整数类型等转换为 bytes 对象,但是却不能将任意一个整数转换为 bytes 对象,更无法对 float 等其他数据类型进行转换。
0 1 2 3 | bytes0 = bytes([0x123456])
>>>
ValueError: bytes must be in range(0, 256)
|
尽管可以使用如下讨巧的方式转换一个整形,但是却不通用。
0 1 2 3 4 5 6 | int0 = 0x12345678
hexstr = hex(int0)
bytes0 = bytes.fromhex(hexstr[2:])
print(bytes0)
>>>
b'\x124Vx'
|
struct 模块可以把任意基本数据类型和 bytes 互相转换,这样就可以使用 python 操作一些二进制文件,比如图片。
pack¶
pack() 方法通过格式化参数将给定的数据类型转换为 bytes 类型,也即打包成一个 bytes 类型的 struct 对象:
0 1 2 3 4 5 6 7 | import struct
sp0 = struct.pack('>I', 0x01020304)
print(type(sp0).__name__)
print(sp0)
>>>
bytes
b'\x01\x02\x03\x04'
|
第一个参数 ‘>I’ 是格式化字符串,其中 > 表示使用大端字节序,I 表示4字节无符号整数。这里采用大端字节序,转换后的结果高位 0x01 在前。
格式化字符串¶
struct 的格式化字符串由两部分组成:字节序和数据类型,字节序是可选项,默认为 @ 。
字节序支持如下几种模式:
- @:本机字节序,进行数据对齐,填充 0 进行对齐,比如 int 型总是对齐到 sizeof(int)(通常为 4)整数倍的地址。
- =:本机字节序,不对齐。
- <:小端字节序,不对齐。
- > 或 !:大端字节序(网络字节序),不对齐。
可以这样获取本机系统的字节序:
0 1 2 3 4 | import sys
print(sys.byteorder)
>>>
little
|
支持的数据类型缩写参考 struct 支持的数据类型字符 。
0 1 2 3 4 5 6 7 8 | fmts = ['ci', '>ci', '<ci']
for i in fmts:
sp0 = struct.pack(i, b'*', 0x12131415)
print('%s\t:' % i, sp0)
>>>
ci : b'*\x00\x00\x00\x15\x14\x13\x12' # 等价于 @ci
>ci : b'*\x12\x13\x14\x15'
<ci : b'*\x15\x14\x13\x12'
|
可以看出默认的 @ 模式会进行数据的对齐,i 表示 int 类型,由于字符 ‘*’ 占用了 1 个地址,所以填充了 3 个 0 使得后边的 int 对齐到位置 4。
格式化字符串 ‘ci’ 等价于 C 语言中结构体:
0 1 2 3 4 | struct ci
{
char c;
int i;
};
|
calcsize() 方法可以计算格式化字符对应的对齐后字节大小。
0 1 2 3 4 5 | print(struct.calcsize('ci'))
print(struct.calcsize('>ci'))
>>>
8
5
|
如果要格式化的参数有多个是连续类型的,例如 10 个连续的字符类型,那么无需写成 10个重复的 c,而直接用 10c 表示:
0 1 2 3 4 | sp0 = struct.pack('2c2i', b'*', b',', 0x12, 0x13)
print(sp0)
>>>
b'*,\x00\x00\x12\x00\x00\x00\x13\x00\x00\x00'
|
在默认对齐方式下,我们可以指定尾部对齐到的类型,比如 ‘0l’ 表示对齐到 long int 型。
0 1 2 3 4 | sp0 = struct.pack('llh0l', 1, 2, 3)
print(sp0)
>>>
b'\x01\x00\x00\x00\x02\x00\x00\x00\x03\x00\x00\x00'
|
也可以把多个要转换的对象放在元组里面,通过可变参数传递:
0 1 2 3 4 5 6 | values = (b'*', 3.14, 'ab'.encode('utf-8'))
s = struct.Struct('c f s') # 空格用于分隔各个格式化字符,被忽略
sp0 = s.pack(*values)
print(sp0)
>>>
b'*\x00\x00\x00\xc3\xf5H@a'
|
unpack¶
unpack() 是 pack() 的逆运算,进行解包处理。结果以元组形式返回。
0 1 2 3 4 5 | sp0 = struct.pack('lch0l', 1, b'*', 3)
up0 = struct.unpack('lch0l', sp0)
print(up0)
>>>
(1, b'*', 3)
|
如果 unpack 与 pack 使用的格式化串不一致,会抛出异常 struct.error。
0 1 2 3 4 | sp0 = struct.pack('lch0l', 1, b'*', 3)
up0 = struct.unpack('llh', sp0)
>>>
error: unpack requires a bytes object of length 10
|
使用 buffer¶
pack_into(fmt, buffer, offset, v1, v2, ...)
unpack_from(fmt, buffer, offset=0) -> (v1, v2, ...)
pack_into() 向 buffer[offset] 中打包,unpack_from() 从 buffer[offset] 中解包。注意偏移参数指定了打包写入或者解包读取的位置。
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | values0 = (b'abc', 3.1415926)
values1 = (b'zyz', 0x12)
s0 = struct.Struct('3sf')
s1 = struct.Struct('3sI')
buffer = bytearray(s0.size + s1.size)
print(buffer)
s0.pack_into(buffer, 0, *values0)
s1.pack_into(buffer, s0.size, *values1)
print(buffer)
print(s0.unpack_from(buffer, 0))
print(s1.unpack_from(buffer, s0.size))
>>>
bytearray(b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')
bytearray(b'abc\x00\xda\x0fI@xyz\x00\x12\x00\x00\x00')
(b'abc', 3.141592502593994)
(b'xyz', 18)
|
读取 bmp 头部¶
一个用 C 语言定义的结构体 bmp_header 描述了一张 bmp 格式的图片头部,转化为 struct 的格式化字符串为 ‘2sIHHI’。
0 1 2 3 4 5 | struct bmp_header{
char id[2]; /* 'BM' */
unsigned int size; /* 文件大小,单位 bytes */
unsigned short int reserved1, reserved2;
unsigned int offset; /* 图像数据偏移,单位 bytes */
};
|
我们使用 ‘2sIHHI’ 进行 unpack 操作:
0 1 2 3 4 5 6 | with open('test.bmp', 'rb') as f:
header = f.read(struct.calcsize('<2sIHHI'))
data = struct.unpack('<2sIHHI', header)
print(data)
>>>
(b'BM', 1214782, 0, 0, 62)
|
注意 bmp 文件采用小端字节序存储,解读出来的文件大小为 1214782 bytes,与实际相符。
pickle¶
有时候我们需要把 Python 对象存储到文件中,以便下次直接使用,而不用再重新生成它,比如机器学习中训练好的模型。或者我们需要在网络中传递一个 Python 对象以进行协同计算,这就需要对 Python 进行字节序列化。
pickle 可以把大部分 Python 数据类型,例如列表,元组和字典,甚至函数,类和对象(只可本地保存和加载, 参考官方 pickle 协议 )进行序列化。如果要在不同版本的 Python 间共享对象数据,需要注意转换协议的版本,可以在 dump() 和 dumps() 方法中指定,默认版本 3 只适用于 Python3。
导出到文件¶
pickle.dump() 方法将对象导出到文件,pickle.load() 加载对象。可以同时序列化多个对象,加载时顺序与写入时顺序一致。
0 1 2 3 4 5 6 7 8 9 10 11 12 | list0 = [1, 2, 3]
tuple0 = ('a', 'b', 'c')
with open('obj.txt','wb') as fw:
pickle.dump(list0, fw)
pickle.dump(tuple0, fw)
with open('obj.txt','rb') as fr:
rlist = pickle.load(fr)
rtuple = pickle.load(fr)
print(rlist, rtuple)
>>>
[1, 2, 3] ('a', 'b', 'c')
|
生成字节序列¶
pickle.dumps() 将对象转化为一个字节序列,pickle.loads() 将序列转换回对象:
0 1 2 3 4 5 6 7 8 9 10 11 | list0 = [1, 2, 3]
p = pickle.dumps(list0)
print(type(p).__name__)
print(p)
list0 = pickle.loads(p)
print(list0)
>>>
bytes
b'\x80\x03]q\x00(K\x01K\x02K\x03e.'
[1, 2, 3]
|
hashlib¶
Hash 算法又称为散列算法。通过它可以把任意长度的输入变换成固定长度的输出,该输出就是哈希值,或者散列值。这种转换是一种压缩映射,散列值的空间通常远小于输入的空间,而对于给定的散列值,没有实用的方法可以计算出一个原始输入,也就是说很难伪造,所以常常把散列值用来做信息摘要(Message Digest)。
散列算法有两个显著特点:
- 原始数据的微小变化(比如一个 1bit 翻转)会导致结果产生巨大差距。
- 运算过程不可逆,理论上无法从结果还原输入数据。
hashlib 提供了多种常见的摘要算法,如 md5,sha,blake等。
md5¶
md5 是最常见的摘要算法,速度快,生成结果是固定的 128 bits,通常用一个32位的16进制字符串表示。
digest() 返回 bytes 字节序列类型,hexdigest() 则直接返回 16 进制的字符串。传入参数必须是 bytes 类型。
0 1 2 3 4 5 6 7 8 9 | import hashlib
md5 = hashlib.md5()
md5.update('你好 Python'.encode('utf-8'))
print(md5.digest())
print(md5.hexdigest())
>>>
b'\x888]P\x8fT[\x83\x1aCj\x80\xad$\x14\x19'
88385d508f545b831a436a80ad241419
|
如果需要计算的数据非常大,那么可以循环调用 update() 方法,例如下面的示例:
0 1 2 3 4 5 6 | md5 = hashlib.md5()
md5.update('你好 '.encode('utf-8'))
md5.update(b'Python')
print(md5.hexdigest())
>>>
88385d508f545b831a436a80ad241419
|
计算结果与上面的例子是一样的,所以在进行新的数据散列计算时,需要重新生成 md5 对象。
sha1 和 sha2¶
sha 是英文 Secure Hash Algorithm 的缩写,简称为安全散列算法。
sha1 和 md5 均由 md4 发展而来,sha1 摘要比 md5 摘要长 32 bits,所以抗攻击性更强。sha224 等是 sha1 的加强版本,其中数字表示生成摘要的 bits,换算为结果的长度为 x/8*2。sha1 命名比较特殊,实际上它就是 sha160,摘要长度为 160/8*2 = 40 个字符。
sha224,sha256,sha384 和 sha512 被称为 sha2 算法。
通常信息摘要的结果越长,冲突碰撞越不易发生,安全性越高,生成的速度也越慢,当然占用的存储空间也越大。
0 1 2 3 4 5 6 7 8 9 10 11 12 13 | shas = ['sha1', 'sha224', 'sha256', 'sha384', 'sha512']
for i in shas:
sha = eval('hashlib.' + i + '()')
sha.update('你好 Python'.encode('utf-8'))
print('%s(%d)\t:' % (i, len(sha.hexdigest())), sha.hexdigest())
>>>
sha1(40) : 8ee264aee7e58f2e680587eacd9535d81e1c07fd
sha224(56) : 8ecbd4ea2e3037db9952f6e8e1df2e1e142d9756e0e26d2825e60757
sha256(64) : d87357274f3e88a5b2473719f15db8804ae8b4737f75b4f05c6dc2b0d8c8ae80
sha384(96) : c8254f627ae7de62c2ab8e94807af30c90ee01169f41e33ec0c90f808535bdd33
a8b89d44c6f790b298622f35e078dff
sha512(128) : 6a3cb3ae283d3491310dde804d196849f2acce6ecaa34156ac9d08e6b2c33e557
: a319d507dbdbf05e8d3c7ea7c6308a3c60303921f68701b768d2982752dd8f4
|
sha3¶
鉴于 sha-1 和 sha-2 潜在的安全性问题,sha3 采用了全新的 Keccak 算法,运算更快,安全性更高。它 sha2 系列算法一样,提供了不同版本。
0 1 2 3 4 5 6 7 8 9 10 11 | shas = ['sha3_224', 'sha3_256', 'sha3_384', 'sha3_512']
for i in shas:
sha = eval('hashlib.' + i + '()')
sha.update('你好 Python'.encode('utf-8'))
print('%s(%d)\t:' % (i, len(sha.hexdigest())), sha.hexdigest())
sha3_224(56) : b1ab601916f7663941fab552dadb19b73a879bf0a37a5664a80a3f26
sha3_256(64) : 7e2329a2dc0e4f64ea93e35cb99319c6cf06b2c5ead6d61e1e77a9164be6cae5
sha3_384(96) : 77172e65b6de373864915a874ba927ecda4fb5cab9e9c8c7dde50d4e3fc4f88a
9f6bb9601e268f5cf183eb6f3e94a3ad
sha3_512(128) : ec23b19dddb658d53d9bb4166c6b1958b2b55293a8d2155bdd05e8644af5ade6
cdf7a0d2d58ab9c22c3ccac995b1e9be8d0c71ca60b9b362a38a7d109bb36121
|
blake2¶
随着硬件计算能力的提升,对 md5 和 sha1 进行伪造原始数据已不再十分困难,blake2 就是为了迎接这一挑战而出现的。
blake2 系列比常见的 md5,sha1,sha2 和 sha3 运算更快,同时提供不低于 sha3 的安全性。blake2 系列从著名的 ChaCha 算法衍生而来,有两个主要版本 blake2b(blake2)和 blake2s 。
- blake2b 针对 64 位 CPU 优化设计,可以生成最长 64 字节的摘要。
- blake2s 针对 32 位 CPU 设计,可以生成最长 32 字节的摘要。
0 1 2 3 4 5 6 7 8 9 | blake2 = ['blake2b', 'blake2s']
for i in blake2:
blake = eval('hashlib.' + i + '()')
blake.update('你好 Python'.encode('utf-8'))
print('%s(%d)\t:' % (i, len(blake.hexdigest())), blake.hexdigest())
>>>
blake2b(128) : 182359731af5836fbcffc536defd712acf4fbcfa4065186b40b13c4945602cf0
709c4c44b4ad287be1b3b4e7af907b6e43eff442cd756328344eaeeb4a0eda9d
blake2s(64) : f3b98aeb02a459a950427a4c22fd6a669dde484eeae0f493c2ea51c909e63065
|
shake¶
shake128 和 shake256 算法是支持可变长度的信息摘要算法,可输出不大于 128 或 256 的任意比特数。
Python 中通过 hexdigest(n) 传递参数 n 指定输出的字节数。
0 1 2 3 4 5 6 7 8 | shas = ['shake_128', 'shake_256']
for i in shas:
sha = eval('hashlib.' + i + '()')
sha.update('你好 Python'.encode('utf-8'))
print('%s(%d)\t:' % (i, len(sha.hexdigest(10))), sha.hexdigest(10))
>>>
shake_128(20) : 72877ec8143a659e8002
shake_256(20) : c4118461f9ebbeb02d3e
|
实用场景¶
实用散列值的几种场景:
- 信息摘要,用于验证原始文档是否被篡改。
- 存储口令密码,无论是本地计算机还是服务器都不会明文存储密码,通常可以存储密码的散列值,这样即便是技术人员也无法获取用户口令。
- 网络传输,明文口令绝不应该出现在网络传输中,通常使用的挑战应答(Challenge-Response)密码验证方法就是通过传输散列值完成。
- 信息摘要类似于一个文件的指纹,同样可以用于相同文件的查找,或者检查两个文件是否相同,比如网盘数据库,不可能为每一个用户维护相同的文件,相同文件只要保存一份即可。
计算文件的 md5 值,示例:
0 1 2 3 4 5 6 7 8 9 10 11 | def file_md5(file):
import hashlib
with open(file, 'rb') as f:
md5 = hashlib.md5()
while True:
data = f.read(10240)
if len(data) > 0:
md5.update(data)
else:
break
return md5.hexdigest()
|
hmac¶
彩虹表是高效的密码散列值攻击方法,为了应对这一挑战,应该在每次对密码进行散列时,加入一个随机的密码值(也称为盐值),这样每次生成的散列值都是变化的,增大了破解难度。
HMAC(Keyed-hash Message Authentication Code 基于密码的散列消息身份验证码)利用哈希算法,以一个密钥和一个消息为输入,在生成消息摘要时将密码混入消息进行计算。
hmac 模块自身不提供散列算法,它借助 hashlib 中的算法实现 HMAC。
0 1 2 3 4 5 6 7 8 9 10 11 12 | key = b'1234567890'
hash_list = ['md5', 'sha1', 'sha224', 'sha256', 'sha384', 'sha512', 'blake2b',
'blake2s','sha3_224', 'sha3_256', 'sha3_384', 'sha3_512']
for i in hash_list:
h = hmac.new(key, b'Hello ', digestmod=i)
h.update(b'Python')
print(h.hexdigest())
>>>
d9a6f4a6e6c986332e337cff24e153ef
fe6ab8031ce989fd7e9da20f2adf80a609c04a0e
......
|
实用场景¶
hmac 主要用于密码认证,通常步骤如下:
- 服务器端生成随机的 key 值,传给客户端。
- 客户端使用 key 将帐号和密码做 HMAC ,生成一串散列值,传给服务器端。
- 服务端使用 key 和数据库中用户和密码做 HMAC 计算散列值,比对来自客户端的散列值。
这就是挑战应答(Challenge-Response)密码验证方式的基本步骤。
itertools¶
itertools 模块提供了一组常用的无限迭代器(生成器)以及一组高效的处理迭代器的函数集。
无限迭代器¶
count¶
count(start=0, step=1) --> count object
count 生成一个累加迭代器,用于生成从 start 开始的等差为 step 的数列,默认则从 0 开始,每次加 1。
由于 Python3.0 开始对数值大小不再有限制,所以它是一个无限生成器。
0 1 2 3 4 5 6 7 8 | import itertools
uints = itertools.count()
for n in uints:
if n > 10:
break
print(n, end=' ')
>>>
0 1 2 3 4 5 6 7 8 9 10
|
start 和 step 参数可以为负数和小数,不支持复数。
0 1 2 3 4 5 6 7 8 9 | uints = itertools.count(1.1, -0.1)
for n in uints:
if n < 0:
break
print(n, end=' ')
>>>
1.1 1.0 0.9 0.8 0.7000000000000001 0.6000000000000001 0.5000000000000001
0.40000000000000013 0.30000000000000016 0.20000000000000015 0.10000000000000014
1.3877787807814457e-16
|
可以借用它为列表进行编号,例如:
0 1 2 3 4 5 6 7 | list0 = ['a', 'b', 'c']
for i in zip(itertools.count(0), list0):
print(i)
>>>
(0, 'a')
(1, 'b')
(2, 'c')
|
cycle¶
cycle(iterable) --> cycle object
cycle() 会把传入的可迭代对象无限重复循环取值:
0 1 2 3 4 | for i in itertools.cycle([1,2,3]):
print(i, end=' ')
>>>
1 2 3 1 2 3......
|
repeat¶
repeat(object [,times]) -> create an iterator which returns the object
for the specified number of times. If not specified, returns the object
endlessly.
repeat() 创建一个迭代器,重复生成 object,times 指定重复计数,如果未提供 times,将无限返回该对象。
0 1 2 3 4 5 6 | for i in itertools.repeat('abc', 3):
print(i)
>>>
abc
abc
abc
|
takewhile 和 dropwhile¶
takewhile 和 dropwhile 可以为迭代器添加条件。
takewhile¶
takewhile(predicate, iterable) --> takewhile object
Return successive entries from an iterable as long as the
predicate evaluates to true for each entry.
predicate 是一个断言函数,只要返回 Flase,停止迭代。它返回一个新的迭代器。
0 1 2 3 4 5 6 7 | uints = itertools.count(0)
tw = itertools.takewhile(lambda x: x <= 10, uints)
for i in tw:
print(i, end=' ')
>>>
0 1 2 3 4 5 6 7 8 9 10
|
dropwhile¶
dropwhile(predicate, iterable) --> dropwhile object
Drop items from the iterable while predicate(item) is true.
Afterwards, return every element until the iterable is exhausted.
dropwhile() 与 takewhile() 相仿,当 predicate 断言函数返回 True 时丢弃生成的元素,一旦返回 False,返回迭代器中剩下来的项。它返回一个新的迭代器。
0 1 2 3 4 5 | dw = itertools.dropwhile(lambda x: x < 3, [1, 2, 3, 0])
for i in dw:
print(i, end=' ')
>>>
3 0
|
chain¶
chain(*iterables) --> chain object
chain() 可以把一组迭代对象串联起来,形成一个新的迭代器,返回的元素按迭代对象在参数中出现的顺序,依次取出。
0 1 2 3 4 | for i in itertools.chain('abc', [1, 2, 3]):
print(i, end=' ')
>>>
a b c 1 2 3
|
groupby¶
groupby(iterable[, keyfunc]) -> create an iterator which returns
(key, sub-iterator) grouped by each value of key(value).
groupby() 把迭代器中相邻的重复元素归类到一个组,每一个组都是一个迭代器。不相邻元素不会归类到同一个组:
0 1 2 3 4 5 6 7 | for key, group in itertools.groupby('aabbcca'):
print(key, [i for i in group])
>>>
a ['a', 'a']
b ['b', 'b']
c ['c', 'c']
a ['a']
|
可以为 groupby() 指定一个 keyfunc,只要作用于函数的元素返回的值相等,就被归类在一组,而函数返回值作为该组的 key 。
下面的例子用于从字符串中挑选出数字和非数字:
0 1 2 3 4 5 6 7 | for key, group in itertools.groupby('a0b1', lambda x: x.isdigit()):
print(key, [i for i in group])
>>>
False ['a']
True ['0']
False ['b']
True ['1']
|
我们把实现稍加改造,就可以把元素分类到多个组中:
0 1 2 3 4 5 6 | result = {True : [], False : []}
for key, group in itertools.groupby('a0b1', lambda x: x.isdigit()):
result[key] += [i for i in group]
print(result)
>>>
{True: ['0', '1'], False: ['a', 'b']}
|
compress¶
compress(data, selectors) --> iterator over selected data
compress() 类似 filter() 函数,只是它接受一个选择器,如果选择器的值为 True,非0值,非 ‘n’ 则返回元素,否则被过滤掉。
0 1 2 3 4 5 6 7 | selector = [True, False, 1, 0, -1, 'y', 'n']
val_list = [str(i) for i in selector]
print(val_list)
for item in itertools.compress(val_list, selector):
print(item, end=' ')
True 1 -1 y n
>>>
|
islice¶
islice(iterable, stop) --> islice object
islice(iterable, start, stop[, step]) --> islice object
islice() 类似序列对象的切片操作,通过索引来选择元素。
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | # 类似 string[:5]
for i in itertools.islice(itertools.count(), 5):
print(i, end=' ')
print()
# 类似 string[5:10]
for i in itertools.islice(itertools.count(), 5, 10):
print(i, end=' ')
print()
# 类似 string[0:100:10]
for i in itertools.islice(itertools.count(), 0, 100, 10):
print(i, end=' ')
print()
>>>
0 1 2 3 4
5 6 7 8 9
0 10 20 30 40 50 60 70 80 90
|
排列组合¶
permutations¶
permutations(iterable[, r]) --> permutations object
Return successive r-length permutations of elements in the iterable.
permutations() 返回一个迭代器,迭代器生成可迭代对象中选取 r 个元素的所有排列组合。
0 1 2 3 4 5 6 7 8 9 10 11 12 13 | for item in itertools.permutations([1, 2, 3], 2):
print(item)
for item in itertools.permutations(range(3)):
print(item)
>>>
(1, 2)
(1, 3)
......
(0, 1, 2)
(0, 2, 1)
(1, 0, 2)
......
|
combinations¶
combinations(iterable, r) --> combinations object
Return successive r-length combinations of elements in the iterable.
combinations() 返回一个迭代器,迭代器生成可迭代对象中选取 r 个元素的所有组合,不考虑排列顺序。
0 1 2 3 4 5 6 7 8 9 10 | for item in itertools.combinations([1, 2, 3], 2):
print(item)
for item in itertools.combinations(range(3)):
print(item)
>>>
(1, 2)
(1, 3)
(2, 3)
(0, 1, 2)
|
combinations_with_replacement() 包含只有元素重复自身形成的组合:
0 1 2 3 4 5 6 7 8 9 | for item in itertools.combinations_with_replacement([1, 2, 3], 2):
print(item)
>>>
(1, 1)
(1, 2)
(1, 3)
(2, 2)
(2, 3)
(3, 3)
|
笛卡尔积¶
product(*iterables, repeat=1) --> product object
Cartesian product of input iterables. Equivalent to nested for-loops.
product() 返回多个可迭代对象的所有排列组合,也即笛卡尔积。
0 1 2 3 4 5 6 7 8 | gp = itertools.product((1, 2), ('a', 'b'))
for i in gp:
print(i)
>>>
(1, 'a')
(1, 'b')
(2, 'a')
(2, 'b')
|
repeat 指定可迭代对象中的每个元素可以重复次数。
0 1 2 3 4 5 6 7 8 9 | gp = itertools.product((1, 2), ('a', 'b'), repeat=2)
for i in gp:
print(i)
>>>
(1, 'a', 1, 'a')
(1, 'a', 1, 'b')
(1, 'a', 2, 'a')
(1, 'a', 2, 'b')
......
|
starmap¶
starmap() 创建一个用传入的函数和可迭代对象计算的迭代器。 map() 和 starmap() 的区别在于参数传递方式:
0 1 2 3 4 5 6 7 8 9 10 11 | func = lambda x, y: (x, y, x * y)
for i in itertools.starmap(func, [(0, 5), (1, 6)]):
print('%d * %d = %d' % i)
for i in map(func, [0, 1], [5, 6]):
print('%d * %d = %d' % i)
>>>
0 * 5 = 0
1 * 6 = 6
0 * 5 = 0
1 * 6 = 6
|
迭代器复制 tee¶
tee(iterable, n=2) --> tuple of n independent iterators.
tee() 从一个可迭代对象创建 n 个独立的迭代器。类似于复制生成了多个迭代器。
0 1 2 3 4 5 6 7 8 9 10 11 | # 等价操作
import copy
uint0 = itertools.count(0)
uint1 = copy.deepcopy(uint0)
print(next(uint0), next(uint1))
uint2, uint3 = itertools.tee(itertools.count(0), 2)
print(next(uint2), next(uint3))
>>>
0 0
0 0
|
累积 accumulate¶
accumulate(iterable[, func]) --> accumulate object
Return series of accumulated sums (or other binary function results).
accumulate() 生成的迭代器返回累积求和结果,默认进行求和,可以通过传入不同的函数完成特定操作:
0 1 2 3 4 5 6 7 8 9 10 11 | # 默认累积求和
ac = itertools.accumulate([1, 2, 3, 4])
print([i for i in ac])
# 累积乘积
func = lambda x, y: x * y
ac = itertools.accumulate([1, 2, 3, 4], func)
print([i for i in ac])
>>>
[1, 3, 6, 10]
[1, 2, 6, 24]
|
注意和 functools.reduce() 的区别,reduce() 直接返回累积结果,参考 reduce 。
filterfalse¶
filterfalse(function or None, sequence) --> filterfalse object
Return those items of sequence for which function(item) is false.
If function is None, return the items that are false.
filterfalse() 与 filter() 恰恰相反,它在断言函数返回 False 时把值加入生成器,否则舍弃。
0 1 2 3 4 5 6 7 8 9 10 11 12 | dw = filter(lambda x: x < 3, [1, 2, 3, 0])
print([i for i in dw])
dw = itertools.filterfalse(lambda x: x < 3, [1, 2, 3, 0])
print([i for i in dw])
# 传入 None 返回值为 False 的元素
dw = itertools.filterfalse(None, [1, 2, 3, 0])
print([i for i in dw])
>>>
[1, 2, 0]
[3]
[0]
|
zip_longest¶
zip_longest(iter1 [,iter2 [...]], [fillvalue=None]) --> zip_longest object
zip_longest() 与 zip() 函数很像,参考 zip 合并 ,用于将两个或多个可迭代对象配对。只是当可迭代对象的长度不同,可以指定默认值。
0 1 2 3 4 5 6 | for i in zip_longest('123', 'ab', 'xy', fillvalue='default'):
print(i)
>>>
('1', 'a', 'x')
('2', 'b', 'y')
('3', 'default', 'default')
|
operator¶
Python 内建了 operator 和 functools 模块来支持函数式编程。
operator 将大部分的运算操作符(算术运算,逻辑运算,比较运算,in 关系等等)都定义了等价函数。例如加法运算:
0 1 2 | def add(a, b):
"Same as a + b."
return a + b
|
operator 模块提供的这类函数可以取代很多 lambda 匿名函数,让代码更简洁和易懂,下面是一个求阶乘的函数示例:
0 1 2 3 4 5 6 7 | def factorial(n):
from functools import reduce
return reduce(lambda x, y: x * y, range(1, n + 1))
def factorial(n):
from functools import reduce
import operator
return reduce(operator.mul, range(1, n + 1))
|
operator 除了定义了运算符等价函数,最重要的是它还定义了一组对象元素和属性的操作函数,它把属性访问转化成一个可调用(Callable)对象。
attrgetter¶
attrgetter(attr, ...) --> attrgetter object
Return a callable object that fetches the given attribute(s) from its operand.
attrgetter() 可以把对象属性转换为一个可调用对象,作为参数传递给其他函数,以实现批量处理。它等价于如下匿名函数:
0 | lambda obj, n='attrname': getattr(obj, n):
|
下面的示例批量获取一个类实例的 arg 属性值:
0 1 2 3 4 5 6 7 8 9 10 11 12 13 | import operator
class AttrObj():
def __init__(self, arg):
self.arg = arg
objs = [AttrObj(i) for i in range(5)]
ga = operator.attrgetter('arg')
# 等价于 [getattr(i, 'arg') for i in objs]
vals = [ga(i) for i in objs]
print(vals)
>>>
[0, 1, 2, 3, 4]
|
排序函数 sorted 参数 key 接受一个可调用对象,使用它的返回值作为排序关键字,示例中使用学生年龄排序:
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | class Student():
def __init__(self, name, grade, age):
self.name = name
self.grade = grade
self.age = age
def __repr__(self):
return repr((self.name, self.grade, self.age))
student_objects = [
Student('John', 'A', 15),
Student('Jane', 'B', 12),
Student('Davie', 'B', 10),
]
print(sorted(student_objects, key=operator.attrgetter('age')))
>>>
[('Davie', 'B', 10), ('Jane', 'B', 12), ('John', 'A', 15)]
|
attrgetter() 还可以传入多个属性,返回一个包含各个属性值的元组:
0 1 2 3 4 | ga = operator.attrgetter('age', 'name', 'grade')
print([ga(i) for i in student_objects])
>>>
[(15, 'John', 'A'), (12, 'Jane', 'B'), (10, 'Davie', 'B')]
|
itemgetter¶
itemgetter() 把字典的键值访问转换为一个可调用对象。
0 1 2 3 4 5 6 7 8 | list0 = [dict(val = -1 * i) for i in range(4)]
print(list0)
ga = operator.itemgetter('val')
print(ga(list0[0]))
>>>
[{'val': 0}, {'val': -1}, {'val': -2}, {'val': -3}]
0
|
可以指定字典中的特定键值来对字典进行排序:
0 1 2 3 | print(sorted(list0, key=ga))
>>>
[{'val': -3}, {'val': -2}, {'val': -1}, {'val': 0}]
|
itemgetter() 也支持多个参数,同时传入多个键,返回一个元组:
0 1 2 3 4 5 | list0 = [{'name':'John', 'age': 15, 'grade' : 'A'},
{'name':'Jane', 'age': 18, 'grade': 'B'}]
print(sorted(list0, key=operator.itemgetter('name', 'age')))
>>>
[{'name': 'Jane', 'age': 18, 'grade': 'B'}, {'name': 'John', 'age': 15, 'grade': 'A'}]
|
sorted() 根据元组进行排序,首先按名字排序,对于名字无法区分顺序的再按年龄排序。
methodcaller¶
methodcaller() 将实例的方法转换为可调用对象,可以把实例作为参数。尽管使用 attrgetter() 也可以间接实现调用,但是没有 methodcaller() 直接和简单。例如:
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | class Student():
def __init__(self, name, grade, age):
self.name = name
self.grade = grade
self.age = age
def __repr__(self):
return repr((self.name, self.grade, self.age))
def print_name(self):
print(self.name)
# 采用 attrgetter 方式
student = Student('John', 'A', 15)
ga = operator.attrgetter('print_name')
ga(student)()
# 采用 methodcaller 方式
mh = operator.methodcaller('print_name')
mh(student)
>>>
John
John
|
contextlib¶
contextlib 是一个用于生成上线文管理器(Context Manager)模块,它提供了一些装饰器,可以把一个生成器转化成上下文管理器。
所谓上下文管理器,就是实现了上下文方法(__enter__ 和 __exit__)的对象,采用 with as 语句,可以在执行一些语句前先自动执行准备工作,当语句执行完成后,再自动执行一些收尾工作。参考 __enter__ 和 __exit__ 。
contextmanager¶
要实现一个自定义的上下文管理器,就需要定义一个实现了__enter__和__exit__两个方法的类,这很麻烦, contextmanager 是一个装饰器,可以把生成器装换成上下文管理器,在 with as 语句中调用。
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | from contextlib import contextmanager
@contextmanager
def ctx_generator():
print("__enter__") # 这里做 __enter__ 动作
yield 1
print("__exit__") # 这里做 __exit__ 动作
print(type(ctx_generator()))
with ctx_generator() as obj:
print(obj)
>>>
__enter__
1
__exit__
|
当然我们也可以不返回任何对象,比如锁机制,这时只需要使用 with 语句:
0 1 2 3 4 5 6 7 8 | @contextmanager
def locked(lock):
lock.acquire()
yield
lock.release()
with locked(lock):
......
# 自动释放锁
|
closing 类¶
contextlib 中定义了一个 closing 类,这个类的定义很简单,它把传入的对象转换成一个支持 with as 语句上下文管理器。
0 1 2 3 4 5 6 | class closing(AbstractContextManager):
def __init__(self, thing):
self.thing = thing
def __enter__(self):
return self.thing
def __exit__(self, *exc_info):
self.thing.close()
|
可以看到 closing 类会把传入的对方赋值给 with as 后的变量,并在 with 语句块退出时执行对象的 close() 方法。
0 1 2 3 4 5 6 7 8 9 | from contextlib import closing
class CloseCls():
def close(self):
print("close")
with closing(CloseCls()):
pass
>>>
close
|
注意事项¶
contextlib 主要用于用户自定义的类或者自定义的上线文管理器,大部分的 Python 内置模块和第三方模块都已经实现了上线文管理器方法,例如 requests 模块, 首先应该尝试 with 语句。
0 1 | with requests.Session() as s:
......
|
即便一个对象没有实现上线文管理器方法,系统也会给出报错提示,然后再借用 contextlib。
0 1 2 3 4 | with object:
pass
>>>
AttributeError: __enter__
|
json¶
JSON(JavaScript Object Notation, JS对象标记法)是一种轻量级的数据交换格式。它原是 JavaScript 用于存储交换对象的格式, 采用完全独立于编程语言的文本格式来存储和表示数据。由于它的简洁和清晰的层次结构,易于人阅读和编写,同时也易于机器解析和生成,并有效地提升网络传输效率,使得 JSON 成为理想的数据交换格式,被很多语言支持。 相对于 xml 格式,JSON 没有过多的冗余标签,编辑更简洁,更轻量化。
JS对象的 JSON 表示¶
在 JavaScript 中,任何支持的类型都可以通过 JSON 来表示,例如字符串、数字、对象、数组等。这里简单看下 JSON 是如何表示这些对象的:
0 1 2 3 4 5 6 7 8 9 10 11 | // test.js
var num0 = 0
var num1 = 3.14
var str0 = "string"
var bool0 = true
var bool1 = false
var n = null
var array = [1, "abc"]
str = JSON.stringify(num0);
console.log(str)
//......
|
JSON.stringify() 实现 JS 数据类型向 JSON 格式的转换,由于 JSON 永远是由可视的字符串构成,可以直接打印到终端。以上内容保存在 test.js 中,通过 nodejs test.js 查看输出结果:
0 1 2 3 4 5 6 | 0
3.14
"string"
[1,"abc"]
true
false
null
|
与各种编程语言类似,数组以 [] 表示,字符串放在双引号(JSON 不支持单引号)中,其他数字,布尔量和 null 直接输出值。再看下一个对象是怎么表示的:
0 1 2 3 4 5 6 7 8 9 10 11 12 | // test1.js
testObj = new Object();
testObj.str0="string";
testObj.num0=0;
testObj.num1=3.14;
testObj.array=[1, 'abc'];
testObj.bool0=true;
testObj.bool1=false;
testObj.nop=null;
testObj.subobj = new Object();
str = JSON.stringify(testObj, null, 2);
console.log(str)
|
一个对象由 {} 表示,内容为键值对,每个键都是一个字符串,是对象的属性名,而值可以为其他任意数据类型。
0 1 2 3 4 5 6 7 8 9 10 11 12 | {
"str0": "string",
"num0": 0,
"num1": 3.14,
"array": [
1,
"abc"
],
"bool0": true,
"bool1": false,
"nop": null,
"subobj": {}
}
|
将 JSON 格式转换为 JS 对象使用 JSON.parse():
0 1 2 | str = JSON.stringify(testObj, null, 2);
newObj = JSON.parse(str)
console.log(newObj.str0) // 输出 string
|
JSON特殊字符¶
JSON 中的特殊字符有以下几种,使用时需要转义:
- ” ,字符串由双引号表示,所以字符串中出现 ” 需要转义。
- \,用于转义,当字符串中出现 \,需要使用 \。
- 控制字符 \r,\n,\f,\t,\b。
- \u 加四个16进制数字,Unicode码值,用于表示一些特殊字符,例如 \0,\v等不可见字符。中文字符在默认 utf-8 编码下可以不转换,json 模块提供了一个 ensure_ascii 开关参数。
0 1 | // 输出 "\u000b\u0000你好"
console.log(JSON.stringify('\v\0你好'))
|
为了JSON的通用性,应该保证输出的JSON文件使用 utf-8 编码。
Python类型和JSON转换¶
json 模块当前支持如下的 Python 类型和 JSON 之间互相转换。
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | +-------------------+---------------+
| Python | JSON |
+===================+===============+
| dict | object |
+-------------------+---------------+
| list, tuple | array |
+-------------------+---------------+
| str | string |
+-------------------+---------------+
| int, float | number |
+-------------------+---------------+
| True | true |
+-------------------+---------------+
| False | false |
+-------------------+---------------+
| None | null |
+-------------------+---------------+
|
dumps 和 dump¶
dumps(obj, *, skipkeys=False, ensure_ascii=True, check_circular=True, allow_nan=True,
cls=None, indent=None, separators=None, default=None, sort_keys=False, **kw)
Serialize ``obj`` to a JSON formatted ``str``.
json 中的 dumps() 函数序列化 Python 对象,生成 JSON 格式的字符串。
- indent=n, 用于缩进显示。
- sort_keys=False, 表示是否对输出进行按键排序。
- ensure_ascii=True,表示是否保证输出的JSON只包含 ASCII 字符。
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | dict0 = {'str0': 'string', 'num0' : 0, 'num1' : 3.14,
'list': [1, 'abc'], 'tuple': (1, 'abc'), 'True': True,
'False': False, 'nop': None, 'subdict': {}}
jsonstr = json.dumps(dict0, indent=2) # indent 用于格式化输出
print(jsonstr)
>>>
{
"str0": "string",
"num0": 0,
"num1": 3.14,
"list": [
1,
"abc"
],
"tuple": [
1,
"abc"
],
"True": true,
"False": false,
"nop": null,
"subdict": {}
}
|
ensure_ascii 参数默认为 True,它保证输出的字符只有 ASCII 码组成,也即所有非 ASCII 码字符,例如中文会被转义表示,格式为 \u 前缀加字符的 Unicode 码值。
0 1 2 3 4 5 | json0 = json.dumps('hello 你好', ensure_ascii=True)
json1 = json.dumps('hello 你好', ensure_ascii=False)
print(json0, json1)
>>>
"hello \u4f60\u597d" "hello 你好"
|
如果 Python 对象含有 JSON 特殊字符,dumps() 方法将自动转义:
0 1 2 3 4 | json0 = json.dumps('"\\\r\n\f\t\b\x00你好', ensure_ascii=True)
print(json0)
>>>
"\"\\\r\n\f\t\b\u0000\u4f60\u597d"
|
dump() 方法支持的参数与 dumps() 基本一致,它将生成的 JSON 写入文件描述符,无返回:
0 1 2 | data = [{'a': 'A', 'b': (2, 4), 'c': 3.0}]
with open('test.json', 'w') as fw:
json.dump(data, fw)
|
loads 和 load¶
loads() 和 load() 将 JSON 转化为 Python 对象。
0 1 2 3 4 5 6 7 | json_str = '{"key0": 1, "key1": "abc", "key3": [1,2,3]}'
dict0 = json.loads(json_str)
print(type(dict0).__name__)
print(dict0)
>>>
dict
{'key0': 1, 'key1': 'abc', 'key3': [1, 2, 3]}
|
load() 从文件描述符加载 JSON 并转化为 Python 对象。
0 1 2 3 4 5 | with open('test.json', 'r') as fr:
dict0 = json.load(fr)
print(type(dict0).__name__)
>>>
list
|
Python对象和JSON转换¶
上面已经介绍过,json 模块默认支持的类型如何与 JSON 互相转换。然而使用最多的用于自定义的 class 实例是否无法转换呢?
0 1 2 3 4 5 6 7 8 9 | class JSONCls():
def __init__(self, name, num):
self.name = name
self.num = num
obj = JSONCls('json', 0)
print(json.dumps(obj))
>>>
TypeError: Object of type 'JSONCls' is not JSON serializable
|
默认情况下,dumps()方法无法实现用户自定义对象和JSON的转换。注意到 default 参数,它可以接受一个函数,用于把对象转换为字典:
0 1 2 3 4 5 6 7 8 9 10 11 | def JSONObj2dict(obj):
d = {
'__class__': obj.__class__.__name__,
'__module__': obj.__module__,
}
d.update(obj.__dict__)
return d
print(json.dumps(obj, default=JSONObj2dict))
>>>
{"__class__": "JSONCls", "__module__": "__main__", "name": "json", "num": 0}
|
loads() 参数中的 object_hook 指定反向转换函数可以实现逆转换:
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | def dict2JSONObj(d):
if '__class__' in d:
class_name = d.pop('__class__')
module_name = d.pop('__module__')
module = __import__(module_name)
cls = getattr(module, class_name)
args = {
key: value
for key, value in d.items()
}
return cls(**args)
return d
jsonstr = '{"__class__": "JSONCls", "__module__": "__main__", "name": "json", "num": 0}'
obj = json.loads(jsonstr, object_hook=dict2JSONObj)
print(type(obj), obj.name, obj.num)
>>>
<class '__main__.JSONCls'> json 0
|
当然,如果我们不保存模块名和类名,也可以导入,只是要保证当前保存的JSON也生成的类实例是要匹配的,并明确使用 JSONCls() 对参数进行实例化:
0 1 2 3 4 5 6 | def JSONObj2dict(obj):
return obj.__dict__
def dict2JSONObj(d):
args = { key: value
for key, value in d.items()}
return JSONCls(**args)
|
base64¶
有时候我们需要把不可见字符变成可见字符来进行传输,比如邮件附件可以是图片等二进制文件,但是传输协议 SMTP 就只能传输纯文本数据,所以必须进行编码。 如果要把一个文件嵌入到另一个文本文件中,也可以使用这种方法。比如 JSON 文件中嵌入另一个 JSON 文件。
Base64 编码是一种非常简单的将任意二进制字节数据编码成可见字符的机制。
Base64 选用了”A-Z、a-z、0-9、+、/” 这 64 个可打印字符作为索引表,索引从 0-63。编码过程如下:
- 对二进制数据每 3 个 bytes 一组,每组有 3 x 8 = 24 个 bits,再将它划为 4 个小组,每组有 6 个 bits。
- 6 个 bits 的值的范围就是 0-63,通过查索引表,转换为对应的 4 个可见字符。
- 如果要编码数据字节数不是 3 的倍数,最后会剩下 1 或 2 个字节,Base64 用 x00 字节在末尾补足后进行编码,再在编码的末尾加上 1 或 2 个 ‘=’ 号,表示补了多少字节的 x00,解码的时候去掉。
所以,Base64 编码会把 3 bytes 的二进制数据编码为 4 bytes 的文本数据,数据大小会比原来增加 1/3。
编解码¶
b64encode(s, altchars=None)
Encode the bytes-like object s using Base64 and return a bytes object.
b64decode(s, altchars=None, validate=False)
Decode the Base64 encoded bytes-like object or ASCII string s.
b64encode() 接受一个 bytes 类型参数,并返回一个 bytes 对象,所以如果需要转换为字符串,则需要格式化。 b64decode() 与 b64encode() 类似,用于解码。
0 1 2 3 4 5 6 7 8 9 10 11 12 13 | import base64
encoded = base64.b64encode(b'\x00\x00\x00\xfb\xef\xff')
print(encoded)
str0 = '{}'.format(encoded) # 格式化为字符串
print(str0[2:-1])
print(base64.b64decode(encoded))
>>>
b'AAAA++//'
AAAA++//
b'\x00\x00\x00\xfb\xef\xff'
|
altchars 参数接受一个 2 字节长度的 bytes 类型,用于替换字符串中的 + 和 /。
0 1 2 3 4 5 6 7 8 9 10 11 | encoded = base64.b64encode(b'\x00\x00\x00\xfb\xef\xff', altchars=b'-_')
print(encoded)
str0 = '{}'.format(encoded)
print(str0[2:-1])
print(base64.b64decode(encoded, altchars=b'-_'))
>>>
b'AAAA--__'
AAAA--__
b'\x00\x00\x00\xfb\xef\xff'
|
URL 编码¶
- URI(Uniform Resource Identifier),统一资源标识符。
- URL(Uniform Resource Locater),统一资源定位符。
- URN(Uniform Resource Name),统一资源名称。
URI 由 URL 和 URN 组成,它们三者的关系如下:
0 1 2 3 4 5 6 | URL Anchor
---------------------------- ----
http://www.fake.com:80/path/hello.html?query=hi&arg=0#id0
----------------------------------------------
URN
-----------------------------------------------------
URI
|
URL 包含了获取资源协议,主机,端口和路径部分,它指定了一个地址位置。 URN 不含获取资源的协议部分,唯一定义了某个位置上的资源。
可以看到一个常见的 URI 可以包含如下部分:
- 获取协议(Scheme),例如 http, https, ftp 等。
- 主机和端口,以冒号分割,如果使用默认 80 端口,可以不提供端口信息。
- 路径,用 ‘/’ 表示目录层次
- 查询字符串,以 ? 开始,& 连接多个查询参数。
- 片段(Anchor),以 # 开始,标记资源中的子资源,也即资源中的某一部分,不发送给服务器,由浏览器处理。
所以我们所说的 URL 编码准确的说是 URI 编码,现实是这两个概念常常混用。
RFC3986文档规定,URL 中允许字符为 [a-zA-Z0-9] 和 “-_.~4” 个特殊字符以及保留字符。
保留字符:URL可以划分成若干个组件,协议、主机、路径等。有一些字符(:/?#[]@)是用作分隔不同组件的。例如:冒号用于分隔协议和主机,/用于分隔主机和路径,?用于分隔路径和查询参数,等等。还有一些字符(!$&’()*+,;=)用于在每个组件中起到分隔作用的,如=用于表示查询参数中的键值对,&符号用于分隔查询多个键值对。当组件中的普通数据包含这些特殊字符时,需要对其进行编码。
RFC3986中指定的保留字符有 ! * ‘ ( ) ; : @ & = + $ , / ? # [ ]。
所有其他字符均需要编码,当保留字符出现在非功能分割字段时也需要编码。
URL 编码采用百分号编码方式:一个百分号 %,后跟两个表示字符 ASCII 码值的16进制数,例如 %20 表示空格。
RFC 推荐字符采用 UTF-8 编码方式,也即一个字符的 UTF-8 编码为 0x111213,那么 URL 编码对应 %11%12%13。
Base64对URL编码¶
由于 URL 编码不支持标准的 Base64 索引表中的 + 和 / 字符,所以可以使用 - 和 _ 替代它们。
base64 模块内置了 urlsafe_b64encode() 和 urlsafe_b64decode() 方法用于 URL 编码:
0 1 2 3 4 5 6 7 8 9 10 11 | encoded = base64.urlsafe_b64encode(b'\0\0\0\xfb\xef\xff')
print(encoded)
str0 = '{}'.format(encoded)
print(str0[2:-1])
print(base64.urlsafe_b64decode(encoded))
>>>
b'AAAA--__'
AAAA--__
b'\x00\x00\x00\xfb\xef\xff'
|
由于=字符也可能出现在Base64编码中,但=在URL和Cookie中不是合法字符,所以,很多Base64编码后会把=去掉。 Base64 把3个字节变为4个字节,Base64编码的长度总是 4 的倍数,当发现编码后长度不是 4 的倍数后,补足= 再解码就可以了。
注意事项¶
Base64 适用于轻量级编码,比如查询字段,Cookie内容和内嵌文件,例如在 JSON 或 XML 中内嵌一段二进制数据。
Base64 编码不适用于加密,它只是简单的字符映射,很容易被破解。
C 语言模块¶
由于 Python 是解释型语言,非常适合处理 IO 密集型任务,但是不善于处理计算密集型任务,例如加解密,压缩解压缩等,这类任务如果全部使用 Python 开发,是非常低效的,通常的做法是将算法密集型任务使用 C 语言实现,并用 Python 封装接口以方便用户调用。
为了同时发挥 Python 快速开发和 C 语言的快速处理的优点,Python 支持调用 C 语言接口。实际上 Python 底层均是由 C 语言开发的,调用 C 语言接口是最基本的功能。通常有两种方式来与 C 语言开发的模块(win 上为 dll,linux 上为 so 文件)交互:
- 原生方式,直接使用 Python-dev 开发包开发 C 模块。
- 通过 ctypes 模块实现 Python 调用 C 接口。
原生方式¶
原生方式直接调用Python-dev 开发包开发 C 模块,这种方式必须使用开发包中的接口函数开发,需要熟悉相关数据类型和函数(这些函数通常被称为包裹函数)。通过这些函数可以方便实现任意参数类型的接口,但是开发出的 .so 通常作为 Python 的专用模块,不可以被其他 C 应用程序调用。
大部分依赖 C 模块的第三方软件库使用这种方式开发,例如科学计算中的 numpy,scipy 软件库。这里以一个累加函数为例,来阐述原生方式的开发流程。
必须说明,这里使用的是 Python3 环境,参考 Extending Python3 with C ;Python2 与此略有不同,参考 Extending Python2.7 with C 。
首先创建 C 语言文件 accumulate.c 和对应函数:
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | #include <stdio.h>
unsigned int accumulate(unsigned int n)
{
unsigned int sum = 0;
unsigned int i = 0;
for(; i <= n; i++)
sum += i;
return sum;
}
int main()
{
printf("%u", accumulate(100));
}
|
在进一步操作之前,我们必须保证这些函数是正确执行的,编译测试它们:
0 1 | $ make accumulate && ./accumulate
5050
|
为了让 Python 解析器 CPython 可以解析这里的 C 函数,需要使用 Python.h 头文件里面的类型和函数来封装我们的 C 语言函数和类型参数。这里需要安装 Python 编译环境,pythonx.y-dev 提供了相关头文件和库文件:
0 | $ sudo apt-get install python3.6-dev
|
接着使用 Python.h 相关类型和函数包裹我们的 C 函数,以编译为 Python 接口模块:
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | #include <Python.h>
static unsigned int _accumulate(unsigned int n)
{
unsigned int sum = 0;
unsigned int i = 0;
for(; i <= n; i++)
sum += i;
return sum;
}
static PyObject *accumulate(PyObject* self, PyObject* args)
{
unsigned int n = 0;
// 类型解析参考 https://docs.python.org/3/c-api/arg.html#c.PyArg_ParseTuple
if(!PyArg_ParseTuple(args, "i", &n))
return NULL;
return Py_BuildValue("i", _accumulate(n));
}
static PyMethodDef accuMethods[] =
{
{"accumulate", accumulate, METH_VARARGS, "Calculate the accumulation of n"},
{NULL, NULL, 0, NULL}
};
static PyModuleDef accuModule =
{
PyModuleDef_HEAD_INIT,
"accuModule", // module name
"accumulate module description",// module description
-1,
accuMethods
};
// 仅有的非 static 函数,对外暴露模块接口,PyInit_name 必须和模块名相同
// only one non-static function
PyMODINIT_FUNC PyInit_accuModule(void)
{
return PyModule_Create(&accuModule);
}
|
最后创建 setup.py 以编译目标 .so 文件,
0 1 2 3 4 5 6 7 | from distutils.core import setup, Extension
module = Extension('accuModule', sources = ['accumulate.c'])
setup(name = 'accuModule',
version = '1.0',
description = 'This is a demo package',
ext_modules = [module])
|
此时文件夹中包含源码文件 accumulate.c 和 setup.py,开始编译:
0 1 2 3 4 5 6 | $ python3.6 setup.py build
running build
running build_ext
building 'accuModule' extension
creating build
creating build/temp.linux-i686-3.6
...
|
编译后当前路径文件目录如下所示:
0 1 2 3 4 5 6 7 8 | $ tree
.
├── accumulate.c
├── build
│ ├── lib.linux-i686-3.6
│ │ └── accuModule.cpython-36m-i386-linux-gnu.so # 目标模块文件
│ └── temp.linux-i686-3.6
│ └── accumulate.o
└── setup.py
|
最后安装模块:
0 1 2 3 4 5 6 7 8 | $ sudo python3.6 setup.py install --record install.txt
running install
running build
running build_ext
running install_lib
copying build/lib.linux-i686-3.6/accuModule.cpython-36m-i386-linux-gnu.so \
-> /usr/local/lib/python3.6/dist-packages
running install_egg_info
Writing /usr/local/lib/python3.6/dist-packages/accuModule-1.0.egg-info
|
可以看到模块被安装在了 /usr/local/lib/python3.6/dist-packages 目录下。
开发中我们可能要多次删除和安装模块,但是 setup.py 没有提供对应的下载命令,这里使用 record 记录了安装到所有文件,如果要卸载,直接删除 install.txt 中记录的文件即可:
0 1 2 3 4 5 | $ cat install.txt
/usr/local/lib/python3.6/dist-packages/accuModule.cpython-36m-i386-linux-gnu.so
/usr/local/lib/python3.6/dist-packages/accuModule-1.0.egg-info
# 卸载软件包
$ cat install.txt | xargs rm -rf
|
最后测试模块,创建 test.py:
0 1 2 | from accuModule import accumulate
print(accumulate(100))
|
执行 python3.6 test.py 可以得到结果 5050。
ctypes¶
采用原生方式对每一个 C 语言接口进行打包是很繁琐的,为了简化接口调用,ctypes 模块提供了和 C 语言兼容的数据类型和函数来加载模块(dll或so)文件,因此在调用时不需对源文件做任何的修改。
关于 ctypes 的更多信息参考 A foreign function library for Python 。
0 1 2 3 4 5 6 7 8 9 10 11 | #include <stdio.h>
unsigned int accumulate(unsigned int n)
{
unsigned int sum = 0;
unsigned int i = 0;
for(; i <= n; i++)
sum += i;
return sum;
}
|
注意对外接口不可定义为 static 的,通过 gcc 编译得到 .so 文件:
0 1 2 3 4 5 6 7 | # Mac
$ gcc -shared -Wl,-install_name,accumulate.so -o accumulate.so -fPIC accumulate.c
# windows
$ gcc -shared -Wl,-soname,adder -o accumulate.dll -fPIC accumulate.c
# Linux
$ gcc -shared -Wl,-soname,adder -o accumulate.so -fPIC accumulate.c
|
修改 test.py 直接通过 ctypes 模块中的 CDLL 方法加载模块:
0 1 2 3 4 | from ctypes import *
#load the module file
accuModule = CDLL('./accumulate.so')
print(accuModule.accumulate(100))
|
ctypes 接口允许我们在调用 C 函数时使用Python 中默认的字符串型和整型。
但是对于其他类型,例如布尔型和浮点型,必须要使用对应的 ctype 类型才可以。这种方法虽然简单,清晰,但是却有很大限制:例如,不能在 C 中对 Python 对象进行操作。
连接数据库¶
对于数据库的操作通常分为以下几个步骤,Python 相关的数据库模块同样遵循它们:
- connect 连接数据库。
- 获取游标(Cursor),用于执行 SQL 语句。
- 数据库操作 execute:删除,插入,查询等。
- 提交数据 commit。
- 关闭游标和数据库 close。
所以 Python 提供了相对统一的数据库访问 API。
sqlite¶
sqlite 是文件型轻量级数据库,也即所有数据信息都保存在一个文件中,对数据库操作也直接访问数据文件,无需服务端,无需网络通信。不需要安装和配置,简单易用,数据易于迁移。 适用于单一存储业务,嵌入式应用,移动应用和游戏。 sqlite 数据库同一时间只允许一个写操作,吞吐量有限,不适合大型并发应用。
sqlite3 默认使用 utf-8 编码存储数据。
连接和关闭¶
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | import sqlite3
# 连接数据库
conn = sqlite3.connect('sqlite.db')
cursor = conn.cursor()
print(type(conn).__name__)
print(type(cursor).__name__)
# 表操作和提交
cursor.close() # 关闭游标
conn.close() # 关闭数据库
>>>
Connection
Cursor
|
表的操作¶
SQL语句中如果包含单引号或者双引号,应使用三引号引用语句。
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | # 创建表
cursor.execute('''create table test
(id char(32) primary key not null,
name text not null,
age int not null);''')
# 插入
cursor.execute('''insert into test (id, name, age) values ('000', 'John', '23')''')
cursor.execute('''insert into test (id, name, age) values ('001', 'Tom', '25')''')
cursor.execute('''insert into test (id, name, age) values ('002', 'Jack', '29')''')
cursor.execute('select * from test')
print(cursor.fetchone()) # 从查询结果集中返回一条,无返回 None
all = cursor.fetchall() # 取查询结果集中所有行(如使用过fetchone,则返回其余行)无返回一个空列表。
print (all)
# 删除表
cursor.execute('drop table test')
conn.commit()
# 返还磁盘空间,否则删除的空间只会插入空闲链表
cursor.execute('vacuum')
cursor.close()
conn.close()
>>>
('000', 'John', 23)
[('001', 'Tom', 25), ('002', 'Jack', 29)]
|
mysql¶
mysql 环境配置¶
使用 mysql 要比 sqlite 复杂一些,需要安装服务端和客户端并进行一些参数配置。它功能强大,支持高并发。
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | # 如已配置过 mysql 环境,使用如下命令测试,应进入交互模式
mysql -u root -p
# 如果提示 host 无法解析,表示本机通信无法建立
cat /etc/hostname # 例如为 ubuntu,在 /etc/hosts 中添加 127.0.0.1 ubuntu
# 以Ubuntu14.04 环境为例,杀死相关进程
ps -A |grep mysql
kill -9 xxxx
# 删除安装包
sudo apt-get remove mysql-*
sudo rm -rf /usr/share/mysql/
sudo rm -rf /etc/mysql/conf.d
# 安装过程中会提示设置 root 密码
sudo apt-get install mysql-server
sudo apt-get install mysql-client
# 查看运行状态
sudo service mysql status
# mysql start/running, process 12193
# 如果没有运行则手动启动
sudo service mysql start
|
为了保证数据的通用性,应该设置 UTF8 编码,通过如下方式查看:
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | # 进入交互终端
mysql -u root -p
mysql> show variables like 'character_set%';
+--------------------------+----------------------------+
| Variable_name | Value |
+--------------------------+----------------------------+
| character_set_client | utf8 |
| character_set_connection | utf8 |
| character_set_database | utf8 |
| character_set_filesystem | binary |
| character_set_results | utf8 |
| character_set_server | utf8 |
| character_set_system | utf8 |
| character_sets_dir | /usr/share/mysql/charsets/ |
+--------------------------+----------------------------+
8 rows in set (0.00 sec)
|
如果编码相关值不是 utf8 ,应该通过配置文件 /etc/mysql/my.cnf 配置:
0 1 2 3 4 5 6 7 8 9 | [client] # client 字段下添加
default-character-set = utf8
[mysqld] # mysqld 字段下添加
character-set-server = utf8
sudo service mysql restart
# 如果在更改配置文件后启动失败,查看日志文件,根据提示修改
cat /var/log/mysql/error.log
|
设置完毕后进入交互终端确认编码生效。
Python 链接 mysql 数据库需要安装第三方驱动,例如 mysql-connector:
0 | pip install mysql-connector
|
数据库操作¶
mysql 数据库操作与 sqlite 流程基本一致:
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | import mysql.connector
# 如果数据库已经存在,可以直接指定 db='dbname' 参数
conn = mysql.connector.connect(host='127.0.0.1', port=3306, user='root',
password='password', charset='utf8')
cursor = conn.cursor()
cursor.execute('show databases') # 查询数据库
print(cursor.fetchall()) # 查询命令后必须进行 fetch 操作
# 创建 test 数据库
cursor.execute('create database if not exists test')
cursor.execute('use test')
try:
cursor.execute('''create table test
(id char(32) primary key not null,
name test not null,
age int not null);''')
except:
pass
cursor.execute('''insert into test (id, name, age) values ('000', 'John', '23')''')
cursor.execute('''insert into test (id, name, age) values ('001', 'Tom', '25')''')
cursor.execute('''insert into test (id, name, age) values ('002', 'Jack', '29')''')
cursor.execute('''select * from test''')
# 查询一个结果
print(cursor.fetchone())
print(cursor.fetchall())
conn.commit()
cursor.close()
conn.close()
|
要注意的是查询命令后必须进行 fetch 操作,并取完所有结果,否则会报 Unread result found 错误。
ORM 框架¶
如果我们要使用多套数据库,那么就要实现多套数据库SQL接口,例如查询,删除等等,这导致代码重复繁琐,是否可以提供一个抽象层,把对数据库的SQL操作转化为对象操作呢?
对象关系映射(ORM,Object Relational Mapping)通过使用描述对象和数据库之间映射的元数据,将程序中的对象操作自动关联到关系数据库中。 ORM 是一种技术解决方案, Python 下提供了很多 ORM 的模块实现,如 peewee,Storm,SQLObject 和 SQLAlchemy。
使用 ORM 操作数据库相当于增加了一个封装转换层,性能上会打折扣,要根据实际情况选择使用。
peewee¶
peewee 是一个非常轻量级的 Python ORM 实现,它提供类似著名的 Django Web 框架 API,非常易于上手。
peewee 中定义了 Model 类,Field 和 Nodel 实例与数据库的映射关系如下:
peewee对象 | 数据库对象 |
---|---|
Model类 | 数据库表 |
Field 实例 | 表中的列 |
Model 实例 | 表中的行 |
安装第三方模块 peewee 非常简单 pip install peewee 。
创建表¶
一个表对应一个类,它继承 Model 类。例如定义一个 Person 类,那么将自动生成一个名为 person 的表,在元类中指定使用的数据库。
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | from peewee import *
db = SqliteDatabase('people.db')
db.connect() # 可选,无需显式连接数据库,但是要显式 close()
class Person(Model):
name = TextField() # Field 对应列
age = IntegerField()
# 元类中指定连接的数据库,这里以Sqlite 数据库为例
class Meta:
database = db
# 在 people.db 中自动创建名为 person 的表
Person.create_table()
# 同时创建多个表
# database.create_tables([Person])
# sqlite 命令查询表
sqlite> .table
person
|
插入行¶
表的插入非常简单,调用类函数 Model.create() 即可,并且会自动生成主键 id:
0 1 2 3 4 5 6 7 8 9 10 11 12 | def print_person(item):
print(item.id, item.name, item.age)
# Person() 实例对应行
grandma = Person.create(name='Grandma', age = 60)
grandpa = Person.create(name='Grandpa', age = 62)
print_person(grandma)
print_person(grandpa)
>>>
1 Grandma 60
2 Grandpa 62
|
如果我们定义 Person 类时指定主键,将不再自动生成 id,例如:
0 1 2 | class Person(Model):
name = TextField(primary_key=True) # 指定 name 为主键
age = IntegerField()
|
可以批量添加数据,只需要将参数集中在一个字典里:
0 1 2 3 4 5 6 | persons = {{name='Grandma', age=60}, {name='Grandpa', age=62}}
for args in persons:
Person.create(**args)
# 或
for args in persons:
Person(**args).save()
|
采用 sqlite 查看建表语句和 person 表中的数据:
0 1 2 3 4 5 6 | sqlite> .schema person
CREATE TABLE "person" ("id" INTEGER NOT NULL PRIMARY KEY,
"name" DATE NOT NULL, "age" INTEGER NOT NULL);
sqlite> select * from person;
1|Grandma|60
2|Grandpa|62
|
如果不指定主键,会自动生成 id ,我们在插入前需要判断当需要插入的数据是否存在。
查询¶
使用 Model.select() 或者 Model.get() 类函数可以查询特定行,或者所有行:
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | person = Person.select().where(Person.name == 'Grandma').get()
print_person(person)
>>>
1 Grandma 60
# 简化的查询方法
person = Person.get(Person.name == 'Grandma')
print_person(person)
>>>
1 Grandma
# 查询所有行
for person in Person.select():
print_person(person)
>>>
1 Grandma 60
2 Grandpa 62
|
也可以指定条件查询:
0 1 | for person in Person.filter(Person.id > 1):
print_person(person)
|
在查询是可以使用 Model.order_by() 方法进行排序:
0 1 | for person in Person.select().order_by(Person.name):
print_person(person)
|
更新数据¶
Model.save() 用于更新数据,也可以用来插入新的行,它返回行 id, 例如:
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | # 更新 id 为 1 的行
p = Person(name='Mother', age=28)
p.id = 1
p.save()
# 添加新行
father = Person(name='Father', age=30)
id = father.save()
print(id)
# 更新 Father 行的 age 信息
father = Person.get(Person.name == 'Father')
father.age = 32
id = father.save()
print(id)
>>>
2
2
|
Model.delete() 类函数可以清空表或某个表项,instance.delete_instance() 用于删除一个特定表项:
0 1 2 3 4 5 6 7 8 9 10 11 12 | # 选择删除,成功返回 1,否则返回 0
Person.delete().where(Person.name == 'father').execute()
# 清空 Person 表项
Person.delete().execute()
# 已实例化的数据删除
father = Person.get(Person.id == 3)
id = father.delete_instance()
print(id)
>>>
2
|
连接各类数据库¶
上面的示例使用 SQLite 数据库,peewee 目前支持 SQLite, MySQL 和 Postgres。
0 1 2 3 4 5 6 7 8 9 10 11 12 | # SQLite 数据库,支持外键,启用 WAL 日志模式,缓存64MB
sqlite_db = SqliteDatabase('test.db', pragmas={
'foreign_keys': 1,
'journal_mode': 'wal',
'cache_size': -1024 * 64})
# 连接 MySQL 数据库 dbname
mysql_db = MySQLDatabase('dbname', user='username', password='password',
host='127.0.0.1', port=3306)
# 连接 Postgres 数据库
pg_db = PostgresqlDatabase('dbname', user='username', password='password',
host='127.0.0.1', port=5432)
|
peewee 使用 pymysql 或 MySQLdb 作为 MySQL 驱动模块,如果没有安装会提示错误。
Foreign Keys¶
如果一个表中一列要引用另一个表中的表项,例如一个家庭成员的表和一个宠物表,每个宠物都有它的主人,那么在主人这一项里面就可以引用家庭成员表中某个成员的 id,这种引用被称为外键。
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | db = SqliteDatabase('people.db')
class Person(Model):
name = CharField()
birthday = DateField()
class Meta:
database = db
class Pet(Model):
name = CharField()
animal_type = CharField()
# 定义外键
owner = ForeignKeyField(Person, backref='pets')
class Meta:
database = db
# 创建 person 和 pet 表
db.create_tables([Person, Pet])
|
上面定义了个两个类,一个用于定义家庭成员,一个用于定义宠物。它们对应数据库中的两张表 person 和 pet,pet 中的主人一栏引用 person 中的 id。
为两个表插入一些表项:
0 1 2 3 4 5 6 7 8 9 10 11 12 13 | bob = Person.create(name='Bob', age=60)
herb = Person.create(name='Herb', age=30)
bob_kitty = Pet.create(owner=bob, name='Kitty', animal_type='cat')
herb_fido = Pet.create(owner=herb, name='Fido', animal_type='dog')
herb_mittens = Pet.create(owner=herb, name='Mittens', animal_type='cat')
sqlite> select * from person;
1|Bob|60
2|Herb|30
sqlite> select * from pet;
1|1|Kitty|cat
2|2|Fido|dog
3|2|Mittens|cat
|
通过 sqlite 可以查询到每个 pet 的 owner 一项都对应它主人的 id 。
0 1 2 3 4 5 6 | query = Pet.select().where(Pet.animal_type == 'cat')
for pet in query:
print(pet.name, pet.owner.name)
>>>
Kitty Bob
Mittens Herb
|
SQLAlchemy¶
SQLAlchemy 是另一个著名的 Python ORM 模块,专为高效率和高性能的数据库访问设计的,代码比较复杂。Python 另一著名的轻量级 Web 框架 Flask 就基于 SQLAlchemy 实现了 flask-sqlaichemy ORM 模型。
SQLAlchemy 的一大特点在于所有的数据库操作通过一个数据库 session 进行,在该session中控制每个对象的生命周期 。
sqlalchemy数据类型¶
sqlalchemy 常用类型 和 Python 数据类型对照表:
数据类型 | Python 类型 | 说明 |
---|---|---|
Integer | int | 整形 |
String | str | 字符串 |
Float | float | 浮点型 |
DECIMAL | decimal.Decimal | 定点型 |
Boolean | bool | 布尔型 |
Enum | str | 枚举类型 |
Text | str | 文本类型 |
LongText | str | 长文本类型 |
Date | datetime.date | 日期 |
DateTime | datetime.datetime | 日期和时间 |
Time | datetime.time | 时间 |
数据库操作¶
相对于 peewee,SQLAlchemy 操作数据库需要创建一个进行数据库操作的 session,它被称为工作单元,一个数据库对应一个 session,如果要进行多个数据库交互,就要创建多个 session,它实现了数据库之间的隔离。
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | from sqlalchemy import create_engine,Column,String,Integer
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm.session import sessionmaker
# 创建对象的基类:
Base = declarative_base()
# 定义Person对象
class Person(Base):
# 表名
__tablename__ = 'person'
# 表结构
id = Column(Integer, primary_key=True , autoincrement=True)
name = Column(String(20))
age = Column(Integer)
# 初始化数据库连接:
engine = create_engine('sqlite:///person.db')
# 创建session对象:
session = sessionmaker(bind=engine)()
# 创建表结构
Base.metadata.create_all(engine)
# 将实例对象添加到 session
session.add(Person(name='Grandma', age=60))
session.add(Person(name='Grandpa', age=62))
# 提交到数据库
session.commit()
# 关闭 session
session.close()
|
这里我们指定自动生成 id,通过 sqlite 查看插入的数据:
0 1 2 | sqlite> select * from person;
1|Grandma|60
2|Grandpa|62
|
附录¶
参考书目¶
参考网站¶
数据库相关¶
图像相关¶
RST语法参考¶
LaTeX数学表达式¶
This: \((x+a)^3\)
this: \(W \approx \sum{f(x_k) \Delta x}\)
this: \(W = \int_{a}^{b}{f(x) dx}\)
\(\sqrt{x}\),不好处理
and this:
\[ \frac{1}{\Bigl(\sqrt{\phi \sqrt{5}}-\phi\Bigr) e^{\frac25 \pi}} = 1+\frac{e^{-2\pi}} {1+\frac{e^{-4\pi}} {1+\frac{e^{-6\pi}} {1+\frac{e^{-8\pi}} {1+\ldots} } } } \]When \(a \ne 0\), there are two solutions to \(ax^2 + bx + c = 0\) and they are \(x = {-b \pm \sqrt{b^2-4ac} \over 2a}.\)
其他语法¶
驱动器 C 中的卷是 系统专区
卷的序列号是 78E7-2220
注意
任何对文件的读取和写入动作,都会自动改变文件的指针偏移位置。
重点(emphasis)通常显示为斜体
重点强调(strong emphasis)通常显示为粗体
解释文字(interpreted text)通常显示为斜体
时间: | 2016年06月21日 |
---|
- 枚举列表1
- 枚举列表2
- 枚举列表3
- 枚举列表1
- 枚举列表2
- 枚举列表3
- 枚举列表1
- 枚举列表2
- 枚举列表3
下面是引用的内容:
“真的猛士,敢于直面惨淡的人生,敢于正视淋漓的鲜血。”
—鲁迅
“人生的意志和劳动将创造奇迹般的奇迹。”
—涅克拉索
0 1 2 | def AAAA(a,b,c):
for num in nums:
print(Num)
|
-a | command-line option “a” |
-b file | options can have arguments and long descriptions |
--long | options can be long also |
--input=file | long options can also have arguments |
/V | DOS/VMS-style options toofdsfds
fdsafdsafdsafsafdsafsa
fdsafdsafsd
|
John Doe wrote:
>> Great idea!
>
> Why didn't I think of that?
You just did! ;-)
A one, two, a one two three fourHalf a bee, philosophically,must, ipso facto, half not be.But half the bee has got to be,vis a vis its entity. D’you see?But can a bee be said to beor not to be an entire bee,when half the bee is not a bee,due to some ancient injury?Singing…
col 1 | col 2 |
---|---|
1 | Second column of row 1. |
2 | Second column of row 2. Second line of paragraph. |
3 |
|
Row 4; column 1 will be empty. |
功能
- 你好 list item. The blank line above the first list item is required; blank lines between list items (such as below this paragraph) are optional.
函数
你好 is the first paragraph in the second item in the list.
This is the second paragraph in the second item in the list. The blank line above this paragraph is required. The left edge of this paragraph lines up with the paragraph above, both indented relative to the bullet.
- This is a sublist. The bullet lines up with the left edge of the text blocks above. A sublist is a new list so requires a blank line above and below.
原始文本块内的任何标记都不会被转换,随便写。
`Bary.com <http://www.bary.com/>`_
这还会显示在原始文本块中。
缩进都会原样显示出来。
只要最后有空行,缩进退回到 :: 的位置,就表示退出了\ `原始文本块`_。
会自动把网址转成超链接,像这样 http://www.bary.com/ ,注意结束的地方要跟空格。
如果你希望网址和文本之间没有空格,可以用转义符号反斜杠 \ 把空格消掉,由于反斜杠是转义符号,所以如果你想在文中显示它,需要打两个反斜杠,也就是用反斜杠转义一个反斜杠。
渲染后紧挨文本和句号的超链接http://www.bary.com/。
其实遇到紧跟常用的标点的情况时,不需要用空格,只是统一使用空格记忆负担小。你看http://www.bary.com/,这样也行。
注解
写完本文我发现我用的渲染器对中文自动消除了空格,行尾不加反斜杠也行,但我不保证其他渲染器也这么智能,所以原样保留了文内的反斜杠。
如果希望硬断行且不自动添加空格(例如中文文章),在行尾添加一个反斜杠。折上去的部分就不会有空格。注意所有的硬换行都要对齐缩进。
打开模式 | r | r+ | w | w+ | a | a+ |
---|---|---|---|---|---|---|
可读 | ||||||
可写 | ||||||
创建 | ||||||
覆盖 | ||||||
指针在开始 | ||||||
指针在结尾 | ||||||
以空格作分隔符,间距均匀。决定了这个表格最多可以有5列,下划线的长度应不小于字符长度。 每一行的下划线,决定了相应列是否合并,如果不打算合并列,可以取消表内分隔线
11 12 | 13 14 15 | |||
21 | 22 | 23 | 24 | 25 |
31 | 32 33 | 34 35 | ||
41 42 42 44 45 |
Date: | 2001-08-16 |
---|---|
Version: | 1 |
Authors: |
|
Indentation: | Since the field marker may be quite long, the second and subsequent lines of the field body do not have to line up with the first line, but they must be indented relative to the field name marker, and they must line up with each other. |
Parameter i: | integer |