Python
入门简介
Python就为我们提供了非常完善的基础代码库,覆盖了网络、文件、GUI、数据库、文本等大量内容。
Python是跨平台的。
目前,Python有两个版本,一个是2.x版,一个是3.x版,这两个版本是不兼容的。
Python 的 3.0 版本,常被称为 Python 3000,或简称 Py3k
在mac,linux上,python启动了python2, python3启动了python3。
在Windows上运行Python时,请先启动命令行,然后运行python;
提示符>>>就表示我们已经在Python交互式环境中了,输入exit()并回车,就可以退出Python交互式环境。
1 | 查看python版本 |
第一个python程序
区分命令行模式、交互模式。
在Python交互式模式下,可以直接输入代码,然后执行,并立刻得到结果。
在命令行模式下,可以直接运行.py文件。
SyntaxError
如果遇到SyntaxError,表示输入的Python代码有语法错误。
退出
在Python中,quit() 和 exit() 函数都用于退出Python解释器。它们的功能是相同的,都会导致解释器退出。这两个函数的不同之处主要在于它们的实现方式。
quit()函数:quit()是一个内置函数,它会引发一个SystemExit异常。当Python解释器捕获到这个异常时,会退出程序。因此,quit()函数实际上是一个异常抛出的快捷方式。exit()函数:exit()也是一个内置函数,它会调用sys.exit()函数。sys.exit()函数也会引发SystemExit异常,从而退出程序。但是,exit()函数提供了一个更方便的方式来调用这个退出机制。
这两个函数在大多数情况下都是等效的。
直接运行python程序
在Windows上像.exe文件那样直接运行.py文件是不行的,但是,在Mac和Linux上是可以的,方法是在.py文件的第一行加上一个特殊的注释:
1 | #!/usr/bin/env python3 |
然后,通过命令给hello.py以执行权限:
1 | chmod a+x hello.py |
就可以直接运行hello.py了。
用print()在括号中加上字符串,就可以向屏幕上输出指定的文字。比如输出'hello, world',用代码实现如下:
1 | >>> print('hello, world') |
print()函数也可以接受多个字符串,用逗号“,”隔开,就可以连成一串输出:
1 | print('The quick brown fox', 'jumps over', 'the lazy dog') |
| 格式化字符 | 含义 |
|---|---|
| %s | 字符串 |
| %d | 有符号十进制整数,%06d 表示输出的整数显示位数,不足的地方使用 0 补全 |
| %f | 浮点数,%.2f 表示小数点后只显示两位 |
| %% | 输出 % |
print()会依次打印每个字符串,遇到逗号“,”会输出一个空格。默认输出是换行的,如果要实现不换行需要在变量末尾加上 end=””
input
Python提供了一个input(),可以让用户输入字符串,并存放到一个变量里。
1 | # 任务1 |
python基础
保留关键字
保留字即关键字,我们不能把它们用作任何标识符名称。Python 的标准库提供了一个 keyword 模块,可以输出当前版本的所有关键字:
1 | import keyword |
注释
#:单行注释
'''或者""":多行注释
行与缩进
Python采用缩进方式表示代码块,同级代码块缩进不一致会报错。
其他每一行都是一个语句,当语句以冒号:结尾时,缩进的语句视为代码块。
多行语句
Python 通常是一行写完一条语句,但如果语句很长,我们可以使用反斜杠 \ 来实现多行语句,例如:
1 | total = item_one + \ |
在 [], {}, 或 () 中的多行语句,不需要使用反斜杠 \,例如:
1 | total = ['item_one', 'item_two', 'item_three', |
数据类型
标准数据类型
Python3 中常见的数据类型有:
- Number(数字)
- String(字符串)
- bool(布尔类型)
- List(列表)
- Tuple(元组)
- Set(集合)
- Dictionary(字典)
Python3 的六个标准数据类型中:
- 不可变数据(3 个):Number(数字)、String(字符串)、Tuple(元组);
- 可变数据(3 个):List(列表)、Dictionary(字典)、Set(集合)。
此外还有一些高级的数据类型,如: 字节数组类型(bytes)。
Python3 中,bool 是 int 的子类,True 和 False 可以和数字相加, True==1、False==0 会返回 True,但可以通过 is 来判断类型。
1
2
3
4
5
6 >>>> issubclass(bool, int)
True
>>>> 1 is True
False
>>> 0 is False
>False在 Python2 中是没有布尔型的,它用数字 0 表示 False,用 1 表示 True。
isinstance()、issubclass() 、type()
在 Python 中,isinstance()、issubclass() 和 type() 都用于类型检查,但它们的作用和使用场景有明显区别:
type()
功能:获取对象的类型,返回一个对象的具体类型(即该对象所属的类),不考虑继承关系
语法:type(obj)1
2
3
4
5
6
7
8
9
10a = 10
print(type(a)) # <class 'int'>
b = "hello"
print(type(b)) # <class 'str'>
class MyClass:
pass
c = MyClass()
print(type(c)) # <class '__main__.MyClass'>isinstance()
功能:判断一个对象是否是指定类(或其派生类)的实例,考虑继承关
语法:isinstance(obj, class_or_tuple),(第二个参数可以是单个类,或多个类组成的元组)1
2
3
4
5
6
7
8
9
10
11class Parent:
pass
class Child(Parent): # Child 继承自 Parent
pass
c = Child()
# 检查对象是否是 Child 的实例
print(isinstance(c, Child)) # True
# 检查对象是否是 Parent 的实例(考虑继承,返回 True)
print(isinstance(c, Parent)) # True
# 检查对象是否是 int 的实例
print(isinstance(c, int)) # Falseissubclass()
功能:判断一个类是否是另一个类(或其派生类)的子类,专门用于类之间的继承关系检查,不涉及对象。
语法:issubclass(sub_class, super_class_or_tuple)`,(第二个参数可以是单个类,或多个类组成的元组)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15class Parent:
pass
class Child(Parent):
pass
class GrandChild(Child):
pass
# Child 是 Parent 的子类
print(issubclass(Child, Parent)) # True
# GrandChild 是 Parent 的子类(间接继承)
print(issubclass(GrandChild, Parent)) # True
# Parent 不是 Child 的子类
print(issubclass(Parent, Child)) # False核心区别总结
关键字 作用对象 核心功能 是否考虑继承 典型场景 type(obj)对象 返回对象的具体类型(类) 不考虑 严格判断对象的类型本身 isinstance(obj, cls)对象 判断对象是否是 cls 或其子类的实例 考虑 检查对象是否符合目标类型(含继承) issubclass(sub, super)类 判断 sub 类是否是 super 类或其子类的子类 考虑 检查类之间的继承关系
1 | 根据场景选择合适的工具:检查对象类型用 `isinstance()`,检查类继承用 `issubclass()`,获取原始类型用 `type()`。 |
is、==、isinstance()
1 | ### 一、`is` 的核心作用:判断对象身份(内存地址是否相同) |
可以通过使用del语句删除单个或多个对象。例如:
1 | del var |
数字(Number)类型
复数 complex
complex , 如 1 + 2j/1.1 + 2.2j/3.14j
整数 int
Python可以处理任意大小的整数,在程序中的表示方法和数学上的写法一模一样,例如:1,100,-8080,0,等等。只有一种整数类型 int,表示为长整型,没有 python2 中的 Long。
十六进制用
0x前缀和0-9,a-f表示,例如:0xff00,0xa5b4c3d2。对于很大的数,例如
10000000000,很难数清楚0的个数。Python允许在数字中间以_分隔,因此,写成10_000_000_000和10000000000是完全一样的。十六进制数也可以写成0xa1b2_c3d4。
浮点数 float
浮点数可以用数学写法,如1.23,3.14,-9.01,等等。但对于很大或很小的浮点数,就必须用科学计数法表示,把10用e替代,1.23e9,或者12.3e8,0.000012可以写成1.2e-5,等等。
整数和浮点数在计算机内部存储的方式是不同的,整数运算永远是精确的,而浮点数运算则可能会有四舍五入的误差。
- 数值的除法包含两个运算符:**/** 返回一个浮点数,**//** 返回一个整数。
- 在混合计算时,Python会把整型转换成为浮点数。
注意:Python的整数没有大小限制,而某些语言的整数根据其存储长度是有大小限制的,例如Java对32位整数的范围限制在
-2147483648-2147483647。Python的浮点数也没有大小限制,但是超出一定范围就直接表示为
inf(无限大)。
运算符
| / | 除 - x 除以 y | b / a 输出结果 2.1 |
|---|---|---|
| % | 取模 - 返回除法的余数 | b % a 输出结果 1 |
| ** | 幂 - 返回x的y次幂 | a**b 为10的21次方 |
| // | 取整除 - 往小的方向取整数 | >>> 9//2 4 >>> -9//2 -5 |
在 Python 3.8 及更高版本中,引入了一种新的语法特性,称为”海象运算符”(Walrus Operator),它使用 := 符号。这个运算符的主要目的是在表达式中同时进行赋值和返回赋值的值。
使用海象运算符可以在一些情况下简化代码,尤其是在需要在表达式中使用赋值结果的情况下。这对于简化循环条件或表达式中的重复计算很有用。
下面是一个简单的实例,演示了海象运算符的使用:
1 | # 传统写法 |
Python成员运算符
| in | 如果在指定的序列中找到值返回 True,否则返回 False。 | x 在 y 序列中 , 如果 x 在 y 序列中返回 True。 |
|---|---|---|
| not in | 如果在指定的序列中没有找到值返回 True,否则返回 False。 | x 不在 y 序列中 , 如果 x 不在 y 序列中返回 True。 |
Python身份运算符
身份运算符用于比较两个对象的存储单元
| 运算符 | 描述 | 实例 |
|---|---|---|
| is | is 是判断两个标识符是不是引用自一个对象 | x is y, 类似 id(x) == id(y) , 如果引用的是同一个对象则返回 True,否则返回 False |
| is not | is not 是判断两个标识符是不是引用自不同对象 | x is not y , 类似 **id(x) != id(y)**。如果引用的不是同一个对象则返回结果 True,否则返回 False。 |
注: id() 函数用于获取对象内存地址。
1 | is 与 == 区别: |
Python3 已不支持 <> 运算符,可以使用!= 代替
字符串 str
- Python 中单引号 ‘ 和双引号 “ 使用完全相同。
- 使用三引号(‘’’ 或 “””)可以指定一个多行字符串。
- 反斜杠可以用来转义,使用 r 可以让反斜杠不发生转义。 如 r”this is a line with \n” 则 \n 会显示,并不是换行。
- 按字面意义级联字符串,如 “this “ “is “ “string” 会被自动转换为 this is string。
- 字符串可以用 + 运算符连接在一起,用 ***** 运算符重复。
- Python 中的字符串有两种索引方式,从左往右以 0 开始,从右往左以 -1 开始。
- Python 中的字符串不能改变。
- Python 没有单独的字符类型,一个字符就是长度为 1 的字符串。
- 字符串切片 **str[start:end]**,其中 start(包含)是切片开始的索引,end(不包含)是切片结束的索引。
- 字符串的切片可以加上步长参数 step,语法格式如下:str[start.end.step]
转义字符
\可以转义很多字符,比如\n表示换行,\t表示制表符,字符\本身也要转义,所以\\表示的字符就是\。
如果字符串内部有很多换行,用\n写在一行里不好阅读,为了简化,Python允许用'''...'''的格式表示多行内容,可以自己试试:
1 | >>> print('''line1 |
1 | #!/usr/bin/python3 |
Python 可以在同一行中使用多条语句,语句之间使用分号 ; 分割
布尔值 bool
布尔值和布尔代数的表示完全一致,一个布尔值只有True、False两种值,要么是True,要么是False,在Python中,可以直接用True、False表示布尔值(请注意大小写),也可以通过布尔运算计算出来:
1 | >>> True |
布尔值可以用and、or和not运算。
and运算是与运算,只有所有都为True,and运算结果才是True。
or运算是或运算,只要其中有一个为True,or运算结果就是True。
not运算是非运算,它是一个单目运算符,把True变成False,False变成True。
bool 是 int 的子类,因此布尔值可以被看作整数来使用,其中 True 等价于 1。
可以使用
bool()函数将其他类型的值转换为布尔值。以下值在转换为布尔值时为False:None、False、零 (0、0.0、0j)、空序列(如''、()、[])和空映射(如{})。其他所有值转换为布尔值时均为True。
空值
空值是Python里一个特殊的值,用None表示。None不能理解为0,因为0是有意义的,而None是一个特殊的空值。
变量
变量名必须是大小写英文、数字和_的组合,且不能用数字开头。
在 Python 3 中,可以用中文作为变量名,非 ASCII 标识符也是允许的了。
在Python中,等号=是赋值语句,可以把任意数据类型赋值给变量,同一个变量可以反复赋值,而且可以是不同类型的变量。
理解变量在计算机内存中的表示也非常重要。当我们写:
1 a = 'ABC'时,Python解释器干了两件事情:
- 在内存中创建了一个
'ABC'的字符串;- 在内存中创建了一个名为
a的变量,并把它指向'ABC'。
多个变量赋值
Python允许你同时为多个变量赋值。例如:
1 | a = b = c = 1 |
以上实例,创建一个整型对象,值为 1,从后向前赋值,三个变量被赋予相同的数值。
您也可以为多个对象指定多个变量。例如:
1 | a, b, c = 1, 2, "runoob" |
以上实例,两个整型对象 1 和 2 的分配给变量 a 和 b,字符串对象 “runoob” 分配给变量 c。
1 | Python中的小整数,通常指的是-5至256之间的整数。 |
常量
通常用全部大写的变量名表示常量:
1 | PI = 3.14159265359 |
但事实上PI仍然是一个变量,Python根本没有任何机制保证PI不会被改变。
最后解释一下整数的除法为什么也是精确的。在Python中,有两种除法,一种除法是/:
1 | >>> 10 / 3 |
/除法计算结果是浮点数,即使是两个整数恰好整除,结果也是浮点数:
1 | >>> 9 / 3 |
还有一种除法是//,称为地板除,两个整数的除法仍然是整数:
1 | >>> 10 // 3 |
你没有看错,整数的地板除//永远是整数,即使除不尽。要做精确的除法,使用/就可以。
因为//除法只取结果的整数部分,所以Python还提供一个余数运算,可以得到两个整数相除的余数:
1 | >>> 10 % 3 |
无论整数做//除法还是取余数,结果永远是整数,所以,整数运算结果永远是精确的。
数据类型转换:
Python 数据类型转换可以分为两种:
- 隐式类型转换 - 自动完成
- 显式类型转换 - 需要使用类型函数来转换
命令行参数
我们在使用脚本形式执行 Python 时,可以接收命令行输入的参数。
1 | #!/usr/bin/python3 |
getopt 模块
getopt 模块是专门处理命令行参数的模块,用于获取命令行选项和参数,也就是 sys.argv。
字符串和编码
字符编码
搞清楚了ASCII、Unicode和UTF-8的关系,我们就可以总结一下现在计算机系统通用的字符编码工作方式:
在计算机内存中,统一使用Unicode编码,当需要保存到硬盘或者需要传输的时候,就转换为UTF-8编码。
用记事本编辑的时候,从文件读取的UTF-8字符被转换为Unicode字符到内存里,编辑完成后,保存的时候再把Unicode转换为UTF-8保存到文件。
Python的字符串
在最新的Python 3版本中,字符串是以Unicode编码的,也就是说,Python的字符串支持多语言,例如:
1 | >>> print('包含中文的str') |
对于单个字符的编码,Python提供了ord()函数获取字符的整数表示,chr()函数把编码转换为对应的字符:
1 | >>> ord('A') |
如果知道字符的整数编码,还可以用十六进制这么写str:
1 | >>> '\u4e2d\u6587' |
两种写法完全是等价的。
由于Python的字符串类型是str,在内存中以Unicode表示,一个字符对应若干个字节。如果要在网络上传输,或者保存到磁盘上,就需要把str变为以字节为单位的bytes。
Python对bytes类型的数据用带b前缀的单引号或双引号表示:
1 | x = b'ABC' |
要注意区分'ABC'和b'ABC',前者是str,后者虽然内容显示得和前者一样,但bytes的每个字符都只占用一个字节。
以Unicode表示的str通过encode()方法可以编码为指定的bytes,例如:
1 | >>> 'ABC'.encode('ascii') |
纯英文的str可以用ASCII编码为bytes,内容是一样的,含有中文的str可以用UTF-8编码为bytes。含有中文的str无法用ASCII编码,因为中文编码的范围超过了ASCII编码的范围,Python会报错。
在bytes中,无法显示为ASCII字符的字节,用\x##显示。
反过来,如果我们从网络或磁盘上读取了字节流,那么读到的数据就是bytes。要把bytes变为str,就需要用decode()方法:
1 | >>> b'ABC'.decode('ascii') |
如果bytes中包含无法解码的字节,decode()方法会报错:
1 | >>> b'\xe4\xb8\xad\xff'.decode('utf-8') |
如果bytes中只有一小部分无效的字节,可以传入errors='ignore'忽略错误的字节:
1 | >>> b'\xe4\xb8\xad\xff'.decode('utf-8', errors='ignore') |
要计算str包含多少个字符,可以用len()函数:
1 | >>> len('ABC') |
len()函数计算的是str的字符数,如果换成bytes,len()函数就计算字节数:
1 | >>> len(b'ABC') |
可见,1个中文字符经过UTF-8编码后通常会占用3个字节,而1个英文字符只占用1个字节。
在操作字符串时,我们经常遇到str和bytes的互相转换。为了避免乱码问题,应当始终坚持使用UTF-8编码对str和bytes进行转换。
由于Python源代码也是一个文本文件,所以,当你的源代码中包含中文的时候,在保存源代码时,就需要务必指定保存为UTF-8编码。当Python解释器读取源代码时,为了让它按UTF-8编码读取,我们通常在文件开头写上这两行:
1 | #!/usr/bin/env python3 |
第一行注释是为了告诉Linux/OS X系统,这是一个Python可执行程序,Windows系统会忽略这个注释;
第二行注释是为了告诉Python解释器,按照UTF-8编码读取源代码,否则,你在源代码中写的中文输出可能会有乱码。
申明了UTF-8编码并不意味着你的.py文件就是UTF-8编码的,必须并且要确保文本编辑器正在使用UTF-8 without BOM编码。
格式化
我们经常会输出类似'亲爱的xxx你好!你xx月的话费是xx,余额是xx'之类的字符串,而xxx的内容都是根据变量变化的,所以,需要一种简便的格式化字符串的方式。
在Python中,采用的格式化方式和C语言是一致的,用%实现,举例如下:
1 | >>> 'Hello, %s' % 'world' |
你可能猜到了,%运算符就是用来格式化字符串的。在字符串内部,%s表示用字符串替换,%d表示用整数替换,有几个%?占位符,后面就跟几个变量或者值,顺序要对应好。如果只有一个%?,括号可以省略。
常见的占位符有:
| 占位符 | 替换内容 |
|---|---|
| %d | 整数 |
| %f | 浮点数 |
| %s | 字符串 |
| %x | 十六进制整数 |
其中,格式化整数和浮点数还可以指定是否补0和整数与小数的位数:
1 | # -*- coding: utf-8 -*- |
如果你不太确定应该用什么,%s永远起作用,它会把任何数据类型转换为字符串:
1 | >>> 'Age: %s. Gender: %s' % (25, True) |
有些时候,字符串里面的%是一个普通字符怎么办?这个时候就需要转义,用%%来表示一个%:
1 | >>> 'growth rate: %d %%' % 7 |
format()
另一种格式化字符串的方法是使用字符串的format()方法,它会用传入的参数依次替换字符串内的占位符{0}、{1}……,不过这种方式写起来比%要麻烦得多:
1 | >>> 'Hello, {0}, 成绩提升了 {1:.1f}%'.format('小明', 17.125) |
f-string
最后一种格式化字符串的方法是使用以f开头的字符串,称之为f-string,它和普通字符串不同之处在于,字符串如果包含{xxx},就会以对应的变量替换:
1 | >>> r = 2.5 |
上述代码中,{r}被变量r的值替换,{s:.2f}被变量s的值替换,并且:后面的.2f指定了格式化参数(即保留两位小数),因此,{s:.2f}的替换结果是19.62。
序列
在python中,有这样一些类型,它们的成员是有序排列的,并且可以通过下标访问成员,这些类型称之为序列。包括:列表、range、元组和字符串;
序列的通用操作
| 函数 | 描述 | 备注 |
|---|---|---|
| len(item) | 计算容器中元素个数 | |
| del(item) | 删除变量 | del 有两种方式 |
| max(item) | 返回容器中元素最大值 | 如果是字典,只针对 key 比较 |
| min(item) | 返回容器中元素最小值 | 如果是字典,只针对 key 比较 |
| 描述 | Python 表达式 | 结果 | 支持的数据类型 |
|---|---|---|---|
| 切片 | “0123456789”[::-2] | “97531” | 字符串、列表、元组 |
切片:起始元素,结束元素,切片步长
| 运算符 | Python 表达式 | 结果 | 描述 | 支持的数据类型 |
|---|---|---|---|---|
| + | [1, 2] + [3, 4] | [1, 2, 3, 4] | 合并 | 字符串、列表、元组 |
| * | [“Hi!”] * 4 | [‘Hi!’, ‘Hi!’, ‘Hi!’, ‘Hi!’] | 重复 | 字符串、列表、元组 |
| in | 3 in (1, 2, 3) | True | 元素是否存在 | 字符串、列表、元组、字典 |
| not in | 4 not in (1, 2, 3) | True | 元素是否不存在 | 字符串、列表、元组、字典 |
| > >= == < <= | (1, 2, 3) < (2, 2, 3) | True | 元素比较 | 字符串、列表、元组 |
== < > 判断大小:一个一个挨着比较
List(列表) 是 Python 中使用 最频繁 的数据类型,在其他语言中通常叫做 数组
专门用于存储 一串 信息
列表用 [ ] 定义,数据 之间使用 , 分隔列表的
索引 从 0 开始索引 就是数据在 列表 中的位置编号,索引 又可以被称为 下标注意:从列表中取值时,如果 超出索引范围,程序会报错
1 | # 列表的创建 |
| 分类 | 关键字 / 函数 / 方法 | 说明 |
|---|---|---|
| 增加 | 列表.insert(索引, 数据) | 在指定位置插入数据 |
| 列表.append(数据) | 在末尾追加数据 | |
| 列表.extend(列表2) | 将列表2 的数据追加到列表 | |
| 修改 | 列表[索引] = 数据 | 修改指定索引的数据 |
| 删除 | del 列表[索引] | 删除指定索引的数据 |
| 列表.remove[数据] | 删除第一个出现的指定数据 | |
| 列表.pop | 删除末尾数据 | |
| 列表.pop(索引) | 删除指定索引数据 | |
| 列表.clear | 清空列表 | |
| 统计 | len(列表) | 列表长度 |
| 列表.count(数据) | 数据在列表中出现的次数 | |
| 排序 | 列表.sort() | 升序排序 |
| 列表.sort(reverse=True) | 降序排序 | |
| 列表.reverse() | 逆序、反转 |
序列是 Python 中最基本的数据结构。
序列中的每个值都有对应的位置值,称之为索引,第一个索引是 0,第二个索引是 1,依此类推。
Python 有 6 个序列的内置类型,但最常见的是列表和元组。
列表都可以进行的操作包括索引,切片,加,乘,检查成员。
此外,Python 已经内置确定序列的长度以及确定最大和最小的元素的方法。
列表的数据项不需要具有相同的类型。
list列表
索引切片时,包括左边的索引,不包括右边的索引。
Python内置的一种数据类型是列表:list。list是一种有序的集合,可以随时添加和删除其中的元素。
比如,列出班里所有同学的名字,就可以用一个list表示:
1 | >>> classmates = ['Michael', 'Bob', 'Tracy'] |
变量classmates就是一个list。用len()函数可以获得list元素的个数:
1 | >>> len(classmates) |
用索引来访问list中每一个位置的元素,索引是从0开始的。当索引超出了范围时,Python会报一个IndexError错误,所以,要确保索引不要越界,记得最后一个元素的索引是len(classmates) - 1。
索引也可以从尾部开始,最后一个元素的索引为 -1,往前一位为 -2,以此类推。
如果要取最后一个元素,除了计算索引位置外,还可以用-1做索引,直接获取最后一个元素:
1 | >>> classmates[-1] |
以此类推,可以获取倒数第2个、倒数第3个:
1 | >>> classmates[-2] |
当然,倒数第4个就越界了。
list是一个可变的有序表,所以,可以往list中追加元素到末尾append:
1 | >>> classmates.append('Adam') |
也可以把元素插入到指定的位置,比如索引号为1的位置insert:
1 | >>> classmates.insert(1, 'Jack') |
要删除list末尾的元素,用pop()方法:
1 | >>> classmates.pop() |
要删除指定位置的元素,用pop(i)方法,其中i是索引位置:
1 | >>> classmates.pop(1) |
要把某个元素替换成别的元素,可以直接赋值给对应的索引位置:
1 | >>> classmates[1] = 'Sarah' |
list里面的元素的数据类型也可以不同,比如:
1 | >>> L = ['Apple', 123, True] |
list元素也可以是另一个list,比如:
1 | >>> s = ['python', 'java', ['asp', 'php'], 'scheme'] |
要注意s只有4个元素,其中s[2]又是一个list,如果拆开写就更容易理解了:
1 | >>> p = ['asp', 'php'] |
要拿到'php'可以写p[1]或者s[2][1],因此s可以看成是一个二维数组,类似的还有三维、四维……数组,不过很少用到。
如果一个list中一个元素也没有,就是一个空的list,它的长度为0。
常用方法
1 | 删除列表元素 |
tuple
另一种有序列表叫元组:tuple。tuple和list非常类似,但是tuple一旦初始化就不能修改,比如同样是列出同学的名字:
1 | >>> classmates = ('Michael', 'Bob', 'Tracy') |
现在,classmates这个tuple不能变了,它也没有append(),insert()这样的方法。其他获取元素的方法和list是一样的,你可以正常地使用classmates[0],classmates[-1],但不能赋值成另外的元素。赋值的时候会报错
不可变的tuple有什么意义?因为tuple不可变,所以代码更安全。如果可能,能用tuple代替list就尽量用tuple。
tuple的陷阱:当你定义一个tuple时,在定义的时候,tuple的元素就必须被确定下来,比如:
1 | >>> t = (1, 2) |
如果要定义一个空的tuple,可以写成():
1 | >>> t = () |
但是,要定义一个只有1个元素的tuple,如果你这么定义:
1 | >>> t = (1) |
定义的不是tuple,是1这个数!这是因为括号()既可以表示tuple,又可以表示数学公式中的小括号,这就产生了歧义,因此,Python规定,这种情况下,按小括号进行计算,计算结果自然是1。
所以,只有1个元素的tuple定义时必须加一个逗号,,来消除歧义:
1 | >>> t = (1,) |
Python在显示只有1个元素的tuple时,也会加一个逗号,,以免你误解成数学计算意义上的括号。
最后来看一个“可变的”tuple:
1 | >>> t = ('a', 'b', ['A', 'B']) |
表面上看,tuple的元素确实变了,但其实变的不是tuple的元素,而是list的元素。tuple一开始指向的list并没有改成别的list,所以,tuple所谓的“不变”是说,tuple的每个元素,指向永远不变。即指向'a',就不能改成指向'b',指向一个list,就不能改成指向其他对象,但指向的这个list本身是可变的!
理解了“指向不变”后,要创建一个内容也不变的tuple怎么做?那就必须保证tuple的每一个元素本身也不能变。
list和tuple是Python内置的有序集合,一个可变,一个不可变。根据需要来选择使用它们。
无法删除元组中的元素,但可以删除元组
del tup。在这之后再次使用tup,会报错:NameError: name ‘tup’ is not defined
1 | len((1, 2, 3)) |
id(tup) # 查看内存地址
Python元组的升级版本 – namedtuple(具名元组)
元组的装包与拆包
Python中,元组装包拆包是自动的,不需要任何函数,导致很多人对于函数返回值一会有括号一会没括号非常迷惑
先看代码:
1
2
3
4 a=1,2,3
#它其实等价于下面的代码
a=(1,2,3)
#因为等号左边只有1个变量,而等号右边有3个值,因此自动装包成为一个元组再看下面的代码:
1
2 a,b,c=(1,2,3)
#自动拆包,得到a=1,b=2,c=3当函数return的时候,其实只能return一个值,并不能return多个值
有人会问,我return了多个值也没有报错啊,运行很正常
那正是因为Python将多个返回值自动装包造成的
因此当你返回多个变量,而外面只用一个变量去接收,会接收到一个元组
而当你用多个变量去接,就能对应的接收到每个值,这是因为自动拆包
理解了这一点,对于理解函数返回值、字典中的键值对等等都是有帮助的
=========================================================
为了加强理解,再看最后一组代码:
1
2
3 a=10
b=20
a,b=b,a,1执行会报错:too many values to unpack
这里很明显的,告诉你元组里有3个值需要拆包,而你只用2个值去接收,证明了元组确实执行了一个拆包的动作
range
1 | # range(start, end, step) 生成的序列 包左不包右 |
set
在 Python 中,set使用大括号 {} 表示,元素之间用逗号 , 分隔。
要创建一个set有以下方式:
1 | >>> s = {1, 2, 3} |
**创建一个空集合必须用 set() 而不是 **{ }**,因为 { } 是用来创建一个空字典。
1 | #!/usr/bin/python3 |
通过add(key)方法可以添加元素到set中,可以重复添加,但不会有效果:
1 | >>> s.add(4) |
通过remove(key)方法可以删除元素:
1 | >>> s.remove(4) |
set不可以放入可变对象。
Dictionary(字典)
字典(dictionary)是Python中另一个非常有用的内置数据类型。
字典是一种映射类型,字典用 { } 标识,它是一个无序的 键(key) : 值(value) 的集合。
键(key)必须使用不可变类型。
在同一个字典中,键(key)必须是唯一的。
1 | #!/usr/bin/python3 |
构造函数 dict() 可以直接从键值对序列中构建字典如下:
1 | >>> dict([('Runoob', 1), ('Google', 2), ('Taobao', 3)]) |
如果key不存在,dict就会报错。要避免key不存在的错误,有两种办法:
一是通过in判断key是否存在:
1 | >>> 'Thomas' in d |
二是通过dict提供的get()方法,如果key不存在,可以返回None,或者自己指定的value:
1 | >>> d.get('Thomas') |
注意:返回None的时候Python的交互环境不显示结果。
要删除一个key,用pop(key)方法,对应的value也会从dict中删除:
1 | >>> d.pop('Bob') |
正确使用dict非常重要,需要牢记的第一条就是dict的key必须是不可变对象。在Python中,字符串、整数等都是不可变的,因此,可以放心地作为key。而list是可变的,就不能作为key:
1 | >>> key = [1, 2, 3] |
从 Python 3.7 开始,dict 类型在设计上已经保证了插入顺序。也就是说,元素会按照它们被添加的顺序进行存储和迭代,这已经成为 Python 的语言规范。因此,在现代的 Python 版本中,当你遍历字典时,元素的顺序会和插入的顺序一致。
bytes 类型
在 Python3 中,bytes 类型表示的是不可变的二进制序列(byte sequence)。
与字符串类型不同的是,bytes 类型中的元素是整数值(0 到 255 之间的整数),而不是 Unicode 字符。
bytes 类型通常用于处理二进制数据,比如图像文件、音频文件、视频文件等等。在网络编程中,也经常使用 bytes 类型来传输二进制数据。
创建 bytes 对象的方式有多种,最常见的方式是使用 b 前缀:
此外,也可以使用 bytes() 函数将其他类型的对象转换为 bytes 类型。bytes() 函数的第一个参数是要转换的对象,第二个参数是编码方式,如果省略第二个参数,则默认使用 UTF-8 编码:
1 | x = bytes("hello", encoding="utf-8") |
与字符串类型类似,bytes 类型也支持许多操作和方法,如切片、拼接、查找、替换等等。同时,由于 bytes 类型是不可变的,因此在进行修改操作时需要创建一个新的 bytes 对象。
条件判断
条件判断
if语句的完整形式就是:
1 | if <条件判断1>: |
if语句执行有个特点,它是从上往下判断,如果在某个判断上是True,把该判断对应的语句执行后,就忽略掉剩下的elif和else。
if判断条件还可以简写,比如写:
1 | if x: |
只要x是非零数值、非空字符串、非空list等,就判断为True,否则为False。
模式匹配
当我们用if ... elif ... elif ... else ...判断时,会写很长一串代码,可读性较差。
如果要针对某个变量匹配若干种情况,可以使用match语句。
1 | score = 'B' |
使用match语句时,我们依次用case xxx匹配,并且可以在最后(且仅能在最后)加一个case _表示“任意值”,代码较if ... elif ... else ...更易读。
复杂匹配
match语句除了可以匹配简单的单个值外,还可以匹配多个值、匹配一定范围,并且把匹配后的值绑定到变量:
1 | age = 15 |
在上面这个示例中,第一个case x if x < 10表示当age < 10成立时匹配,且赋值给变量x,第二个case 10仅匹配单个值,第三个case 11|12|...|18能匹配多个值,用|分隔。
可见,match语句的case匹配非常灵活。
匹配列表
match语句还可以匹配列表,功能非常强大。
我们假设用户输入了一个命令,用args = ['gcc', 'hello.c']存储,下面的代码演示了如何用match匹配来解析这个列表:
1 | args = ['gcc', 'hello.c', 'world.c'] |
第一个case ['gcc']表示列表仅有'gcc'一个字符串,没有指定文件名,报错;
第二个case ['gcc', file1, *files]表示列表第一个字符串是'gcc',第二个字符串绑定到变量file1,后面的任意个字符串绑定到*files(符号*的作用将在函数的参数中讲解),它实际上表示至少指定一个文件;
第三个case ['clean']表示列表仅有'clean'一个字符串;
最后一个case _表示其他所有情况。
可见,match语句的匹配规则非常灵活,可以写出非常简洁的代码。
循环
Python的循环有两种,一种是for…in循环,依次把list或tuple中的每个元素迭代出来:
1 | names = ['Michael', 'Bob', 'Tracy'] |
执行这段代码,会依次打印names的每一个元素:
1 | Michael |
所以for x in ...循环就是把每个元素代入变量x,然后执行缩进块的语句。
如果要计算1-100的整数之和,Python提供一个range()函数,可以生成一个整数序列,再通过list()函数可以转换为list。比如range(5)生成的序列是从0开始小于5的整数:
1 | >>> list(range(5)) |
第二种循环是while循环,只要条件满足,就不断循环,条件不满足时退出循环。比如我们要计算100以内所有奇数之和,可以用while循环实现:
1 | sum = 0 |
在循环内部变量n不断自减,直到变为-1时,不再满足while条件,循环退出。
break
break的作用是提前结束循环。
continue
continue跳过当前的这次循环,直接开始下一次循环。
可变类型 & 不可变类型
不可变数据类型在创建后,其值就不能被改变。Python中的以下数据类型是不可变的:
- 数字(例如:int, float, complex)
- 字符串(例如:str)
- 元组(例如:tuple)
- 布尔类型(例如:bool)
可变数据类型的值可以在创建后被改变。Python中的以下数据类型是可变的:
- 列表(例如:list)
- 字典(例如:dict)
- 集合(例如:set)
tuple虽然是不变对象,但试试把(1, 2, 3)和(1, [2, 3])放入dict或set中,并解释结果。
1 | #定义元组 |
虽然t2作为tuple是不可变对象,但是这里的不可变是指“指向不变”,内容是可以发生改变的。这里为了保证Hash算法,key应同时满足内容不变和指向不变。t1是内容和指向都不变的元组,因此可以作为dict的key,而t2则不可以。
函数
要调用一个函数,需要知道函数的名称和参数,比如求绝对值的函数abs,只有一个参数。可以直接从Python的官方网站查看文档:http://docs.python.org/3/library/functions.html#abs,也可以在交互式命令行通过`help(abs)`查看`abs`函数的帮助信息。
1 | 常见函数: |
调用函数的时候,如果传入的参数数量不对,或参数类型不能被函数所接受,会报TypeError的错误,并且给出错误信息。
函数名其实就是指向一个函数对象的引用,完全可以把函数名赋给一个变量,相当于给这个函数起了一个“别名”:
1 | >>> a = abs # 变量a指向abs函数 |
请注意,函数体内部的语句在执行时,一旦执行到return时,函数就执行完毕,并将结果返回。因此,函数内部通过条件判断和循环可以实现非常复杂的逻辑。
如果没有
return语句,函数执行完毕后也会返回结果,只是结果为None。return None可以简写为return。
在Python交互环境中定义函数时,注意Python会出现
...的提示。函数定义结束后需要按两次回车重新回到>>>提示符下.
如果你已经把my_abs()的函数定义保存为abstest.py文件了,那么,可以在该文件的当前目录下启动Python解释器,用from abstest import my_abs来导入my_abs()函数。
空函数
如果想定义一个什么事也不做的空函数,可以用pass语句:
1 | def nop(): |
pass还可以用在其他语句里,比如:
1 | if age >= 18: |
缺少了pass,代码运行就会有语法错误。
参数检查
当传入了不恰当的参数时,内置函数abs会检查出参数错误,而我们定义的my_abs没有参数检查,会导致出错。
让我们修改一下my_abs的定义,对参数类型做检查,只允许整数和浮点数类型的参数。数据类型检查可以用内置函数isinstance()实现:
1 | def my_abs(x): |
添加了参数检查后,如果传入错误的参数类型,函数就可以抛出一个错误。
返回多个值
Python函数可以返回多个值,但其实这只是一种假象,Python函数返回的仍然是单一值,原来返回值是一个tuple!但是,在语法上,返回一个tuple可以省略括号,而多个变量可以同时接收一个tuple,按位置赋给对应的值,所以,Python的函数返回多值其实就是返回一个tuple。
函数的参数
Python的函数定义除了正常定义的必选参数外,还可以使用默认参数、可变参数和关键字参数。
位置参数
默认参数
由于我们经常计算x2,所以,完全可以把第二个参数n的默认值设定为2:
x是位置参数,n是默认参数
1 | def power(x, n=2): |
默认参数可以简化函数的调用。设置默认参数时,有几点要注意:
一是必选参数在前,默认参数在后,否则Python的解释器会报错;
二是如何设置默认参数:有多个默认参数时,调用的时候,既可以按顺序提供默认参数,也可以不按顺序提供部分默认参数。当不按顺序提供部分默认参数时,需要把参数名写上。
默认参数很有用,但使用不当,也会掉坑里。默认参数有个最大的坑,演示如下:
先定义一个函数,传入一个list,添加一个END再返回:
1 | def add_end(L=[]): |
当你正常调用时,结果似乎不错:
1 | >>> add_end([1, 2, 3]) |
当你使用默认参数调用时,一开始结果也是对的:
1 | >>> add_end() |
但是,再次调用add_end()时,结果就不对了:
1 | >>> add_end() |
很多初学者很疑惑,默认参数是[],但是函数似乎每次都“记住了”上次添加了'END'后的list。
原因解释如下:
**Python函数在定义的时候,默认参数L的值就被计算出来了,即[]**,因为默认参数L也是一个变量,它指向对象[],每次调用该函数,如果改变了L的内容,则下次调用时,默认参数的内容就变了,不再是函数定义时的[]了。
定义默认参数要牢记一点:默认参数必须指向不变对象!
要修改上面的例子,我们可以用None这个不变对象来实现:
1 | def add_end(L=None): |
现在,无论调用多少次,都不会有问题:
1 | >>> add_end() |
为什么要设计str、None这样的不变对象呢?因为不变对象一旦创建,对象内部的数据就不能修改,这样就减少了由于修改数据导致的错误。此外,由于对象不变,多任务环境下同时读取对象不需要加锁,同时读一点问题都没有。
可变参数
在Python函数中,还可以定义可变参数。顾名思义,可变参数就是传入的参数个数是可变的,可以是1个、2个到任意个,还可以是0个。
参数个数不确定,可以将参数定义为list或者tuple,但是这样的话,调用时,也需要将参数先构建成一个明显的list或者tuple才能进行调用。
1 | def calc(numbers): |
定义可变参数和定义一个list或tuple参数相比,仅仅在参数前面加了一个*号。在函数内部,参数numbers接收到的是一个tuple,因此,函数代码完全不变。
如果已经有一个list或者tuple,要调用一个可变参数怎么办?Python允许你在list或tuple前面加一个*号,把list或tuple的元素变成可变参数传进去,*nums表示把nums这个list的所有元素作为可变参数传进去。这种写法相当有用,而且很常见。
可变参数允许你传入0个或任意个参数,这些可变参数在函数调用时自动组装为一个tuple。
关键字参数
而关键字参数允许你传入0个或任意个含参数名的参数,这些关键字参数在函数内部自动组装为一个dict。请看示例:
1 | def person(name, age, **kw): |
函数person除了必选参数name和age外,还接受关键字参数kw。在调用该函数时,可以只传入必选参数:
1 | >>> person('Michael', 30) |
也可以传入任意个数的关键字参数:
1 | >>> person('Bob', 35, city='Beijing') |
关键字参数有什么用?它可以扩展函数的功能。比如,在person函数里,我们保证能接收到name和age这两个参数,但是,如果调用者愿意提供更多的参数,我们也能收到。试想你正在做一个用户注册的功能,除了用户名和年龄是必填项外,其他都是可选项,利用关键字参数来定义这个函数就能满足注册的需求。
1 | >>> extra = {'city': 'Beijing', 'job': 'Engineer'} |
**extra表示把extra这个dict的所有key-value用关键字参数传入到函数的**kw参数,kw将获得一个dict,注意kw获得的dict是extra的一份拷贝,对kw的改动不会影响到函数外的extra。
命名关键字参数
对于关键字参数,函数的调用者可以传入任意不受限制的关键字参数。至于到底传入了哪些,就需要在函数内部通过kw检查。
仍以person()函数为例,我们希望检查是否有city和job参数:
1 | def person(name, age, **kw): |
但是调用者仍可以传入不受限制的关键字参数:
1 | >>> person('Jack', 24, city='Beijing', addr='Chaoyang', zipcode=123456) |
如果要限制关键字参数的名字,就可以用命名关键字参数,例如,只接收city和job作为关键字参数。这种方式定义的函数如下:
1 | def person(name, age, *, city, job): |
和关键字参数**kw不同,命名关键字参数需要一个特殊分隔符*,*后面的参数被视为命名关键字参数。
调用方式如下:
1 | >>> person('Jack', 24, city='Beijing', job='Engineer') |
如果函数定义中已经有了一个可变参数,后面跟着的命名关键字参数就不再需要一个特殊分隔符*了:
1 | def person(name, age, *args, city, job): |
命名关键字参数必须传入参数名,这和位置参数不同。如果没有传入参数名,调用将报错:
1 | >>> person('Jack', 24, 'Beijing', 'Engineer') |
由于调用时缺少参数名city和job,Python解释器把前两个参数视为位置参数,后两个参数传给*args,但缺少命名关键字参数导致报错。
命名关键字参数可以有缺省值,从而简化调用:
1 | def person(name, age, *, city='Beijing', job): |
由于命名关键字参数city具有默认值,调用时,可不传入city参数:
1 | >>> person('Jack', 24, job='Engineer') |
使用命名关键字参数时,要特别注意,如果没有可变参数,就必须加一个*作为特殊分隔符。如果缺少*,Python解释器将无法识别位置参数和命名关键字参数:
1 | def person(name, age, city, job): |
参数组合
关键字参数顺序:笔墨可明官
在Python中定义函数,可以用必选参数、默认参数、可变参数、关键字参数和命名关键字参数,这5种参数都可以组合使用。但是请注意,参数定义的顺序必须是:必选参数、默认参数、可变参数、命名关键字参数和关键字参数。
比如定义一个函数,包含上述若干种参数:
1 | def f1(a, b, c=0, *args, **kw): |
在函数调用的时候,Python解释器自动按照参数位置和参数名把对应的参数传进去。
1 | >>> f1(1, 2) |
最神奇的是通过一个tuple和dict,你也可以调用上述函数:
1 | >>> args = (1, 2, 3, 4) |
所以,对于任意函数,都可以通过类似func(*args, **kw)的形式调用它,无论它的参数是如何定义的。
虽然可以组合多达5种参数,但不要同时使用太多的组合,否则函数接口的可理解性很差。
小结
默认参数一定要用不可变对象!
要注意定义可变参数和关键字参数的语法:
*args是可变参数,args接收的是一个tuple;
**kw是关键字参数,kw接收的是一个dict。
使用*args和**kw是Python的习惯写法,当然也可以用其他参数名,但最好使用习惯用法。
命名的关键字参数是为了限制调用者可以传入的参数名,同时可以提供默认值。
定义命名的关键字参数在没有可变参数的情况下不要忘了写分隔符*,否则定义的将是位置参数。
1 |
|
高级特性
切片
取一个list或tuple的部分元素是非常常见的操作
1 | L = ['Michael', 'Sarah', 'Tracy', 'Bob', 'Jack'] |
迭代:
在Python中,只要是可迭代对象,无论有无下标,都可以迭代。默认情况下,dict迭代的是key。如果要迭代value,可以用for value in d.values(),如果要同时迭代key和value,可以用for k, v in d.items()。
那么,如何判断一个对象是可迭代对象呢?方法是通过collections.abc模块的Iterable类型判断:
1 | >>> from collections.abc import Iterable |
最后一个小问题,如果要对list实现类似Java那样的下标循环怎么办?Python内置的enumerate函数可以把一个list变成索引-元素对,这样就可以在for循环中同时迭代索引和元素本身:
1 | >>> for i, value in enumerate(['A', 'B', 'C']): |
上面的for循环里,同时引用了两个变量,在Python里是很常见的,比如下面的代码:
1 | >>> for x, y in [(1, 1), (2, 4), (3, 9)]: |
列表生成式
列表生成式即List Comprehensions,是Python内置的非常简单却强大的可以用来创建list的生成式。
举个例子,要生成list [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]可以用list(range(1, 11)):
1 | >>> list(range(1, 11)) |
但如果要生成[1x1, 2x2, 3x3, ..., 10x10]怎么做?方法一是循环:
1 | >>> L = [] |
但是循环太繁琐,而列表生成式则可以用一行语句代替循环生成上面的list:
1 | >>> [x * x for x in range(1, 11)] |
写列表生成式时,把要生成的元素x * x放到前面,后面跟for循环,就可以把list创建出来,十分有用,多写几次,很快就可以熟悉这种语法。
for循环后面还可以加上if判断,这样我们就可以筛选出仅偶数的平方:
1 | >>> [x * x for x in range(1, 11) if x % 2 == 0] |
还可以使用两层循环,可以生成全排列:
1 | >>> [m + n for m in 'ABC' for n in 'XYZ'] |
三层和三层以上的循环就很少用到了。
运用列表生成式,可以写出非常简洁的代码。例如,列出当前目录下的所有文件和目录名,可以通过一行代码实现:
1 | >>> import os # 导入os模块,模块的概念后面讲到 |
for循环其实可以同时使用两个甚至多个变量,比如dict的items()可以同时迭代key和value:
1 | >>> d = {'x': 'A', 'y': 'B', 'z': 'C' } |
因此,列表生成式也可以使用两个变量来生成list:
1 | >>> d = {'x': 'A', 'y': 'B', 'z': 'C' } |
最后把一个list中所有的字符串变成小写:
1 | >>> L = ['Hello', 'World', 'IBM', 'Apple'] |
if … else
使用列表生成式的时候,有些童鞋经常搞不清楚if...else的用法。
例如,以下代码正常输出偶数:
1 | >>> [x for x in range(1, 11) if x % 2 == 0] |
但是,我们不能在最后的if加上else:
1 | >>> [x for x in range(1, 11) if x % 2 == 0 else 0] |
这是因为跟在for后面的if是一个筛选条件,不能带else,否则如何筛选?
另一些童鞋发现把if写在for前面必须加else,否则报错:
1 | >>> [x if x % 2 == 0 for x in range(1, 11)] |
这是因为for前面的部分是一个表达式,它必须根据x计算出一个结果。因此,考察表达式:x if x % 2 == 0,它无法根据x计算出结果,因为缺少else,必须加上else:
1 | >>> [x if x % 2 == 0 else -x for x in range(1, 11)] |
上述for前面的表达式x if x % 2 == 0 else -x才能根据x计算出确定的结果。
可见,在一个列表生成式中,for前面的if ... else是表达式,而for后面的if是过滤条件,不能带else。
生成器
通过列表生成式,我们可以直接创建一个列表。但是,受到内存限制,列表容量肯定是有限的。而且,创建一个包含100万个元素的列表,不仅占用很大的存储空间,如果我们仅仅需要访问前面几个元素,那后面绝大多数元素占用的空间都白白浪费了。
所以,如果列表元素可以按照某种算法推算出来,那我们是否可以在循环的过程中不断推算出后续的元素呢?这样就不必创建完整的list,从而节省大量的空间。在Python中,这种一边循环一边计算的机制,称为生成器:generator。
要创建一个generator,有很多种方法。第一种方法很简单,只要把一个列表生成式的[]改成(),就创建了一个generator:
1 | >>> L = [x * x for x in range(10)] |
创建L和g的区别仅在于最外层的[]和(),L是一个list,而g是一个generator。
我们可以直接打印出list的每一个元素,但我们怎么打印出generator的每一个元素呢?
如果要一个一个打印出来,可以通过next()函数获得generator的下一个返回值:
1 | >>> next(g) |
我们讲过,generator保存的是算法,每次调用next(g),就计算出g的下一个元素的值,直到计算到最后一个元素,没有更多的元素时,抛出StopIteration的错误。
当然,上面这种不断调用next(g)实在是太变态了,正确的方法是使用for循环,因为generator也是可迭代对象:
1 | >>> g = (x * x for x in range(10)) |
所以,我们创建了一个generator后,基本上永远不会调用next(),而是通过for循环来迭代它,并且不需要关心StopIteration的错误。
generator非常强大。如果推算的算法比较复杂,用类似列表生成式的for循环无法实现的时候,还可以用函数来实现。
比如,著名的斐波拉契数列(Fibonacci),除第一个和第二个数外,任意一个数都可由前两个数相加得到:
1, 1, 2, 3, 5, 8, 13, 21, 34, …
斐波拉契数列用列表生成式写不出来,但是,用函数把它打印出来却很容易:
1 | def fib(max): |
注意,赋值语句:
1 | a, b = b, a + b |
相当于:
1 | t = (b, a + b) # t是一个tuple |
但不必显式写出临时变量t就可以赋值。
上面的函数可以输出斐波那契数列的前N个数:
1 | >>> fib(6) |
仔细观察,可以看出,fib函数实际上是定义了斐波拉契数列的推算规则,可以从第一个元素开始,推算出后续任意的元素,这种逻辑其实非常类似generator。
也就是说,上面的函数和generator仅一步之遥。要把fib函数变成generator函数,只需要把print(b)改为yield b就可以了:
1 | def fib(max): |
这就是定义generator的另一种方法。如果一个函数定义中包含yield关键字,那么这个函数就不再是一个普通函数,而是一个generator函数,调用一个generator函数将返回一个generator:
1 | >>> f = fib(6) |
这里,最难理解的就是generator函数和普通函数的执行流程不一样。普通函数是顺序执行,遇到return语句或者最后一行函数语句就返回。而变成generator的函数,在每次调用next()的时候执行,遇到yield语句返回,再次执行时从上次返回的yield语句处继续执行。
举个简单的例子,定义一个generator函数,依次返回数字1,3,5:
1 | def odd(): |
调用该generator函数时,首先要生成一个generator对象,然后用next()函数不断获得下一个返回值:
1 | >>> o = odd() |
可以看到,odd不是普通函数,而是generator函数,在执行过程中,遇到yield就中断,下次又继续执行。执行3次yield后,已经没有yield可以执行了,所以,第4次调用next(o)就报错。
请务必注意:调用generator函数会创建一个generator对象,多次调用generator函数会创建多个相互独立的generator。
有的童鞋会发现这样调用next()每次都返回1:
1 | >>> next(odd()) |
原因在于odd()会创建一个新的generator对象,上述代码实际上创建了3个完全独立的generator,对3个generator分别调用next()当然每个都会返回第一个值。
正确的写法是创建一个generator对象,然后不断对这一个generator对象调用next():
1 | >>> g = odd() |
回到fib的例子,我们在循环过程中不断调用yield,就会不断中断。当然要给循环设置一个条件来退出循环,不然就会产生一个无限数列出来。
同样的,把函数改成generator函数后,我们基本上从来不会用next()来获取下一个返回值,而是直接使用for循环来迭代:
1 | >>> for n in fib(6): |
但是用for循环调用generator时,发现拿不到generator的return语句的返回值。如果想要拿到返回值,必须捕获StopIteration错误,返回值包含在StopIteration的value中:
1 | >>> g = fib(6) |
关于如何捕获错误,后面的错误处理还会详细讲解。
迭代器
我们已经知道,可以直接作用于for循环的数据类型有以下几种:
一类是集合数据类型,如list、tuple、dict、set、str等;
一类是generator,包括生成器和带yield的generator function。
这些可以直接作用于for循环的对象统称为可迭代对象:Iterable。
可以使用isinstance()判断一个对象是否是Iterable对象:
1 | >>> from collections.abc import Iterable |
而生成器不但可以作用于for循环,还可以被next()函数不断调用并返回下一个值,直到最后抛出StopIteration错误表示无法继续返回下一个值了。
可以被next()函数调用并不断返回下一个值的对象称为迭代器:Iterator。
可以使用isinstance()判断一个对象是否是Iterator对象:
1 | >>> from collections.abc import Iterator |
生成器都是Iterator对象,但list、dict、str虽然是Iterable,却不是Iterator。
把list、dict、str等Iterable变成Iterator可以使用iter()函数:
1 | >>> isinstance(iter([]), Iterator) |
你可能会问,为什么list、dict、str等数据类型不是Iterator?
这是因为Python的Iterator对象表示的是一个数据流,Iterator对象可以被next()函数调用并不断返回下一个数据,直到没有数据时抛出StopIteration错误。可以把这个数据流看做是一个有序序列,但我们却不能提前知道序列的长度,只能不断通过next()函数实现按需计算下一个数据,所以Iterator的计算是惰性的,只有在需要返回下一个数据时它才会计算。
Iterator甚至可以表示一个无限大的数据流,例如全体自然数。而使用list是永远不可能存储全体自然数的。
小结
凡是可作用于for循环的对象都是Iterable类型;
凡是可作用于next()函数的对象都是Iterator类型,它们表示一个惰性计算的序列;
集合数据类型如list、dict、str等是Iterable但不是Iterator,不过可以通过iter()函数获得一个Iterator对象。
Python的for循环本质上就是通过不断调用next()函数实现的,例如:
1 | for x in [1, 2, 3, 4, 5]: |
实际上完全等价于:
1 | # 首先获得Iterator对象: |
函数式编程
函数是Python内建支持的一种封装,我们通过把大段代码拆成函数,通过一层一层的函数调用,就可以把复杂任务分解成简单的任务,这种分解可以称之为面向过程的程序设计。函数就是面向过程的程序设计的基本单元。
而函数式编程(请注意多了一个“式”字)——Functional Programming,虽然也可以归结到面向过程的程序设计,但其思想更接近数学计算。
我们首先要搞明白计算机(Computer)和计算(Compute)的概念。
在计算机的层次上,CPU执行的是加减乘除的指令代码,以及各种条件判断和跳转指令,所以,汇编语言是最贴近计算机的语言。
而计算则指数学意义上的计算,越是抽象的计算,离计算机硬件越远。
对应到编程语言,就是越低级的语言,越贴近计算机,抽象程度低,执行效率高,比如C语言;越高级的语言,越贴近计算,抽象程度高,执行效率低,比如Lisp语言。
函数式编程就是一种抽象程度很高的编程范式,纯粹的函数式编程语言编写的函数没有变量,因此,任意一个函数,只要输入是确定的,输出就是确定的,这种纯函数我们称之为没有副作用。而允许使用变量的程序设计语言,由于函数内部的变量状态不确定,同样的输入,可能得到不同的输出,因此,这种函数是有副作用的。
函数式编程的一个特点就是,允许把函数本身作为参数传入另一个函数,还允许返回一个函数!
Python对函数式编程提供部分支持。由于Python允许使用变量,因此,Python不是纯函数式编程语言。
高阶函数
高阶函数英文叫Higher-order function。什么是高阶函数?
变量可以指向函数
以Python内置的求绝对值的函数abs()为例,调用该函数用以下代码:
1 | >>> abs(-10) |
但是,如果只写abs呢?
1 | >>> abs |
可见,abs(-10)是函数调用,而abs是函数本身。
要获得函数调用结果,我们可以把结果赋值给变量:
1 | >>> x = abs(-10) |
但是,如果把函数本身赋值给变量呢?
1 | >>> f = abs |
结论:函数本身也可以赋值给变量,即:变量可以指向函数。
如果一个变量指向了一个函数,那么,可否通过该变量来调用这个函数?用代码验证一下:
1 | >>> f = abs |
成功!说明变量f现在已经指向了abs函数本身。直接调用abs()函数和调用变量f()完全相同。
函数名也是变量
那么函数名是什么呢?函数名其实就是指向函数的变量!对于abs()这个函数,完全可以把函数名abs看成变量,它指向一个可以计算绝对值的函数!
如果把abs指向其他对象,会有什么情况发生?
1 | >>> abs = 10 |
把abs指向10后,就无法通过abs(-10)调用该函数了!因为abs这个变量已经不指向求绝对值函数而是指向一个整数10!
当然实际代码绝对不能这么写,这里是为了说明函数名也是变量。要恢复abs函数,请重启Python交互环境。
注:由于abs函数实际上是定义在import builtins模块中的,所以要让修改abs变量的指向在其它模块也生效,要用import builtins; builtins.abs = 10。
传入函数
既然变量可以指向函数,函数的参数能接收变量,那么一个函数就可以接收另一个函数作为参数,这种函数就称之为高阶函数。
一个最简单的高阶函数:
1 | def add(x, y, f): |
当我们调用add(-5, 6, abs)时,参数x,y和f分别接收-5,6和abs,根据函数定义,我们可以推导计算过程为:
1 | x = -5 |
Python内建了map()和reduce()函数。
如果你读过Google的那篇大名鼎鼎的论文“MapReduce: Simplified Data Processing on Large Clusters”,你就能大概明白map/reduce的概念。
我们先看map。map()函数接收两个参数,一个是函数,一个是Iterable,map将传入的函数依次作用到序列的每个元素,并把结果作为新的Iterator返回。
举例说明,比如我们有一个函数f(x)=x2,要把这个函数作用在一个list [1, 2, 3, 4, 5, 6, 7, 8, 9]上,就可以用map()实现如下:
1 | f(x) = x * x |
现在,我们用Python代码实现:
1 | >>> def f(x): |
map()传入的第一个参数是f,即函数对象本身。由于结果r是一个Iterator,Iterator是惰性序列,因此通过list()函数让它把整个序列都计算出来并返回一个list。
你可能会想,不需要map()函数,写一个循环,也可以计算出结果:
1 | L = [] |
的确可以,但是,从上面的循环代码,能一眼看明白“把f(x)作用在list的每一个元素并把结果生成一个新的list”吗?
所以,map()作为高阶函数,事实上它把运算规则抽象了,因此,我们不但可以计算简单的f(x)=x2,还可以计算任意复杂的函数,比如,把这个list所有数字转为字符串:
1 | >>> list(map(str, [1, 2, 3, 4, 5, 6, 7, 8, 9])) |
只需要一行代码。
再看reduce的用法。reduce把一个函数作用在一个序列[x1, x2, x3, ...]上,这个函数必须接收两个参数,reduce把结果继续和序列的下一个元素做累积计算,其效果就是:
1 | reduce(f, [x1, x2, x3, x4]) = f(f(f(x1, x2), x3), x4) |
比方说对一个序列求和,就可以用reduce实现:
1 | >>> from functools import reduce |
当然求和运算可以直接用Python内建函数sum(),没必要动用reduce。
但是如果要把序列[1, 3, 5, 7, 9]变换成整数13579,reduce就可以派上用场:
1 | >>> from functools import reduce |
这个例子本身没多大用处,但是,如果考虑到字符串str也是一个序列,对上面的例子稍加改动,配合map(),我们就可以写出把str转换为int的函数:
1 | >>> from functools import reduce |
整理成一个str2int的函数就是:
1 | from functools import reduce |
还可以用lambda函数进一步简化成:
1 | from functools import reduce |
也就是说,假设Python没有提供int()函数,你完全可以自己写一个把字符串转化为整数的函数,而且只需要几行代码!
Python内建的filter()函数用于过滤序列。
和map()类似,filter()也接收一个函数和一个序列。和map()不同的是,filter()把传入的函数依次作用于每个元素,然后根据返回值是True还是False决定保留还是丢弃该元素。
例如,在一个list中,删掉偶数,只保留奇数,可以这么写:
1 | def is_odd(n): |
把一个序列中的空字符串删掉,可以这么写:
1 | def not_empty(s): |
可见用filter()这个高阶函数,关键在于正确实现一个“筛选”函数。
注意到filter()函数返回的是一个Iterator,也就是一个惰性序列,所以要强迫filter()完成计算结果,需要用list()函数获得所有结果并返回list。
用filter求素数
首先,列出从2开始的所有自然数,构造一个序列:
2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, …
取序列的第一个数2,它一定是素数,然后用2把序列的2的倍数筛掉:
3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, …
取新序列的第一个数3,它一定是素数,然后用3把序列的3的倍数筛掉:
5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, …
取新序列的第一个数5,然后用5把序列的5的倍数筛掉:
7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, …
不断筛下去,就可以得到所有的素数。
用Python来实现这个算法,可以先构造一个从3开始的奇数序列:
1 | def _odd_iter(): |
注意这是一个生成器,并且是一个无限序列。
然后定义一个筛选函数:
1 | def _not_divisible(n): |
最后,定义一个生成器,不断返回下一个素数:
1 | def primes(): |
这个生成器先返回第一个素数2,然后,利用filter()不断产生筛选后的新的序列。
由于primes()也是一个无限序列,所以调用时需要设置一个退出循环的条件:
1 | # 打印1000以内的素数: |
注意到Iterator是惰性计算的序列,所以我们可以用Python表示“全体自然数”,“全体素数”这样的序列,而代码非常简洁。
sorted
排序算法
排序也是在程序中经常用到的算法。无论使用冒泡排序还是快速排序,排序的核心是比较两个元素的大小。如果是数字,我们可以直接比较,但如果是字符串或者两个dict呢?直接比较数学上的大小是没有意义的,因此,比较的过程必须通过函数抽象出来。
Python内置的sorted()函数就可以对list进行排序:
1 | >>> sorted([36, 5, -12, 9, -21]) |
此外,sorted()函数也是一个高阶函数,它还可以接收一个key函数来实现自定义的排序,例如按绝对值大小排序:
1 | >>> sorted([36, 5, -12, 9, -21], key=abs) |
key指定的函数将作用于list的每一个元素上,并根据key函数返回的结果进行排序。对比原始的list和经过key=abs处理过的list:
1 | list = [36, 5, -12, 9, -21] |
然后sorted()函数按照keys进行排序,并按照对应关系返回list相应的元素:
1 | keys排序结果 => [5, 9, 12, 21, 36] |
我们再看一个字符串排序的例子:
1 | >>> sorted(['bob', 'about', 'Zoo', 'Credit']) |
默认情况下,对字符串排序,是按照ASCII的大小比较的,由于'Z' < 'a',结果,大写字母Z会排在小写字母a的前面。
现在,我们提出排序应该忽略大小写,按照字母序排序。要实现这个算法,不必对现有代码大加改动,只要我们能用一个key函数把字符串映射为忽略大小写排序即可。忽略大小写来比较两个字符串,实际上就是先把字符串都变成大写(或者都变成小写),再比较。
这样,我们给sorted传入key函数,即可实现忽略大小写的排序:
1 | >>> sorted(['bob', 'about', 'Zoo', 'Credit'], key=str.lower) |
要进行反向排序,不必改动key函数,可以传入第三个参数reverse=True:
1 | >>> sorted(['bob', 'about', 'Zoo', 'Credit'], key=str.lower, reverse=True) |
从上述例子可以看出,高阶函数的抽象能力是非常强大的,而且,核心代码可以保持得非常简洁。
sorted()也是一个高阶函数。用sorted()排序的关键在于实现一个映射函数。
返回函数
函数作为返回值
高阶函数除了可以接受函数作为参数外,还可以把函数作为结果值返回。
我们来实现一个可变参数的求和。通常情况下,求和的函数是这样定义的:
1 | def calc_sum(*args): |
但是,如果不需要立刻求和,而是在后面的代码中,根据需要再计算怎么办?可以不返回求和的结果,而是返回求和的函数:
1 | def lazy_sum(*args): |
当我们调用lazy_sum()时,返回的并不是求和结果,而是求和函数:
1 | >>> f = lazy_sum(1, 3, 5, 7, 9) |
调用函数f时,才真正计算求和的结果:
1 | >>> f() |
在这个例子中,我们在函数lazy_sum中又定义了函数sum,并且,内部函数sum可以引用外部函数lazy_sum的参数和局部变量,当lazy_sum返回函数sum时,相关参数和变量都保存在返回的函数中,这种称为“闭包(Closure)”的程序结构拥有极大的威力。
请再注意一点,当我们调用lazy_sum()时,每次调用都会返回一个新的函数,即使传入相同的参数:
1 | >>> f1 = lazy_sum(1, 3, 5, 7, 9) |
f1()和f2()的调用结果互不影响。
闭包
注意到返回的函数在其定义内部引用了局部变量args,所以,当一个函数返回了一个函数后,其内部的局部变量还被新函数引用,所以,闭包用起来简单,实现起来可不容易。
另一个需要注意的问题是,返回的函数并没有立刻执行,而是直到调用了f()才执行。我们来看一个例子:
1 | def count(): |
在上面的例子中,每次循环,都创建了一个新的函数,然后,把创建的3个函数都返回了。
你可能认为调用f1(),f2()和f3()结果应该是1,4,9,但实际结果是:
1 | >>> f1() |
全部都是9!原因就在于返回的函数引用了变量i,但它并非立刻执行。等到3个函数都返回时,它们所引用的变量i已经变成了3,因此最终结果为9。
返回闭包时牢记一点:返回函数不要引用任何循环变量,或者后续会发生变化的变量。
如果一定要引用循环变量怎么办?方法是再创建一个函数,用该函数的参数绑定循环变量当前的值,无论该循环变量后续如何更改,已绑定到函数参数的值不变:
1 | def count(): |
再看看结果:
1 | >>> f1, f2, f3 = count() |
缺点是代码较长,可利用lambda函数缩短代码。
nonlocal
使用闭包,就是内层函数引用了外层函数的局部变量。如果只是读外层变量的值,我们会发现返回的闭包函数调用一切正常:
1 | def inc(): |
但是,如果对外层变量赋值,由于Python解释器会把x当作函数fn()的局部变量,它会报错:
1 | # -*- coding: utf-8 -*- |
Run
原因是x作为局部变量并没有初始化,直接计算x+1是不行的。但我们其实是想引用inc()函数内部的x,所以需要在fn()函数内部加一个nonlocal x的声明。加上这个声明后,解释器把fn()的x看作外层函数的局部变量,它已经被初始化了,可以正确计算x+1。
使用闭包时,对外层变量赋值前,需要先使用nonlocal声明该变量不是当前函数的局部变量。
练习
利用闭包返回一个计数器函数,每次调用它返回递增整数:
1 | # -*- coding: utf-8 -*- |
Run
小结
一个函数可以返回一个计算结果,也可以返回一个函数。
返回一个函数时,牢记该函数并未执行,返回函数中不要引用任何可能会变化的变量。
https://github.com/michaelliao/learn-python3/blob/master/samples/functional/return_func.py
匿名函数
当我们在传入函数时,有些时候,不需要显式地定义函数,直接传入匿名函数更方便。
在Python中,对匿名函数提供了有限支持。还是以map()函数为例,计算f(x)=x2时,除了定义一个f(x)的函数外,还可以直接传入匿名函数:
1 | >>> list(map(lambda x: x * x, [1, 2, 3, 4, 5, 6, 7, 8, 9])) |
通过对比可以看出,匿名函数lambda x: x * x实际上就是:
1 | def f(x): |
关键字lambda表示匿名函数,冒号前面的x表示函数参数。
匿名函数有个限制,就是只能有一个表达式,不用写return,返回值就是该表达式的结果。
用匿名函数有个好处,因为函数没有名字,不必担心函数名冲突。此外,匿名函数也是一个函数对象,也可以把匿名函数赋值给一个变量,再利用变量来调用该函数:
1 | >>> f = lambda x: x * x |
同样,也可以把匿名函数作为返回值返回,比如:
1 | def build(x, y): |
Python对匿名函数的支持有限,只有一些简单的情况下可以使用匿名函数
装饰器
由于函数也是一个对象,而且函数对象可以被赋值给变量,所以,通过变量也能调用该函数。
1 | >>> def now(): |
函数对象有一个__name__属性(注意:是前后各两个下划线),可以拿到函数的名字:
1 | >>> now.__name__ |
现在,假设我们要增强now()函数的功能,比如,在函数调用前后自动打印日志,但又不希望修改now()函数的定义,这种在代码运行期间动态增加功能的方式,称之为“装饰器”(Decorator)。
本质上,decorator就是一个返回函数的高阶函数。所以,我们要定义一个能打印日志的decorator,可以定义如下:
1 | def log(func): |
观察上面的log,因为它是一个decorator,所以接受一个函数作为参数,并返回一个函数。我们要借助Python的@语法,把decorator置于函数的定义处:
1 | @log |
调用now()函数,不仅会运行now()函数本身,还会在运行now()函数前打印一行日志:
1 | >>> now() |
把@log放到now()函数的定义处,相当于执行了语句:
1 | now = log(now) |
由于log()是一个decorator,返回一个函数,所以,原来的now()函数仍然存在,只是现在同名的now变量指向了新的函数,于是调用now()将执行新函数,即在log()函数中返回的wrapper()函数。
wrapper()函数的参数定义是(*args, **kw),因此,wrapper()函数可以接受任意参数的调用。在wrapper()函数内,首先打印日志,再紧接着调用原始函数。
如果decorator本身需要传入参数,那就需要编写一个返回decorator的高阶函数,写出来会更复杂。比如,要自定义log的文本:
1 | def log(text): |
这个3层嵌套的decorator用法如下:
1 | @log('execute') |
执行结果如下:
1 | >>> now() |
和两层嵌套的decorator相比,3层嵌套的效果是这样的:
1 | >>> now = log('execute')(now) |
我们来剖析上面的语句,首先执行log('execute'),返回的是decorator函数,再调用返回的函数,参数是now函数,返回值最终是wrapper函数。
以上两种decorator的定义都没有问题,但还差最后一步。因为我们讲了函数也是对象,它有__name__等属性,但你去看经过decorator装饰之后的函数,它们的__name__已经从原来的'now'变成了'wrapper':
1 | >>> now.__name__ |
因为返回的那个wrapper()函数名字就是'wrapper',所以,需要把原始函数的__name__等属性复制到wrapper()函数中,否则,有些依赖函数签名的代码执行就会出错。
不需要编写wrapper.__name__ = func.__name__这样的代码,Python内置的functools.wraps就是干这个事的,所以,一个完整的decorator的写法如下:
1 | import functools |
或者针对带参数的decorator:
1 | import functools |
import functools是导入functools模块。模块的概念稍候讲解。现在,只需记住在定义wrapper()的前面加上@functools.wraps(func)即可。
小结
在面向对象(OOP)的设计模式中,decorator被称为装饰模式。OOP的装饰模式需要通过继承和组合来实现,而Python除了能支持OOP的decorator外,直接从语法层次支持decorator。Python的decorator可以用函数实现,也可以用类实现。
decorator可以增强函数的功能,定义起来虽然有点复杂,但使用起来非常灵活和方便。
请编写一个decorator,能在函数调用的前后打印出'begin call'和'end call'的日志。
再思考一下能否写出一个@log的decorator,使它既支持:
1 | @log |
又支持:
1 | @log('execute') |
偏函数
Python的functools模块提供了很多有用的功能,其中一个就是偏函数(Partial function)。要注意,这里的偏函数和数学意义上的偏函数不一样。
在介绍函数参数的时候,我们讲到,通过设定参数的默认值,可以降低函数调用的难度。而偏函数也可以做到这一点。举例如下:
int()函数可以把字符串转换为整数,当仅传入字符串时,int()函数默认按十进制转换:
1 | >>> int('12345') |
但int()函数还提供额外的base参数,默认值为10。如果传入base参数,就可以做N进制的转换:
1 | >>> int('12345', base=8) |
假设要转换大量的二进制字符串,每次都传入int(x, base=2)非常麻烦,于是,我们想到,可以定义一个int2()的函数,默认把base=2传进去:
1 | def int2(x, base=2): |
这样,我们转换二进制就非常方便了:
1 | >>> int2('1000000') |
functools.partial就是帮助我们创建一个偏函数的,不需要我们自己定义int2(),可以直接使用下面的代码创建一个新的函数int2:
1 | >>> import functools |
所以,简单总结functools.partial的作用就是,把一个函数的某些参数给固定住(也就是设置默认值),返回一个新的函数,调用这个新函数会更简单。
注意到上面的新的int2函数,仅仅是把base参数重新设定默认值为2,但也可以在函数调用时传入其他值:
1 | >>> int2('1000000', base=10) |
最后,创建偏函数时,实际上可以接收函数对象、*args和**kw这3个参数,当传入:
1 | int2 = functools.partial(int, base=2) |
实际上固定了int()函数的关键字参数base,也就是:
1 | int2('10010') |
相当于:
1 | kw = { 'base': 2 } |
当传入:
1 | max2 = functools.partial(max, 10) |
实际上会把10作为*args的一部分自动加到左边,也就是:
1 | max2(5, 6, 7) |
相当于:
1 | args = (10, 5, 6, 7) |
结果为10。
面向对象编程
在Python中,所有数据类型都可以视为对象,也可以自定义对象。自定义的对象数据类型就是面向对象中的类(Class)的概
类和实例
和普通的函数相比,在类中定义的函数只有一点不同,就是第一个参数永远是实例变量self,并且,调用时,不用传递该参数。除此之外,类的方法和普通函数没有什么区别,所以,你仍然可以用默认参数、可变参数、关键字参数和命名关键字参数。
数据封装
在Student类的内部定义访问数据的函数,这样,就把“数据”给封装起来了。这些封装数据的函数是和Student类本身是关联起来的,我们称之为类的方法。
如果要让内部属性不被外部访问,可以把属性的名称前加上两个下划线__,在Python中,实例的变量名如果以__开头,就变成了一个私有变量(private),只有内部可以访问,外部不能访问。这样就确保了外部代码不能随意修改对象内部的状态,这样通过访问限制的保护,代码更加健壮。
但是如果外部代码要获取name和score怎么办?可以给Student类增加get_name和get_score这样的方法。如果又要允许外部代码修改score怎么办?可以再给Student类增加set_score方法。
需要注意的是,在Python中,变量名类似__xxx__的,也就是以双下划线开头,并且以双下划线结尾的,是特殊变量,特殊变量是可以直接访问的,不是private变量,所以,不能用__name__、__score__这样的变量名。
有些时候,你会看到以一个下划线开头的实例变量名,比如_name,这样的实例变量外部是可以访问的,但是,按照约定俗成的规定,当你看到这样的变量时,意思就是,“虽然我可以被访问,但是,请把我视为私有变量,不要随意访问”。
双下划线开头的实例变量是不是一定不能从外部访问呢?其实也不是。不能直接访问__name是因为Python解释器对外把__name变量改成了_Student__name,所以,仍然可以通过_Student__name来访问__name变量:
1 | >>> bart._Student__name |
但是强烈建议你不要这么干,因为不同版本的Python解释器可能会把__name改成不同的变量名。
总的来说就是,Python本身没有任何机制阻止你干坏事,一切全靠自觉。
最后注意下面的这种错误写法:
1 | >>> bart = Student('Bart Simpson', 59) |
表面上看,外部代码“成功”地设置了__name变量,但实际上这个__name变量和class内部的__name变量不是一个变量!内部的__name变量已经被Python解释器自动改成了_Student__name,而外部代码给bart新增了一个__name变量。
继承和多态
继承有什么好处?最大的好处是子类获得了父类的全部功能。
当子类和父类都存在相同的run()方法时,我们说,子类的run()覆盖了父类的run(),在代码运行的时候,总是会调用子类的run()。这样,我们就获得了继承的另一个好处:多态。
这就是著名的“开闭”原则:
对扩展开放:允许新增Animal子类;
对修改封闭:不需要修改依赖Animal类型的run_twice()等函数。
静态语言 vs 动态语言
对于静态语言(例如Java)来说,如果需要传入Animal类型,则传入的对象必须是Animal类型或者它的子类,否则,将无法调用run()方法。
对于Python这样的动态语言来说,则不一定需要传入Animal类型。我们只需要保证传入的对象有一个run()方法就可以了:
这就是动态语言的“鸭子类型”,它并不要求严格的继承体系,一个对象只要“看起来像鸭子,走起路来像鸭子”,那它就可以被看做是鸭子。
获取对象信息
当我们拿到一个对象的引用时,如何知道这个对象是什么类型、有哪些方法呢?
使用type()
首先,我们来判断对象类型,使用type()函数:
基本类型都可以用type()判断:
1 | >>> type(123) |
如果一个变量指向函数或者类,也可以用type()判断:
1 | >>> type(abs) |
但是type()函数返回的是什么类型呢?它返回对应的Class类型。如果我们要在if语句中判断,就需要比较两个变量的type类型是否相同:
1 | >>> type(123)==type(456) |
判断基本数据类型可以直接写int,str等,但如果要判断一个对象是否是函数怎么办?可以使用types模块中定义的常量:
1 | >>> import types |
使用isinstance()
对于class的继承关系来说,使用type()就很不方便。我们要判断class的类型,可以使用isinstance()函数。
我们回顾上次的例子,如果继承关系是:
1 | object -> Animal -> Dog -> Husky |
那么,isinstance()就可以告诉我们,一个对象是否是某种类型。先创建3种类型的对象:
1 | >>> a = Animal() |
然后,判断:
1 | >>> isinstance(h, Husky) |
再判断:
1 | >>> isinstance(h, Dog) |
因此,我们可以确信,h还是Animal类型:
1 | >>> isinstance(h, Animal) |
同理,实际类型是Dog的d也是Animal类型:
1 | >>> isinstance(d, Dog) and isinstance(d, Animal) |
但是,d不是Husky类型:
1 | >>> isinstance(d, Husky) |
能用type()判断的基本类型也可以用isinstance()判断:
1 | >>> isinstance('a', str) |
并且还可以判断一个变量是否是某些类型中的一种,比如下面的代码就可以判断是否是list或者tuple:
1 | >>> isinstance([1, 2, 3], (list, tuple)) |
总是优先使用isinstance()判断类型,可以将指定类型及其子类“一网打尽”。
使用dir()
如果要获得一个对象的所有属性和方法,可以使用dir()函数,它返回一个包含字符串的list,比如,获得一个str对象的所有属性和方法:
1 | >>> dir('ABC') |
类似__xxx__的属性和方法在Python中都是有特殊用途的,比如__len__方法返回长度。在Python中,如果你调用len()函数试图获取一个对象的长度,实际上,在len()函数内部,它自动去调用该对象的__len__()方法,所以,下面的代码是等价的:
1 | >>> len('ABC') |
我们自己写的类,如果也想用len(myObj)的话,就自己写一个__len__()方法:
1 | >>> class MyDog(object): |
剩下的都是普通属性或方法,比如lower()返回小写的字符串:
1 | >>> 'ABC'.lower() |
仅仅把属性和方法列出来是不够的,配合getattr()、setattr()以及hasattr(),我们可以直接操作一个对象的状态:
1 | >>> class MyObject(object): |
紧接着,可以测试该对象的属性:
1 | >>> hasattr(obj, 'x') # 有属性'x'吗? |
如果试图获取不存在的属性,会抛出AttributeError的错误:
1 | >>> getattr(obj, 'z') # 获取属性'z' |
可以传入一个default参数,如果属性不存在,就返回默认值:
1 | >>> getattr(obj, 'z', 404) # 获取属性'z',如果不存在,返回默认值404 |
也可以获得对象的方法:
1 | >>> hasattr(obj, 'power') # 有属性'power'吗? |
小结
通过内置的一系列函数,我们可以对任意一个Python对象进行剖析,拿到其内部的数据。要注意的是,只有在不知道对象信息的时候,我们才会去获取对象信息。如果可以直接写:
1 | sum = obj.x + obj.y |
就不要写:
1 | sum = getattr(obj, 'x') + getattr(obj, 'y') |
一个正确的用法的例子如下:
1 | def readImage(fp): |
假设我们希望从文件流fp中读取图像,我们首先要判断该fp对象是否存在read方法,如果存在,则该对象是一个流,如果不存在,则无法读取。hasattr()就派上了用场。
请注意,在Python这类动态语言中,根据鸭子类型,有read()方法,不代表该fp对象就是一个文件流,它也可能是网络流,也可能是内存中的一个字节流,但只要read()方法返回的是有效的图像数据,就不影响读取图像的功能。
isinstance() 与 type() 区别:
- type() 不会认为子类是一种父类类型,不考虑继承关系。
- isinstance() 会认为子类是一种父类类型,考虑继承关系。
如果要判断两个类型是否相同推荐使用 isinstance()。
type() 可判断的常见类型 (python 中常见内置类型):
object:所有类的基类int、float、complex:分别表示整数、浮点数、复数类型bool:布尔类型,值为True或Falsestr:字符串类型,用于表示文本数据list、turple、set、dict、frozenset:分别表示列表、元组、集合、字典和不可变集合类型,用于存储集合数据range:表示整数序列,可以用于迭代bytes、bytearray、memoryview:分别表示字节串、可变字节串和可读写的二进制数据类型type:表示 Python 对象的类型
实例属性和类属性
由于Python是动态语言,根据类创建的实例可以任意绑定属性。
给实例绑定属性的方法是通过实例变量,或者通过self变量:
如果Student类本身需要绑定一个属性,可以直接在class中定义属性,这种属性是类属性,归Student类所有:
1 | class Student(object): |
当我们定义了一个类属性后,这个属性虽然归类所有,但类的所有实例都可以访问到。
1 | >>> class Student(object): |
从上面的例子可以看出,在编写程序的时候,千万不要对实例属性和类属性使用相同的名字,因为相同名称的实例属性将屏蔽掉类属性,但是当你删除实例属性后,再使用相同的名称,访问到的将是类属性。

