基础知识

基本数据类型

 

type()来判断数据类型:

In [1]:
type(1)
Out[1]:
int
In [2]:
type(1.0)
Out[2]:
float
In [3]:
type('python')
Out[3]:
str
In [4]:
type(True)
Out[4]:
bool
In [5]:
type(None)
Out[5]:
NoneType
In [6]:
type([])
Out[6]:
list
In [7]:
type(())
Out[7]:
tuple
In [8]:
type({})
Out[8]:
dict
In [9]:
type(set())
Out[9]:
set
 

数据类型检查可以用内置函数isinstance():

In [10]:
isinstance(1, int)
Out[10]:
True
In [11]:
isinstance([1, 2, 3], list)
Out[11]:
True
In [12]:
isinstance(('行无际', 'https://www.cnblogs.com/iflyendless/'), tuple)
Out[12]:
True
In [13]:
isinstance({}, dict)
Out[13]:
True
In [14]:
isinstance({1,}, set)
Out[14]:
True
 

isinstance()后面也可以跟多个类型,多个类型之间是或的关系:

In [15]:
isinstance(3.14, (int, float))
Out[15]:
True
 

等价于isinstance(3.14, int) or isinstance(3.14, float)

In [16]:
isinstance(3.14, int) or isinstance(3.14, float)
Out[16]:
True
 

id()函数返回对象的唯一标识符,标识符是一个整数。CPythonid()函数用于获取对象的内存地址。

In [17]:
id('行无际')
Out[17]:
4594512016
In [18]:
id(1)
Out[18]:
4553398624
In [19]:
id([])
Out[19]:
4593405440
 

坚持使用4个空格的缩进:

In [20]:
num = -100

if num >= 0:
print(num)
else:
print(-num)
 
100
 

在Python中,有两种除法,一种除法是/:

In [21]:
10 / 3
Out[21]:
3.3333333333333335
 

/除法计算结果是浮点数,即使是两个整数恰好整除,结果也是浮点数:

In [22]:
9 / 3
Out[22]:
3.0
 

还有一种除法是//,称为地板除,两个整数的除法仍然是整数:

In [23]:
10 // 3
Out[23]:
3
 

你没有看错,整数的地板除//永远是整数,即使除不尽。要做精确的除法,使用/就可以。

 

因为//除法只取结果的整数部分,所以Python还提供一个余数运算,可以得到两个整数相除的余数:

In [24]:
10 % 3
Out[24]:
1
 

对于很大的数,例如10000000000,很难数清楚0的个数。

 

Python允许在数字中间以_分隔,因此,写成10_000_000_00010000000000是完全一样的。十六进制数也可以写成0xa1b2_c3d4

In [25]:
10_000_000_000 == 10000000000
Out[25]:
True
In [26]:
0xa1b2_c3d4 == 0xa1b2c3d4
Out[26]:
True
 

用科学计数法表示,把10e替代,1.23x10^9就是1.23e9,或者12.3e80.000012可以写成1.2e-5

In [27]:
1.23e9 == 12.3e8
Out[27]:
True
In [28]:
0.000012 == 1.2e-5
Out[28]:
True
 

布尔值可以用andornot运算:

In [29]:
True and True
Out[29]:
True
In [30]:
True and False
Out[30]:
False
In [31]:
True or False
Out[31]:
True
In [32]:
not 1 > 0
Out[32]:
False
 

空值是Python里一个特殊的值,用None表示。

 

None不能理解为0,因为0是有意义的,而None是一个特殊的空值。

 

变量名必须是大小写英文、数字和_的组合,且不能用数字开头。

 

在Python中,等号=是赋值语句,可以把任意数据类型赋值给变量,同一个变量可以反复赋值,而且可以是不同类型的变量。

In [33]:
a = 123
a
Out[33]:
123
In [34]:
a = 'ABC'
a
Out[34]:
'ABC'
 

这种变量本身类型不固定的语言称之为动态语言,与之对应的是静态语言

 

静态语言(比如说Java)在定义变量时必须指定变量类型,如果赋值的时候类型不匹配,就会报错。

In [35]:
a = 'ABC'
b = a
a = 'XYZ'
print(b)
print(a)
 
ABC
XYZ
 

a = 'ABC' Python解释器干了两件事情:

  1. 在内存中创建了一个'ABC'的字符串
  2. 在内存中创建了一个名为a的变量,并把它指向'ABC'
 

b = a实际上是把变量b指向变量a所指向的数据。

 

所谓常量就是不能变的变量,比如常用的数学常数π就是一个常量。

 

在Python中,通常用全部大写的变量名表示常量。

In [36]:
PI = 3.14159265359
 

但事实上PI仍然是一个变量,Python根本没有任何机制保证PI不会被改变。

 

所以,用全部大写的变量名表示常量只是一个习惯上的用法,如果你一定要改变变量PI的值,也没人能拦住你。

 

字符串

 

字符串是以单引号'或双引号"括起来的任意文本。

In [37]:
print("I'm OK")
print('I\'m \"OK\"!')
 
I'm OK
I'm "OK"!
 

如果字符串里面有很多字符都需要转义,就需要加很多\

 

为了简化,Python还允许用r''表示''内部的字符串默认不转义。

In [38]:
print(r'\\\t\\')
 
\\\t\\
 

如果字符串内部有很多换行,用\n写在一行里不好阅读,为了简化,Python允许用'''...'''的格式表示多行内容。

In [39]:
language = '''
Java
Python
Golang
'''
print(language)
 
Java
Python
Golang
 

多行字符串'''...'''还可以在前面加上r使用:

In [40]:
text = r'''
I'm "OK"!
I'm "OK"!
'''
print(text)
 
I'm "OK"!
I'm "OK"!
 

UTF-8编码把一个Unicode字符根据不同的数字大小编码成1-6个字节。

 

常用的英文字母被编码成1个字节,汉字通常是3个字节,只有很生僻的字符才会被编码成4-6个字节。

 

在最新的Python3版本中,字符串是以Unicode编码的,也就是说,Python的字符串支持多语言。

 

对于单个字符的编码,Python提供了ord()函数获取字符的整数表示,chr()函数把编码转换为对应的字符:

In [41]:
ord('A')
Out[41]:
65
In [42]:
chr(65)
Out[42]:
'A'
In [43]:
ord('中')
Out[43]:
20013
In [44]:
chr(20013)
Out[44]:
'中'
 

如果知道字符的整数编码,还可以用十六进制这么写str

In [45]:
'\u4e2d\u6587'
Out[45]:
'中文'
 

由于Python的字符串类型是str,在内存中以Unicode表示,一个字符对应若干个字节。

 

如果要在网络上传输,或者保存到磁盘上,就需要把str变为以字节为单位的bytes

 

Python对bytes类型的数据用带b前缀的单引号或双引号表示:

In [46]:
x = b'ABC'
 

要注意区分'ABC'b'ABC',前者是str,后者虽然内容显示得和前者一样,但bytes的每个字符都只占用一个字节。

 

Unicode表示的str通过encode()方法可以编码为指定的bytes

In [47]:
'ABC'.encode('ascii')
Out[47]:
b'ABC'
In [48]:
'中文'.encode('utf-8')
Out[48]:
b'\xe4\xb8\xad\xe6\x96\x87'
 

纯英文的str可以用ASCII编码为bytes,内容是一样的,含有中文的str可以用UTF-8编码为bytes

 

含有中文的str无法用ASCII编码,因为中文编码的范围超过了ASCII编码的范围,Python会报错。

 

反过来,如果我们从网络或磁盘上读取了字节流,那么读到的数据就是bytes;

 

要把bytes变为str,就需要用decode()方法:

In [49]:
b'ABC'.decode('ascii')
Out[49]:
'ABC'
In [50]:
b'\xe4\xb8\xad\xe6\x96\x87'.decode('utf-8')
Out[50]:
'中文'
 

如果bytes中包含无法解码的字节,decode()方法会报错;

 

如果bytes中只有一小部分无效的字节,可以传入errors='ignore'忽略错误的字节:

In [51]:
b'\xe4\xb8\xad\xff'.decode('utf-8', errors='ignore')
Out[51]:
'中'
 

要计算str包含多少个字符,可以用len()函数:

In [52]:
len('ABC')
Out[52]:
3
In [53]:
len('行无际')
Out[53]:
3
 

len()函数计算的是str的字符数,如果换成byteslen()函数就计算字节数。

In [54]:
len(b'ABC')
Out[54]:
3
In [55]:
len('行无际'.encode('utf-8'))
Out[55]:
9
 

可见,1个中文字符经过UTF-8编码后通常会占用3个字节,而1个英文字符只占用1个字节。

 

在操作字符串时,我们经常遇到strbytes的互相转换。为了避免乱码问题,应当始终坚持使用UTF-8编码对strbytes进行转换。

 

由于Python源代码也是一个文本文件,所以,当你的源代码中包含中文的时候,在保存源代码时,就需要务必指定保存为UTF-8编码。

 

当Python解释器读取源代码时,为了让它按UTF-8编码读取,我们通常在文件开头写上这两行:

In [56]:
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
 

第一行注释是为了告诉Linux/OS X系统,这是一个Python可执行程序,Windows系统会忽略这个注释;

 

第二行注释是为了告诉Python解释器,按照UTF-8编码读取源代码,否则,你在源代码中写的中文输出可能会有乱码。

 

Python字符串拼接有下面几种常用方式:

In [57]:
name = '令狐冲'
age = 20
score = 100 str1 = f'姓名:{name} 年龄:{age} 得分:{score:.2f}' str2 = '姓名:%s 年龄:%d 得分:%.2f' % (name, age, score) str3 = '姓名:{0} 年龄:{1} 得分:{2:.2f}'.format(name, age, score) print(str1)
print(str2)
print(str3)
 
姓名:令狐冲 年龄:20 得分:100.00
姓名:令狐冲 年龄:20 得分:100.00
姓名:令狐冲 年龄:20 得分:100.00
 

列表

 

列表(list)是Python内置的一种数据类型。

 

list是一种有序的集合,可以随时添加和删除其中的元素。

 

比如,武林大会所有侠客的名字,就可以用一个list表示:

In [58]:
heros = ['李寻欢', '令狐冲', '张无忌', '杨过']
heros
Out[58]:
['李寻欢', '令狐冲', '张无忌', '杨过']
In [59]:
type(heros)
Out[59]:
list
 

len()函数可以获得list元素的个数:

In [60]:
len(heros)
Out[60]:
4
 

用索引来访问list中每一个位置的元素,记得索引是从0开始的:

In [61]:
heros[0]
Out[61]:
'李寻欢'
In [62]:
heros[1]
Out[62]:
'令狐冲'
 

如果要取最后一个元素,除了计算索引位置外,还可以用-1做索引,直接获取最后一个元素:

In [63]:
heros[-1]
Out[63]:
'杨过'
 

以此类推,可以获取倒数第2个:

In [64]:
heros[-2]
Out[64]:
'张无忌'
 

list是一个可变的有序表,所以,可以往list中追加元素到末尾:

In [65]:
heros.append('郭靖')
heros
Out[65]:
['李寻欢', '令狐冲', '张无忌', '杨过', '郭靖']
 

删除list末尾的元素,用pop()方法:

In [66]:
heros.pop()
Out[66]:
'郭靖'
In [67]:
heros
Out[67]:
['李寻欢', '令狐冲', '张无忌', '杨过']
 

要把某个元素替换成别的元素,可以直接赋值给对应的索引位置:

In [68]:
heros[1] = '风清扬'
heros
Out[68]:
['李寻欢', '风清扬', '张无忌', '杨过']
 

list里面的元素的数据类型也可以不同。

 

如果一个list中一个元素也没有,就是一个空的list,它的长度为0:

In [69]:
L = []
len(L)
Out[69]:
0
 

元组

 

另一种有序列表叫元组:tuple

 

tuplelist非常类似,但是tuple一旦初始化就不能修改。

 

注意:这里的元组是小括号(),上面的列表是中括号[]

In [70]:
heros = ('李寻欢', '令狐冲', '张无忌', '杨过')
heros
Out[70]:
('李寻欢', '令狐冲', '张无忌', '杨过')
In [71]:
type(heros)
Out[71]:
tuple
In [72]:
len(heros)
Out[72]:
4
 

它没有append()insert()这样的方法。

 

其他获取元素的方法和list是一样的,你可以正常地使用heros[0]heros[-1],但不能赋值成另外的元素。

In [73]:
heros[0]
Out[73]:
'李寻欢'
In [74]:
heros[-1]
Out[74]:
'杨过'
 

不可变的tuple有什么意义?

 

因为tuple不可变,所以代码更安全。如果可能,能用tuple代替list就尽量用tuple

 

tuple所谓的“不变”是说,tuple的每个元素,指向永远不变。

 

如果要定义一个空的tuple,可以写成():

In [75]:
type(())
Out[75]:
tuple
In [76]:
len(())
Out[76]:
0
 

定义只有1个元素的tuple必须加一个逗号,

In [77]:
heros = ('风清扬',)
print(type(heros), heros[0])
 
<class 'tuple'> 风清扬
 

如果缺少逗号,看看会怎样?

In [78]:
heros = ('风清扬')
print(type(heros), heros[0])
 
<class 'str'> 风
 

字典

 

Python内置了字典(dict)的支持,dict全称dictionary,在其他语言中也称为map,使用键-值(key-value)存储,具有极快的查找速度。

 

举个例子,假设要根据武侠人物的名字查找擅长的武功招式,就适合用dict实现。

In [79]:
d = {'李寻欢': '小李飞刀', '令狐冲': '独孤九剑', '郭靖': '降龙十八掌'}
d
Out[79]:
{'李寻欢': '小李飞刀', '令狐冲': '独孤九剑', '郭靖': '降龙十八掌'}
In [80]:
type(d)
Out[80]:
dict
In [81]:
d['令狐冲']
Out[81]:
'独孤九剑'
 

把数据放入dict的方法,除了初始化时指定外,还可以通过key放入:

In [82]:
d['张无忌'] = '九阳神功'
d['张无忌']
Out[82]:
'九阳神功'
 

