言简意赅 の Python3
Python 诞生于 20 世纪 90 年代初,是一款非常简洁易读的解释型脚本语言;擅长于科学计算与图形处理,传统的计算机视觉库 OpenCV、三维可视化库 VTK、医学图像处理库 ITK都提供了 Python 调用接口,Python 也原生提供了 NumPy、SciPy、matplotlib等强大的科学计算扩展库。Web 应用开发方面,Python 也提供有 Django、Tornado 等开发框架。概而言之,得益于强大的开源社区支持,Python 已经成为一门功能丰富的胶水语言。
本文示例代码基于Python
3.6.6版本编写,在简单介绍相关语法以及 pip、virtualenv
等扩展库的使用之后,将会实现一个基于官方 XML
解析库ElementTree
的 SVG
图片合并小程序。文中涉及的代码已经提交至笔者的 Github,需要的朋友可以直接进行克隆,任何缪误或者建议请向我提交issue。如果需要采用
Python 进行 Web 开发,可以参见我的《使用 Django2
快速开发 Web 项目》一文。
Hello World
Python 运行环境安装非常方便,Windows 操作系统下直接前往Python 官网下载安装包(注意添加环境变量),使用 Debian 软件包格式的 Linux 操作系统可以通过如下命令安装:
1 | ➜ sudo apt install python3 |
笔者的 Linux
开发环境下,同时存在Python 3.6.6
和Python 2.7.15
两个版本,因此在
Z-Shell
命令行中运行Hello World
程序时,需要显式输入python3
,以指定操作系统打开Python 3.6.6
运行环境。
1 | ➜ / python3 |
代码执行完毕后,可以通过按下
CTRL + D
或者输入exit()
退出 Python 运行环境。
当然,也可以单独将print("Hello World!")
保存到一个独立的test.py
代码文件,然后在命令行当中直接开始运行。
1 | ➜ 1-hello-world python3 test1.py |
默认情况下,Python
源代码是以UTF-8
格式进行编码的,但可以通过向源文件头部添加如下声明来指定编码格式。
1 | # -*- coding: utf-8 -*- |
可以在.py
脚本文件的头部手动添加 Python
解释器的路径,从而在 Linux
系统可以方便的通过./test1.py
执行该脚本,避免python3 test1.py
写法的繁琐。
1 | #!/usr/bin/python3 |
Python 使用缩进来表示代码块,相同代码块的语句必须包含相同的缩进空格数。
1 | # -*- coding: utf-8 -*- |
代码将只会执行最后的print
语句,运行结果如下:
1 | ➜ 1-hello-world git:(master) ✗ python3 test2.py |
变量
Python
是弱类型语言,因此声明变量时不需要指定数据类型,现在修改上一步的例子,声明一个infomation
变量然后输出:
1 | infomation = "Hello World!" |
执行结果如下:
1 | ➜ python3 test1.py |
相对 C、Go、Java 等强类型语言,Python 的语法结构更为松散,但是变量的命名依然需要遵循以下规则:
- 变量名只能包含字母、数字、下划线,但不能以数字开头,例如
infomation_1
是合法的变量声明,但是1_infomation
则属于非法。 - 变量名不能包含空格,不过可以使用下划线来分隔单词,例如
print_something
是合法的,但是print something
属于非法。 - 不能使用 Python
关键字、函数名称作为变量名,例如
lambda
、yield
、raise
、def
、nonlocal
、elif
、assert
、except
、pass
、with
。 - 建议使用小写字母作为变量名,并且谨慎使用小写字母
l
和大写字母O
,因为两者很容易被代码阅读者混淆为数字1
和0
。 - 变量命名尽量见文知意,避免过度的缩写,例如
person_name
明显比person_n
更加易读。
熟悉了变量命名规则之后,进一步扩展上面的程序,对变量infomation
重新进行赋值然后打印输出:
1 | infomation = "Hello World!" |
执行后将会输出两次打印结果:
1 | ➜ python3 test2.py |
traceback(回溯,追踪)是 Python 语法解析器提供给开发人员的代码错误提示信息,可以更加直观的定位错误发生的代码位置。
1 | ➜ python test3.py |
注释
Python 使用#
作为注释声明符号:
1 | # Comment |
或者使用多个井号来声明多行注释:
1 | # First Comment |
网络上有些文章介绍使用
3
个单引号'''
或双引号"""
声明多行注释是不正确的做法,因为字符串内声明的内容虽然不会被直接显示给用户,但是 Python 语法解释器会耗费额外的计算资源解析这些字符串,而使用#
号声明的注释并不会在存在这样的问题。
数据类型
Python
是弱类型语言,使用前不需要专门声明,赋值之后变量即被创建。Python
一共拥有 5
种标准的数据类型:数值(Number)、字符串(String)、列表(List)、元组(Tuple)、字典(Dictionary)。Python
这 5
种标准数据类型除了通过字面量方式声明之外,还可以通过构造函数int()
、float()
、complex()
、str()
、list()
、tuple()
、dict()
进行声明。
Python
当中除了这五种标准数据类型之外,还存在二进制序列类型(bytes
,
bytearray
,
memoryview
)、集合类型(set
,
frozenset
)等衍生的数据类型,本文这里并不将其作为基本数据类型进行介绍,开发人员可以结合官方文档的《Internal
Objects》章节按需进行查阅。
数值 Number
由于 Python 非常适合于科学计算用途,因此对于数值类型方面内容的讲解篇幅相对较大。Python 的数值类型分为整型(精度不限)、浮点类型(底层使用 C 语言的双精度浮点类型实现)、复数类型(包含实部和虚部)三种,其中布尔类型是作为整型的子类型出现。
当前硬件设备对 Python 浮点数精度的相关支持信息,可以通过如下代码进行查看。
1 | import sys |
上述代码在笔者的 64 位 Linux 系统上执行的结果如下:
1 | sys.float_info(max=1.7976931348623157e+308, max_exp=1024, max_10_exp=308, min=2.2250738585072014e-308, min_exp=-1021, min_10_exp=-307, dig=15, mant_dig=53, epsilon=2.220446049250313e-16, radix=2, rounds=1) |
全部数值类型都支持的运算符:
操作 | 结果 | 示例 |
---|---|---|
x + y |
x 与y 的和 |
3 + 2 ,结果为5 。 |
x - y |
x 与y 的差 |
5 - 1 ,结果为4 。 |
x * y |
x 与y 的乘积 |
3 * 9 ,结果为27 。 |
x ** y |
x 的y 幂次方 |
2 ** 3 ,结果为8 。 |
x / y |
x 和y 的商 |
8 / 2 ,结果为4.0 。 |
x // y |
x 和y 的商取整 |
4 // 3 ,结果为1 。 |
x % y |
x 与y 的余数 |
12 % 7 ,结果为5 。 |
-x |
取x 的负数 |
-3 。 |
+x |
取x 的正数,原值不会变化 |
+3 。 |
全部数值类型都支持的方法:
操作 | 结果 | 示例 |
---|---|---|
abs(x) |
取x 的绝对值 |
abs(-32.3) ,结果为32.3 。 |
int(x) |
取x 的整型 |
int(3.84) ,结果为3 。 |
float(x) |
取x 的浮点类型 |
float(5) ,结果为5.0 。 |
complex(re, im) |
一个以re 作为实部im (默认为0 )作为虚部的实数 |
complex(2.02, 3.3) ,结果为(2.02+3.3j) 。 |
c.conjugate() |
复数c 的共轭复数 |
(complex(2.02, 3.3)).conjugate() ,结果为(2.02-3.3j) 。 |
divmod(x, y) |
由(x // y, x % y) 组成的值对 |
divmod(12, 7) ,结果为(1, 5) 。 |
pow(x, y) |
x 的y 幂次方,等效于x ** y |
pow(2, 3) ,结果为8 。 |
整型的位操作:
操作 | 结果 | 示例 |
---|---|---|
x & y |
按位与 | 3 & 2 ,结果为2 。 |
x | y |
按位或 | 3 | 2 ,结果为3 。 |
x ^ y |
按位异或 | 3 ^ 2 ,结果为1 。 |
x << n |
左移位 | 3 << 2 ,结果为12 。 |
x >> n |
右移位 | 3 >> 2 ,结果为0 。 |
~x |
按位取反 | ~3 ,结果为-4 。 |
Python 的 Boolean
值由False
和True
两个静态对象组成,其它对象参与布尔运算时,通常被认为是True
(除非重写其类定义当中的__bool__()
方法并返回False
,或者重写__len__()
并返回0
),下列对象在布尔运算中会被认为是False
。
- 被定义为
False
的等效常量:None
、False
。 - 任意值为
0
的数值类型:0
、0.0
、0j
、Decimal(0)
、Fraction(0, 1)
。 - 空的序列或者集合:
''
、()
、[]
、{}
、set()
、range(0)
。
包含布尔结果的 Python 内建函数总是返回
0
和False
或者1
和True
。
特别需要注意的是:布尔运算or
和and
总是返回其中一个操作数本身,请参见下面的布尔运算符说明表:
操作 | 结果 | 示例 |
---|---|---|
x or y |
如果x 为假,那么返回y ,否则返回x |
2 or 3 ,结果为3 。 |
x and y |
如果x 为假,那么返回x ,
否则返回y |
2 and 3 ,结果为2 。 |
not x |
如果x 为假,
那么返回True , 否则返回False |
not 0 ,结果为True 。 |
Python 拥有 6
个比较运算符,其各自的运算优先级相同,可以随意链式使用。例如:x < y <= z
与x < y and y <= z
等效。
操作符 | 结果 | 示例 |
---|---|---|
x < y |
严格小于 | 3 < 1 ,结果为False 。 |
x <= y |
小于或等于 | 4 <= 4 ,结果为True 。 |
x > y |
严格大于 | 12 > 33 ,结果为False 。 |
x >= y |
大于或等于 | 21 >= 19 ,结果为True 。 |
x == y |
等于 | ["A", "B"] == ["A", "B"] ,结果为True 。 |
x != y |
不等于 | 21 != 21 ,结果为False 。 |
x is y |
对象引用地址的相等性判断 | ["A", "B"] is ["A", "B"] 结果为False 。 |
x is not y |
对象引用地址的相等性判断的结果取反 | ["A", "B"] is not ["A", "B"] 结果为True 。 |
字符串 String
Python 中的字符串是一个不可变的 Unicode
字符序列,可以保存在str
对象或者字符串字面量当中。其中,字符串字面量可以通过如下
3 种方式书写:
单引号:'allows embedded "double" quotes'
。双引号:"allows embedded 'single' quotes"
。三引号:'''Three single quotes'''
、"""Three double quotes"""
。
三引号字符串可以书写到多行,并且所有的空格都将会完整的保存下来。
Python
字符串同样可以通过class str(object=b'', encoding='utf-8', errors='strict')
构造器进行创建。
1 | str("hello python!") == "hello python!" # True |
Python 字符串可以进行索引,字符串第 1
个字符的索引为0
,子字符串可以使用分割符:
来指定。
1 | hank = "uinika" |
列表 List
列表 List 是一个可变的序列(即可以对列表的每个数据项进行修改),用于存储同类数据的集合,可以通过如下方式进行创建:
- 使用方括号
[]
表达一个空的列表,例如[]
; - 使用方括号
[]
并且使用逗号,
分隔每项数据,例如:[1, 2, 3]
; - 使用列表理解,例如
[x for x in iterable]
; - 使用
list()
或者list(iterable)
构造器;
1 | # 定义列表 |
可以使用class range(stop)
或者class range(start, stop[, step])
生成一系列数值,通常指定for
循环的次数。
1 | for value in range(1, 5): |
使用list()
构造函数可以将range()
生成的结果直接转换为列表。
1 | # 生成range |
元组 Tuple
元组([ˈtʌpəl])是不可修改的序列类型,即不能对其中的元素进行修改。
- 使用圆括号
()
表达一个空的元组,例如()
; - 向只拥有一个数据项的元组最后添加逗号,例如:
(1,)
- 使用逗号分隔不同的数据项,例如:
(1, 2, 3)
; - 使用列表理解,例如
[x for x in iterable]
; - 使用内建的
tuple()
或者tuple(iterable)
构造函数;
1 | # 定义元组 |
元组都可以使用+
和*
进行运算,也可以组合或者复制,然后获得一个新的元组。
1 | (1, 2, 3, "A", "B") + (4, 5, 6, "C", "D") # 输出(1, 2, 3, 'A', 'B', 4, 5, 6, 'C', 'D') |
上面介绍的列表 List,也可以进行类似操作。
字典 Dictionary
Python
官方文档当中,将列表(List)和元组(Tuple)归纳为序列类型(Sequence
Types),而将字典(_
dict_)类型归为映射类型(Mapping
Types)。Python
中的字典类型是使用花括号{}
包裹并通过逗号,
分隔的key-value
键值对,例如:{"name": "hank", "age": 33}
。当然,同样也可以通过class dict(**kwarg)
、class dict(mapping, **kwarg)
、class dict(iterable, **kwarg)
构造函数进行创建。
1 | # 定义字典 |
字典的key
值必须是不可变的,因此可以使用数字、字符串、元组作为键值。但是由于列表的元素是可变的,因此被不能作为字典的键值。
1 | # 打印一个使用元组作为key的字典元素 |
条件判断
Python 的条件判断语句与其它类 C 语言相似,但是每个 condition
后面需要使用冒号:
表示后面是满足条件后执行的语句块。需要注意的是,Python
语句块的划分是通过缩来完成的,相同缩进数量的语句构成一个语句块。
1 | if condition1: |
下面的代码是一个条件判断语句的例子,当if
和elif
判断的结果都为False
时,最终打印else
子句的结果。
1 | if False: |
Python 当中没有
switch/case
语句。
循环
while 循环
Python 中使用while
循环时,需要特别注意与其它类 C
语言语法的不同,即语句后的标识符:
与代码块缩进。
1 | index = 1 |
注意了,Python 是没有
do/while
循环的。
可以通过设置条件表达为True
实现无限循环,请看下面的例子:
1 | while True: |
如果while
循环体当中只拥有一条语句,可以将其与while
关键字书写在同一行,因此上面无限循环的示例也可以写成下面这样:
1 | while True: print("Thit is an infinite loop") |
Python 的while
循环拥有一个与其它类 C
语言截然不同的用法,即使用while/else
语句在条件判断为False
时执行else
语句块当中的内容。
1 | index = 1 |
for 循环
Python
的for
语句可以用来对列表、元组、字典、字符串进行遍历操作。
1 | cars = ['FORD', 'HONDA', 'BMW'] |
for
语句同样可以结合else
一起使用,for
循环完毕之后,就会执行else
子句中的代码。
1 | string = "成都" |
break 与 continue
在 Python
的循环语句当中,可以通过break
强行跳出当前循环体。
1 | for number in [0, 1, 2, 3, 4, 5]: |
也可以通过continue
直接略过本次循环。
1 | for number in [0, 1, 2, 3, 4, 5]: |
pass
pass
被执行的时候,并不进行任何操作,只起到一个语法占位符的作用。
1 | def demo(arg): pass # 定义一个不进行任何操作的函数 |
可以通过定义一个只有pass
语句的类,来实现类似 C
语言结构体的功能。
1 | class Engineer: |
函数
Python
使用关键字def
来定义函数,使用方式和声明规则与其它类 C
语言相似。
1 | # 定义函数 |
同其它类 C 语言一样,Python 中的变量也可以分为局部变量(定义在函数内部)和全局变量(定义在函数外部)。
1 | # 定义函数 |
命名参数
调用 Python 函数时,可以向其传递命名参数,指定该参数值由哪个函数参数进行接收,避免按照顺序接收所可能带来的潜在错误。
1 | def function(parameter): |
默认参数
定义 Python 函数的时候,对于缺省的参数可以赋予其一个默认值。
1 | # 定义函数的时候声明了parameter2的默认参数 |
可变参数列表
定义函数时,可以使用星号*
作为前缀来声明一个可变参数列表,用来接收调用函数时传递的任意个数参数,这些参数会被包装至一个元组。
1 | # 在可变参数之前,可以放置任意数量的普通参数 |
也可以使用**
作为前缀来声明参数,此时这些参数会包装为一个字典。
1 | def demo(parameter, **dictionary): |
如果*
号单独出现在函数参数当中,那么后续的参数则必须使用命名参数显式传入。
1 | def demo(parameter1, *, parameter2, parameter3): |
lambda 函数
Python 的 lambda 函数是只由一个 Python 表达式所组成的匿名的内联函数,其语法书写形式如下:
1 | lambda [parameters]: expression |
lamdba 函数的语法只能包含一条语句,例如:
1 | demo = lambda parameterA, parameterB: print(parameterA / parameterB) |
return 语句
return
语句用于退出函数并返回函数的执行结果值,具体用法如下代码所示:
1 | def demo(parameter1, parameter2): |
当 Python
中的return
语句没有返回值时,则默认返回值为None
。
1 | def demo(): |
不带参数值的
return
语句返回None
。
作用域
Python
当中仅module
模块、class
类、def
或lambda
函数会引入新作用域,if/elif/else
、try/except
、for/while
等代码块并不会引入新作用域,即这些语句当中定义的变量在其外部也能访问。
1 | def function(): |
global
关键字
使用global
关键字声明的标识符将会引用至全局变量,在局部作用域中对其进行的修改操作都将会保留下来,就如同操作真正的全局变量一样。
1 | text = "全局变量" |
nonlocal
关键字
使用nonlocal
关键字声明的标识符将会引用至当前作用域外层的变量,在当前作用域对其进行的修改操作都将会保留,如同在真正的操作该作用域外层的变量一样。
1 | def outer(): |
类与实例
Python
使用class
关键字创建一个类,然后直接调用类名即其初始化方法就可以创建这个类的实例。类进行实例化时,会自动调用该类的初始化方法__init__(self)
(作用类似于
Java 当中的构造函数)。
1 | # Dog类 |
注意:类方法(包括初始化方法)中的
self
参数是不能省略的,该参数指向类的实例,而非类本身。当然,根据个人编码习惯,也可以将self
置换为其它语言中更为常用的this
进行命名。
继承
Python 做为面向对象的语言,自然是支持继承的。需要继承一个类,只需要在定义子类时传入父类的名称即可,同时为了保证子类和父类都能够正确的实例化,子类的初始化方法需要显示调用父类的初始化方法。
1 | # 定义Dog父类 |
如同 Java 一样,Python
也是可以实现多重继承的。多重继承时,为了保证继承树能够正确的进行实例化,需要在子类的初始化方法__init__
内显式的调用父类们的初始化方法,并将子类的self
属性传递过去。
1 | class A: |
私有属性和方法
在类中声明属性和方法时添加两条下划线__
,就可以将这个属性和方法声明为私有的。私有属性和方法只能在类中通过self.__private
进行访问,而不能在类实例化后进行访问。
1 | class demo: |
方法重写 override
如果父类中定义的方法不能满足要求,那么可以考虑在子类中对父类的方法进行重写。
1 | class Parent: |
当然,也可以通过super()
函数显式调用被子类重写了的父类方法。对于上面的例子,可以使用如下语句调用父类的method()
方法。
1 | super(Child, childInstance).method() # 输出:Parent |
迭代器
大家可能注意到许多容器对象能够使用for
语句进行循环处理,就像下面代码这样:
1 | for element in [1, 2, 3]: |
这样的处理方法简单明了,实质上for
语句调用容器对象上的iter()
方法,该方法返回一个迭代器对象,这个迭代器对象当中定义了一个__next__()
方法用于操作容器对象的元素,当没有更多可供迭代的元素时,该方法将会抛出一个StopIteration
异常通知for
语句终止循环操作。这里你可以通过next()
内置函数去调用__next__()
方法,参见下面的例子:
1 | string = 'abc' |
定义一个iter()
方法,该方法用next()
方法返回一个对象。如果该类定义了next()
,那么iter()
就可以返回self
。
1 | # 迭代器,用于反向循环一个序列 |
生成器
生成器 Generator 是一个用于建立迭代器 iterators
的简单又强大的工具,其书写方式类似于函数,但是在返回数据的时候使用了yield
语句。当生成器的next()
每次被调用的时候,生成器会恢复到其离开的位置(生成器能够记忆所有的数据和最后执行的语句位置)。
1 | def reverse(data): |
上述代码实现了之前迭代器示例相同的功能,生成器函数代码如此短小精悍的原因在于iter()
和next()
方法的创建以及StopIteration
异常抛出都是自动进行的。另外生成器函数的局部变量和执行状态在每次调用都会被保存,这样比前面基于
class
的迭代器总是手动处理self.index
和self.data
更加便捷。
一些简单的生成器可以使用特殊语法书(与列表解析相似,不过使用圆括号代替了)书写为一种更加简捷的表达式,即生成器表达式。这种表达式常用于在闭包函数内使用生成器的情况,语法上比完整的生成器定义更紧凑,并且比同等的列表理解更加容易记忆。
1 | # 求平方和 |
异常处理
与其它类 C 语言一样,Python
通过try...except
语句块提供了健全的错误和异常处理机制。首先,try
和except
当中的子句被执行,此时如果没有异常出现,except
子句会被跳过,同时try
语句块正常执行完成。如果try
的子句当中发生了异常,则会中断剩下子句的执行流程,并跳转去执行except
关键字后声明异常类型所对应的语句,完成后继续执行该try
语句块后续的内容。如果对应的异常类型没有找到,该异常会被传递到try
语句块之外,如果语句块外依然没有进行相应的处理,那么程序的执行流程会被中断
,并且向控制台打印出异常信息。
1 | try: |
当然,也可以在一个except
子句中捕捉多个异常。
1 | except (RuntimeError, TypeError, NameError): |
如果except
子句中的异常类具有继承关系,则它们都将会被触发。
1 | # 继承Exception类 |
如果将上面代码中
except
子句的处理顺序颠倒一下,那么打印结果会变为ExceptionA ExceptionB ExceptionC
,这是因为异常类 A、B、C 产生的异常全部都会被异常类 A 捕捉然后中断执行。
值得注意的是,最后一条except
子句可以省略掉异常名称,从而可以补捉到全部的异常类型,虽然同样可以执行打印错误信息和抛出异常的操作,不过要十分小心的使用,因为它可能会掩盖掉真实发生的异常信息。
1 | try: |
try…except…
异常处理语句还拥有一个else…
子句,用来在try
子句没有捕捉到异常时执行一些必要的代码(如果try…except…
时发生了异常,则else…
子句中的异常将不会得到执行)。
1 | try: |
可以在raise
子句当中声明异常的参数,并在except
当中通过as
关键字进行实例化以后进行接收。
1 | try: |
由于
Exception
类里定义了__str__()
方法,所以可以通过直接打印异常对象来获取异常参数。
Python
的异常处理机制,不光能处理try
子句当中发生的异常,还能够处理try
中调用的函数内发生的异常。
1 | def division(): |
raise
子句当中所要抛出的异常类必须继承自Exception
,当异常被触发的时候,该异常类会被自动实例化并将执行流程带入except
子句。
1 | class MyError(Exception): |
如果希望不对抛出的异常进行处理,那么可以选择在except
子句内重新将这个异常抛出。
1 | try: |
开发人员可以自定义异常类,这些自定义的异常类必须继承自内置的Exception
类。自定义异常类通常只会定义几个用来描述异常的属性,从而保持类定义的简单明了。对于一个模块发生多个错误,可以通过建立一个异常类的继承树来进行体现,来看下面的例子:
1 | # 针对该模块的自定义异常基类 |
Python 异常的命名通常会以
"Error"
结尾,建议自定义异常时保持这样的惯例。
try
语句还拥有一个可以用来进行一些清除操作的finally
子句,该子句无论不否发生异常都会被执行(与
else 的最大不同点),读者可以参考下面的例子:
1 | def divide(x, y): |
模块管理
Python
通过import
关键字来引入其它模块,并且需要将其放置到代码的顶部。Python
当中.py
文件的名称就是模块的名称,模块的名称可以通过全局变量__name__
进行访问(如果该模块是
python
命令行直接执行的模块,则__name__
属性的打印结果为"__main__"
)。
1 | # myModule.py |
1 | # main.py |
1 | # 打印结果 |
每个模块都拥有自己的私有符号表(Symbol
Table,一种存储键值对的数据结构),因为被引入的模块名称会被放置在引入模块的全局全局符号表当中,所以模块当中定义的函数能够以全局符号表的方式进行使用。因此,模块的作者可以放心的在模块中使用全局变量,而毋须担心与其它用户的变量发生冲突。另一方面,如要需要访问这些模块里定义的全局变量
,那么可以通过module_name.variable_name
的方式进行访问。
如果觉得module_name.variable_name
方式过于繁琐,那么可以通过from module_name import iitem_name_in_module
语句指定从模块导入的内容,而无须总是在使用的时候添加模块的名称,请见下面的例子:
1 | # module.py |
1 | # main.py |
当然,如果觉得比较麻烦,还可以使用from module import *
一次性导入module
模块当中的内容。但是需要注意的是这样并不能导入模块中以下划线_
作为前缀的内容,比如下面这样:
1 | # module.py |
1 | from module import * |
这种一忺导入全部模块内容的方式在 Python 官方文档中是不被鼓励的,因此在现实开发场景下需要酌情使用。
如果引入模块的名称与当前模块定义的变量或者函数有冲突,那么可以考虑通过as
关键字使模块中的内容绑定到一个别名上。
1 | # module.py |
1 | # main.py |
当然,也可以将from...import...as...
结合起来使用,这样做会让代码更加简化。
1 | # main.py |
出于性能方面的考量,一个模块只会在 Python
的每个解释器会话当中被引入一次,所以如果开发人员在解释器运行之后修改了模块的代码,则必须重新启动解释器。当然,如果你只有一个模块需要进行交互式的测试,则可以使用importlib.reload()
方法暂时解决这个问题。
1 | import module |
以脚本方式执行模块
当在控制台直接执行 Python
脚本文件的时候,模块的__name__
属性值会被设置为"__main__"
,可以利用这个特性在模块文件在命令行以python module.py
直接进行执行的时候,进行一些特定的交互和操作。
1 | # module.py |
1 | ➜ git:(master) ✗ python3 main.py 2018 |
模块的搜索的路径
当一个模块名字被引入时,Python
解释器会首先搜索内置模块是否存在该名称,如果不存在,则会按照sys.path
属性的顺序进行搜索。即首先是当前.py
脚本所在的目录,然后是
Python 环境变量相关的目录,最后进行 Python 安装相关的目录。
1 | import sys |
预编译
为了加快模块的加载速度,Pytho
缓存了__pycache__
目录下每个模块的编译版本至module.version.pyc
名称下,例如CPython
release
3.6里的main.py
模块将会被缓存为__pycache__/main.cpython-36.pyc
,这样的命名约定可以使不同
Python 版本的编译模块能够同时共存。
Python 根据编译版本检查源代码的修改日期,以确定它是否过期,是否需要重新编译。这是一个完全自动的过程。此外,编译后的模块是独立于平台的,因此相同的库可以在具有不同体系结构的系统之间共享。
Python 在两种情况下不检查缓存。首先,它总是重新编译,不存储直接从命令行加载的模块的结果。其次,如果没有源模块,它不会检查缓存。要支持非源(仅编译)发行版,编译后的模块必须位于源目录中,并且不允许有源模块。
Python 在两种情况下不会检查缓存。首先,从命令行直接加载的模块总是会重新进行编译;其次,如果当前没有源模块时就不会检查缓存。为了支持非源(仅编译可用)的发行版,被编译的模块必须位于源目录,并且它们必须不能是一个源模块。
需要提醒一些资深的使用者:
- 你可以使用
-O
或-OO
控制 Python 命令编译模块的尺寸,参数-O
会移除 assert 语句,-OO
会移除 assert 语句和__doc__
字符串。由于有些程序可能依赖于这些选项,所以只有在知道自己在做什么时才应该使用这此选项。经过优化的模块会拥有一个opt-
标识并且通常情况下尺寸会更小。但是未来的 Python 版本可能会调整这些优化的效果。 - 从
.pyc
文件读取的程序并不会比从.py
文件读取的运行速度更快,.pyc文件
唯一提高的是加载速度。 - 模块
compileall
能够为一个目录下的所有模块建立.pyc
文件。
Python 内置的标准模块,有些是依赖于操作系统底层实现的,例如
winreg
模块只供在 Windows 系统上使用。但是模块sys
比较特殊,它可以用于几乎所有平台的 Python 解释器。
dir()
方法
Python
内置的dir()
方法能够以字符串格式输出模块当中所定义的内容。
1 | # module.py |
1 | # main.py |
1 | main.py里的属性与方法: ['__annotations__', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', 'module', 'sys'] |
可以通过向dir()
方法传递内置的标准模块builtins
来获取
Python 内建的函数和变量。
1 | import builtins |
1 | ['ArithmeticError', 'AssertionError', 'AttributeError', 'BaseException', 'BlockingIOError', 'BrokenPipeError', 'BufferError', 'BytesWarning', 'ChildProcessError', 'ConnectionAbortedError', 'ConnectionError', 'ConnectionRefusedError', 'ConnectionResetError', 'DeprecationWarning', 'EOFError', 'Ellipsis', 'EnvironmentError', 'Exception', 'False', 'FileExistsError', 'FileNotFoundError', 'FloatingPointError', 'FutureWarning', 'GeneratorExit', 'IOError', 'ImportError', 'ImportWarning', 'IndentationError', 'IndexError', 'InterruptedError', 'IsADirectoryError', 'KeyError', 'KeyboardInterrupt', 'LookupError', 'MemoryError', 'ModuleNotFoundError', 'NameError', 'None', 'NotADirectoryError', 'NotImplemented', 'NotImplementedError', 'OSError', 'OverflowError', 'PendingDeprecationWarning', 'PermissionError', 'ProcessLookupError', 'RecursionError', 'ReferenceError', 'ResourceWarning', 'RuntimeError', 'RuntimeWarning', 'StopAsyncIteration', 'StopIteration', 'SyntaxError', 'SyntaxWarning', 'SystemError', 'SystemExit', 'TabError', 'TimeoutError', 'True', 'TypeError', 'UnboundLocalError', 'UnicodeDecodeError', 'UnicodeEncodeError', 'UnicodeError', 'UnicodeTranslateError', 'UnicodeWarning', 'UserWarning', 'ValueError', 'Warning', 'ZeroDivisionError', '__build_class__', '__debug__', '__doc__', '__import__', '__loader__', '__name__', '__package__', '__spec__', 'abs', 'all', 'any', 'ascii', 'bin', 'bool', 'bytearray', 'bytes', 'callable', 'chr', 'classmethod', 'compile', 'complex', 'copyright', 'credits', 'delattr', 'dict', 'dir', 'divmod', 'enumerate', 'eval', 'exec', 'exit', 'filter', 'float', 'format', 'frozenset', 'getattr', 'globals', 'hasattr', 'hash', 'help', 'hex', 'id', 'input', 'int', 'isinstance', 'issubclass', 'iter', 'len', 'license', 'list', 'locals', 'map', 'max', 'memoryview', 'min', 'next', 'object', 'oct', 'open', 'ord', 'pow', 'print', 'property', 'quit', 'range', 'repr', 'reversed', 'round', 'set', 'setattr', 'slice', 'sorted', 'staticmethod', 'str', 'sum', 'super', 'tuple', 'type', 'vars', 'zip'] |
包
Python
将多个模块的集合称为包,包通过带点的模块名来构建 Python
的命名空间,例如模块名A.B
表示在A
包下建立的B
子模块。
1 | # ./directory/module.py |
1 | # main.py |
1 | # main.py |
注意,当使用from package import item
格式语法的时候,item
可以是一个子模块、子包、或者是定义在包中的函数、类、变量。这种情况下import
语句首先会测试item
是否定义在包中,如果没有则会被认为是一个模块,并且尝试去加载它。如果加载出现问题,则会抛出一个ImportError
异常。
相反,使用import item.subitem.subsubitem
格式语句的时候,除最后一个item
以外每个item
都必须是包,最后一个项目可以是模块或者包,但不能是前一个item
中定义的类、函数、变量。
from ... import *
如果需要引入一个包下的所有子模块,必须显式的提供一个包的索引。如果一个包的__init__.py
代码里定义了一个名为__all__
的列表,它将被视为将要被from package import *
引入的模块名称的列表。例如,对于一个package
目录内的__init__.py
文件可能包含如下内容:
1 | __all__ = ["echo", "surround", "reverse"] |
上面意味着from package import *
将会引入package
目录下的echo
、surround
、reverse
3
个子模块。如果这里的__all__
属性没有被定义,则from package import *
语句就不会将子模块引入当前的命名空间,它只会确保package
包被引入,并且也会执行__init__.py
中的其它代码,然后引入包内定义的各种名称(包括由__init__.py
以及子模块定义的)。
通过相对路径引用包
Python
当中,from...import...
同样可以通过相对路径访问包。
1 | from . import package |
注意:相对路径的
import
是基于当前模块名称的,由于主模块名称总是"__main__"
,所以用于作为 Python 应用程序的主模块必须始终使用绝对导入。
处理多个目录中的包
Python
中的包支持一个特殊的属性__path__
,它可以被初始化成一个包含目录名称的列表,这个列表可以在该代码文件执行之前处理包的__init__.py
。这个变量可以修改的,这样做会影响将来对包中包含的模块和子包的搜索。
虽然通常不需要这个特性,但是可以通过它来扩展包中的模块集合。
虚拟环境
虚拟环境(Virtual
Environment)是一个自包含的目录树,用来管理 Python 第 3
方包依赖。不同的 Python
项目可以使用不同的虚拟环境,例如:应用程序A
可以安装自己的1.0
版本的虚拟环境,而应用程序B
具有另一个2.0
版本的虚拟环境,如果应用程序B
需要将依赖库升级至3.0
版本,这并不会影响应用程序B
的虚拟环境。
Python 官方提供了一个虚拟环境的轻量级实现模块venv,较新版本的 Python 发行包里已经默认内置了其实现,可以直接进行使用。
1 | python3 -m venv my-project |
上面的语句执行之后,将会建立一个my-project
目录,里面包含一个
Python 解释器的拷贝,以及相关的第 3 方依赖库,在 Python3.6.6
下执行得到的目录结构如下:
当建立完成项目的虚拟环境之后,可以通过如下命令进行激活:
1 | # On Windows |
激活后将会改变当前 Shell 的显示信息,以提示开发人员当前正在使用的是哪个虚拟环境。
1 | ➜ /workspace source my-project/bin/activate |
virtualenv是一个第 3 方社区提供的独立 Python 虚拟环境,同时支持 Python2 和 Python3 版本。在 Python3.3 以后,官方提供了上述的
venv
模块原生支持虚拟环境,因此virtualenv
逐步废弃使用。
包管理
Python
可以使用应用程序pip
安装、升级、移除第三方依赖包,正常情况下pip
已经伴随
Python 解释器默认安装,如果当前 Python 默认安装的 pip
版本过低,那么可以通过pip install -U pip
或python -m pip install --upgrade pip
命令手动进行安装。
1 | # 搜索包 |
pip freeze
将会生成已安装的包列表,但输出格式使用了pip install
所期望的格式,通常
Python 约定将该列表放置到一个requirements.txt
文件。
1 | (my-project) ➜ pip freeze > requirements.txt |
然后可以将requirements.txt
文件伴随应用程序一起提交至版本管理系统当中,然后其它用户可以在同步代码之后使用pip install -r requirements.txt
安装所需的包。
1 | (my-project) ➜ pip install -r requirements.txt |
输入输出
Python 内置了许多方法去完成输入输出操作,这些方法能够将数据以人类可读的形式打印出来,也可以将其写入文件当中供将来使用。
输出格式化
Python
提供了两种字符串格式化输出的方法,一种是使用format()
函数进行格式化输出,另一种是通过print()
函数的格式化占位符完成。
1 | print("{}是中国的一个{}".format("成都", "省")) |
1 | print("%s是中国的一个%s" % ("成都","省")) |
Python
当中可以使用str()
和repr()
方法将任意值转换成为字符串。其中str()
会返回人类可读的字符串,repr()
则会生成
Python 解释器能够读取的格式。
1 | string = str("Hank\n"); |
文件读写
Python
当中open(filename, mode, encoding)
函数会返回一个file
对象,其中filename
是需要打开的文件名,mode
用于标识以何种方式打开文件,encoding
指定读写操作的编码格式。
模式 | 意义 |
---|---|
"r" |
以读方式打开(默认)。 |
"w" |
以写方式打开,并清除之前内容。 |
"x" |
创建文件,如果文件已经存在则操作失败。 |
"a" |
以写方式打开,并在之前内容的尾部追加新内容。 |
"b" |
二进制模式。 |
"t" |
文本模式(默认)。 |
"+" |
打开一个磁盘文件进行读写操作。 |
"U" |
通用换行模式(已废弃)。 |
使用file
对象的最佳实践是与with
关键字结合在一起,从而保证file
对象总是能在恰当的时间关闭,即使出现异常,使用with
关键字也比书写等效的try-finally
简洁,这个在接下来的对象清理章节有更详细的讲解。
1 | with open("demo.txt", mode = "r", encoding="utf8") as file: |
如果file
对象已经被with
关键字或者close()
方法关闭,后续任何对file
对象的操作都将会失败,比如下面的例子:
1 | file.close() |
使用readline()
方法可以每次只读取一行数据。
1 | with open("demo.txt", mode = "r", encoding="utf8") as file: |
当然,也可以通过循环file
对象来读取目标文件中的每行数据。
1 | with open("demo.txt", mode = "r", encoding="utf8") as file: |
如果需要读取指定文件的全部内容,还可以采用
list(file)
或file.readlines()
方法。
write(string)
用于写入字符串内容到文件,并返回写入的字符数量。
1 | with open("demo.txt", mode = "w", encoding="utf8") as file: |
对象清理
Python 当中的一些预定义对象会内置清理行为,可以在对象不再需要的时候被自动执行。
1 | for line in open("myfile.txt"): |
上面这段代码的问题在于,代码执行完后没有立即关闭打开的文件。这在相对简单的脚本代码中不算问题,但对于更大规模的应用而言就是严重的错误。因此,Python
提供了with
语句来确保file
这样的对象在使用后能够被正确的清理和关闭。
1 | with open("myfile.txt") as file: |
上面语句执行之后,即使读取文件数据时出现问题,file
对象也能正常关闭,因为file
对象已经预定义了相关清除行为。
Python 之禅
可以在 Python 命令行模式输入import this
得到一份关于
Python 的优秀指导原则《Python 之禅》。
- 优美胜于丑陋。
- 明了胜于晦涩。
- 简洁胜于复杂。
- 复杂胜于凌乱。
- 扁平胜于嵌套。
- 间隔胜于紧凑。
- 保持良好可读性。
- 即便是特例也不可打压破这些规则。
- 实用性胜过纯粹性。
- 对错误与异常不能保持沉默,除非你刻意需要这样做。
- 面对模棱两可拒绝猜测,应该寻找最好的一个解决方案。
- 虽然动手做好过于什么都不做,但是仔细思考以后再动手胜过于盲目的去做。
- 如果实现难以解释,这必然是一个坏主意。
- 如果实现易于解释,这可能是一个好主意。
- 命名空间是非常好的主意,要善于进行利用。
通过 JSON 保存数据
JSON 可以用来保存诸如嵌套的字典或者列表这样的结构化数据,Python
提供了json
模块来处理 JSON
格式数据,具体使用方法请参见下面的示例代码:
1 | import json |
也可以在打开文件之后,将文件内容序列化为 JSON 格式。
1 | import json |
通过 XML 方式合成 SVG
SVG 本质是基于 XML 语言进行描述的矢量图形,由于 Python
内置的ElementTree
组件类提供了丰富的操作 XML
树形结构的方法;因此在下面这份简单的示例代码当中,将基于ElementTree
来完成
SVG 图片文件的合并工作。
1 | import xml.etree.ElementTree as ET |
言简意赅 の Python3