面向对象高级编程
使用__ slots __
正常情况下,当我们定义了一个class,创建了一个class的实例后,我们可以给该实例绑定任何属性和方法,这就是动态语言的灵活性。先定义class:
1 | class Student(object): |
然后,尝试给实例绑定一个属性:
1 | >>> s = Student() |
还可以尝试给实例绑定一个方法:
1 | >>> def set_age(self, age): # 定义一个函数作为实例方法 |
但是,给一个实例绑定的方法,对另一个实例是不起作用的:
1 | >>> s2 = Student() # 创建新的实例 |
为了给所有实例都绑定方法,可以给class绑定方法:
1 | >>> def set_score(self, score): |
给class绑定方法后,所有实例均可调用:
1 | >>> s.set_score(100) |
通常情况下,上面的set_score方法可以直接定义在class中,但动态绑定允许我们在程序运行的过程中动态给class加上功能,这在静态语言中很难实现。
使用__ slots __
但是,如果我们想要限制实例的属性怎么办?比如,只允许对Student实例添加name和age属性。
为了达到限制的目的,Python允许在定义class的时候,定义一个特殊的__slots__变量,来限制该class实例能添加的属性:
1 | class Student(object): |
然后,我们试试:
1 | >>> s = Student() # 创建新的实例 |
由于'score'没有被放到__slots__中,所以不能绑定score属性,试图绑定score将得到AttributeError的错误。
使用__slots__要注意,__slots__定义的属性仅对当前类实例起作用,对继承的子类是不起作用的:
1 | >>> class GraduateStudent(Student): |
除非在子类中也定义__slots__,这样,子类实例允许定义的属性就是自身的__slots__加上父类的__slots__。
能否通过这种方式添加方法?不行
1 | def set_age2(self,age): |
- 如果一个类定义了
__slots__,那么它的实例将没有__dict__属性,因此不能通过__dict__来动态添加属性。 - 子类如果没有定义
__slots__,则会继承父类的__slots__;如果子类定义了__slots__,那么子类的__slots__是父类__slots__和子类自身__slots__的并集。
使用@property
还记得装饰器(decorator)可以给函数动态加上功能。Python内置的@property装饰器就是负责把一个方法变成属性调用的:
1 | class Student(object): |
@property的实现比较复杂,我们先考察如何使用。把一个getter方法变成属性,只需要加上@property就可以了,此时,@property本身又创建了另一个装饰器@score.setter,负责把一个setter方法变成属性赋值,于是,我们就拥有一个可控的属性操作:
1 | >>> s = Student() |
注意到这个神奇的@property,我们在对实例属性操作的时候,就知道该属性很可能不是直接暴露的,而是通过getter和setter方法来实现的。
还可以定义只读属性,只定义getter方法,不定义setter方法就是一个只读属性:
1 | class Student(object): |
上面的birth是可读写属性,而age就是一个只读属性,因为age可以根据birth和当前时间计算出来。
要特别注意:属性的方法名不要和实例变量重名。例如,以下的代码是错误的:
1 | class Student(object): |
这是因为调用s.birth时,首先转换为方法调用,在执行return self.birth时,又视为访问self的属性,于是又转换为方法调用,造成无限递归,最终导致栈溢出报错RecursionError。
使用@property的时候注意:
1.方法名和变量名不要相同,否则会栈溢出。经验是:变量名使用_横线的格式。
2.对一个方法名使用了@property后,就可以通过.的方式来调用这个方法。
3.对一个方法使用了@‘@property标注的方法名’.setter后,就可以通过.的方式来调用这个方法。
使用的时候,@property标注的方法和属性保持一致,有利于简单易读。
1 | class Student(object): |
如果不一致:
读属性时:.属性 和.@property标注的方法名,都可以调用到这个方法
写属性时:需要.属性名称
配置时,标注的方法名.setter的名字需要和@proerty标注的方法名一致。
多重继承
python允许多重继承。
在设计类的继承关系时,通常,主线都是单一继承下来的,例如,Ostrich继承自Bird。但是,如果需要“混入”额外的功能,通过多重继承就可以实现,比如,让Ostrich除了继承自Bird外,再同时继承Runnable。这种设计通常称之为MixIn。
为了更好地看出继承关系,我们把Runnable和Flyable改为RunnableMixIn和FlyableMixIn。类似的,你还可以定义出肉食动物CarnivorousMixIn和植食动物HerbivoresMixIn,让某个动物同时拥有好几个MixIn:
1 | class Dog(Mammal, RunnableMixIn, CarnivorousMixIn): |
MixIn的目的就是给一个类增加多个功能。
定制类
__slots__:限制类的属性
__len__():能让class作用于len()函数。除此之外,Python的class中还有许多这样有特殊用途的
__str__():调整print()方法展示内容
但是细心的朋友会发现直接敲变量不用print,打印出来的实例还是不好看:
1 | >>> s = Student('Michael') |
这是因为直接显示变量调用的不是__str__(),而是__repr__(),两者的区别是__str__()返回用户看到的字符串,而__repr__()返回程序开发者看到的字符串,也就是说,__repr__()是为调试服务的。
解决办法是再定义一个__repr__()。但是通常__str__()和__repr__()代码都是一样的,所以,有个偷懒的写法:
1 | class Student(object): |
__iter__():该方法返回一个迭代对象,然后,Python的for循环就会不断调用该迭代对象的__next__()方法拿到循环的下一个值,直到遇到StopIteration错误时退出循环。
getitem
iter虽然能作用于for循环,看起来和list有点像,但是,把它当成list来使用还是不行,比如,取第5个元素:
1 | >>> Fib()[5] |
要表现得像list那样按照下标取出元素,需要实现__getitem__()方法:
1 | class Fib(object): |
但是list有个神奇的切片方法:
1 | >>> list(range(100))[5:10] |
对于Fib却报错。原因是__getitem__()传入的参数可能是一个int,也可能是一个切片对象slice,所以要做判断:
1 | class Fib(object): |
现在试试Fib的切片:
1 | >>> f = Fib() |
但是没有对step参数作处理:
1 | >>> f[:10:2] |
也没有对负数作处理,所以,要正确实现一个__getitem__()还是有很多工作要做的。
此外,如果把对象看成dict,__getitem__()的参数也可能是一个可以作key的object,例如str。
与之对应的是__setitem__()方法,把对象视作list或dict来对集合赋值。最后,还有一个__delitem__()方法,用于删除某个元素。
总之,通过上面的方法,我们自己定义的类表现得和Python自带的list、tuple、dict没什么区别,这完全归功于动态语言的“鸭子类型”,不需要强制继承某个接口。
getattr
正常情况下,当我们调用类的方法或属性时,如果不存在,就会报错。比如定义Student类:
1 | class Student(object): |
调用name属性,没问题,但是,调用不存在的score属性,就有问题了:
1 | >>> s = Student() |
错误信息很清楚地告诉我们,没有找到score这个attribute。
要避免这个错误,除了可以加上一个score属性外,Python还有另一个机制,那就是写一个__getattr__()方法,动态返回一个属性。修改如下:
1 | class Student(object): |
当调用不存在的属性时,比如score,Python解释器会试图调用__getattr__(self, 'score')来尝试获得属性,这样,我们就有机会返回score的值:
1 | >>> s = Student() |
返回函数也是完全可以的:
1 | class Student(object): |
只是调用方式要变为:
1 | >>> s.age() |
注意,只有在没有找到属性的情况下,才调用__getattr__,已有的属性,比如name,不会在__getattr__中查找。
此外,注意到任意调用如s.abc都会返回None,这是因为我们定义的__getattr__默认返回就是None。要让class只响应特定的几个属性,我们就要按照约定,抛出AttributeError的错误:
1 | class Student(object): |
这实际上可以把一个类的所有属性和方法调用全部动态化处理了,不需要任何特殊手段。
这种完全动态调用的特性有什么实际作用呢?作用就是,可以针对完全动态的情况作调用。
举个例子:
现在很多网站都搞REST API,比如新浪微博、豆瓣啥的,调用API的URL类似:
如果要写SDK,给每个URL对应的API都写一个方法,那得累死,而且,API一旦改动,SDK也要改。
利用完全动态的__getattr__,我们可以写出一个链式调用:
1 | class Chain(object): |
试试:
1 | >>> Chain().status.user.timeline.list |
这样,无论API怎么变,SDK都可以根据URL实现完全动态的调用,而且,不随API的增加而改变!
还有些REST API会把参数放到URL中,比如GitHub的API:
1 | GET /users/:user/repos |
调用时,需要把:user替换为实际用户名。如果我们能写出这样的链式调用:
1 | Chain().users('michael').repos |
就可以非常方便地调用API了。有兴趣的童鞋可以试试写出来。
call
一个对象实例可以有自己的属性和方法,当我们调用实例方法时,我们用instance.method()来调用。能不能直接在实例本身上调用呢?在Python中,答案是肯定的。
任何类,只需要定义一个__call__()方法,就可以直接对实例进行调用。请看示例:
1 | class Student(object): |
调用方式如下:
1 | >>> s = Student('Michael') |
__call__()还可以定义参数。对实例进行直接调用就好比对一个函数进行调用一样,所以你完全可以把对象看成函数,把函数看成对象,因为这两者之间本来就没啥根本的区别。
如果你把对象看成函数,那么函数本身其实也可以在运行期动态创建出来,因为类的实例都是运行期创建出来的,这么一来,我们就模糊了对象和函数的界限。
那么,怎么判断一个变量是对象还是函数呢?其实,更多的时候,我们需要判断一个对象是否能被调用,能被调用的对象就是一个Callable对象,比如函数和我们上面定义的带有__call__()的类实例:
1 | >>> callable(Student()) |
通过callable()函数,我们就可以判断一个对象是否是“可调用”对象。
本节介绍的是最常用的几个定制方法,还有很多可定制的方法,请参考Python的官方文档。
使用枚举类
Python提供了Enum类来实现这个功能:
1 | from enum import Enum |
这样我们就获得了Month类型的枚举类,可以直接使用Month.Jan来引用一个常量,或者枚举它的所有成员:
1 | for name, member in Month.__members__.items(): |
value属性则是自动赋给成员的int常量,默认从1开始计数。
如果需要更精确地控制枚举类型,可以从Enum派生出自定义类:
1 | from enum import Enum, unique |
@unique装饰器可以帮助我们检查保证没有重复值。
访问这些枚举类型可以有若干种方法:
1 | >>> day1 = Weekday.Mon |
可见,既可以用成员名称引用枚举常量,又可以直接根据value的值获得枚举常量。
使用元类
type()
动态语言和静态语言最大的不同,就是函数和类的定义,不是编译时定义的,而是运行时动态创建的。
比方说我们要定义一个Hello的class,就写一个hello.py模块:
1 | class Hello(object): |
当Python解释器载入hello模块时,就会依次执行该模块的所有语句,执行结果就是动态创建出一个Hello的class对象,测试如下:
1 | >>> from hello import Hello |
type()函数可以查看一个类型或变量的类型,Hello是一个class,它的类型就是type,而h是一个实例,它的类型就是class Hello。
我们说class的定义是运行时动态创建的,而创建class的方法就是使用type()函数。
type()函数既可以返回一个对象的类型,又可以创建出新的类型,比如,我们可以通过type()函数创建出Hello类,而无需通过class Hello(object)...的定义:
1 | >>> def fn(self, name='world'): # 先定义函数 |
要创建一个class对象,type()函数依次传入3个参数:
- class的名称;
- 继承的父类集合,注意Python支持多重继承,如果只有一个父类,别忘了tuple的单元素写法;
- class的方法名称与函数绑定,这里我们把函数
fn绑定到方法名hello上。
通过type()函数创建的类和直接写class是完全一样的,因为Python解释器遇到class定义时,仅仅是扫描一下class定义的语法,然后调用type()函数创建出class。
正常情况下,我们都用class Xxx...来定义类,但是,type()函数也允许我们动态创建出类来,也就是说,动态语言本身支持运行期动态创建类,这和静态语言有非常大的不同,要在静态语言运行期创建类,必须构造源代码字符串再调用编译器,或者借助一些工具生成字节码实现,本质上都是动态编译,会非常复杂。
metaclass
除了使用type()动态创建类以外,要控制类的创建行为,还可以使用metaclass。
metaclass,直译为元类,简单的解释就是:
当我们定义了类以后,就可以根据这个类创建出实例,所以:先定义类,然后创建实例。
但是如果我们想创建出类呢?那就必须根据metaclass创建出类,所以:先定义metaclass,然后创建类。
连接起来就是:先定义metaclass,就可以创建类,最后创建实例。
所以,metaclass允许你创建类或者修改类。换句话说,你可以把类看成是metaclass创建出来的“实例”。
metaclass是Python面向对象里最难理解,也是最难使用的魔术代码。正常情况下,你不会碰到需要使用metaclass的情况,所以,以下内容看不懂也没关系,因为基本上你不会用到。
我们先看一个简单的例子,这个metaclass可以给我们自定义的MyList增加一个add方法:
定义ListMetaclass,按照默认习惯,metaclass的类名总是以Metaclass结尾,以便清楚地表示这是一个metaclass:
1 | # metaclass是类的模板,所以必须从`type`类型派生: |
有了ListMetaclass,我们在定义类的时候还要指示使用ListMetaclass来定制类,传入关键字参数metaclass:
1 | class MyList(list, metaclass=ListMetaclass): |
当我们传入关键字参数metaclass时,魔术就生效了,它指示Python解释器在创建MyList时,要通过ListMetaclass.__new__()来创建,在此,我们可以修改类的定义,比如,加上新的方法,然后,返回修改后的定义。
__new__()方法接收到的参数依次是:
- 当前准备创建的类的对象;
- 类的名字;
- 类继承的父类集合;
- 类的方法集合。
测试一下MyList是否可以调用add()方法:
1 | >>> L = MyList() |
而普通的list没有add()方法:
1 | >>> L2 = list() |
动态修改有什么意义?直接在MyList定义中写上add()方法不是更简单吗?正常情况下,确实应该直接写,通过metaclass修改纯属变态。
但是,总会遇到需要通过metaclass修改类定义的。ORM就是一个典型的例子。
ORM全称“Object Relational Mapping”,即对象-关系映射,就是把关系数据库的一行映射为一个对象,也就是一个类对应一个表,这样,写代码更简单,不用直接操作SQL语句。
要编写一个ORM框架,所有的类都只能动态定义,因为只有使用者才能根据表的结构定义出对应的类来。
让我们来尝试编写一个ORM框架。
编写底层模块的第一步,就是先把调用接口写出来。比如,使用者如果使用这个ORM框架,想定义一个User类来操作对应的数据库表User,我们期待他写出这样的代码:
1 | class User(Model): |
其中,父类Model和属性类型StringField、IntegerField是由ORM框架提供的,剩下的魔术方法比如save()全部由父类Model自动完成。虽然metaclass的编写会比较复杂,但ORM的使用者用起来却异常简单。
现在,我们就按上面的接口来实现该ORM。
首先来定义Field类,它负责保存数据库表的字段名和字段类型:
1 | class Field(object): |
在Field的基础上,进一步定义各种类型的Field,比如StringField,IntegerField等等:
1 | class StringField(Field): |
下一步,就是编写最复杂的ModelMetaclass了:
1 | class ModelMetaclass(type): |
以及基类Model:
1 | class Model(dict, metaclass=ModelMetaclass): |
当用户定义一个class User(Model)时,Python解释器首先在当前类User的定义中查找metaclass,如果没有找到,就继续在父类Model中查找metaclass,找到了,就使用Model中定义的metaclass的ModelMetaclass来创建User类,也就是说,metaclass可以隐式地继承到子类,但子类自己却感觉不到。
在ModelMetaclass中,一共做了几件事情:
- 排除掉对
Model类的修改; - 在当前类(比如
User)中查找定义的类的所有属性,如果找到一个Field属性,就把它保存到一个__mappings__的dict中,同时从类属性中删除该Field属性,否则,容易造成运行时错误(实例的属性会遮盖类的同名属性); - 把表名保存到
__table__中,这里简化为表名默认为类名。
在Model类中,就可以定义各种操作数据库的方法,比如save(),delete(),find(),update等等。
我们实现了save()方法,把一个实例保存到数据库中。因为有表名,属性到字段的映射和属性值的集合,就可以构造出INSERT语句。
编写代码试试:
1 | u = User(id=12345, name='Michael', email='test@orm.org', password='my-pwd') |
输出如下:
1 | Found model: User |
可以看到,save()方法已经打印出了可执行的SQL语句,以及参数列表,只需要真正连接到数据库,执行该SQL语句,就可以完成真正的功能。
不到100行代码,我们就通过metaclass实现了一个精简的ORM框架,是不是非常简单?
Python3 中类的静态方法、普通方法、类方法
静态方法: 用 @staticmethod 装饰的不带 self 参数的方法叫做静态方法,类的静态方法可以没有参数,可以直接使用类名调用。
普通方法: 默认有个self参数,且只能被对象调用。
类方法: 默认有个 cls 参数,可以被类和对象调用,需要加上 @classmethod 装饰器。
1 | class Classname: |
IO编程
文件读写
在磁盘上读写文件的功能都是由操作系统提供的,现代操作系统不允许普通的程序直接操作磁盘,所以,读写文件就是请求操作系统打开一个文件对象(通常称为文件描述符),然后,通过操作系统提供的接口从这个文件对象中读取数据(读文件),或者把数据写入这个文件对象(写文件)。
读文件
由于文件读写时都有可能产生IOError,一旦出错,后面的f.close()就不会调用。所以,为了保证无论是否出错都能正确地关闭文件,我们可以使用try ... finally来实现:
1 | try: |
但是每次都这么写实在太繁琐,所以,Python引入了with语句来自动帮我们调用close()方法:
1 | with open('/path/to/file', 'r') as f: |
这和前面的try ... finally是一样的,但是代码更佳简洁,并且不必调用f.close()方法。
调用read()会一次性读取文件的全部内容,如果文件有10G,内存就爆了,所以,要保险起见,可以反复调用read(size)方法,每次最多读取size个字节的内容。另外,调用readline()可以每次读取一行内容,调用readlines()一次读取所有内容并按行返回list。因此,要根据需要决定怎么调用。
如果文件很小,read()一次性读取最方便;如果不能确定文件大小,反复调用read(size)比较保险;如果是配置文件,调用readlines()最方便:
1 | for line in f.readlines(): |
1 | # 代码片段尝试以只读模式("r")打开名为much.txt的文件,并尝试在文件可读取时持续打印文件内容的一个字符(read(1)每次读取1个字节)。但是,这里存在一个问题:f3.readable()是一个布尔函数,它仅表示文件是否可读,而不是在文件内容耗尽后变为不可读。因此,你的循环将无限制地执行,因为只要文件保持打开且未损坏,它就是可读的。 |
file-like Object
像open()函数返回的这种有个read()方法的对象,在Python中统称为file-like Object。除了file外,还可以是内存的字节流,网络流,自定义流等等。file-like Object不要求从特定类继承,只要写个read()方法就行。
StringIO就是在内存中创建的file-like Object,常用作临时缓冲。
二进制文件
前面讲的默认都是读取文本文件,并且是UTF-8编码的文本文件。要读取二进制文件,比如图片、视频等等,用'rb'模式打开文件即可:
1 | >>> f = open('/Users/michael/test.jpg', 'rb') |
字符编码
要读取非UTF-8编码的文本文件,需要给open()函数传入encoding参数,例如,读取GBK编码的文件:
1 | >>> f = open('/Users/michael/gbk.txt', 'r', encoding='gbk') |
遇到有些编码不规范的文件,你可能会遇到UnicodeDecodeError,因为在文本文件中可能夹杂了一些非法编码的字符。遇到这种情况,open()函数还接收一个errors参数,表示如果遇到编码错误后如何处理。最简单的方式是直接忽略:
1 | >>> f = open('/Users/michael/gbk.txt', 'r', encoding='gbk', errors='ignore') |
写文件
写文件和读文件是一样的,唯一区别是调用open()函数时,传入标识符'w'或者'wb'表示写文本文件或写二进制文件,写文件的方法是write()
你可以反复调用write()来写入文件,但是务必要调用f.close()来关闭文件。当我们写文件时,操作系统往往不会立刻把数据写入磁盘,而是放到内存缓存起来,空闲的时候再慢慢写入。只有调用close()方法时,操作系统才保证把没有写入的数据全部写入磁盘。忘记调用close()的后果是数据可能只写了一部分到磁盘,剩下的丢失了。所以,还是用with语句来得保险:
1 | with open('/Users/michael/test.txt', 'w') as f: |
要写入特定编码的文本文件,请给open()函数传入encoding参数,将字符串自动转换成指定编码。
细心的童鞋会发现,以'w'模式写入文件时,如果文件已存在,会直接覆盖(相当于删掉后新写入一个文件)。如果我们希望追加到文件末尾怎么办?可以传入'a'以追加(append)模式写入。
must have exactly one of create/read/write/append mode
mode 参数有:
| 模式 | 描述 |
|---|---|
| t | 文本模式 (默认)。 |
| x | 写模式,新建一个文件,如果该文件已存在则会报错。 |
| b | 二进制模式。 |
| + | 打开一个文件进行更新(可读可写)。 |
| U | 通用换行模式(Python 3 不支持)。 |
| r | 以只读方式打开文件。文件的指针将会放在文件的开头。这是默认模式。 |
| rb | 以二进制格式打开一个文件用于只读。文件指针将会放在文件的开头。这是默认模式。一般用于非文本文件如图片等。 |
| r+ | 打开一个文件用于读写。文件指针将会放在文件的开头。 |
| rb+ | 以二进制格式打开一个文件用于读写。文件指针将会放在文件的开头。一般用于非文本文件如图片等。 |
| w | 打开一个文件只用于写入。如果该文件已存在则打开文件,并从开头开始编辑,即原有内容会被删除。如果该文件不存在,创建新文件。 |
| wb | 以二进制格式打开一个文件只用于写入。如果该文件已存在则打开文件,并从开头开始编辑,即原有内容会被删除。如果该文件不存在,创建新文件。一般用于非文本文件如图片等。 |
| w+ | 打开一个文件用于读写。如果该文件已存在则打开文件,并从开头开始编辑,即原有内容会被删除。如果该文件不存在,创建新文件。 |
| wb+ | 以二进制格式打开一个文件用于读写。如果该文件已存在则打开文件,并从开头开始编辑,即原有内容会被删除。如果该文件不存在,创建新文件。一般用于非文本文件如图片等。 |
| a | 打开一个文件用于追加。如果该文件已存在,文件指针将会放在文件的结尾。也就是说,新的内容将会被写入到已有内容之后。如果该文件不存在,创建新文件进行写入。 |
| ab | 以二进制格式打开一个文件用于追加。如果该文件已存在,文件指针将会放在文件的结尾。也就是说,新的内容将会被写入到已有内容之后。如果该文件不存在,创建新文件进行写入。 |
| a+ | 打开一个文件用于读写。如果该文件已存在,文件指针将会放在文件的结尾。文件打开时会是追加模式。如果该文件不存在,创建新文件用于读写。 |
| ab+ | 以二进制格式打开一个文件用于追加。如果该文件已存在,文件指针将会放在文件的结尾。如果该文件不存在,创建新文件用于读写。 |
默认为文本模式,如果要以二进制模式打开,加上 b
StringIO和BytesIO
StringIO
很多时候,数据读写不一定是文件,也可以在内存中读写。
StringIO顾名思义就是在内存中读写str。
要把str写入StringIO,我们需要先创建一个StringIO,然后,像文件一样写入即可:
1 | >>> from io import StringIO |
getvalue()方法用于获得写入后的str。
要读取StringIO,可以用一个str初始化StringIO,然后,像读文件一样读取:
1 | >>> from io import StringIO |
BytesIO
StringIO操作的只能是str,如果要操作二进制数据,就需要使用BytesIO。
BytesIO实现了在内存中读写bytes,我们创建一个BytesIO,然后写入一些bytes:
1 | >>> from io import BytesIO |
请注意,写入的不是str,而是经过UTF-8编码的bytes。
和StringIO类似,可以用一个bytes初始化BytesIO,然后,像读文件一样读取:
1 | >>> from io import BytesIO |
StringIO和BytesIO是在内存中操作str和bytes的方法,使得和读写文件具有一致的接口。
操作文件和目录
Python内置的os模块也可以直接调用操作系统提供的接口函数。
打开Python交互式命令行,我们来看看如何使用os模块的基本功能:
1 | >>> import os |
如果是posix,说明系统是Linux、Unix或Mac OS X,如果是nt,就是Windows系统。
要获取详细的系统信息,可以调用uname()函数:
1 | >>> os.uname() |
注意uname()函数在Windows上不提供,也就是说,os模块的某些函数是跟操作系统相关的。
环境变量
在操作系统中定义的环境变量,全部保存在os.environ这个变量中,可以直接查看:
1 | >>> os.environ |
要获取某个环境变量的值,可以调用os.environ.get('key'):
1 | >>> os.environ.get('PATH') |
操作文件和目录
操作文件和目录的函数一部分放在os模块中,一部分放在os.path模块中,这一点要注意一下。查看、创建和删除目录可以这么调用:
1 | # 查看当前目录的绝对路径: |
把两个路径合成一个时,不要直接拼字符串,而要通过os.path.join()函数,这样可以正确处理不同操作系统的路径分隔符。在Linux/Unix/Mac下,os.path.join()返回这样的字符串:
1 | part-1/part-2 |
而Windows下会返回这样的字符串:
1 | part-1\part-2 |
同样的道理,要拆分路径时,也不要直接去拆字符串,而要通过os.path.split()函数,这样可以把一个路径拆分为两部分,后一部分总是最后级别的目录或文件名:
1 | >>> os.path.split('/Users/michael/testdir/file.txt') |
os.path.splitext()可以直接让你得到文件扩展名,很多时候非常方便:
1 | >>> os.path.splitext('/path/to/file.txt') |
这些合并、拆分路径的函数并不要求目录和文件要真实存在,它们只对字符串进行操作。
文件操作使用下面的函数。假定当前目录下有一个test.txt文件:
1 | # 对文件重命名: |
但是复制文件的函数居然在os模块中不存在!原因是复制文件并非由操作系统提供的系统调用。理论上讲,我们通过上一节的读写文件可以完成文件复制,只不过要多写很多代码。
幸运的是shutil模块提供了copyfile()的函数,你还可以在shutil模块中找到很多实用函数,它们可以看做是os模块的补充。
最后看看如何利用Python的特性来过滤文件。比如我们要列出当前目录下的所有目录,只需要一行代码:
1 | >>> [x for x in os.listdir('.') if os.path.isdir(x)] |
要列出所有的.py文件,也只需一行代码:
1 | >>> [x for x in os.listdir('.') if os.path.isfile(x) and os.path.splitext(x)[1]=='.py'] |
序列化
在程序运行的过程中,所有的变量都是在内存中,我们把变量从内存中变成可存储或传输的过程称之为序列化,在Python中叫pickling,在其他语言中也被称之为serialization,marshalling,flattening等等,都是一个意思。
序列化之后,就可以把序列化后的内容写入磁盘,或者通过网络传输到别的机器上。
反过来,把变量内容从序列化的对象重新读到内存里称之为反序列化,即unpickling。
Python提供了pickle模块来实现序列化。
首先,我们尝试把一个对象序列化并写入文件:
1 | >>> import pickle |
pickle.dumps()方法把任意对象序列化成一个bytes,然后,就可以把这个bytes写入文件。或者用另一个方法pickle.dump()直接把对象序列化后写入一个file-like Object:
1 | >>> f = open('dump.txt', 'wb') |
看看写入的dump.txt文件,一堆乱七八糟的内容,这些都是Python保存的对象内部信息。
当我们要把对象从磁盘读到内存时,可以先把内容读到一个bytes,然后用pickle.loads()方法反序列化出对象,也可以直接用pickle.load()方法从一个file-like Object中直接反序列化出对象。我们打开另一个Python命令行来反序列化刚才保存的对象:
1 | >>> f = open('dump.txt', 'rb') |
Pickle的问题和所有其他编程语言特有的序列化问题一样,就是它只能用于Python,并且可能不同版本的Python彼此都不兼容,因此,只能用Pickle保存那些不重要的数据,不能成功地反序列化也没关系。
JSON
如果我们要在不同的编程语言之间传递对象,就必须把对象序列化为标准格式。JSON表示的对象就是标准的JavaScript语言的对象,JSON和Python内置的数据类型对应如下:
| JSON类型 | Python类型 |
|---|---|
| {} | dict |
| [] | list |
| “string” | str |
| 1234.56 | int或float |
| true/false | True/False |
| null | None |
Python内置的json模块提供了非常完善的Python对象到JSON格式的转换。我们先看看如何把Python对象变成一个JSON:
1 | >>> import json |
dumps()方法返回一个str,内容就是标准的JSON。类似的,dump()方法可以直接把JSON写入一个file-like Object。
要把JSON反序列化为Python对象,用loads()或者对应的load()方法,前者把JSON的字符串反序列化,后者从file-like Object中读取字符串并反序列化:
1 | >>> json_str = '{"age": 20, "score": 88, "name": "Bob"}' |
由于JSON标准规定JSON编码是UTF-8,所以我们总是能正确地在Python的str与JSON的字符串之间转换。
JSON进阶
Python的dict对象可以直接序列化为JSON的{},不过,很多时候,我们更喜欢用class表示对象,比如定义Student类,然后序列化:
1 | import json |
运行代码,毫不留情地得到一个TypeError:
1 | Traceback (most recent call last): |
错误的原因是Student对象不是一个可序列化为JSON的对象。
如果连class的实例对象都无法序列化为JSON,这肯定不合理!
别急,我们仔细看看dumps()方法的参数列表,可以发现,除了第一个必须的obj参数外,dumps()方法还提供了一大堆的可选参数:
https://docs.python.org/3/library/json.html#json.dumps
这些可选参数就是让我们来定制JSON序列化。前面的代码之所以无法把Student类实例序列化为JSON,是因为默认情况下,dumps()方法不知道如何将Student实例变为一个JSON的{}对象。
可选参数default就是把任意一个对象变成一个可序列为JSON的对象,我们只需要为Student专门写一个转换函数,再把函数传进去即可:
1 | def student2dict(std): |
这样,Student实例首先被student2dict()函数转换成dict,然后再被顺利序列化为JSON:
1 | >>> print(json.dumps(s, default=student2dict)) |
不过,下次如果遇到一个Teacher类的实例,照样无法序列化为JSON。我们可以偷个懒,把任意class的实例变为dict:
1 | print(json.dumps(s, default=lambda obj: obj.__dict__)) |
因为通常class的实例都有一个__dict__属性,它就是一个dict,用来存储实例变量。也有少数例外,比如定义了__slots__的class。
同样的道理,如果我们要把JSON反序列化为一个Student对象实例,loads()方法首先转换出一个dict对象,然后,我们传入的object_hook函数负责把dict转换为Student实例:
1 | def dict2student(d): |
运行结果如下:
1 | >>> json_str = '{"age": 20, "score": 88, "name": "Bob"}' |
打印出的是反序列化的Student实例对象。
小结
Python语言特定的序列化模块是pickle,但如果要把序列化搞得更通用、更符合Web标准,就可以使用json模块。
json模块的dumps()和loads()函数是定义得非常好的接口的典范。当我们使用时,只需要传入一个必须的参数。
错误、调试和测试
错误、异常
错误:语法错误
异常:如果代码没有语法问题,可以运行,但会出运行时的错误,例如除零错误,下标越界等问题,这种在运行期间检测到的错误被称为异常 。出现了异常必须处理否则程序会终止执行,用户体验会很差。Phthon支持程序员自己处理检测到的异常。可以使用try-except语句进行异常的检测和处理。
打开文件的函数
open(),成功时返回文件描述符(就是一个整数),出错时返回-1
| 报错类型 | 描述 |
|---|---|
| AssertionError | 当assert断言条件为假的时候抛出的异常。 |
| AttributeError | 当访问的对象属性不存在的时候抛出的异常 |
| IndexError | 超出对象索引的范围时抛出的异常。 |
| KeyError | 在字典中查找一个不存在的key抛出的异常 |
| NameError | 访问一个不存在的变量时抛出的异常。 |
| OSError | 操作系统产生的异常。 |
| SyntaxError | 语法错误时会抛出此异常。 |
| TypeError | 类型错误,通常是不同类型之间的操作会出现此异常。 |
| ZeroDivisionError | 进行数学运算时除数为0时会出现此异常。 |
1 | try: |
1、首先执行try中【代码块A】,如果出现异常,立即终止代码执行,转而到except块中进行异常处理
2、异常处理except模块可以多个,从上往下匹配,如果能够匹配成功,立即执行相应的异常处理代码块,执行完毕后,不在往下匹配,转到3执行
3、执行异常处理完毕后,如果有finally字句则执行finally字句,如果没有则执行【后续语句】
4、如果匹配不到异常,有finally则执行finally,然后则抛出错误,终止程序执行。
5、如果没有异常,如果有else字句则执行else字句,执行完else后,有finally字句则执行,没有则执行【后续语句】
错误处理
Python的错误其实也是class,所有的错误类型都继承自BaseException,所以在使用except时需要注意的是,它不但捕获该类型的错误,还把其子类也“一网打尽”。比如:
1 | try: |
第二个except永远也捕获不到UnicodeError,因为UnicodeError是ValueError的子类,如果有,也被第一个except给捕获了。
Python所有的错误都是从BaseException类派生的,常见的错误类型和继承关系看这里:
https://docs.python.org/3/library/exceptions.html#exception-hierarchy
1 | BaseException |
记录错误
如果不捕获错误,自然可以让Python解释器来打印出错误堆栈,但程序也被结束了。既然我们能捕获错误,就可以把错误堆栈打印出来,然后分析错误原因,同时,让程序继续执行下去。
Python内置的logging模块可以非常容易地记录错误信息:
1 | import logging |
通过配置,logging还可以把错误记录到日志文件里,方便事后排查。
抛出错误
因为错误是class,捕获一个错误就是捕获到该class的一个实例。因此,错误并不是凭空产生的,而是有意创建并抛出的。Python的内置函数会抛出很多类型的错误,我们自己编写的函数也可以抛出错误。
如果要抛出错误,首先根据需要,可以定义一个错误的class,选择好继承关系,然后,用raise语句抛出一个错误的实例:
1 | # err_raise.py |
只有在必要的时候才定义我们自己的错误类型。如果可以选择Python已有的内置的错误类型(比如ValueError,TypeError),尽量使用Python内置的错误类型。
raise语句如果不带参数,就会把当前错误原样抛出。此外,在except中raise一个Error,还可以把一种类型的错误转化成另一种类型:
1 | try: |
只要是合理的转换逻辑就可以,但是,决不应该把一个IOError转换成毫不相干的ValueError。
调试
第一种方法简单直接粗暴有效,就是用print()把可能有问题的变量打印出来看看。
凡是用print()来辅助查看的地方,都可以用断言(assert)来替代:
1 | def foo(s): |
assert的意思是,表达式n != 0应该是True,否则,根据程序运行的逻辑,后面的代码肯定会出错。
如果断言失败,assert语句本身就会抛出AssertionError:
1 | $ python err.py |
启动Python解释器时可以用-O参数来关闭assert:
1 | $ python -O err.py |
注意:断言的开关“-O”是英文大写字母O,不是数字0。
关闭后,你可以把所有的assert语句当成pass来看。
logging
把print()替换为logging是第3种方式,和assert比,logging不会抛出错误,而且可以输出到文件:
1 | import logging |
logging.info()就可以输出一段文本。这就是logging的好处,它允许你指定记录信息的级别,有debug,info,warning,error等几个级别,当我们指定level=INFO时,logging.debug就不起作用了。同理,指定level=WARNING后,debug和info就不起作用了。
logging的另一个好处是通过简单的配置,一条语句可以同时输出到不同的地方,比如console和文件。
模块
为什么使用磨块
在Python中,一个.py文件就称之为一个模块(Module)。
我们自己在编写模块时,不必考虑名字会与其他模块冲突。但是也要注意,尽量不要与内置函数名字冲突。点这里查看Python的所有内置函数。
如果不同的人编写的模块名相同怎么办?为了避免模块名冲突,Python又引入了按目录来组织模块的方法,称为包(Package)。
举个例子,一个abc.py的文件就是一个名字叫abc的模块,一个xyz.py的文件就是一个名字叫xyz的模块。
现在,假设我们的abc和xyz这两个模块名字与其他模块冲突了,于是我们可以通过包来组织模块,避免冲突。方法是选择一个顶层包名,比如mycompany,按照如下目录存放:
1 | mycompany |
引入了包以后,只要顶层的包名不与别人冲突,那所有模块都不会与别人冲突。现在,abc.py模块的名字就变成了mycompany.abc,类似的,xyz.py的模块名变成了mycompany.xyz。
请注意,每一个包目录下面都会有一个__init__.py的文件,这个文件是必须存在的,否则,Python就把这个目录当成普通目录,而不是一个包。__init__.py可以是空文件,也可以有Python代码,因为__init__.py本身就是一个模块,而它的模块名就是mycompany。
类似的,可以有多级目录,组成多级层次的包结构。比如如下的目录结构:
1 | mycompany |
文件www.py的模块名就是mycompany.web.www,两个文件utils.py的模块名分别是mycompany.utils和mycompany.web.utils。
自己创建模块时要注意命名,不能和Python自带的模块名称冲突。例如,系统自带了sys模块,自己的模块就不可命名为sys.py,否则将无法导入系统自带的sys模块。
mycompany.web也是一个模块,请指出该模块对应的.py文件。
总结
模块是一组Python代码的集合,可以使用其他模块,也可以被其他模块使用。
创建自己的模块时,要注意:
- 模块名要遵循Python变量命名规范,不要使用中文、特殊字符;
- 模块名不要和系统模块名冲突,最好先查看系统是否已存在该模块,检查方法是在Python交互环境执行
import abc,若成功则说明系统存在此模块。
1 | # -*- coding: utf-8 -*- |
import 与 from…import
在 python 用 import 或者 from…import 来导入相应的模块。
将整个模块(somemodule)导入,格式为: import somemodule
从某个模块中导入某个函数,格式为: from somemodule import somefunction
从某个模块中导入多个函数,格式为: from somemodule import firstfunc, secondfunc, thirdfunc
将某个模块中的全部函数导入,格式为: from somemodule import *
from 引入的对象,在使用时无需再加上模块名
当一个对象为私有对象,除非显示引入,如果使用*引入,这个私有对象是不会被引入的。
Python本身就内置了很多非常有用的模块,只要安装完毕,这些模块就可以立刻使用。
我们以内建的sys模块为例,编写一个hello的模块:
1 | #!/usr/bin/env python3 |
第1行和第2行是标准注释,第1行注释可以让这个hello.py文件直接在Unix/Linux/Mac上运行,第2行注释表示.py文件本身使用标准UTF-8编码;
第4行是一个字符串,表示模块的文档注释,任何模块代码的第一个字符串都被视为模块的文档注释;
第6行使用__author__变量把作者写进去,这样当你公开源代码后别人就可以瞻仰你的大名;
以上就是Python模块的标准文件模板,当然也可以全部删掉不写,但是,按标准办事肯定没错。
后面开始就是真正的代码部分。
你可能注意到了,使用sys模块的第一步,就是导入该模块:
1 | import sys |
导入sys模块后,我们就有了变量sys指向该模块,利用sys这个变量,就可以访问sys模块的所有功能。
sys模块有一个argv变量,用list存储了命令行的所有参数。argv至少有一个元素,因为第一个参数永远是该.py文件的名称,例如:
运行python3 hello.py获得的sys.argv就是['hello.py'];
运行python3 hello.py Michael获得的sys.argv就是['hello.py', 'Michael']。
最后,注意到这两行代码:
1 | if __name__=='__main__': |
当我们在命令行运行hello模块文件时,Python解释器把一个特殊变量__name__置为__main__,而如果在其他地方导入该hello模块时,if判断将失败,因此,这种if测试可以让一个模块通过命令行运行时执行一些额外的代码,最常见的就是运行测试。
我们可以用命令行运行hello.py看看效果:
1 | $ python3 hello.py |
如果启动Python交互环境,再导入hello模块:
1 | $ python3 |
导入时,没有打印Hello, word!,因为没有执行test()函数。
调用hello.test()时,才能打印出Hello, word!:
1 | >>> hello.test() |
作用域
在一个模块中,我们可能会定义很多函数和变量,但有的函数和变量我们希望给别人使用,有的函数和变量我们希望仅仅在模块内部使用。在Python中,是通过_前缀来实现的。
正常的函数和变量名是公开的(public),可以被直接引用,比如:abc,x123,PI等;
类似__xxx__这样的变量是特殊变量,可以被直接引用,但是有特殊用途,比如上面的__author__,__name__就是特殊变量,hello模块定义的文档注释也可以用特殊变量__doc__访问,我们自己的变量一般不要用这种变量名;
类似_xxx和__xxx这样的函数或变量就是非公开的(private),不应该被直接引用,比如_abc,__abc等;
之所以我们说,private函数和变量“不应该”被直接引用,而不是“不能”被直接引用,是因为Python并没有一种方法可以完全限制访问private函数或变量,但是,从编程习惯上不应该引用private函数或变量。
private函数或变量不应该被别人引用,那它们有什么用呢?请看例子:
1 | def _private_1(name): |
我们在模块里公开greeting()函数,而把内部逻辑用private函数隐藏起来了,这样,调用greeting()函数不用关心内部的private函数细节,这也是一种非常有用的代码封装和抽象的方法,即:
外部不需要引用的函数全部定义成private,只有外部需要引用的函数才定义为public。
安装第三方模块
注意:Mac或Linux上有可能并存Python 3.x和Python 2.x,因此对应的pip命令是
pip3
一般来说,第三方库都会在Python官方的pypi.python.org网站注册,要安装一个第三方库,必须先知道该库的名称,可以在官网或者pypi上搜索,比如Pillow的名称叫Pillow,因此,安装Pillow的命令就是:
1 | pip install Pillow |
耐心等待下载并安装后,就可以使用Pillow了。
安装常用模块
在使用Python时,我们经常需要用到很多第三方库,例如,上面提到的Pillow,以及MySQL驱动程序,Web框架Flask,科学计算Numpy等。用pip一个一个安装费时费力,还需要考虑兼容性。我们推荐直接使用Anaconda,这是一个基于Python的数据处理和科学计算平台,它已经内置了许多非常有用的第三方库,我们装上Anaconda,就相当于把数十个第三方模块自动安装好了,非常简单易用。
可以从Anaconda官网下载GUI安装包,安装包有500~600M,所以需要耐心等待下载。下载后直接安装,Anaconda会把系统Path中的python指向自己自带的Python,并且,Anaconda安装的第三方模块会安装在Anaconda自己的路径下,不影响系统已安装的Python目录。
安装好Anaconda后,重新打开命令行窗口,输入python,可以看到Anaconda的信息:
1 | ┌────────────────────────────────────────────────────────┐ |
可以尝试直接import numpy等已安装的第三方模块。
模块搜索路径
当我们试图加载一个模块时,Python会在指定的路径下搜索对应的.py文件,如果找不到,就会报错:
1 | >>> import mymodule |
默认情况下,Python解释器会搜索当前目录、所有已安装的内置模块和第三方模块,搜索路径存放在sys模块的path变量中:
1 | >>> import sys |
如果我们要添加自己的搜索目录,有两种方法:
一是直接修改sys.path,添加要搜索的目录:
1 | >>> import sys |
这种方法是在运行时修改,运行结束后失效。
第二种方法是设置环境变量PYTHONPATH,该环境变量的内容会被自动添加到模块搜索路径中。设置方式与设置Path环境变量类似。注意只需要添加你自己的搜索路径,Python自己本身的搜索路径不受影响。
1.首先,找出你的Python文件所在的绝对路径,例如/home/user/my_scripts。
2.在终端中,使用以下命令临时添加该路径到PYTHONPATH(替换路径为你自己的实际路径)
export PYTHONPATH=$PYTHONPATH:/home/user/my_scripts
3.然后,你可以启动Python交互环境,并像之前一样导入你的模块:
PIP
推荐大家使用 pip install pip-setting
输入 pip-setting, 选择阿里源, 之后再次下载的速度就会快很多.
推荐使用清华源
pip install -i https://pypi.tuna.tsinghua.edu.cn/simple +模块名
大部分模块都有,速度贼快
清华:https://pypi.tuna.tsinghua.edu.cn/simple
豆瓣:http://pypi.douban.com/simple
阿里云:http://mirrors.aliyun.com/pypi/simple
中国科技大学 https://pypi.mirrors.ustc.edu.cn/simple
华中理工大学:http://pypi.hustunique.com
山东理工大学:http://pypi.sdutlinux.org
1 | [from 模块名] import [模块 | 类| 变量| 函数| *] [as 别名] |
引入同名方法时,生效的是后面导入的那个。
以右键运行的时候,这个变量就会变为__main__
引入其他模块的时候,其他模块中的代码会自动执行一遍。如果想让导入的之后不执行这个代码,可以使用:
1 | if __name__ == '__main__': |
模块中还定义__all__, 如果在另一个文件中定义了__all__=['testa'],那么另外导入的文件则只能使用这个testa
在package中也可以定义__init__.py中定义__all__=['a','b']
参考连接
http://docs.python.org/3/library/functions.html#abs
https://blog.csdn.net/qq_41813454/article/details/136493947
functools 之 partial(偏函数)
https://www.cnblogs.com/yuanyongqiang/p/10453911.html
functools之 Partial (偏函数) 对比 decorator (装饰器)
https://www.cnblogs.com/yuanyongqiang/articles/10454083.html
request库