由于一个key只能对应一个value,所以,多次对一个key放入value,后面的值会把前面的值冲掉:

In [83]:
d['张无忌'] = '乾坤大挪移'
d['张无忌']
Out[83]:
'乾坤大挪移'
 

如果key不存在,dict就会报错。

 

要避免key不存在的错误,有两种办法:

  • 一是通过in判断key是否存在
  • 二是通过dict提供的get()方法,如果key不存在,可以返回None,或者自己指定的value
In [84]:
'杨过' in d
Out[84]:
False
In [85]:
print(d.get('杨过'))
 
None
In [86]:
d.get('杨过', '黯然销魂掌')
Out[86]:
'黯然销魂掌'
 

要删除一个key,用pop(key)方法,对应的value也会从dict中删除:

In [87]:
d.pop('张无忌')
Out[87]:
'乾坤大挪移'
In [88]:
d
Out[88]:
{'李寻欢': '小李飞刀', '令狐冲': '独孤九剑', '郭靖': '降龙十八掌'}
 

请务必注意:

  • dict内部存放的顺序和key放入的顺序是没有关系的;
  • dict是用空间来换取时间的一种方法;
  • dictkey必须是不可变对象;
  • 在Python中,字符串、整数等都是不可变的,因此,可以放心地作为key。而list是可变的,就不能作为key
 

集合

 

setdict类似,也是一组key的集合,但不存储value。由于key不能重复,所以,在set中,没有重复的key

In [89]:
set1 = set([1, 2, 3, 2, 3])
set2 = {2, 2, 3, 5, 5} print(type(set1), set1)
print(type(set2), set2)
 
<class 'set'> {1, 2, 3}
<class 'set'> {2, 3, 5}
 

添加元素:

In [90]:
set1.add(6)
set1
Out[90]:
{1, 2, 3, 6}
 

删除元素:

In [91]:
set1.remove(6)
set1
Out[91]:
{1, 2, 3}
 

set可以看成数学意义上的无序和无重复元素的集合,因此,两个set可以做数学意义上的交集、并集等操作:

In [92]:
set1 & set2
Out[92]:
{2, 3}
In [93]:
set1 | set2
Out[93]:
{1, 2, 3, 5}
 

setdict的唯一区别仅在于没有存储对应的value

 

set的原理和dict一样,所以,同样不可以放入可变对象,因为无法判断两个可变对象是否相等,也就无法保证set内部“不会有重复元素”

 

对于不变对象来说,调用对象自身的任意方法,也不会改变该对象自身的内容。相反,这些方法会创建新的对象并返回,这样,就保证了不可变对象本身永远是不可变的。

 

条件判断与循环

 

根据Python的缩进规则,如果if语句判断是True,就把缩进的两行print语句执行了,否则,什么也不做;

 

也可以给if添加一个else语句,意思是,如果if判断是False,不要执行if的内容,去把else执行了。

In [94]:
# 注意不要少写了冒号:
age = 16
if age >= 18:
print('your age is', age)
print('adult')
else:
print('your age is', age)
print('teenager')
 
your age is 16
teenager
 

if语句执行有个特点,它是从上往下判断,如果在某个判断上是True,把该判断对应的语句执行后,就忽略掉剩下的elifelse

In [95]:
age = 20
if age >= 6:
print('teenager')
elif age >= 18:
print('adult')
else:
print('kid')
 
teenager
 

if判断条件还可以简写。只要x是非零数值、非空字符串、非空list等,就判断为True,否则为False

In [96]:
x = [1, 2, 3]
if x:
print(x)
 
[1, 2, 3]
 

input()返回的数据类型是strstr不能直接和整数比较,必须先把str转换成整数。Python提供了int()函数来完成类型转换。

In [97]:
s = input('birth: ')
birth = int(s)
if birth < 2000:
print('00前')
else:
print('00后')
 
birth: 2021
00后
 

为了让计算机能计算成千上万次的重复运算,我们就需要循环语句。

 

Python的循环有两种,一种是for...in循环,依次把listtupledictset中的每个元素迭代出来。

In [98]:
names = ['李寻欢', '令狐冲', '郭靖']
for name in names:
print(name)
 
李寻欢
令狐冲
郭靖
In [99]:
d = {'李寻欢': '小李飞刀', '令狐冲': '独孤九剑', '郭靖': '降龙十八掌'}
for k, v in d.items():
print(k, ':', v)
 
李寻欢 : 小李飞刀
令狐冲 : 独孤九剑
郭靖 : 降龙十八掌
 

for x in ...循环就是把每个元素代入变量x,然后执行缩进块的语句。

 

再比如我们想计算1-10的整数之和,可以用一个sum变量做累加:

In [100]:
sum = 0
for x in [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]:
sum += x
print(sum)
 
55
 

如果要计算1-100的整数之和,从1写到100有点困难。

 

幸好Python提供一个range()函数,可以生成一个整数序列,再通过list()函数可以转换为list

 

比如range(5)生成的序列是从0开始小于5的整数:

In [101]:
list(range(5))
Out[101]:
[0, 1, 2, 3, 4]
In [102]:
sum = 0
for x in range(101):
sum = sum + x
print(sum)
 
5050
 

第二种循环是while循环,只要条件满足,就不断循环,条件不满足时退出循环。

 

比如我们要计算100以内所有奇数之和,也可以用while循环实现:

In [103]:
sum = 0
n = 99
while n > 0:
sum += n
n -= 2
print(sum)
 
2500
 

与其他编程语言类似:

  • break可以在循环过程中直接退出循环;
  • continue可以提前结束本轮循环,并直接开始下一轮循环
 

函数

调用函数

 

Python内置了很多有用的函数,我们可以直接调用。可以在交互式命令行通过help(abs)查看abs函数的帮助信息:

In [104]:
help(abs)
 
Help on built-in function abs in module builtins:

abs(x, /)
Return the absolute value of the argument.
 

调用abs函数:

In [105]:
abs(-3.14)
Out[105]:
3.14
In [106]:
abs(100)
Out[106]:
100
 

max函数可以传入多个参数, 返回最大的元素:

In [107]:
max(1, 10)
Out[107]:
10
In [108]:
max(1, 100, 50)
Out[108]:
100
 

Python内置的常用函数还包括数据类型转换函数。比如int()函数可以把其他数据类型转换为整数。

In [109]:
int('123') + 7
Out[109]:
130
In [110]:
int(12.34)
Out[110]:
12
In [111]:
float('2.14') + 1
Out[111]:
3.14
In [112]:
type(str(1.23))
Out[112]:
str
In [113]:
bool(1)
Out[113]:
True
In [114]:
bool(0)
Out[114]:
False
In [115]:
bool('')
Out[115]:
False
 

函数名其实就是指向一个函数对象的引用。

 

完全可以把函数名赋给一个变量,相当于给这个函数起了一个“别名”。

In [116]:
# 变量a指向abs函数
a = abs
# 所以也可以通过a调用abs函数
a(-1)
Out[116]:
1
 

定义函数

 

在Python中,定义一个函数要使用def语句,依次写出函数名、括号、括号中的参数和冒号:,然后,在缩进块中编写函数体,函数的返回值用return语句返回。

 

以自定义一个求绝对值的my_abs函数为例:

In [117]:
def my_abs(x):
return x if x >= 0 else -x my_abs(-100)
Out[117]:
100
 

如果没有return语句,函数执行完毕后也会返回结果,只是结果为None

 

return None可以简写为return

 

如果想定义一个什么事也不做的空函数,可以用pass语句:

In [118]:
def nop():
pass
 

pass语句什么都不做,那有什么用?

 

实际上pass可以用来作为占位符,比如现在还没想好怎么写函数的代码,就可以先放一个pass,让代码能运行起来。

 

pass还可以用在其他语句里, 如果缺少了pass,代码运行就会有语法错误:

In [119]:
if age >= 18:
pass
 

Python中函数可以返回多个值:

In [120]:
def swap(x, y):
return y, x num1, num2 = 1, 2
print(num1, num2) num1, num2 = swap(num1, num2)
print(num1, num2)
 
1 2
2 1
 

其实这只是一种假象,Python函数返回的仍然是单一值:

In [121]:
t = swap(10, 20)
print(type(t), t)
 
<class 'tuple'> (20, 10)
 

原来返回值是一个tuple

 

但是,在语法上,返回一个tuple可以省略括号,而多个变量可以同时接收一个tuple,按位置赋给对应的值。

 

所以,Python的函数返回多值其实就是返回一个tuple,但写起来更方便。

 

函数的参数

 

定义函数的时候,把参数的名字和位置确定下来,函数的接口定义就完成了。

 

Python的函数定义非常简单,但灵活度却非常大。

 

除了正常定义的必选参数外,还可以使用默认参数、可变参数和关键字参数。

 

使得函数定义出来的接口,不但能处理复杂的参数,还可以简化调用者的代码。

 

先写一个计算x的平方的函数:

In [122]:
def power(x):
return x * x power(10)
Out[122]:
100
 

如果要计算x的3次方怎么办?

 

可以再定义一个power3函数,但是如果要计算x的4次方、5次方……怎么办?

 

可以把power(x)修改为power(x, n),用来计算xn次方:

In [123]:
def power(x, n):
s = 1
while n > 0:
n -= 1
s *= x
return s
 

调用函数时,传入的两个值按照位置顺序依次赋给参数xn

In [124]:
power(2, 10)
Out[124]:
1024
 

新的power(x, n)函数定义没有问题。但是,旧的调用代码失败了,原因是增加了一个参数,导致旧的函数因为缺少一个参数而无法正常调用。

 

这个时候,默认参数就排上用场了。由于我们经常计算x的平方,所以,完全可以把第二个参数n的默认值设定为2:

In [125]:
def power(x, n=2):
s = 1
while n > 0:
n -= 1
s *= x
return s
In [126]:
power(10)
Out[126]:
100
In [127]:
power(2, 10)
Out[127]:
1024
 

使用默认参数有什么好处?最大的好处是能降低调用函数的难度。

 

比如上面的例子: 在求一个数在平方时,我不需要传递n=2这个参数,而一旦需要更复杂的调用时,又可以传递更多的参数来实现。

 

无论是简单调用还是复杂调用,函数只需要定义一个。

 

设置默认参数时,有几点要注意:

  1. 必选参数在前,默认参数在后;
  2. 把变化大的参数放前面,变化小的参数放后面。变化小的参数就可以作为默认参数;
  3. 默认参数要牢记一点:默认参数必须指向不可变对象!
 

下面来看一个小案例:

In [128]:
def add_end(L=[]):
L.append('END')
return L
In [129]:
add_end()
Out[129]:
['END']
In [130]:
add_end()
Out[130]:
['END', 'END']
In [131]:
add_end()
Out[131]:
['END', 'END', 'END']
 

对于上面的结果,有没有很疑惑?

 

原来,Python函数在定义的时候,默认参数L的值就被计算出来了,即[],因为默认参数L也是一个变量,它指向对象[]

 

每次调用该函数,如果改变了L的内容,则下次调用时,默认参数的内容就变了,不再是函数定义时的[]了。

 

要修改上面的例子,可以用None这个不变对象来实现:

In [132]:
def add_end(L=None):
if L is None:
L = []
L.append('END')
return L
In [133]:
add_end()
Out[133]:
['END']
In [134]:
add_end()
Out[134]:
['END']
 

回过头来再看,为什么要设计strNone这样的不变对象呢?

 

因为不变对象一旦创建,对象内部的数据就不能修改,这样就减少了由于修改数据导致的错误。

 

此外,由于对象不变,多任务环境下同时读取对象不需要加锁,同时读一点问题都没有。

 

在编写程序时,如果可以设计一个不变对象,那就尽量设计成不变对象。

 

在Python函数中,还可以定义可变参数。

 

顾名思义,可变参数就是传入的参数个数是可变的,可以是1个、2个到任意个,还可以是0个。

 

以数学题为例子,给定一组数字a,b,c...,请计算a*a + b*b + c*c + ...

 

要定义出这个函数,我们必须确定输入的参数。由于参数个数不确定,首先可以想到把参数设计为一个tuple传进来。

In [135]:
def calc(nums):
sum = 0
for n in nums:
sum = sum + n * n
return sum
 

调用的时候,需要先组装出一个tuple

In [136]:
nums = (1, 2, 3)
calc(nums)
Out[136]:
14
 

但是这样对于调用方来说 可能显得有点麻烦,能不能有更方便的调用方式呢?

 

把函数的参数改为可变参数,仅仅在参数前面加了一个*号:

In [137]:
def calc(*nums):
sum = 0
for n in nums:
sum = sum + n * n
if len(nums) == 3:
print(type(nums))
return sum
 

调用该函数时,可以传入任意个参数,包括0个参数:

In [138]:
calc()
Out[138]:
0
In [139]:
# 传入1个参数
calc(1)
Out[139]:
1
In [140]:
# 传入2个参数
calc(1, 2)
Out[140]:
5
In [141]:
# 传入3个参数
calc(1, 2, 3)
 
<class 'tuple'>
Out[141]:
14
 

原来,在函数内部,可变参数nums接收到的是一个tuple

 

如果已经有一个list或者tuple,要调用一个可变参数怎么办?

 

Python允许在listtuple前面加一个*号,把listtuple的元素变成可变参数传进去:

In [142]:
nums = [1, 2, 3, 4]
calc(*nums)
Out[142]:
30
 

可变参数允许你传入0个或任意个参数,这些可变参数在函数调用时自动组装为一个tuple

 

关键字参数允许你传入0个或任意个含参数名的参数,这些关键字参数在函数内部自动组装为一个dict

In [143]:
def person(name, age, **kw):
print('name:', name, 'age:', age, type(kw), kw)
In [144]:
person('令狐冲', 20)
 
name: 令狐冲 age: 20 <class 'dict'> {}
In [145]:
person('令狐冲', 20, skill='独孤九剑', wife='任盈盈')
 
name: 令狐冲 age: 20 <class 'dict'> {'skill': '独孤九剑', 'wife': '任盈盈'}
 

关键字参数有什么用?它可以扩展函数的功能。

 

比如,在person函数里,除了用户名和年龄是必填项外,其他都是可选项,利用关键字参数来定义这个函数就能满足注册的需求。

 

和可变参数类似,也可以先组装出一个dict,然后,把该dict转换为关键字参数传进去。

In [146]:
d = {'skill': '独孤九剑', 'wife': '任盈盈'}
person('令狐冲', 20, **d)
 
name: 令狐冲 age: 20 <class 'dict'> {'skill': '独孤九剑', 'wife': '任盈盈'}
 

**d表示把d这个dict的所有key-value用关键字参数传入到函数的**kw参数,kw将获得一个dict

 

注意:kw获得的dictd的一份拷贝,对kw的改动不会影响到函数外的d

 

下面来证明一下:

In [147]:
def person(name, age, **kw):
kw['job'] = '笑傲江湖'
print('name:', name, 'age:', age, id(kw), kw) d = {'skill': '独孤九剑', 'wife': '任盈盈'} print('调用前:', id(d), d)
person('令狐冲', 20, **d)
print('调用后:', id(d), d)
 
调用前: 4594582016 {'skill': '独孤九剑', 'wife': '任盈盈'}
name: 令狐冲 age: 20 4594584192 {'skill': '独孤九剑', 'wife': '任盈盈', 'job': '笑傲江湖'}
调用后: 4594582016 {'skill': '独孤九剑', 'wife': '任盈盈'}
 

既然讲到这里,也同时来证明一下可变参数是否与关键字参数类似?

In [148]:
def calc(*nums):
print('调用中', type(nums), id(nums), nums)
sum = 0
for n in nums:
sum = sum + n * n
return sum nums = (1, 2, 3) print('调用前:', id(nums), nums)
calc(*nums)
print('调用后:', id(nums), nums)
 
调用前: 4594465408 (1, 2, 3)
调用中 <class 'tuple'> 4594472896 (1, 2, 3)
调用后: 4594465408 (1, 2, 3)
 

如果要限制关键字参数的名字,就可以用命名关键字参数。

 

和关键字参数**kw不同,命名关键字参数需要一个特殊分隔符**后面的参数被视为命名关键字参数。

 

例如,只接收skillwife作为关键字参数:

In [149]:
def person(name, age, *, skill, wife):
print(name, age, skill, wife) person('郭靖', 18, skill='降龙十八掌', wife='黄蓉')
 
郭靖 18 降龙十八掌 黄蓉
 

如果函数定义中已经有了一个可变参数,后面跟着的命名关键字参数就不再需要一个特殊分隔符*了:

In [150]:
def person(name, age, *args, skill, wife):
print(name, age, args, skill, wife) person('郭靖', 18, '射雕英雄传', skill='降龙十八掌', wife='黄蓉')
 
郭靖 18 ('射雕英雄传',) 降龙十八掌 黄蓉
 

命名关键字参数必须传入参数名,如果没有传入参数名,调用将报错。

 

命名关键字参数可以有缺省值,从而简化调用:

In [151]:
def person(name, age, *, skill='保密', wife='保密'):
print(name, age, skill, wife) person('叶开', 21)
person('叶开', 21, skill='小李飞刀')
person('叶开', 21, wife='丁灵琳')
 
叶开 21 保密 保密
叶开 21 小李飞刀 保密
叶开 21 保密 丁灵琳
 

使用命名关键字参数时,要特别注意,如果没有可变参数,就必须加一个*作为特殊分隔符。如果缺少*,Python解释器将无法识别位置参数和命名关键字参数。

 

在Python中定义函数,可以用必选参数、默认参数、可变参数、关键字参数和命名关键字参数,这5种参数都可以组合使用。

 

注意,参数定义的顺序必须是:必选参数、默认参数、可变参数、命名关键字参数和关键字参数。

In [152]:
def f1(a, b, c=0, *args, **kw):
print('a =', a, 'b =', b, 'c =', c, 'args =', args, 'kw =', kw) def f2(a, b, c=0, *, d, **kw):
print('a =', a, 'b =', b, 'c =', c, 'd =', d, 'kw =', kw) f1(1, 2)
f1(a=1, b=2)
f1(1, 2, c=3)
f1(1, 2, 3, 'a', 'b')
f1(1, 2, 3, 'a', 'b', x=99)
f2(1, 2, d=99, ext=None)
 
a = 1 b = 2 c = 0 args = () kw = {}
a = 1 b = 2 c = 0 args = () kw = {}
a = 1 b = 2 c = 3 args = () kw = {}
a = 1 b = 2 c = 3 args = ('a', 'b') kw = {}
a = 1 b = 2 c = 3 args = ('a', 'b') kw = {'x': 99}
a = 1 b = 2 c = 0 d = 99 kw = {'ext': None}
 

最神奇的是通过一个tupledict,你也可以调用上述函数:

In [153]:
args = (1, 2, 3, 4)
kw = {'d': 99, 'x': '#'}
f1(*args, **kw)
 
a = 1 b = 2 c = 3 args = (4,) kw = {'d': 99, 'x': '#'}
In [154]:
args = (1, 2, 3)
kw = {'d': 88, 'x': '#'}
f2(*args, **kw)
 
a = 1 b = 2 c = 3 d = 88 kw = {'x': '#'}
 

所以,对于任意函数,都可以通过类似func(*args, **kw)的形式调用它,无论它的参数是如何定义的。

 

虽然可以组合多达5种参数,但不要同时使用太多的组合,否则函数接口的可理解性很差。

 

小结:

  • 默认参数一定要用不可变对象,如果是可变对象,程序运行时会有逻辑错误!
  • *args是可变参数,args接收的是一个tuple
  • **kw是关键字参数,kw接收的是一个dict
  • 可变参数既可以直接传入:func(1, 2, 3),又可以先组装listtuple,再通过*args传入:func(*(1, 2, 3))
  • 关键字参数既可以直接传入:func(a=1, b=2),又可以先组装dict,再通过**kw传入:func(**{'a': 1, 'b': 2})
  • 使用*args**kw是Python的习惯写法,当然也可以用其他参数名,但最好使用习惯用法
  • 命名的关键字参数是为了限制调用者可以传入的参数名,同时可以提供默认值
  • 定义命名的关键字参数在没有可变参数的情况下不要忘了写分隔符*,否则定义的将是位置参数
 

递归函数

 

在函数内部,可以调用其他函数。如果一个函数在内部调用自身本身,这个函数就是递归函数。

 

fact(n)可以表示为n x fact(n-1),只有n=1时需要特殊处理:

In [155]:
def fact(n):
if n==1:
return 1
return n * fact(n - 1)
In [156]:
fact(1)
Out[156]:
1
In [157]:
fact(3)
Out[157]:
6
In [158]:
fact(5)
Out[158]:
120
 

高级特性

在Python中,代码不是越多越好,而是越少越好。代码不是越复杂越好,而是越简单越好。

基于这一思想,下面介绍Python中非常有用的高级特性,1行代码能实现的功能,决不写5行代码。请始终牢记,代码越少,开发效率越高。

 

切片

 

对经常取指定索引范围的操作,用循环十分繁琐,因此,Python提供了切片(Slice)操作符,能大大简化这种操作。

In [159]:
L = ['李寻欢', '令狐冲', '张无忌', '杨过']
In [160]:
# 取前3个元素
L[0:3]
Out[160]:
['李寻欢', '令狐冲', '张无忌']
 

L[0:3]表示,从索引0开始取,直到索引3为止,但不包括索引3。即索引0,1,2,正好是3个元素。

In [161]:
# 如果第一个索引是0,还可以省略
L[:3]
Out[161]:
['李寻欢', '令狐冲', '张无忌']
In [162]:
# 从索引1开始,取出2个元素出来
L[1:3]
Out[162]:
['令狐冲', '张无忌']
 

Python支持L[-1]取倒数第一个元素,它同样支持倒数切片。

In [163]:
L[-2:]
Out[163]:
['张无忌', '杨过']
 

记住倒数第一个元素的索引是-1。

 

下面来个实战。

In [164]:
# 先创建一个0-19的数列
L = list(range(20))
L
Out[164]:
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]
In [165]:
# 取前10个数
L[:10]
Out[165]:
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
In [166]:
# 取后10个数
L[-10:]
Out[166]:
[10, 11, 12, 13, 14, 15, 16, 17, 18, 19]
In [167]:
# 前11-20个数
L[10:20]
Out[167]:
[10, 11, 12, 13, 14, 15, 16, 17, 18, 19]
In [168]:
# 前10个数,每两个取一个
L[:10:2]
Out[168]:
[0, 2, 4, 6, 8]
In [169]:
# 所有数,每5个取一个
L[::5]
Out[169]:
[0, 5, 10, 15]
In [170]:
# 什么都不写,只写[:]就可以原样复制一个list
L[:]
Out[170]:
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]
 

tuple也是一种list,唯一区别是tuple不可变。因此,tuple也可以用切片操作,只是操作的结果仍是tuple

In [171]:
(0, 1, 2, 3, 4, 5)[:3]
Out[171]:
(0, 1, 2)
 

字符串'xxx'也可以看成是一种list,每个元素就是一个字符。因此,字符串也可以用切片操作,只是操作结果仍是字符串。

In [172]:
'iflyendless'[:4]
Out[172]:
'ifly'
In [173]:
'iflyendless'[::3]
Out[173]:
'iyds'
 

在很多编程语言中,针对字符串提供了很多各种截取函数(例如,substring),其实目的就是对字符串切片。Python没有针对字符串的截取函数,只需要切片一个操作就可以完成,非常简单。

 

迭代

 

给定一个listtuple,我们可以通过for循环来遍历这个listtuple,这种遍历我们称为迭代(Iteration)。

 

Python的for循环不仅可以用在listtuple上,还可以作用在其他可迭代对象上。

 

list这种数据类型虽然有下标,但很多其他数据类型是没有下标的,但是,只要是可迭代对象,无论有无下标,都可以迭代。

In [174]:
# 遍历列表
for x, y in [(1, 1), (2, 4), (3, 9)]:
print(x, y)
 
1 1
2 4
3 9
In [175]:
# 遍历元组
for e in ('李寻欢', 30, '小李飞刀'):
print(e)
 
李寻欢
30
小李飞刀
In [176]:
# 遍历集合
for name in {'李寻欢', '令狐冲', '张无忌', '杨过'}:
print(name)
 
李寻欢
张无忌
杨过
令狐冲
 

下面看如何遍历字典:

In [177]:
d = {'李寻欢': '小李飞刀', '令狐冲': '独孤九剑', '郭靖': '降龙十八掌'}
In [178]:
# 遍历字典的key
for key in d:
print(key)
 
李寻欢
令狐冲
郭靖
 

默认情况下,dict迭代的是key

In [179]:
# 遍历字典的value
for value in d.values():
print(value)
 
小李飞刀
独孤九剑
降龙十八掌
In [180]:
# 同时遍历key和value
for k, v in d.items():
print(k, v)
 
李寻欢 小李飞刀
令狐冲 独孤九剑
郭靖 降龙十八掌
 

字符串也是可迭代对象,因此,也可以作用于for循环:

In [181]:
for ch in '行无际的博客':
print(ch)
 





 

所以,当使用for循环时,只要作用于一个可迭代对象,for循环就可以正常运行,而不需要关心该对象究竟是list还是其他数据类型。

 

那么,如何判断一个对象是可迭代对象呢?方法是通过collections模块的Iterable类型判断:

In [182]:
from collections.abc import Iterable

isinstance('https://www.cnblogs.com/iflyendless/', Iterable)
Out[182]:
True
In [183]:
isinstance([1,2,3], Iterable)
Out[183]:
True
In [184]:
isinstance(123, Iterable)
Out[184]:
False
 

如果要对list实现类似Java那样的下标循环怎么办?Python内置的enumerate函数可以把一个list变成索引-元素对,这样就可以在for循环中同时迭代索引和元素本身:

In [185]:
for i, value in enumerate(['李寻欢', '令狐冲', '张无忌']):
print(i, value)
 
0 李寻欢
1 令狐冲
2 张无忌
 

列表生成式

 

列表生成式即List Comprehensions,是Python内置的非常简单却强大的可以用来创建list的生成式。

 

举个例子,要生成list [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]可以用list(range(1, 11))

In [186]:
list(range(1, 11))
Out[186]:
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
 

但如果要生成[1x1, 2x2, 3x3, ..., 10x10]怎么做?列表生成式则可以用一行语句生成:

In [187]:
[x * x for x in range(1, 11)]
Out[187]:
[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
 

写列表生成式时,把要生成的元素x * x放到前面,后面跟for循环,就可以把list创建出来,十分有用,多写几次,很快就可以熟悉这种语法。

 

for循环后面还可以加上if判断,这样就可以筛选出仅偶数的平方:

In [188]:
[x * x for x in range(1, 11) if x % 2 == 0]
Out[188]:
[4, 16, 36, 64, 100]
 

还可以使用两层循环,可以生成全排列:

In [189]:
[x + y for x in 'ABC' for y in '12']
Out[189]:
['A1', 'A2', 'B1', 'B2', 'C1', 'C2']
 

运用列表生成式,可以写出非常简洁的代码。下面看几个例子:

 

列表生成式也可以使用两个变量来生成list:

In [190]:
d = {'李寻欢': '小李飞刀', '令狐冲': '独孤九剑', '郭靖': '降龙十八掌'}
[k + '=' + v for k, v in d.items()]
Out[190]:
['李寻欢=小李飞刀', '令狐冲=独孤九剑', '郭靖=降龙十八掌']
 

把一个list中所有的字符串变成小写:

In [191]:
L = ['JAVA', 'SCALA', 'PYTHON', 'GOLANG']
[x.lower() for x in L]
Out[191]:
['java', 'scala', 'python', 'golang']
In [192]:
[x if x % 2 == 0 else -x for x in range(1, 11)]
Out[192]:
[-1, 2, -3, 4, -5, 6, -7, 8, -9, 10]
 

在一个列表生成式中,for前面的if ... else是表达式,而for后面的if是过滤条件,不能带else

 

生成器

 

通过列表生成式,可以直接创建一个列表。但是,受到内存限制,列表容量肯定是有限的。而且,创建一个包含100万个元素的列表,不仅占用很大的存储空间,如果我们仅仅需要访问前面几个元素,那后面绝大多数元素占用的空间都白白浪费了。

 

所以,如果列表元素可以按照某种算法推算出来,那是否可以在循环的过程中不断推算出后续的元素呢?这样就不必创建完整的list,从而节省大量的空间。在Python中,这种一边循环一边计算的机制,称为生成器:generator

 

要创建一个generator,有很多种方法。第一种方法很简单,只要把一个列表生成式的[]改成(),就创建了一个generator

In [193]:
g = (x * x for x in range(10))
g
Out[193]:
<generator object <genexpr> at 0x11364ee40>
 

如果要一个一个打印出来,可以通过next()函数获得generator的下一个返回值:

In [194]:
next(g)
Out[194]:
0
In [195]:
next(g)
Out[195]:
1
In [196]:
next(g)
Out[196]:
4
 

generator保存的是算法,每次调用next(g),就计算出g的下一个元素的值,直到计算到最后一个元素,没有更多的元素时,抛出StopIteration的错误。

 

可以使用for循环,因为generator也是可迭代对象:

In [197]:
g = (x * x for x in range(5))

for n in g:
print(n)
 
0
1
4
9
16
 

generator非常强大。如果推算的算法比较复杂,用类似列表生成式的for循环无法实现的时候,还可以用函数来实现。

 

比如,著名的斐波拉契数列(Fibonacci),除第一个和第二个数外,任意一个数都可由前两个数相加得到:1, 1, 2, 3, 5, 8, 13, 21, 34, ...

 

斐波拉契数列用列表生成式写不出来,但是,用函数把它打印出来却很容易:

In [198]:
def fib(max):
n, a, b = 0, 0, 1
while n < max:
print(b)
a, b = b, a + b
n = n + 1
return 'done'
In [199]:
fib(5)
 
1
1
2
3
5
Out[199]:
'done'
 

仔细观察,可以看出,fib函数实际上是定义了斐波拉契数列的推算规则,可以从第一个元素开始,推算出后续任意的元素,这种逻辑其实非常类似generator

 

也就是说,上面的函数和generator仅一步之遥。要把fib函数变成generator,只需要把print(b)改为yield b就可以了:

In [200]:
def fib(max):
n, a, b = 0, 0, 1
while n < max:
yield b
a, b = b, a + b
n = n + 1
return 'done'
 

这就是定义generator的另一种方法。如果一个函数定义中包含yield关键字,那么这个函数就不再是一个普通函数,而是一个generator

In [201]:
f = fib(6)
f
Out[201]:
<generator object fib at 0x11368e660>
 

这里,最难理解的就是generator和函数的执行流程不一样。函数是顺序执行,遇到return语句或者最后一行函数语句就返回。而变成generator的函数,在每次调用next()的时候执行,遇到yield语句返回,再次执行时从上次返回的yield语句处继续执行。

In [202]:
next(f)
Out[202]:
1
In [203]:
next(f)
Out[203]:
1
In [204]:
next(f)
Out[204]:
2
In [205]:
next(f)
Out[205]:
3
In [206]:
next(f)
Out[206]:
5
 

迭代器

 

可以直接作用于for循环的数据类型有以下几种:

  • 集合数据类型,如listtupledictsetstr
  • generator,包括生成器和带yieldgenerator function
 

这些可以直接作用于for循环的对象统称为可迭代对象:Iterable

 

可以使用isinstance()判断一个对象是否是Iterable对象:

In [207]:
from collections.abc import Iterable
isinstance([], Iterable)
Out[207]:
True
In [208]:
isinstance({}, Iterable)
Out[208]:
True
In [209]:
isinstance('abc', Iterable)
Out[209]:
True
In [210]:
isinstance((x for x in range(10)), Iterable)
Out[210]:
True
In [211]:
isinstance(100, Iterable)
Out[211]:
False
 

而生成器不但可以作用于for循环,还可以被next()函数不断调用并返回下一个值,直到最后抛出StopIteration错误表示无法继续返回下一个值了。

 

可以被next()函数调用并不断返回下一个值的对象称为迭代器:Iterator

 

可以使用isinstance()判断一个对象是否是Iterator对象:

In [212]:
from collections.abc import Iterator

isinstance((x for x in range(10)), Iterator)
Out[212]:
True
In [213]:
isinstance([], Iterator)
Out[213]:
False
In [214]:
isinstance({}, Iterator)
Out[214]:
False
In [215]:
isinstance('abc', Iterator)
Out[215]:
False
 

生成器都是Iterator对象,但listdictstr虽然是Iterable,却不是Iterator

 

listdictstrIterable变成Iterator可以使用iter()函数:

In [216]:
isinstance(iter([]), Iterator)
Out[216]:
True
In [217]:
isinstance(iter('abc'), Iterator)
Out[217]:
True
 

为什么listdictstr等数据类型不是Iterator

 

这是因为Python的Iterator对象表示的是一个数据流,Iterator对象可以被next()函数调用并不断返回下一个数据,直到没有数据时抛出StopIteration错误。可以把这个数据流看做是一个有序序列,但我们却不能提前知道序列的长度,只能不断通过next()函数实现按需计算下一个数据,所以Iterator的计算是惰性的,只有在需要返回下一个数据时它才会计算。

 

Iterator甚至可以表示一个无限大的数据流,例如全体自然数。而使用list是永远不可能存储全体自然数的。

 

小结:

  • 凡是可作用于for循环的对象都是Iterable类型;
  • 凡是可作用于next()函数的对象都是Iterator类型,它们表示一个惰性计算的序列;
  • 集合数据类型如listdictstr等是Iterable但不是Iterator,不过可以通过iter()函数获得一个Iterator对象。
 

Python的for循环本质上就是通过不断调用next()函数实现的。

In [218]:
for x in [1, 2, 3, 4, 5]:
pass
 

实际上完全等价于:

In [219]:
# 首先获得Iterator对象:
it = iter([1, 2, 3, 4, 5])
# 循环:
while True:
try:
# 获得下一个值:
x = next(it)
except StopIteration:
# 遇到StopIteration就退出循环
break
 

函数式编程

 

函数式编程(请注意多了一个“式”字)——Functional Programming,虽然也可以归结到面向过程的程序设计,但其思想更接近数学计算。

 

函数式编程的一个特点就是,允许把函数本身作为参数传入另一个函数,还允许返回一个函数!

 

高阶函数

 

变量可以指向函数, 函数本身也可以赋值给变量。

In [220]:
f = abs
f
Out[220]:
<function abs(x, /)>
In [221]:
f(-10)
Out[221]:
10
 

函数名也是变量, 函数名其实就是指向函数的变量!对于abs()这个函数,完全可以把函数名abs看成变量,它指向一个可以计算绝对值的函数!

In [222]:
def my_abs(x):
return x if x >= 0 else -x print(my_abs(-10))
print(my_abs(10)) # 把my_abs指向其他对象
my_abs = 100
try:
my_abs(-10)
except BaseException as e:
print(e)
 
10
10
'int' object is not callable
 

my_abs指向100后,就无法通过my_abs(-10)调用该函数了!因为my_abs这个变量已经不指向求绝对值函数而是指向一个整数10!

 

当然实际代码绝对不能这么写,这里是为了说明函数名也是变量。

 

注:实际上由于abs函数实际上是定义在import builtins模块中的,所以要让修改abs变量的指向在其它模块也生效,要用import builtins; builtins.abs = 10

 

既然变量可以指向函数,函数的参数能接收变量,那么一个函数就可以接收另一个函数作为参数,这种函数就称之为高阶函数。

In [223]:
# 一个最简单的高阶函数
def add(x, y, f):
return f(x) + f(y)
In [224]:
def f1(x):
return x * x add(2, 3, f1)
Out[224]:
13
 

map

 

map()函数接收两个参数,一个是函数,一个是Iterablemap将传入的函数依次作用到序列的每个元素,并把结果作为新的Iterator返回。

In [225]:
r = map(f1, [1, 2, 3, 4, 5])
list(r)
Out[225]:
[1, 4, 9, 16, 25]
 

map()传入的第一个参数是f1,即函数对象本身。由于结果r是一个IteratorIterator是惰性序列,因此通过list()函数让它把整个序列都计算出来并返回一个list。

 

所以,map()作为高阶函数,事实上它把运算规则抽象了,因此,我们不但可以计算简单的f(x)=x*x,还可以计算任意复杂的函数,比如,把这个list所有数字转为字符串

In [226]:
list(map(str, [1,2,3]))
Out[226]:
['1', '2', '3']
 

reduce

 

reduce把一个函数作用在一个序列[x1, x2, x3, ...]上,这个函数必须接收两个参数,reduce把结果继续和序列的下一个元素做累积计算,其效果就是:reduce(f, [x1, x2, x3, x4]) = f(f(f(x1, x2), x3), x4)

 

比方说对一个序列求和,就可以用reduce实现,当然求和运算可直接用Python内建函数sum()

In [227]:
def add(x, y):
return x + y from functools import reduce
reduce(add, [1, 2, 3, 4, 5])
Out[227]:
15
In [228]:
# 当然还可以用lambda函数进一步简化成
reduce(lambda x, y: x + y, [1, 2, 3, 4, 5])
Out[228]:
15
 

filter

 

filter()函数用于过滤序列。

 

map()类似,filter()也接收一个函数和一个序列。和map()不同的是,filter()把传入的函数依次作用于每个元素,然后根据返回值是True还是False决定保留还是丢弃该元素。

In [229]:
# 在一个list中,只保留奇数,可以这么写
list(filter(lambda x: x % 2 == 1, [1, 2, 3, 4, 5]))
Out[229]:
[1, 3, 5]
In [230]:
# 把一个序列中的空字符串删掉,可以这么写
list(filter(lambda x: x and x.strip(), ['ab','','c',' ','d', None]))
Out[230]:
['ab', 'c', 'd']
 

可见用filter()这个高阶函数,关键在于正确实现一个“筛选”函数。

 

注意到filter()函数返回的是一个Iterator,也就是一个惰性序列,所以要强迫filter()完成计算结果,需要用list()函数获得所有结果并返回list。

 

sorted

 

排序也是在程序中经常用到的算法。无论使用冒泡排序还是快速排序,排序的核心是比较两个元素的大小。如果是数字,我们可以直接比较,但如果是字符串或者两个dict呢?直接比较数学上的大小是没有意义的,因此,比较的过程必须通过函数抽象出来。

In [231]:
# Python内置的sorted()函数就可以对list进行排序
sorted([36, 5, -12, 9, -21])
Out[231]:
[-21, -12, 5, 9, 36]
 

此外,sorted()函数也是一个高阶函数,它还可以接收一个key函数来实现自定义的排序,例如按绝对值大小排序:

In [232]:
sorted([36, 5, -12, 9, -21], key=lambda x: x if x >= 0 else -x)
Out[232]:
[5, 9, -12, -21, 36]
 

要进行反向排序,不必改动key函数,可以传入第三个参数reverse=True

In [233]:
sorted([36, 5, -12, 9, -21], key=lambda x: x if x >= 0 else -x, reverse=True)
Out[233]:
[36, -21, -12, 9, 5]
 

从上述例子可以看出,高阶函数的抽象能力是非常强大的,而且,核心代码可以保持得非常简洁。

 

返回函数

 

高阶函数除了可以接受函数作为参数外,还可以把函数作为结果值返回。

In [234]:
# 比如返回一个数学上的y=a*x+b函数
def f(a, b):
def y(x):
return a * x + b;
return y # y = 2*x + 1
y = f(2, 1)
y(10)
Out[234]:
21
 

上面在函数f(a, b)中又定义了函数y,并且,内部函数y可以引用外部函数的参数a, b,当f(a, b)返回函数y时,相关参数和变量都保存在返回的函数中,这种称为闭包(Closure)的程序结构拥有极大的威力。

 

请再注意一点,当我们调用f(a, b)时,每次调用都会返回一个新的函数,即使传入相同的参数:

In [235]:
y1 = f(2, 1)
y2 = f(2, 1)
y1 == y2
Out[235]:
False
 

闭包需要注意的问题是,返回的函数并没有立刻执行,而是直到被调用了才执行,下面看个例子。

In [236]:
def count():
fs = []
for i in range(1, 4):
def f():
return i*i
fs.append(f)
return fs f1, f2, f3 = count()
 

你可能认为调用f1()f2()f3()结果应该是1,4,9,但实际结果是:

In [237]:
f1()
Out[237]:
9
In [238]:
f2()
Out[238]:
9
In [239]:
f3()
Out[239]:
9
 

全部都是9!原因就在于返回的函数引用了变量i,但它并非立刻执行。等到3个函数都返回时,它们所引用的变量i已经变成了3,因此最终结果为9。

 

返回闭包时牢记一点:返回函数不要引用任何循环变量,或者后续会发生变化的变量。

 

匿名函数

 

在传入函数时,有些时候,不需要显式地定义函数,直接传入匿名函数更方便。下面看一个例子。

In [240]:
list(map(lambda x: x * x, [1, 2, 3, 4, 5]))
Out[240]:
[1, 4, 9, 16, 25]
 

关键字lambda表示匿名函数,:前面的x表示函数参数。

 

匿名函数有个限制,就是只能有一个表达式,不用写return,返回值就是该表达式的结果。

 

用匿名函数有个好处,因为函数没有名字,不必担心函数名冲突。

 

此外,匿名函数也是一个函数对象,也可以把匿名函数赋值给一个变量,再利用变量来调用该函数。

In [241]:
f = lambda x: x * x
f
Out[241]:
<function __main__.<lambda>(x)>
In [242]:
f(6)
Out[242]:
36
 

同样,也可以把匿名函数作为返回值返回,比如:

In [243]:
def f(a, b):
return lambda x: a * x + b
In [244]:
y = f(2, 1)
y
Out[244]:
<function __main__.f.<locals>.<lambda>(x)>
In [245]:
y(3)
Out[245]:
7
 

装饰器

 

函数对象有一个__name__属性,可以拿到函数的名字:

In [246]:
def hello():
print("My blog is https://www.cnblogs.com/iflyendless/.") hello.__name__
Out[246]:
'hello'
 

假设要增强hello()函数的功能,比如,在函数调用前后自动打印日志,但又不希望修改hello()函数的定义,这种在代码运行期间动态增加功能的方式,称之为“装饰器”(Decorator)。

 

本质上,decorator就是一个返回函数的高阶函数。所以,我们要定义一个能打印日志的decorator,可以定义如下:

In [247]:
def log(func):
def wrapper(*args, **kw):
print(f'***Before {func.__name__}函数***')
r = func(*args, **kw)
print(f'***After {func.__name__}函数***')
return r
return wrapper
 

观察上面的log,因为它是一个decorator,所以接受一个函数作为参数,并返回一个函数。我们要借助Python的@语法,把decorator置于函数的定义处:

In [248]:
@log
def hello():
print("My blog is https://www.cnblogs.com/iflyendless/.")
In [249]:
hello()
 
***Before hello函数***
My blog is https://www.cnblogs.com/iflyendless/.
***After hello函数***
 

偏函数

 

在介绍函数参数的时候,我们知道通过设定参数的默认值,可以降低函数调用的难度。而偏函数也可以做到这一点。

 

int()函数可以把字符串转换为整数,当仅传入字符串时,int()函数默认按十进制转换

In [250]:
int('11')
Out[250]:
11
 

int()函数还提供额外的base参数,默认值为10。如果传入base参数,就可以做N进制的转换:

In [251]:
int('11', base = 2)
Out[251]:
3
 

如果要转换大量的二进制字符串,每次都传入int(x, base=2)非常麻烦,于是,可以定义一个int2()的函数,默认把base=2传进去,如下:

In [252]:
def int2(x, base=2):
return int(x, base) # 此时转换二进制就比较方便了
int2('11')
Out[252]:
3
 

functools.partial就是帮助我们创建一个偏函数的,不需要我们自己定义int2(),可以直接使用下面的代码创建一个新的函数int2

In [253]:
import functools
int2 = functools.partial(int, base=2) int2('11')
Out[253]:
3
 

所以,简单总结functools.partial的作用就是,把一个函数的某些参数给固定住(也就是设置默认值),返回一个新的函数,调用这个新函数会更简单。

 

注意到上面的新的int2函数,仅仅是把base参数重新设定默认值为2,但也可以在函数调用时传入其他值:

In [254]:
int2('11', base = 10)
Out[254]:
11
 

创建偏函数时,实际上可以接收函数对象*args**kw这3个参数,当传入:

In [255]:
int2 = functools.partial(int, base=2)
 

实际上固定了int()函数的关键字参数base,也就是,int2('11')相当于:

In [256]:
kw = { 'base': 2 }
int('11', **kw)
Out[256]:
3
 

再举个例子:

In [257]:
my_max = functools.partial(max, 10)

my_max(1, 3, 5)
Out[257]:
10
 

实际上会把10作为*args的一部分自动加到左边,也就是,my_max(1, 3, 5)相当于:

In [258]:
args = (10, 1, 3, 5)
max(*args)
Out[258]:
10
 

小结:当函数的参数个数太多,需要简化时,使用functools.partial可以创建一个新的函数,这个新函数可以固定住原函数的部分参数,从而在调用时更简单。

 

模块

 

开发过程中,随着程序代码越写越多,在一个文件里代码就会越来越长,越来越不容易维护。

 

为了编写可维护的代码,把很多函数分组,分别放到不同的文件里,这样,每个文件包含的代码就相对较少,很多编程语言都采用这种组织代码的方式。在Python中,一个.py文件就称之为一个模块(Module)。

 

使用模块有什么好处?

 
  • 大大提高了代码的可维护性。
  • 编写代码不必从零开始。当一个模块编写完毕,就可以被其他地方引用。我们在编写程序的时候,也经常引用其他模块,包括Python内置的模块和来自第三方的模块。
  • 避免函数名和变量名冲突。相同名字的函数和变量完全可以分别存在不同的模块中,因此,我们自己在编写模块时,不必考虑名字会与其他模块冲突。但是也要注意,尽量不要与内置函数名字冲突。
 

补充Python的所有内置函数:

https://docs.python.org/3/library/functions.html

 

如果不同的人编写的模块名相同怎么办?为了避免模块名冲突,Python又引入了按目录来组织模块的方法,称为包(Package)。

 

举个例子,一个abc.py的文件就是一个名字叫abc的模块,一个xyz.py的文件就是一个名字叫xyz的模块。

 

现在,假设我们的abcxyz这两个模块名字与其他模块冲突了,于是我们可以通过包来组织模块,避免冲突。方法是选择一个顶层包名,比如mycompany,按照如下目录存放:

mycompany
├─ __init__.py
├─ abc.py
└─ xyz.py
 

引入了包以后,只要顶层的包名不与别人冲突,那所有模块都不会与别人冲突。现在,abc.py模块的名字就变成了mycompany.abc,类似的,xyz.py的模块名变成了mycompany.xyz

 

请注意,每一个包目录下面都会有一个__init__.py的文件,这个文件是必须存在的,否则,Python就把这个目录当成普通目录,而不是一个包。

__init__.py可以是空文件,也可以有Python代码,因为__init__.py本身就是一个模块,而它的模块名就是mycompany

 

类似的,可以有多级目录,组成多级层次的包结构。比如如下的目录结构:

mycompany
├─ web
│ ├─ __init__.py
│ ├─ utils.py
│ └─ www.py
├─ __init__.py
├─ abc.py
└─ utils.py
 

文件www.py的模块名就是mycompany.web.www,两个文件utils.py的模块名分别是mycompany.utilsmycompany.web.utils

 

注意:自己创建模块时要注意命名,不能和Python自带的模块名称冲突。例如,系统自带了sys模块,自己的模块就不可命名为sys.py,否则将无法导入系统自带的sys模块。

 

总结:模块是一组Python代码的集合,可以使用其他模块,也可以被其他模块使用。

 

创建自己的模块时,要注意:

  • 模块名要遵循Python变量命名规范,不要使用中文、特殊字符;
  • 模块名不要和系统模块名冲突,最好先查看系统是否已存在该模块,检查方法是在Python交互环境执行import abc,若成功则说明系统存在此模块。
 

使用模块

 

Python本身就内置了很多非常有用的模块,只要安装完毕,这些模块就可以立刻使用。

 

以内建的sys模块为例,编写一个hello的模块

#!/usr/bin/env python3
# -*- coding: utf-8 -*- ' a test module ' __author__ = 'Michael Liao' import sys def test():
args = sys.argv
if len(args)==1:
print('Hello, world!')
elif len(args)==2:
print('Hello, %s!' % args[1])
else:
print('Too many arguments!') if __name__=='__main__':
test()
 
  • 第1行和第2行是标准注释,第1行注释可以让这个hello.py文件直接在Unix/Linux/Mac上运行,第2行注释表示.py文件本身使用标准UTF-8编码;
  • 第4行是一个字符串,表示模块的文档注释,任何模块代码的第一个字符串都被视为模块的文档注释;
  • 第6行使用__author__变量把作者写进去,这样当你公开源代码后别人就可以瞻仰你的大名;
 

以上就是Python模块的标准文件模板,当然也可以全部删掉不写,但是,按标准办事肯定没错。

 

后面开始就是真正的代码部分。

 

使用sys模块的第一步,就是导入该模块:import sys

 

导入sys模块后,我们就有了变量sys指向该模块,利用sys这个变量,就可以访问sys模块的所有功能。

 

sys模块有一个argv变量,用list存储了命令行的所有参数。argv至少有一个元素,因为第一个参数永远是该.py文件的名称,例如:

  • 运行python3 hello.py获得的sys.argv就是['hello.py']
  • 运行python3 hello.py iflyendless获得的sys.argv就是['hello.py', 'iflyendless']
 

最后,注意到这两行代码:

if __name__=='__main__':
test()
 

当我们在命令行运行hello模块文件时,Python解释器把一个特殊变量__name__置为__main__,而如果在其他地方导入该hello模块时,if判断将失败,因此,这种if测试可以让一个模块通过命令行运行时执行一些额外的代码,最常见的就是运行测试。

 

在一个模块中,我们可能会定义很多函数和变量,但有的函数和变量我们希望给别人使用,有的函数和变量我们希望仅仅在模块内部使用。在Python中,是通过_前缀来实现的。

 

正常的函数和变量名是公开的(public),可以被直接引用,比如:abcx123PI等;

 

类似__xxx__这样的变量是特殊变量,可以被直接引用,但是有特殊用途,比如上面的__author____name__就是特殊变量,hello模块定义的文档注释也可以用特殊变量__doc__访问,我们自己的变量一般不要用这种变量名;

 

类似_xxx__xxx这样的函数或变量就是非公开的(private),不应该被直接引用,比如_abc__abc等;

 

之所以我们说,private函数和变量不应该被直接引用,而不是“不能”被直接引用,是因为Python并没有一种方法可以完全限制访问private函数或变量,但是,从编程习惯上不应该引用private函数或变量。

 

为什么我们不应该引用private函数或变量呢?

 

内部逻辑用private对外隐藏,一般表示这属于内部的实现细节,而外部调用者只需要关心对外的公开的接口,这是一种非常有用的代码封装与抽象。如果你引用了别人的内部实现细节,就意味着别人的内部实现一旦发生变化,你的程序也就需要跟着去改动。这是非常糟糕的事!!!

 

总结:外部不需要引用的函数全部定义成private,只有外部需要引用的函数才定义为public

 

安装第三方模块

 

安装第三方模块,可以通过包管理工具pip完成。

 

在使用Python时,我们经常需要用到很多第三方库,例如,MySQL驱动程序,Web框架Flask,科学计算Numpy等。用pip一个一个安装费时费力,还需要考虑兼容性。我们推荐直接使用Anaconda,这是一个基于Python的数据处理和科学计算平台,它已经内置了许多非常有用的第三方库,我们装上Anaconda,就相当于把许多常用第三方模块自动安装好了,非常简单易用。

 

下载后直接安装,Anaconda会把系统Path中的python指向自己自带的Python,并且,Anaconda安装的第三方模块会安装在Anaconda自己的路径下,不影响系统已安装的Python目录。

 

当我们试图加载一个模块时,Python会在指定的路径下搜索对应的.py文件,如果找不到,就会报错:ImportError: No module named xxx

 

默认情况下,Python解释器会搜索当前目录、所有已安装的内置模块和第三方模块,搜索路径存放在sys模块的path变量中:

In [259]:
import sys
sys.path
Out[259]:
['/Users/wind/notebook',
'/Volumes/300g/opt/anaconda3/envs/test/lib/python38.zip',
'/Volumes/300g/opt/anaconda3/envs/test/lib/python3.8',
'/Volumes/300g/opt/anaconda3/envs/test/lib/python3.8/lib-dynload',
'',
'/Volumes/300g/opt/anaconda3/envs/test/lib/python3.8/site-packages',
'/Volumes/300g/opt/anaconda3/envs/test/lib/python3.8/site-packages/aeosa',
'/Volumes/300g/opt/anaconda3/envs/test/lib/python3.8/site-packages/IPython/extensions',
'/Users/wind/.ipython']
 

如果我们要添加自己的搜索目录,有两种方法:

 
  • 直接修改sys.path,添加要搜索的目录:sys.path.append('...path/to/py_scripts...'),这种方法是在运行时修改,运行结束后失效;
  • 设置环境变量PYTHONPATH,该环境变量的内容会被自动添加到模块搜索路径中。设置方式与设置Path环境变量类似。注意只需要添加你自己的搜索路径,Python自己本身的搜索路径不受影响。
 

异常处理

 

高级语言通常都内置了一套try...except...finally...的错误处理机制,Python也不例外。

 

try

 

先看个例子:

In [260]:
try:
print('try...')
r = 10 / 0
print('result:', r)
except ZeroDivisionError as e:
print('except:', e)
finally:
print('finally...')
print('END')
 
try...
except: division by zero
finally...
END
 

当我们认为某些代码可能会出错时,就可以用try来运行这段代码,如果执行出错,则后续代码不会继续执行,而是直接跳转至错误处理代码,即except语句块,执行完except后,如果有finally语句块,则执行finally语句块,至此,执行完毕。

 

错误有很多种类,如果发生了不同类型的错误,应该由不同的except语句块处理,可以有多个except来捕获不同类型的错误:

In [261]:
def f1(s):
try:
print('try...')
r = 10 / int(s)
print('result:', r)
except ValueError as e:
print('ValueError:', e)
except ZeroDivisionError as e:
print('ZeroDivisionError:', e)
finally:
print('finally...')
print('END')
 

正常执行的情况如下:

In [262]:
f1('2')
 
try...
result: 5.0
finally...
END
 

参数s转为int失败的情况如下:

In [263]:
f1('a')
 
try...
ValueError: invalid literal for int() with base 10: 'a'
finally...
END
 

除数为0的情况如下:

In [264]:
f1('0')
 
try...
ZeroDivisionError: division by zero
finally...
END
 

注意:Python所有的错误都是从BaseException类派生的,常见的错误类型和继承关系看这里。

https://docs.python.org/3/library/exceptions.html#exception-hierarchy

 

顺便提一句,与Java等高级语言类似,函数main()调用bar()bar()调用foo(),结果foo()出错了,这时,只要main()捕获到了,就可以处理。

 

调用栈

 

如果错误没有被捕获,它就会一直往上抛,最后被Python解释器捕获,打印一个错误信息,然后程序退出。

 

出错并不可怕,可怕的是不知道哪里出错了。解读错误信息是定位错误的关键。我们从上往下可以看到整个错误的调用函数链。

 

注意:出错的时候,一定要分析错误的调用栈信息,才能定位错误的位置。

 

Python内置的logging模块可以非常容易地记录错误信息:

In [265]:
import logging

def foo(s):
return 10 / int(s) def bar(s):
return foo(s) * 2 def f():
try:
bar('0')
except Exception as e:
logging.exception(e) f()
print('END')
 
ERROR:root:division by zero
Traceback (most recent call last):
File "<ipython-input-265-38f2d311f3cb>", line 11, in f
bar('0')
File "<ipython-input-265-38f2d311f3cb>", line 7, in bar
return foo(s) * 2
File "<ipython-input-265-38f2d311f3cb>", line 4, in foo
return 10 / int(s)
ZeroDivisionError: division by zero
 
END
 

如果要手动抛出错误,首先根据需要,可以定义一个错误的class,选择好继承关系,然后,用raise语句抛出一个错误的实例:

In [266]:
class FooError(ValueError):
pass def foo(s):
n = int(s)
if n==0:
raise FooError('invalid value: %s' % s)
return 10 / n try:
foo('0')
except FooError as e:
logging.exception(e)
 
ERROR:root:invalid value: 0
Traceback (most recent call last):
File "<ipython-input-266-b07c742deb70>", line 11, in <module>
foo('0')
File "<ipython-input-266-b07c742deb70>", line 7, in foo
raise FooError('invalid value: %s' % s)
FooError: invalid value: 0
 

只有在必要的时候才定义我们自己的错误类型。如果可以选择Python已有的内置的错误类型(比如ValueErrorTypeError),尽量使用Python内置的错误类型。

 

与其他高级语言同样类似,如果不想处理异常,也可以抛出去让调用方去处理或者继续抛出。

In [267]:
def foo(s):
n = int(s)
if n==0:
raise ValueError('invalid value: %s' % s)
return 10 / n def bar():
try:
foo('0')
except ValueError as e:
print('ValueError!')
raise
 

面向对象

 

面向对象编程——Object Oriented Programming,简称OOP,是一种程序设计思想。上面介绍的大多属于面向过程编程的思想。

 

OOP把对象作为程序的基本单元,一个对象包含了数据操作数据的函数

 

面向对象的程序设计把计算机程序视为一组对象的集合,而每个对象都可以接收其他对象发过来的消息,并处理这些消息,计算机程序的执行就是一系列消息在各个对象之间传递。

 

在Python中,所有数据类型都可以视为对象,当然也可以自定义对象。自定义的对象数据类型就是面向对象中的类(Class)的概念。

 

来个快速入门面向对象编程的案例,感受一下什么是面向对象。

In [268]:
class Hero(object):

    def __init__(self, name, skill):
self.name = name
self.skill = skill def say_hi(self):
print(f'大家好,我是{self.name},我会{self.skill}')
In [269]:
hero1 = Hero('李寻欢', '小李飞刀')
hero1.say_hi()
 
大家好,我是李寻欢,我会小李飞刀
In [270]:
hero2 = Hero('令狐冲', '独孤九剑')
hero2.say_hi()
 
大家好,我是令狐冲,我会独孤九剑
 

如果用前面介绍的面向过程的思想来实现这一需求,是怎么做的呢?

In [271]:
def say_hi(name, skill):
print(f'大家好,我是{name},我会{skill}')
In [272]:
say_hi('李寻欢', '小李飞刀')
say_hi('令狐冲', '独孤九剑')
 
大家好,我是李寻欢,我会小李飞刀
大家好,我是令狐冲,我会独孤九剑
 

咦,好像面向过程的代码更简单一些。实际情况是不是这样呢?如果还有其他的需求呢,比如说Hero要参加英雄大会,再比如说Hero在英雄大会上交战过哪几个对手,赢了几场,输了几场。用面向过程来实现的话,且不说每次都需要把nameskill等参数传到函数里面去,然后外层还需要用list数据结构存储交战过几个对手,外层也需要用2个变量存储该Hero赢了几场,输了几场。另外,如果要记录多个Hero的情况,用面向过程的思想,就需要在外部维护大量的描述信息与中间状态,程序的复杂度较大,扩展性也较差。而使用面向对象的思想来实现,外层不需要关心这些细节,只需要创建出来Hero对象,然后到哪一步了,直接调用对象的方法,不需要关心对象中间状态的存储与维护,因为这些信息都统一封装在对象内部了,非常符合软件编程高内聚的设计思想!

 

物以类聚,人以群分。面向对象的设计思想是从自然界中来的,因为在自然界中,类(Class)和实例(Instance)的概念是很自然的。Class是一种抽象概念,比如我们定义的Class——Hero,是指武侠人物这个概念,而实例(Instance)则是一个个具体的武侠人物,比如,李寻欢令狐冲是两个具体的Hero

 

一个Class既包含数据,又包含操作数据的方法。封装、继承和多态是面向对象的三大特性,随着程序学习的深入,你会自然理解。

 

类和实例

 

面向对象最重要的概念就是类(Class)和实例(Instance),必须牢记类是抽象的模板,比如Hero类,而实例是根据类创建出来的一个个具体的“对象”,每个对象都拥有相同的方法,但各自的数据可能不同。

 

在Python中,定义类是通过class关键字:

In [273]:
class Hero(object):
pass Hero
Out[273]:
__main__.Hero
 

class后面紧接着是类名,即Hero,类名通常是大写开头的单词,紧接着是(object),表示该类是从哪个类继承下来的,继承的概念后面再讲,通常,如果没有合适的继承类,就使用object类,这是所有类最终都会继承的类。

 

定义好了Hero类,就可以根据Hero类创建出实例,创建实例是通过类名+()实现的:

In [274]:
hero1 = Hero()
hero1
Out[274]:
<__main__.Hero at 0x1136dd490>
 

可以自由地给一个实例变量绑定属性,比如,给实例hero1绑定一个name属性:

In [275]:
hero1.name = '张无忌'
hero1.name
Out[275]:
'张无忌'
 

由于类可以起到模板的作用,因此,可以在创建实例的时候,把一些我们认为必须绑定的属性强制填写进去。通过定义一个特殊的__init__方法,在创建实例的时候,就把nameskill等属性绑上去:

In [276]:
class Hero(object):

    def __init__(self, name, skill):
self.name = name
self.skill = skill
 

注意:特殊方法__init__前后分别有两个下划线!!!

 

__init__方法的第一个参数永远是self,表示创建的实例本身,因此,在__init__方法内部,就可以把各种属性绑定到self,因为self就指向创建的实例本身。

 

有了__init__方法,在创建实例的时候,就不能传入空的参数了,必须传入与__init__方法匹配的参数,但self不需要传,Python解释器自己会把实例变量传进去:

In [277]:
hero1 = Hero('张无忌', '乾坤大挪移')
In [278]:
hero1.name
Out[278]:
'张无忌'
In [279]:
hero1.skill
Out[279]:
'乾坤大挪移'
 

和普通的函数相比,在类中定义的函数只有一点不同,就是第一个参数永远是实例变量self,并且,调用时,不用传递该参数。除此之外,类的方法和普通函数没有什么区别,所以,你仍然可以用默认参数、可变参数、关键字参数和命名关键字参数。

 

既然Hero实例本身就拥有nameskill这些数据,要访问这些数据,就没有必要从外面的函数去访问,可以直接在Hero类的内部定义访问数据的函数,这样,就把“数据”给封装起来了。这些封装数据的函数是和Hero类本身是关联起来的,我们称之为类的方法

In [280]:
class Hero(object):

    def __init__(self, name, skill):
self.name = name
self.skill = skill def say_hi(self):
print(f'大家好,我是{self.name},我会{self.skill}')
 

要定义一个方法,除了第一个参数是self外,其他和普通函数一样。要调用一个方法,只需要在实例变量上直接调用,除了self不用传递,其他参数正常传入:

In [281]:
hero1 = Hero('张无忌', '乾坤大挪移')
hero1.say_hi()
 
大家好,我是张无忌,我会乾坤大挪移
 

这样一来,从外部看Hero类,就只需要知道,创建实例需要给出nameskill,而如何自我介绍,都是在Hero类的内部定义的,这些数据和逻辑被封装起来了,调用很容易,但却不用知道内部实现的细节。

 

小结:

  • 类是创建实例的模板,而实例则是一个一个具体的对象,各个实例拥有的数据都互相独立,互不影响;
  • 方法就是与实例绑定的函数,和普通函数不同,方法可以直接访问实例的数据;
 

和静态语言不同,Python允许对实例变量绑定任何数据,也就是说,对于两个实例变量,虽然它们都是同一个类的不同实例,但拥有的变量名称都可能不同:

In [282]:
hero1 = Hero('李寻欢', '小李飞刀')
hero2 = Hero('令狐冲', '独孤九剑')
In [283]:
hero2.wife = '任盈盈'
hero2.wife
Out[283]:
'任盈盈'
In [284]:
import logging
try:
hero1.wife
except AttributeError as e:
logging.exception(e)
 
ERROR:root:'Hero' object has no attribute 'wife'
Traceback (most recent call last):
File "<ipython-input-284-e88ecc5593b4>", line 3, in <module>
hero1.wife
AttributeError: 'Hero' object has no attribute 'wife'
 

访问限制

 

在Class内部,可以有属性和方法,而外部代码可以通过直接调用实例变量的方法来操作数据,这样,就隐藏了内部的复杂逻辑。

 

外部代码还是可以自由地修改一个实例的属性

In [285]:
hero1 = Hero('张无忌', '乾坤大挪移')
hero1.skill
Out[285]:
'乾坤大挪移'
In [286]:
hero1.skill = '九阳神功'
hero1.skill
Out[286]:
'九阳神功'
 

如果要让内部属性不被外部访问,可以把属性的名称前加上两个下划线__,在Python中,实例的变量名如果以__开头,就变成了一个私有变量(private),只有内部可以访问,外部不能访问,所以,我们把Hero类改一改:

In [287]:
class Hero(object):

    def __init__(self, name, skill):
self.__name = name
self.__skill = skill def say_hi(self):
print(f'大家好,我是{self.__name},我会{self.__skill}')
 

改完后,对于外部代码来说,没什么变动,但是已经无法从外部访问实例变量.__skill了:

In [288]:
import logging

hero1 = Hero('张无忌', '乾坤大挪移')
try:
hero1.__skill
except AttributeError as e:
logging.exception(e)
 
ERROR:root:'Hero' object has no attribute '__skill'
Traceback (most recent call last):
File "<ipython-input-288-92a9da12cb20>", line 5, in <module>
hero1.__skill
AttributeError: 'Hero' object has no attribute '__skill'
 

这样就确保了外部代码不能随意修改对象内部的状态,这样通过访问限制的保护,代码更加健壮。

 

但是如果外部代码要获取nameskill怎么办?可以给Hero类增加get_nameget_skill这样的方法:

In [289]:
class Hero(object):

    def __init__(self, name, skill):
self.__name = name
self.__skill = skill def say_hi(self):
print(f'大家好,我是{self.__name},我会{self.__skill}') def get_name(self):
return self.__name def get_skill(self):
return self.__skill
In [290]:
hero1 = Hero('张无忌', '乾坤大挪移')
hero1.get_skill()
Out[290]:
'乾坤大挪移'
 

如果又要允许外部代码修改skill怎么办?可以再给Hero类增加set_skill方法:

In [291]:
class Hero(object):

    def __init__(self, name, skill):
self.__name = name
self.__skill = skill def say_hi(self):
print(f'大家好,我是{self.__name},我会{self.__skill}') def get_name(self):
return self.__name def get_skill(self):
return self.__skill def set_skill(self, skill):
self.__skill = skill
In [292]:
hero1 = Hero('张无忌', '乾坤大挪移')
hero1.set_skill('九阳神功')
hero1.get_skill()
Out[292]:
'九阳神功'
 

需要注意的是,在Python中,变量名类似__xxx__的,也就是以双下划线开头,并且以双下划线结尾的,是特殊变量,特殊变量是可以直接访问的,不是private变量,所以,不能用__name____skill__这样的变量名。

 

有些时候,你会看到以一个下划线开头的实例变量名,比如_name,这样的实例变量外部是可以访问的,但是,按照约定俗成的规定,当你看到这样的变量时,意思就是,“虽然我可以被访问,但是,请把我视为私有变量,不要随意访问”。

 

双下划线开头的实例变量是不是一定不能从外部访问呢?

 

其实也不是。不能直接访问__name是因为Python解释器对外把__name变量改成了_Hero__name,所以,仍然可以通过_Hero__name来访问__name变量:

In [293]:
hero1._Hero__name
Out[293]:
'张无忌'
 

但是强烈建议你不要这么干,因为不同版本的Python解释器可能会把__name改成不同的变量名。

 

总的来说就是,Python本身没有任何机制阻止你干坏事,一切全靠自觉。

 

最后注意下面的这种错误写法:

In [294]:
hero1 = Hero('张无忌', '乾坤大挪移')
hero1.get_name()
Out[294]:
'张无忌'
In [295]:
hero1.__name = '行无际'
hero1.__name
Out[295]:
'行无际'
 

表面上看,外部代码“成功”地设置了__name变量,但实际上这个__name变量和class内部的__name变量不是一个变量!内部的__name变量已经被Python解释器自动改成了_Hero__name,而外部代码给该对象新增了一个__name变量。不信试试:

In [296]:
hero1.get_name()
Out[296]:
'张无忌'
In [297]:
hero1._Hero__name = '行无际'
hero1.get_name()
Out[297]:
'行无际'
 

继承和多态

 

OOP程序设计中,当我们定义一个class的时候,可以从某个现有的class继承,新的class称为子类(Subclass),而被继承的class称为基类、父类或超类(Base classSuper class)。

 

比如,我们已经编写了一个名为Animal的class,有一个run()方法可以直接打印:

In [298]:
class Animal(object):
def run(self):
print('Animal is running...')
 

当我们需要编写DogCat类时,就可以直接从Animal类继承:

In [299]:
class Dog(Animal):
pass class Cat(Animal):
pass
 

对于Dog来说,Animal就是它的父类,对于Animal来说,Dog就是它的子类。

 

继承有什么好处?最大的好处是子类获得了父类的全部功能。由于Animial实现了run()方法,因此,DogCat作为它的子类,什么事也没干,就自动拥有了run()方法:

In [300]:
a1 = Dog()
a2 = Cat() a1.run()
a2.run()
 
Animal is running...
Animal is running...
 

继承的第二个好处是允许我们对代码做一点改进。你看到了,无论是Dog还是Cat,它们run()的时候,显示的都是Animal is running...,符合逻辑的做法是分别显示Dog is running...Cat is running...,因此,对DogCat类改进如下:

In [301]:
class Dog(Animal):

    def run(self):
print('Dog is running...') class Cat(Animal): def run(self):
print('Cat is running...')
 

再次运行,结果如下:

In [302]:
a1 = Dog()
a2 = Cat() a1.run()
a2.run()
 
Dog is running...
Cat is running...
 

当子类和父类都存在相同的run()方法时,我们说,子类的run()覆盖了父类的run(),在代码运行的时候,总是会调用子类的run()。这样,我们就获得了继承的另一个好处:多态。

 

判断一个变量是否是某个类型可以用isinstance()判断:

In [303]:
isinstance(a1, Animal)
Out[303]:
True
In [304]:
isinstance(a1, Dog)
Out[304]:
True
 

对于一个变量,我们只需要知道它是Animal类型,无需确切地知道它的子类型,就可以放心地调用run()方法,而具体调用的run()方法是作用在AnimalDogCat还是其他派生对象上,由运行时该对象的确切类型决定,这就是多态真正的威力:调用方只管调用,不管细节,而当我们新增一种Animal的子类时,只要确保run()方法编写正确,不用管原来的代码是如何调用的。这就是著名的开闭原则

 

其实,一般情况下,在有了一定的项目经验后才能真正理解多态、灵活运用多态,并配合常用的一些设计模式,能极大地提高程序的扩展性与可维护性。应用多态的关键在于抽象,能够捕捉到程序中变化的行为、可扩展的地方。这就是所谓的面向抽象编程面向接口编程。本质上属于一种内功心法,平时项目中应该多锻炼抽象的能力。

 

继承还可以一级一级地继承下来,就好比从爷爷到爸爸、再到儿子这样的关系。而任何类,最终都可以追溯到根类object,比如如下的继承树:

 

 

对于静态语言(例如Java)来说,如果需要传入Animal类型,则传入的对象必须是Animal类型或者它的子类,否则,将无法调用run()方法。

 

对于Python这样的动态语言来说,则不一定需要传入Animal类型。我们只需要保证传入的对象有一个run()方法就可以了。

 

这就是动态语言的“鸭子类型”,它并不要求严格的继承体系,一个对象只要“看起来像鸭子,走起路来像鸭子”,那它就可以被看做是鸭子。

 

获取对象信息

 

当拿到一个对象的引用时,如何知道这个对象是什么类型、有哪些方法呢?

 

判断对象类型,使用type()函数:

In [305]:
type(1024)
Out[305]:
int
In [306]:
type('行无际')
Out[306]:
str
In [307]:
type(None)
Out[307]:
NoneType
In [308]:
type(abs)
Out[308]:
builtin_function_or_method
In [309]:
type(a1)
Out[309]:
__main__.Dog
In [310]:
type(a2)
Out[310]:
__main__.Cat
In [311]:
type(1024) == int
Out[311]:
True
In [312]:
import types
type(lambda x: x) == types.LambdaType
Out[312]:
True
 

对于class的继承关系来说,使用type()就很不方便。我们要判断class的类型,可以使用isinstance()函数。

In [313]:
isinstance(a2, Animal)
Out[313]:
True
In [314]:
isinstance(a2, Cat)
Out[314]:
True
In [315]:
isinstance(1024, int)
Out[315]:
True
In [316]:
isinstance('行无际', str)
Out[316]:
True
In [317]:
isinstance([], list)
Out[317]:
True
 

还可以判断一个变量是否是某些类型中的一种:

In [318]:
isinstance([1, 2, 3], (list, tuple))
Out[318]:
True
In [319]:
isinstance({1, 2, 3}, (set, dict))
Out[319]:
True
In [320]:
isinstance(a1, (Dog, Cat))
Out[320]:
True
 

使用isinstance()判断类型,可以将指定类型及其子类“一网打尽”。

 

如果要获得一个对象的所有属性和方法,可以使用dir()函数,它返回一个包含字符串的list,比如,获得一个str对象的所有属性和方法:

In [321]:
dir(a1)
Out[321]:
['__class__',
'__delattr__',
'__dict__',
'__dir__',
'__doc__',
'__eq__',
'__format__',
'__ge__',
'__getattribute__',
'__gt__',
'__hash__',
'__init__',
'__init_subclass__',
'__le__',
'__lt__',
'__module__',
'__ne__',
'__new__',
'__reduce__',
'__reduce_ex__',
'__repr__',
'__setattr__',
'__sizeof__',
'__str__',
'__subclasshook__',
'__weakref__',
'run']
In [322]:
dir(Dog)
Out[322]:
['__class__',
'__delattr__',
'__dict__',
'__dir__',
'__doc__',
'__eq__',
'__format__',
'__ge__',
'__getattribute__',
'__gt__',
'__hash__',
'__init__',
'__init_subclass__',
'__le__',
'__lt__',
'__module__',
'__ne__',
'__new__',
'__reduce__',
'__reduce_ex__',
'__repr__',
'__setattr__',
'__sizeof__',
'__str__',
'__subclasshook__',
'__weakref__',
'run']
 

类似__xxx__的属性和方法在Python中都是有特殊用途的,比如__len__方法返回长度。在Python中,如果你调用len()函数试图获取一个对象的长度,实际上,在len()函数内部,它自动去调用该对象的__len__()方法

In [323]:
len('行无际')
Out[323]:
3
In [324]:
'行无际'.__len__()
Out[324]:
3
 

自己写的类,如果也想用len(myObj)的话,就自己写一个__len__()方法

In [325]:
class MyDog(object):
def __len__(self):
return 100 len(MyDog())
Out[325]:
100
 

仅仅把属性和方法列出来是不够的,配合getattr()setattr()以及hasattr(),我们可以直接操作一个对象的状态。看起来有点像Java或者Golang语言里面的反射哦。

In [326]:
hero = Hero('令狐冲', '独孤九剑')

# 有属性'_Hero__name'吗?
hasattr(hero, '_Hero__name')
Out[326]:
True
In [327]:
# 有属性'wife'吗?
hasattr(hero, 'wife')
Out[327]:
False
In [328]:
# 设置一个属性'wife'
setattr(hero, 'wife', '任盈盈') # 有属性'wife'吗?
hasattr(hero, 'wife')
Out[328]:
True
In [329]:
# 获取属性'wife'
getattr(hero, 'wife')
Out[329]:
'任盈盈'
 

也可以获得对象的方法

In [330]:
hasattr(hero, 'say_hi')
Out[330]:
True
In [331]:
hi = getattr(hero, 'say_hi')
hi
Out[331]:
<bound method Hero.say_hi of <__main__.Hero object at 0x1136c6d30>>
In [332]:
hi()
 
大家好,我是令狐冲,我会独孤九剑
 

要注意的是,只有在不知道对象信息的时候,才会去获取对象信息然后尝试操作对象状态。一个正确的用法的例子如下:

In [333]:
def readImage(fp):
if hasattr(fp, 'read'):
return readData(fp)
return None
 

实例属性和类属性

 

给实例绑定属性的方法是通过实例变量,或者通过self变量。

 

如果Hero类本身需要绑定属性呢?可以直接在class中定义属性,这种属性是类属性,归Hero类所有:

In [334]:
class Hero(object):
book = '射雕英雄传'
count = 0 def __init__(self, name, skill):
self.name = name
self.skill = skill Hero.count += 1 def say_hi(self):
print(f'大家好,我是{self.name},我会{self.skill}')
In [335]:
Hero.book
Out[335]:
'射雕英雄传'
In [336]:
Hero.count
Out[336]:
0
 

这个属性虽然归类所有,但类的所有实例都可以访问到。

In [337]:
hero = Hero('郭靖', '降龙十八掌')
In [338]:
hero.book
Out[338]:
'射雕英雄传'
In [339]:
hero.count
Out[339]:
1
In [340]:
# 给实例绑定book属性
hero.book = '神雕侠侣' # 由于实例属性优先级比类属性高,会屏蔽掉类的book属性
hero.book
Out[340]:
'神雕侠侣'
In [341]:
# 类属性没有被修改
Hero.book
Out[341]:
'射雕英雄传'
In [342]:
# 删除实例的book属性
del hero.book # 由于实例的book属性没有找到,类的book属性就显示出来了
hero.book
Out[342]:
'射雕英雄传'
 

可以看出,在编写程序的时候,千万不要对实例属性和类属性使用相同的名字,因为相同名称的实例属性将屏蔽掉类属性,但是当你删除实例属性后,再使用相同的名称,访问到的将是类属性。程序的可读性非常差,给自己找麻烦。

 

使用slots

 

创建了一个class的实例后,我们可以给该实例绑定任何属性和方法,这就是动态语言的灵活性。

 

但是,如果我们想要限制实例的属性怎么办?比如,只允许对Hero实例添加nameskill属性。

 

为了达到限制的目的,Python允许在定义class的时候,定义一个特殊的__slots__变量,来限制该class实例能添加的属性:

In [343]:
class Hero(object):
# 用tuple定义允许绑定的属性名称
__slots__ = ('name', 'skill') def __init__(self, name):
self.name = name
In [344]:
hero = Hero('杨过')
hero.skill = '黯然销魂掌'
hero.skill
Out[344]:
'黯然销魂掌'
In [345]:
import logging

try:
hero.wife = '小龙女'
except AttributeError as e:
logging.exception(e)
 
ERROR:root:'Hero' object has no attribute 'wife'
Traceback (most recent call last):
File "<ipython-input-345-4d5477712a42>", line 4, in <module>
hero.wife = '小龙女'
AttributeError: 'Hero' object has no attribute 'wife'
 

由于wife没有被放到__slots__中,所以不能绑定wife属性,试图绑定wife将得到AttributeError的错误。

 

使用__slots__要注意,__slots__定义的属性仅对当前类实例起作用,对继承的子类是不起作用的。

In [346]:
class ChineseHero(Hero):
pass h = ChineseHero('中国人')
h.country = '中国'
h.country
Out[346]:
'中国'
 

除非在子类中也定义__slots__,这样,子类实例允许定义的属性就是自身的__slots__加上父类的__slots__

 

使用@property

 

在绑定属性时,如果直接把属性暴露出去,虽然写起来很简单,但是,没办法检查参数,导致可以把对象属性随便改:

In [347]:
hero = Hero('杨过')
hero.name = '郭靖'
hero.name
Out[347]:
'郭靖'
 

这可能就不符合实际业务逻辑。

  • 为了让某属性只读,可以不提供类似set_xxx()的方法;
  • 为了限制某属性的范围,则可以通过一个set_xxx()方法来设置属性,再通过一个get_xxx()来获取属性,这样,在set_xxx()方法里,可以检查参数。
 

但是,这样的调用方法又略显复杂,没有直接用属性这么直接简单。有没有既能检查参数,又可以用类似属性这样简单的方式来访问类的变量呢?

 

还记得装饰器(decorator)可以给函数动态加上功能吗?对于类的方法,装饰器一样起作用。Python内置的@property装饰器就是负责把一个方法变成属性调用的。

In [348]:
class Hero(object):

    def __init__(self, name):
self._name = name @property
def name(self):
return self._name @property
def wife(self):
return self._wife @wife.setter
def wife(self, value):
if len(value) == 0:
print("wife不能为空")
else:
self._wife = value
 

@property可以把一个getter方法变成属性,@xxx.setter把一个setter方法变成属性赋值,于是,就拥有一个可控的属性操作。

In [349]:
hero = Hero('令狐冲')
# 实际转化为hero.get_name()
hero.name
Out[349]:
'令狐冲'
In [350]:
try:
hero.name = '李寻欢'
except AttributeError as e:
print(e)
 
can't set attribute
 

name就是一个只读属性。

In [351]:
# 实际转化为hero.set_wife('')
hero.wife = ''
 
wife不能为空
In [352]:
hero.wife = '任盈盈'
hero.wife
Out[352]:
'任盈盈'
 

wife是可读写属性。并且在设置属性时对参数做了非空检验。

 

@property广泛应用在类的定义中,可以让调用者写出简短的代码,同时保证对参数进行必要的检查,这样,程序运行时就减少了出错的可能性。

 

多重继承

 

继承是面向对象编程的一个重要的方式,因为通过继承,子类就可以扩展父类的功能。

In [353]:
class Animal(object):
def say_hi(self):
print('hi, Animal...')
 

现在,如果要给动物再加上RunnableFlyable的功能,只需要先定义好RunnableFlyable的类:

In [354]:
class Runnable(object):
def run(self):
print('Running...') class Flyable(object):
def fly(self):
print('Flying...')
 

对于需要Runnable功能的动物,就多继承一个Runnable,例如Dog

In [355]:
class Dog(Animal, Runnable):
pass dog = Dog()
dog.say_hi()
dog.run()
 
hi, Animal...
Running...
 

对于需要Flyable功能的动物,就多继承一个Flyable,例如Bird

In [356]:
class Bird(Animal, Flyable):
pass bird = Bird()
bird.say_hi()
bird.fly()
 
hi, Animal...
Flying...
 

通过多重继承,一个子类就可以同时获得多个父类的所有功能。

 

在设计类的继承关系时,通常,主线都是单一继承下来的,例如,Bird继承自Animal。但是,如果需要混入额外的功能,通过多重继承就可以实现,比如,让Bird除了继承自Animal外,再同时继承Flyable。这种设计通常称之为MixIn

 

为了更好地看出继承关系,可以把RunnableFlyable改为RunnableMixInFlyableMixIn

 

MixIn的目的就是给一个类增加多个功能,这样,在设计类的时候,我们优先考虑通过多重继承来组合多个MixIn的功能,而不是设计多层次的复杂的继承关系。

 

Python自带的很多库也使用了MixIn。举个例子,Python自带了TCPServerUDPServer这两类网络服务,而要同时服务多个用户就必须使用多进程或多线程模型,这两种模型由ForkingMixInThreadingMixIn提供。通过组合,我们就可以创造出合适的服务来。

 

比如,编写一个多进程模式的TCP服务,定义如下:

In [357]:
from socketserver import TCPServer
from socketserver import ForkingMixIn class MyTCPServer(TCPServer, ForkingMixIn):
pass
 

编写一个多线程模式的UDP服务,定义如下:

In [358]:
from socketserver import UDPServer
from socketserver import ThreadingMixIn class MyUDPServer(UDPServer, ThreadingMixIn):
pass
 

这样一来,我们不需要复杂而庞大的继承链,只要选择组合不同的类的功能,就可以快速构造出所需的子类。

 

定制类

 

看到类似__slots__这种形如__xxx__的变量或者函数名就要注意,这些在Python中是有特殊用途的。

 

__slots__我们已经知道怎么用了,__len__()方法我们也知道是为了能让class作用于len()函数。

 

除此之外,Python的class中还有许多这样有特殊用途的函数,可以帮助我们定制类。

 

__str__

 

先定义一个Hero类,打印一个实例:

In [359]:
class Hero(object):
def __init__(self, name):
self.name = name print(Hero('张无忌'))
 
<__main__.Hero object at 0x113646a60>
 

打印出一堆<__main__.Hero object at 0x...>,不好看。

 

怎么才能打印得好看呢?只需要定义好__str__()方法,返回一个好看的字符串就可以了:

In [360]:
class Hero(object):
def __init__(self, name):
self.name = name def __str__(self):
return f'Hero object (name: {self.name})' print(Hero('张无忌'))
 
Hero object (name: 张无忌)
 

这样打印出来的实例,不但好看,而且容易看出实例内部重要的数据。

 

但是细心的朋友会发现直接敲变量不用print,打印出来的实例还是不好看:

In [361]:
Hero('张无忌')
Out[361]:
<__main__.Hero at 0x113661700>
 

这是因为直接显示变量调用的不是__str__(),而是__repr__(),两者的区别是__str__()返回用户看到的字符串,而__repr__()返回程序开发者看到的字符串,也就是说,__repr__()是为调试服务的。

 

解决办法是再定义一个__repr__()。但是通常__str__()__repr__()代码都是一样的,所以,有个偷懒的写法:

In [362]:
class Hero(object):
def __init__(self, name):
self.name = name def __str__(self):
return f'Hero object (name: {self.name})' __repr__ = __str__ Hero('张无忌')
Out[362]:
Hero object (name: 张无忌)
 

__iter__

 

如果一个类想被用于for ... in循环,类似listtuple那样,就必须实现一个__iter__()方法,该方法返回一个迭代对象,然后,Python的for循环就会不断调用该迭代对象的__next__()方法拿到循环的下一个值,直到遇到StopIteration错误时退出循环。

 

以斐波那契数列为例,写一个Fib类,可以作用于for循环:

In [363]:
class Fib(object):
def __init__(self):
# 初始化两个计数器a,b
self.a, self.b = 0, 1 def __iter__(self):
# 实例本身就是迭代对象,故返回自己
return self def __next__(self):
# 计算下一个值
self.a, self.b = self.b, self.a + self.b
# 退出循环的条件
if self.a > 20:
raise StopIteration()
# 返回下一个值
return self.a for n in Fib():
print(n)
 
1
1
2
3
5
8
13
 

__getitem__

 

Fib实例虽然能作用于for循环,看起来和list有点像,但是,把它当成list来使用还是不行,比如,取第5个元素:

In [364]:
try:
Fib()[5]
except TypeError as e:
print(e)
 
'Fib' object is not subscriptable
 

要表现得像list那样按照下标取出元素,需要实现__getitem__()方法:

In [365]:
class Fib(object):
def __getitem__(self, n):
a, b = 1, 1
for x in range(n):
a, b = b, a + b
return a
In [366]:
f = Fib()
In [367]:
f[0]
Out[367]:
1
In [368]:
f[5]
Out[368]:
8
In [369]:
f[6]
Out[369]:
13
 

此外,如果把对象看成dict__getitem__()的参数也可能是一个可以作keyobject,例如str

 

与之对应的是__setitem__()方法,把对象视作listdict来对集合赋值。最后,还有一个__delitem__()方法,用于删除某个元素。

 

总之,通过上面的方法,我们自己定义的类表现得和Python自带的listtupledict没什么区别,这完全归功于动态语言的“鸭子类型”,不需要强制继承某个接口。

 

__getattr__

 

正常情况下,当调用类的方法或属性时,如果不存在,就会报错。

 

要避免这个错误,除了可以加上这个属性外,Python还有另一个机制,那就是写一个__getattr__()方法,动态返回一个属性。当调用不存在的属性时,比如friend,Python解释器会试图调用__getattr__(self, 'friend')来尝试获得属性,这样,我们就有机会返回friend的值。

In [370]:
class Hero(object):
def __init__(self, name):
self.name = name def __getattr__(self, attr):
if attr == 'friend':
return '有朋自远方来,不亦乐乎!'
elif attr == 'count':
return lambda: 1024 hero = Hero('令狐冲')
 

当调用不存在的属性时:

In [371]:
hero.friend
Out[371]:
'有朋自远方来,不亦乐乎!'
 

当调用不存在的方法时:

In [372]:
hero.count()
Out[372]:
1024
 

注意,只有在没有找到属性的情况下,才调用__getattr__,已有的属性不会在__getattr__中查找。

 

此外,注意到任意调用如hero.xxx都会返回None,这是因为我们定义的__getattr__默认返回就是None

In [373]:
print(hero.xxx)
 
None
 

这实际上可以把一个类的所有属性和方法调用全部动态化处理了,不需要任何特殊手段。

 

Python的class允许定义许多定制方法,可以让我们非常方便地生成特定的类。这里只介绍最常用的几个定制方法,还有很多可定制的方法,请参考Python的官方文档。

 

使用枚举类

 

Python也提供了Enum

In [374]:
from enum import Enum

Month = Enum('Month', ('Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'))
 

这样我们就获得了Month类型的枚举类,可以直接使用Month.Jan来引用一个常量,或者枚举它的所有成员:

In [375]:
for name, member in Month.__members__.items():
print(name, '=>', member, ',', member.value)
 
Jan => Month.Jan , 1
Feb => Month.Feb , 2
Mar => Month.Mar , 3
Apr => Month.Apr , 4
May => Month.May , 5
Jun => Month.Jun , 6
Jul => Month.Jul , 7
Aug => Month.Aug , 8
Sep => Month.Sep , 9
Oct => Month.Oct , 10
Nov => Month.Nov , 11
Dec => Month.Dec , 12
 

value属性则是自动赋给成员的int常量,默认从1开始计数。

 

如果需要更精确地控制枚举类型,可以从Enum派生出自定义类:

In [376]:
from enum import Enum, unique

# @unique装饰器可以帮助我们检查保证没有重复值
@unique
class Weekday(Enum):
# Sun的value被设定为0
Sun = 0
Mon = 1
Tue = 2
Wed = 3
Thu = 4
Fri = 5
Sat = 6
 

访问这些枚举类型可以有若干种方法:

In [377]:
Weekday.Mon
Out[377]:
<Weekday.Mon: 1>
In [378]:
Weekday['Mon']
Out[378]:
<Weekday.Mon: 1>
In [379]:
Weekday.Mon.value
Out[379]:
1
In [380]:
Weekday(1)
Out[380]:
<Weekday.Mon: 1>
 

可见,既可以用成员名称引用枚举常量,又可以直接根据value的值获得枚举常量。

 

使用元类

 

动态语言和静态语言最大的不同,就是函数和类的定义,不是编译时定义的,而是运行时动态创建的。

 

使用type()

 

比方说上面我们要定义一个Hero的class,就写一个hero.py模块。当Python解释器载入hero模块时,就会依次执行该模块的所有语句,执行结果就是动态创建出一个Hero的class对象。

In [381]:
hero = Hero('令狐冲')
In [382]:
print(type(hero))
 
<class '__main__.Hero'>
In [383]:
print(type(Hero))
 
<class 'type'>
 

type()函数可以查看一个类型或变量的类型,Hero是一个class,它的类型就是type,而hero是一个实例,它的类型就是class Hero

 

我们说class的定义是运行时动态创建的,而创建class的方法就是使用type()函数。

 

type()函数既可以返回一个对象的类型,又可以创建出新的类型,比如,我们可以通过type()函数创建出Hello类,而无需通过class Hello(object)...的定义

In [384]:
# 先定义函数
def fn(self, name='world'):
print('Hello, %s.' % name) # 创建Hello class
Hello = type('Hello', (object,), dict(hello=fn)) h = Hello()
h.hello()
 
Hello, world.
In [385]:
print(type(Hello))
 
<class 'type'>
In [386]:
print(type(h))
 
<class '__main__.Hello'>
 

要创建一个class对象,type()函数依次传入3个参数:

 
  1. class的名称;
  2. 继承的父类集合,注意Python支持多重继承,如果只有一个父类,别忘了tuple的单元素写法;
  3. class的方法名称与函数绑定,这里我们把函数fn绑定到方法名hello上
 

通过type()函数创建的类和直接写class是完全一样的,因为Python解释器遇到class定义时,仅仅是扫描一下class定义的语法,然后调用type()函数创建出class

 

正常情况下,我们都用class Xxx...来定义类,但是,type()函数也允许我们动态创建出类来,也就是说,动态语言本身支持运行期动态创建类,这和静态语言有非常大的不同,要在静态语言运行期创建类,必须构造源代码字符串再调用编译器,或者借助一些工具生成字节码实现,本质上都是动态编译,会非常复杂。

 

使用metaclass

 

除了使用type()动态创建类以外,要控制类的创建行为,还可以使用metaclass

 

metaclass,直译为元类,简单的解释就是:

当我们定义了类以后,就可以根据这个类创建出实例,所以:先定义类,然后创建实例。 但是如果我们想创建出类呢?那就必须根据metaclass创建出类,所以:先定义metaclass,然后创建类。 连接起来就是:先定义metaclass,就可以创建类,最后创建实例。

 

所以,metaclass允许你创建类或者修改类。换句话说,你可以把类看成是metaclass创建出来的“实例”。

 

先看一个简单的例子,这个metaclass可以给我们自定义的MyList增加一个add方法:

 

定义ListMetaclass,按照默认习惯,metaclass的类名总是以Metaclass结尾,以便清楚地表示这是一个metaclass

In [387]:
# metaclass是类的模板,所以必须从`type`类型派生:
class ListMetaclass(type):
def __new__(cls, name, bases, attrs):
attrs['add'] = lambda self, value: self.append(value)
return type.__new__(cls, name, bases, attrs)
 

有了ListMetaclass,我们在定义类的时候还要指示使用ListMetaclass来定制类,传入关键字参数metaclass

In [388]:
class MyList(list, metaclass=ListMetaclass):
pass
 

当我们传入关键字参数metaclass时,魔术就生效了,它指示Python解释器在创建MyList时,要通过ListMetaclass.__new__()来创建,在此,我们可以修改类的定义,比如,加上新的方法,然后,返回修改后的定义。

 

__new__()方法接收到的参数依次是:

  1. 当前准备创建的类的对象;
  2. 类的名字;
  3. 类继承的父类集合;
  4. 类的方法集合。
 

测试一下MyList是否可以调用add()方法:

In [389]:
L = MyList()
L.add(1)
L.add(0)
L.add(2)
L.add(4)
L
Out[389]:
[1, 0, 2, 4]
 

动态修改有什么意义?直接在MyList定义中写上add()方法不是更简单吗?正常情况下,确实应该直接写,但是,总会遇到需要通过metaclass修改类定义的。

 

其实,读到这里,如果熟悉Java的朋友应该能看出,这与Java中的字节码增强技术非常类似,不过因为Python动态语言的特性,比Java运行时修改类定义要容易许多。

最新文章

  1. [Nginx] - PHP+FPM相关的配置
  2. RHEL7用户管理
  3. POJ-2299 Ultra_QuickSort 线段树+逆序对数
  4. 用户体验测试(UE测试)
  5. ASP.NET Web – 输入的有效性验证
  6. 对&lt;&lt; ubuntu 12.04编译安装linux-3.6.10内核笔记&gt;&gt;的修正
  7. Java实现文件的RSA和DES加密算法
  8. 搭建Struts2开发环境
  9. Codeforces Round#201(div1) D. Lucky Common Subsequence
  10. Git相关操作汇总
  11. 使用外部容器运行spring-boot项目:不使用spring-boot内置容器让spring-boot项目运行在外部tomcat容器中
  12. TensorFlow的Bazel构建文件结构
  13. Python时间的简单使用
  14. web开发——入门篇(上)
  15. python 获取文件目录位置
  16. 软件测试基础Ⅲ(osi7层协议,测试模型,LoadRunner组件,软件质量模型)
  17. HDU 1203 01背包变形题,(新思路)
  18. [CB] 中国超算前100 (联想40 曙光40 浪潮12 国防科大4 华为2 国家并行工程中心2 )
  19. vue 起步_code
  20. 在IE9中检查up6.2配置

热门文章

  1. JUnit5学习之四:按条件执行
  2. linux查看目录文件以及子目录文件大小的命令
  3. C语言:贪心算法之装箱问题
  4. 基于【腾讯云函数】/【GitHub Actions】/【Docker】的每日签到脚本(支持多账号使用)
  5. 使用 SVG transform rotate 解决画框中的数字跟随旋转的问题
  6. iot漏洞mips汇编基础
  7. Debian中的NVIDIA显卡驱动安装——超简单,一行命令
  8. CVE-2015-1635-HTTP.SYS远程执行代码
  9. 从sql语句的角度解刨SqlServer插入语句的并发问题
  10. python基础学习之描述符和装饰器