一文上手Python3
案例参考:廖雪峰——Python教程
用type()
来判断数据类型:
type(1)
int
type(1.0)
float
type('python')
str
type(True)
bool
type(None)
NoneType
type([])
list
type(())
tuple
type({})
dict
type(set())
set
数据类型检查可以用内置函数isinstance()
:
isinstance(1, int)
True
isinstance([1, 2, 3], list)
True
isinstance(('行无际', 'https://www.cnblogs.com/iflyendless/'), tuple)
True
isinstance({}, dict)
True
isinstance({1,}, set)
True
isinstance()
后面也可以跟多个类型,多个类型之间是或的关系:
isinstance(3.14, (int, float))
True
等价于isinstance(3.14, int) or isinstance(3.14, float)
:
isinstance(3.14, int) or isinstance(3.14, float)
True
id()
函数返回对象的唯一标识符,标识符是一个整数。CPython
中id()
函数用于获取对象的内存地址。
id('行无际')
4594512016
id(1)
4553398624
id([])
4593405440
坚持使用4个空格的缩进:
num = -100
if num >= 0:
print(num)
else:
print(-num)
100
在Python中,有两种除法,一种除法是/
:
10 / 3
3.3333333333333335
/
除法计算结果是浮点数,即使是两个整数恰好整除,结果也是浮点数:
9 / 3
3.0
还有一种除法是//
,称为地板除,两个整数的除法仍然是整数:
10 // 3
3
你没有看错,整数的地板除//
永远是整数,即使除不尽。要做精确的除法,使用/
就可以。
因为//
除法只取结果的整数部分,所以Python还提供一个余数运算,可以得到两个整数相除的余数:
10 % 3
1
对于很大的数,例如10000000000
,很难数清楚0的个数。
Python允许在数字中间以_
分隔,因此,写成10_000_000_000
和10000000000
是完全一样的。十六进制数也可以写成0xa1b2_c3d4
10_000_000_000 == 10000000000
True
0xa1b2_c3d4 == 0xa1b2c3d4
True
用科学计数法表示,把10
用e
替代,1.23x10^9
就是1.23e9
,或者12.3e8
,0.000012
可以写成1.2e-5
1.23e9 == 12.3e8
True
0.000012 == 1.2e-5
True
布尔值可以用and
、or
和not
运算:
True and True
True
True and False
False
True or False
True
not 1 > 0
False
空值是Python里一个特殊的值,用None
表示。
None
不能理解为0,因为0是有意义的,而None
是一个特殊的空值。
变量名必须是大小写英文、数字和_的组合,且不能用数字开头。
在Python中,等号=是赋值语句,可以把任意数据类型赋值给变量,同一个变量可以反复赋值,而且可以是不同类型的变量。
a = 123
a
123
a = 'ABC'
a
'ABC'
这种变量本身类型不固定的语言称之为动态语言
,与之对应的是静态语言
。
静态语言(比如说Java)在定义变量时必须指定变量类型,如果赋值的时候类型不匹配,就会报错。
a = 'ABC'
b = a
a = 'XYZ'
print(b)
print(a)
ABC XYZ
a = 'ABC'
Python解释器干了两件事情:
- 在内存中创建了一个'ABC'的字符串
- 在内存中创建了一个名为a的变量,并把它指向'ABC'
b = a
实际上是把变量b指向变量a所指向的数据。
所谓常量就是不能变的变量,比如常用的数学常数π
就是一个常量。
在Python中,通常用全部大写的变量名表示常量。
PI = 3.14159265359
但事实上PI
仍然是一个变量,Python根本没有任何机制保证PI
不会被改变。
所以,用全部大写的变量名表示常量只是一个习惯上的用法,如果你一定要改变变量PI
的值,也没人能拦住你。
字符串是以单引号'
或双引号"
括起来的任意文本。
print("I'm OK")
print('I\'m \"OK\"!')
I'm OK I'm "OK"!
如果字符串里面有很多字符都需要转义,就需要加很多\
。
为了简化,Python还允许用r''
表示''
内部的字符串默认不转义。
print(r'\\\t\\')
\\\t\\
如果字符串内部有很多换行,用\n
写在一行里不好阅读,为了简化,Python允许用'''...'''
的格式表示多行内容。
language = '''
Java
Python
Golang
'''
print(language)
Java Python Golang
多行字符串'''...'''
还可以在前面加上r
使用:
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()
函数把编码转换为对应的字符:
ord('A')
65
chr(65)
'A'
ord('中')
20013
chr(20013)
'中'
如果知道字符的整数编码,还可以用十六进制这么写str
'\u4e2d\u6587'
'中文'
由于Python的字符串类型是str
,在内存中以Unicode
表示,一个字符对应若干个字节。
如果要在网络上传输,或者保存到磁盘上,就需要把str
变为以字节为单位的bytes
。
Python对bytes
类型的数据用带b
前缀的单引号或双引号表示:
x = b'ABC'
要注意区分'ABC'
和b'ABC'
,前者是str
,后者虽然内容显示得和前者一样,但bytes
的每个字符都只占用一个字节。
以Unicode
表示的str
通过encode()
方法可以编码为指定的bytes
:
'ABC'.encode('ascii')
b'ABC'
'中文'.encode('utf-8')
b'\xe4\xb8\xad\xe6\x96\x87'
纯英文的str
可以用ASCII
编码为bytes
,内容是一样的,含有中文的str
可以用UTF-8
编码为bytes
。
含有中文的str
无法用ASCII
编码,因为中文编码的范围超过了ASCII
编码的范围,Python会报错。
反过来,如果我们从网络或磁盘上读取了字节流,那么读到的数据就是bytes
;
要把bytes
变为str
,就需要用decode()
方法:
b'ABC'.decode('ascii')
'ABC'
b'\xe4\xb8\xad\xe6\x96\x87'.decode('utf-8')
'中文'
如果bytes
中包含无法解码的字节,decode()
方法会报错;
如果bytes
中只有一小部分无效的字节,可以传入errors='ignore'
忽略错误的字节:
b'\xe4\xb8\xad\xff'.decode('utf-8', errors='ignore')
'中'
要计算str
包含多少个字符,可以用len()
函数:
len('ABC')
3
len('行无际')
3
len()
函数计算的是str
的字符数,如果换成bytes
,len()
函数就计算字节数。
len(b'ABC')
3
len('行无际'.encode('utf-8'))
9
可见,1个中文字符经过UTF-8
编码后通常会占用3个字节,而1个英文字符只占用1个字节。
在操作字符串时,我们经常遇到str
和bytes
的互相转换。为了避免乱码问题,应当始终坚持使用UTF-8
编码对str
和bytes
进行转换。
由于Python源代码也是一个文本文件,所以,当你的源代码中包含中文的时候,在保存源代码时,就需要务必指定保存为UTF-8
编码。
当Python解释器读取源代码时,为了让它按UTF-8
编码读取,我们通常在文件开头写上这两行:
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
第一行注释是为了告诉Linux/OS X
系统,这是一个Python可执行程序,Windows
系统会忽略这个注释;
第二行注释是为了告诉Python解释器,按照UTF-8
编码读取源代码,否则,你在源代码中写的中文输出可能会有乱码。
Python字符串拼接有下面几种常用方式:
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
表示:
heros = ['李寻欢', '令狐冲', '张无忌', '杨过']
heros
['李寻欢', '令狐冲', '张无忌', '杨过']
type(heros)
list
用len()
函数可以获得list
元素的个数:
len(heros)
4
用索引来访问list
中每一个位置的元素,记得索引是从0
开始的:
heros[0]
'李寻欢'
heros[1]
'令狐冲'
如果要取最后一个元素,除了计算索引位置外,还可以用-1
做索引,直接获取最后一个元素:
heros[-1]
'杨过'
以此类推,可以获取倒数第2个:
heros[-2]
'张无忌'
list
是一个可变的有序表,所以,可以往list
中追加元素到末尾:
heros.append('郭靖')
heros
['李寻欢', '令狐冲', '张无忌', '杨过', '郭靖']
删除list
末尾的元素,用pop()
方法:
heros.pop()
'郭靖'
heros
['李寻欢', '令狐冲', '张无忌', '杨过']
要把某个元素替换成别的元素,可以直接赋值给对应的索引位置:
heros[1] = '风清扬'
heros
['李寻欢', '风清扬', '张无忌', '杨过']
list
里面的元素的数据类型也可以不同。
如果一个list
中一个元素也没有,就是一个空的list
,它的长度为0:
L = []
len(L)
0
另一种有序列表叫元组:tuple
tuple
和list
非常类似,但是tuple
一旦初始化就不能修改。
注意:这里的元组是小括号()
,上面的列表是中括号[]
。
heros = ('李寻欢', '令狐冲', '张无忌', '杨过')
heros
('李寻欢', '令狐冲', '张无忌', '杨过')
type(heros)
tuple
len(heros)
4
它没有append()
,insert()
这样的方法。
其他获取元素的方法和list
是一样的,你可以正常地使用heros[0]
,heros[-1]
,但不能赋值成另外的元素。
heros[0]
'李寻欢'
heros[-1]
'杨过'
不可变的tuple
有什么意义?
因为tuple
不可变,所以代码更安全。如果可能,能用tuple
代替list
就尽量用tuple
。
tuple
所谓的“不变”是说,tuple
的每个元素,指向永远不变。
如果要定义一个空的tuple
,可以写成()
:
type(())
tuple
len(())
0
定义只有1个元素的tuple
必须加一个逗号,
heros = ('风清扬',)
print(type(heros), heros[0])
<class 'tuple'> 风清扬
如果缺少逗号,看看会怎样?
heros = ('风清扬')
print(type(heros), heros[0])
<class 'str'> 风
Python内置了字典(dict
)的支持,dict
全称dictionary
,在其他语言中也称为map
,使用键-值(key-value
)存储,具有极快的查找速度。
举个例子,假设要根据武侠人物的名字查找擅长的武功招式,就适合用dict
实现。
d = {'李寻欢': '小李飞刀', '令狐冲': '独孤九剑', '郭靖': '降龙十八掌'}
d
{'李寻欢': '小李飞刀', '令狐冲': '独孤九剑', '郭靖': '降龙十八掌'}
type(d)
dict
d['令狐冲']
'独孤九剑'
把数据放入dict
的方法,除了初始化时指定外,还可以通过key
放入:
d['张无忌'] = '九阳神功'
d['张无忌']
'九阳神功'
由于一个key
只能对应一个value
,所以,多次对一个key
放入value
,后面的值会把前面的值冲掉:
d['张无忌'] = '乾坤大挪移'
d['张无忌']
'乾坤大挪移'
如果key
不存在,dict
就会报错。
要避免key
不存在的错误,有两种办法:
- 一是通过in判断key是否存在
- 二是通过
dict
提供的get()
方法,如果key
不存在,可以返回None
,或者自己指定的value
'杨过' in d
False
print(d.get('杨过'))
None
d.get('杨过', '黯然销魂掌')
'黯然销魂掌'
要删除一个key
,用pop(key)
方法,对应的value
也会从dict
中删除:
d.pop('张无忌')
'乾坤大挪移'
d
{'李寻欢': '小李飞刀', '令狐冲': '独孤九剑', '郭靖': '降龙十八掌'}
请务必注意:
dict
内部存放的顺序和key
放入的顺序是没有关系的;dict
是用空间来换取时间的一种方法;dict
的key
必须是不可变对象;- 在Python中,字符串、整数等都是不可变的,因此,可以放心地作为
key
。而list
是可变的,就不能作为key
。
set
和dict
类似,也是一组key
的集合,但不存储value
。由于key
不能重复,所以,在set
中,没有重复的key
。
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}
添加元素:
set1.add(6)
set1
{1, 2, 3, 6}
删除元素:
set1.remove(6)
set1
{1, 2, 3}
set
可以看成数学意义上的无序和无重复元素的集合,因此,两个set
可以做数学意义上的交集、并集等操作:
set1 & set2
{2, 3}
set1 | set2
{1, 2, 3, 5}
set
和dict
的唯一区别仅在于没有存储对应的value
。
set
的原理和dict
一样,所以,同样不可以放入可变对象,因为无法判断两个可变对象是否相等,也就无法保证set
内部“不会有重复元素”
对于不变对象来说,调用对象自身的任意方法,也不会改变该对象自身的内容。相反,这些方法会创建新的对象并返回,这样,就保证了不可变对象本身永远是不可变的。
根据Python的缩进规则,如果if
语句判断是True
,就把缩进的两行print
语句执行了,否则,什么也不做;
也可以给if
添加一个else
语句,意思是,如果if
判断是False
,不要执行if
的内容,去把else
执行了。
# 注意不要少写了冒号:
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
,把该判断对应的语句执行后,就忽略掉剩下的elif
和else
:
age = 20
if age >= 6:
print('teenager')
elif age >= 18:
print('adult')
else:
print('kid')
teenager
if
判断条件还可以简写。只要x
是非零数值、非空字符串、非空list
等,就判断为True
,否则为False
:
x = [1, 2, 3]
if x:
print(x)
[1, 2, 3]
input()
返回的数据类型是str
,str
不能直接和整数比较,必须先把str
转换成整数。Python提供了int()
函数来完成类型转换。
s = input('birth: ')
birth = int(s)
if birth < 2000:
print('00前')
else:
print('00后')
birth: 2021 00后
为了让计算机能计算成千上万次的重复运算,我们就需要循环语句。
Python的循环有两种,一种是for...in
循环,依次把list
、tuple
、dict
、set
中的每个元素迭代出来。
names = ['李寻欢', '令狐冲', '郭靖']
for name in names:
print(name)
李寻欢 令狐冲 郭靖
d = {'李寻欢': '小李飞刀', '令狐冲': '独孤九剑', '郭靖': '降龙十八掌'}
for k, v in d.items():
print(k, ':', v)
李寻欢 : 小李飞刀 令狐冲 : 独孤九剑 郭靖 : 降龙十八掌
for x in ...
循环就是把每个元素代入变量x
,然后执行缩进块的语句。
再比如我们想计算1-10
的整数之和,可以用一个sum
变量做累加:
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的整数:
list(range(5))
[0, 1, 2, 3, 4]
sum = 0
for x in range(101):
sum = sum + x
print(sum)
5050
第二种循环是while
循环,只要条件满足,就不断循环,条件不满足时退出循环。
比如我们要计算100以内所有奇数之和,也可以用while
循环实现:
sum = 0
n = 99
while n > 0:
sum += n
n -= 2
print(sum)
2500
与其他编程语言类似:
break
可以在循环过程中直接退出循环;continue
可以提前结束本轮循环,并直接开始下一轮循环
Python内置了很多有用的函数,我们可以直接调用。可以在交互式命令行通过help(abs)
查看abs
函数的帮助信息:
help(abs)
Help on built-in function abs in module builtins: abs(x, /) Return the absolute value of the argument.
调用abs
函数:
abs(-3.14)
3.14
abs(100)
100
max
函数可以传入多个参数, 返回最大的元素:
max(1, 10)
10
max(1, 100, 50)
100
Python内置的常用函数还包括数据类型转换函数。比如int()
函数可以把其他数据类型转换为整数。
int('123') + 7
130
int(12.34)
12
float('2.14') + 1
3.14
type(str(1.23))
str
bool(1)
True
bool(0)
False
bool('')
False
函数名其实就是指向一个函数对象的引用。
完全可以把函数名赋给一个变量,相当于给这个函数起了一个“别名”。
# 变量a指向abs函数
a = abs
# 所以也可以通过a调用abs函数
a(-1)
1
在Python中,定义一个函数要使用def
语句,依次写出函数名、括号、括号中的参数和冒号:
,然后,在缩进块中编写函数体,函数的返回值用return
语句返回。
以自定义一个求绝对值的my_abs
函数为例:
def my_abs(x):
return x if x >= 0 else -x
my_abs(-100)
100
如果没有return
语句,函数执行完毕后也会返回结果,只是结果为None
。
return None
可以简写为return
。
如果想定义一个什么事也不做的空函数,可以用pass
语句:
def nop():
pass
pass
语句什么都不做,那有什么用?
实际上pass
可以用来作为占位符,比如现在还没想好怎么写函数的代码,就可以先放一个pass
,让代码能运行起来。
pass
还可以用在其他语句里, 如果缺少了pass
,代码运行就会有语法错误:
if age >= 18:
pass
Python中函数可以返回多个值:
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函数返回的仍然是单一值:
t = swap(10, 20)
print(type(t), t)
<class 'tuple'> (20, 10)
原来返回值是一个tuple
!
但是,在语法上,返回一个tuple
可以省略括号,而多个变量可以同时接收一个tuple
,按位置赋给对应的值。
所以,Python的函数返回多值其实就是返回一个tuple
,但写起来更方便。
定义函数的时候,把参数的名字和位置确定下来,函数的接口定义就完成了。
Python的函数定义非常简单,但灵活度却非常大。
除了正常定义的必选参数外,还可以使用默认参数、可变参数和关键字参数。
使得函数定义出来的接口,不但能处理复杂的参数,还可以简化调用者的代码。
先写一个计算x
的平方的函数:
def power(x):
return x * x
power(10)
100
如果要计算x
的3次方怎么办?
可以再定义一个power3
函数,但是如果要计算x
的4次方、5次方……怎么办?
可以把power(x)
修改为power(x, n)
,用来计算x
的n
次方:
def power(x, n):
s = 1
while n > 0:
n -= 1
s *= x
return s
调用函数时,传入的两个值按照位置顺序依次赋给参数x
和n
:
power(2, 10)
1024
新的power(x, n)
函数定义没有问题。但是,旧的调用代码失败了,原因是增加了一个参数,导致旧的函数因为缺少一个参数而无法正常调用。
这个时候,默认参数就排上用场了。由于我们经常计算x
的平方,所以,完全可以把第二个参数n
的默认值设定为2
:
def power(x, n=2):
s = 1
while n > 0:
n -= 1
s *= x
return s
power(10)
100
power(2, 10)
1024
使用默认参数有什么好处?最大的好处是能降低调用函数的难度。
比如上面的例子: 在求一个数在平方时,我不需要传递n=2
这个参数,而一旦需要更复杂的调用时,又可以传递更多的参数来实现。
无论是简单调用还是复杂调用,函数只需要定义一个。
设置默认参数时,有几点要注意:
- 必选参数在前,默认参数在后;
- 把变化大的参数放前面,变化小的参数放后面。变化小的参数就可以作为默认参数;
- 默认参数要牢记一点:默认参数必须指向不可变对象!
下面来看一个小案例:
def add_end(L=[]):
L.append('END')
return L
add_end()
['END']
add_end()
['END', 'END']
add_end()
['END', 'END', 'END']
对于上面的结果,有没有很疑惑?
原来,Python函数在定义的时候,默认参数L
的值就被计算出来了,即[]
,因为默认参数L
也是一个变量,它指向对象[]
。
每次调用该函数,如果改变了L
的内容,则下次调用时,默认参数的内容就变了,不再是函数定义时的[]
了。
要修改上面的例子,可以用None
这个不变对象来实现:
def add_end(L=None):
if L is None:
L = []
L.append('END')
return L
add_end()
['END']
add_end()
['END']
回过头来再看,为什么要设计str
、None
这样的不变对象呢?
因为不变对象一旦创建,对象内部的数据就不能修改,这样就减少了由于修改数据导致的错误。
此外,由于对象不变,多任务环境下同时读取对象不需要加锁,同时读一点问题都没有。
在编写程序时,如果可以设计一个不变对象,那就尽量设计成不变对象。
在Python函数中,还可以定义可变参数。
顾名思义,可变参数就是传入的参数个数是可变的,可以是1个、2个到任意个,还可以是0个。
以数学题为例子,给定一组数字a,b,c...
,请计算a*a + b*b + c*c + ...
要定义出这个函数,我们必须确定输入的参数。由于参数个数不确定,首先可以想到把参数设计为一个tuple
传进来。
def calc(nums):
sum = 0
for n in nums:
sum = sum + n * n
return sum
调用的时候,需要先组装出一个tuple
:
nums = (1, 2, 3)
calc(nums)
14
但是这样对于调用方来说 可能显得有点麻烦,能不能有更方便的调用方式呢?
把函数的参数改为可变参数,仅仅在参数前面加了一个*
号:
def calc(*nums):
sum = 0
for n in nums:
sum = sum + n * n
if len(nums) == 3:
print(type(nums))
return sum
调用该函数时,可以传入任意个参数,包括0个参数:
calc()
0
# 传入1个参数
calc(1)
1
# 传入2个参数
calc(1, 2)
5
# 传入3个参数
calc(1, 2, 3)
<class 'tuple'>
14
原来,在函数内部,可变参数nums
接收到的是一个tuple
。
如果已经有一个list
或者tuple
,要调用一个可变参数怎么办?
Python允许在list
或tuple
前面加一个*
号,把list
或tuple
的元素变成可变参数传进去:
nums = [1, 2, 3, 4]
calc(*nums)
30
可变参数允许你传入0个或任意个参数,这些可变参数在函数调用时自动组装为一个tuple
;
关键字参数允许你传入0个或任意个含参数名的参数,这些关键字参数在函数内部自动组装为一个dict
:
def person(name, age, **kw):
print('name:', name, 'age:', age, type(kw), kw)
person('令狐冲', 20)
name: 令狐冲 age: 20 <class 'dict'> {}
person('令狐冲', 20, skill='独孤九剑', wife='任盈盈')
name: 令狐冲 age: 20 <class 'dict'> {'skill': '独孤九剑', 'wife': '任盈盈'}
关键字参数有什么用?它可以扩展函数的功能。
比如,在person
函数里,除了用户名和年龄是必填项外,其他都是可选项,利用关键字参数来定义这个函数就能满足注册的需求。
和可变参数类似,也可以先组装出一个dict
,然后,把该dict
转换为关键字参数传进去。
d = {'skill': '独孤九剑', 'wife': '任盈盈'}
person('令狐冲', 20, **d)
name: 令狐冲 age: 20 <class 'dict'> {'skill': '独孤九剑', 'wife': '任盈盈'}
**d
表示把d
这个dict
的所有key-value
用关键字参数传入到函数的**kw
参数,kw
将获得一个dict
。
注意:kw
获得的dict
是d
的一份拷贝,对kw
的改动不会影响到函数外的d
。
下面来证明一下:
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': '任盈盈'}
既然讲到这里,也同时来证明一下可变参数是否与关键字参数类似?
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
不同,命名关键字参数需要一个特殊分隔符*
,*
后面的参数被视为命名关键字参数。
例如,只接收skill
和wife
作为关键字参数:
def person(name, age, *, skill, wife):
print(name, age, skill, wife)
person('郭靖', 18, skill='降龙十八掌', wife='黄蓉')
郭靖 18 降龙十八掌 黄蓉
如果函数定义中已经有了一个可变参数,后面跟着的命名关键字参数就不再需要一个特殊分隔符*
了:
def person(name, age, *args, skill, wife):
print(name, age, args, skill, wife)
person('郭靖', 18, '射雕英雄传', skill='降龙十八掌', wife='黄蓉')
郭靖 18 ('射雕英雄传',) 降龙十八掌 黄蓉
命名关键字参数必须传入参数名,如果没有传入参数名,调用将报错。
命名关键字参数可以有缺省值,从而简化调用:
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种参数都可以组合使用。
注意,参数定义的顺序必须是:必选参数、默认参数、可变参数、命名关键字参数和关键字参数。
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}
最神奇的是通过一个tuple
和dict
,你也可以调用上述函数:
args = (1, 2, 3, 4)
kw = {'d': 99, 'x': '#'}
f1(*args, **kw)
a = 1 b = 2 c = 3 args = (4,) kw = {'d': 99, 'x': '#'}
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)
,又可以先组装list
或tuple
,再通过*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
时需要特殊处理:
def fact(n):
if n==1:
return 1
return n * fact(n - 1)
fact(1)
1
fact(3)
6
fact(5)
120
对经常取指定索引范围的操作,用循环十分繁琐,因此,Python提供了切片(Slice
)操作符,能大大简化这种操作。
L = ['李寻欢', '令狐冲', '张无忌', '杨过']
# 取前3个元素
L[0:3]
['李寻欢', '令狐冲', '张无忌']
L[0:3]
表示,从索引0开始取,直到索引3为止,但不包括索引3。即索引0,1,2,正好是3个元素。
# 如果第一个索引是0,还可以省略
L[:3]
['李寻欢', '令狐冲', '张无忌']
# 从索引1开始,取出2个元素出来
L[1:3]
['令狐冲', '张无忌']
Python支持L[-1]取倒数第一个元素,它同样支持倒数切片。
L[-2:]
['张无忌', '杨过']
记住倒数第一个元素的索引是-1。
下面来个实战。
# 先创建一个0-19的数列
L = list(range(20))
L
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]
# 取前10个数
L[:10]
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
# 取后10个数
L[-10:]
[10, 11, 12, 13, 14, 15, 16, 17, 18, 19]
# 前11-20个数
L[10:20]
[10, 11, 12, 13, 14, 15, 16, 17, 18, 19]
# 前10个数,每两个取一个
L[:10:2]
[0, 2, 4, 6, 8]
# 所有数,每5个取一个
L[::5]
[0, 5, 10, 15]
# 什么都不写,只写[:]就可以原样复制一个list
L[:]
[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
(0, 1, 2, 3, 4, 5)[:3]
(0, 1, 2)
字符串'xxx'也可以看成是一种list
,每个元素就是一个字符。因此,字符串也可以用切片操作,只是操作结果仍是字符串。
'iflyendless'[:4]
'ifly'
'iflyendless'[::3]
'iyds'
在很多编程语言中,针对字符串提供了很多各种截取函数(例如,substring
),其实目的就是对字符串切片。Python没有针对字符串的截取函数,只需要切片一个操作就可以完成,非常简单。
给定一个list
或tuple
,我们可以通过for
循环来遍历这个list
或tuple
,这种遍历我们称为迭代(Iteration
)。
Python的for
循环不仅可以用在list
或tuple
上,还可以作用在其他可迭代对象上。
list
这种数据类型虽然有下标,但很多其他数据类型是没有下标的,但是,只要是可迭代对象,无论有无下标,都可以迭代。
# 遍历列表
for x, y in [(1, 1), (2, 4), (3, 9)]:
print(x, y)
1 1 2 4 3 9
# 遍历元组
for e in ('李寻欢', 30, '小李飞刀'):
print(e)
李寻欢 30 小李飞刀
# 遍历集合
for name in {'李寻欢', '令狐冲', '张无忌', '杨过'}:
print(name)
李寻欢 张无忌 杨过 令狐冲
下面看如何遍历字典:
d = {'李寻欢': '小李飞刀', '令狐冲': '独孤九剑', '郭靖': '降龙十八掌'}
# 遍历字典的key
for key in d:
print(key)
李寻欢 令狐冲 郭靖
默认情况下,dict
迭代的是key
。
# 遍历字典的value
for value in d.values():
print(value)
小李飞刀 独孤九剑 降龙十八掌
# 同时遍历key和value
for k, v in d.items():
print(k, v)
李寻欢 小李飞刀 令狐冲 独孤九剑 郭靖 降龙十八掌
字符串也是可迭代对象,因此,也可以作用于for
循环:
for ch in '行无际的博客':
print(ch)
行 无 际 的 博 客
所以,当使用for
循环时,只要作用于一个可迭代对象,for
循环就可以正常运行,而不需要关心该对象究竟是list
还是其他数据类型。
那么,如何判断一个对象是可迭代对象呢?方法是通过collections
模块的Iterable
类型判断:
from collections.abc import Iterable
isinstance('https://www.cnblogs.com/iflyendless/', Iterable)
True
isinstance([1,2,3], Iterable)
True
isinstance(123, Iterable)
False
如果要对list
实现类似Java那样的下标循环怎么办?Python内置的enumerate
函数可以把一个list
变成索引-
元素对,这样就可以在for
循环中同时迭代索引和元素本身:
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))
:
list(range(1, 11))
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
但如果要生成[1x1, 2x2, 3x3, ..., 10x10]
怎么做?列表生成式则可以用一行语句生成:
[x * x for x in range(1, 11)]
[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
写列表生成式时,把要生成的元素x * x
放到前面,后面跟for
循环,就可以把list
创建出来,十分有用,多写几次,很快就可以熟悉这种语法。
for
循环后面还可以加上if
判断,这样就可以筛选出仅偶数的平方:
[x * x for x in range(1, 11) if x % 2 == 0]
[4, 16, 36, 64, 100]
还可以使用两层循环,可以生成全排列:
[x + y for x in 'ABC' for y in '12']
['A1', 'A2', 'B1', 'B2', 'C1', 'C2']
运用列表生成式,可以写出非常简洁的代码。下面看几个例子:
列表生成式也可以使用两个变量来生成list
:
d = {'李寻欢': '小李飞刀', '令狐冲': '独孤九剑', '郭靖': '降龙十八掌'}
[k + '=' + v for k, v in d.items()]
['李寻欢=小李飞刀', '令狐冲=独孤九剑', '郭靖=降龙十八掌']
把一个list
中所有的字符串变成小写:
L = ['JAVA', 'SCALA', 'PYTHON', 'GOLANG']
[x.lower() for x in L]
['java', 'scala', 'python', 'golang']
[x if x % 2 == 0 else -x for x in range(1, 11)]
[-1, 2, -3, 4, -5, 6, -7, 8, -9, 10]
在一个列表生成式中,for
前面的if ... else
是表达式,而for
后面的if
是过滤条件,不能带else
。
通过列表生成式,可以直接创建一个列表。但是,受到内存限制,列表容量肯定是有限的。而且,创建一个包含100万个元素的列表,不仅占用很大的存储空间,如果我们仅仅需要访问前面几个元素,那后面绝大多数元素占用的空间都白白浪费了。
所以,如果列表元素可以按照某种算法推算出来,那是否可以在循环的过程中不断推算出后续的元素呢?这样就不必创建完整的list
,从而节省大量的空间。在Python中,这种一边循环一边计算的机制,称为生成器:generator
。
要创建一个generator
,有很多种方法。第一种方法很简单,只要把一个列表生成式的[]
改成()
,就创建了一个generator
:
g = (x * x for x in range(10))
g
<generator object <genexpr> at 0x11364ee40>
如果要一个一个打印出来,可以通过next()
函数获得generator
的下一个返回值:
next(g)
0
next(g)
1
next(g)
4
generator
保存的是算法,每次调用next(g)
,就计算出g
的下一个元素的值,直到计算到最后一个元素,没有更多的元素时,抛出StopIteration
的错误。
可以使用for
循环,因为generator
也是可迭代对象:
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, ...
斐波拉契数列用列表生成式写不出来,但是,用函数把它打印出来却很容易:
def fib(max):
n, a, b = 0, 0, 1
while n < max:
print(b)
a, b = b, a + b
n = n + 1
return 'done'
fib(5)
1 1 2 3 5
'done'
仔细观察,可以看出,fib
函数实际上是定义了斐波拉契数列的推算规则,可以从第一个元素开始,推算出后续任意的元素,这种逻辑其实非常类似generator
。
也就是说,上面的函数和generator
仅一步之遥。要把fib
函数变成generator
,只需要把print(b)
改为yield b
就可以了:
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
:
f = fib(6)
f
<generator object fib at 0x11368e660>
这里,最难理解的就是generator
和函数的执行流程不一样。函数是顺序执行,遇到return
语句或者最后一行函数语句就返回。而变成generator
的函数,在每次调用next()
的时候执行,遇到yield
语句返回,再次执行时从上次返回的yield
语句处继续执行。
next(f)
1
next(f)
1
next(f)
2
next(f)
3
next(f)
5
可以直接作用于for
循环的数据类型有以下几种:
- 集合数据类型,如
list
、tuple
、dict
、set
、str
等 generator
,包括生成器和带yield
的generator function
这些可以直接作用于for
循环的对象统称为可迭代对象:Iterable
。
可以使用isinstance()
判断一个对象是否是Iterable
对象:
from collections.abc import Iterable
isinstance([], Iterable)
True
isinstance({}, Iterable)
True
isinstance('abc', Iterable)
True
isinstance((x for x in range(10)), Iterable)
True
isinstance(100, Iterable)
False
而生成器不但可以作用于for
循环,还可以被next()
函数不断调用并返回下一个值,直到最后抛出StopIteration
错误表示无法继续返回下一个值了。
可以被next()
函数调用并不断返回下一个值的对象称为迭代器:Iterator
。
可以使用isinstance()
判断一个对象是否是Iterator
对象:
from collections.abc import Iterator
isinstance((x for x in range(10)), Iterator)
True
isinstance([], Iterator)
False
isinstance({}, Iterator)
False
isinstance('abc', Iterator)
False
生成器都是Iterator
对象,但list
、dict
、str
虽然是Iterable
,却不是Iterator
。
把list
、dict
、str
等Iterable
变成Iterator
可以使用iter()
函数:
isinstance(iter([]), Iterator)
True
isinstance(iter('abc'), Iterator)
True
为什么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()
函数实现的。
for x in [1, 2, 3, 4, 5]:
pass
实际上完全等价于:
# 首先获得Iterator对象:
it = iter([1, 2, 3, 4, 5])
# 循环:
while True:
try:
# 获得下一个值:
x = next(it)
except StopIteration:
# 遇到StopIteration就退出循环
break
函数式编程(请注意多了一个“式”字)——Functional Programming
,虽然也可以归结到面向过程的程序设计,但其思想更接近数学计算。
函数式编程的一个特点就是,允许把函数本身作为参数传入另一个函数,还允许返回一个函数!
变量可以指向函数, 函数本身也可以赋值给变量。
f = abs
f
<function abs(x, /)>
f(-10)
10
函数名也是变量, 函数名其实就是指向函数的变量!对于abs()
这个函数,完全可以把函数名abs
看成变量,它指向一个可以计算绝对值的函数!
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
。
既然变量可以指向函数,函数的参数能接收变量,那么一个函数就可以接收另一个函数作为参数,这种函数就称之为高阶函数。
# 一个最简单的高阶函数
def add(x, y, f):
return f(x) + f(y)
def f1(x):
return x * x
add(2, 3, f1)
13
map()
函数接收两个参数,一个是函数,一个是Iterable
,map
将传入的函数依次作用到序列的每个元素,并把结果作为新的Iterator
返回。
r = map(f1, [1, 2, 3, 4, 5])
list(r)
[1, 4, 9, 16, 25]
map()
传入的第一个参数是f1
,即函数对象本身。由于结果r是一个Iterator
,Iterator
是惰性序列,因此通过list()
函数让它把整个序列都计算出来并返回一个list。
所以,map()
作为高阶函数,事实上它把运算规则抽象了,因此,我们不但可以计算简单的f(x)=x*x
,还可以计算任意复杂的函数,比如,把这个list所有数字转为字符串
list(map(str, [1,2,3]))
['1', '2', '3']
reduce
把一个函数作用在一个序列[x1, x2, x3, ...]
上,这个函数必须接收两个参数,reduce
把结果继续和序列的下一个元素做累积计算,其效果就是:reduce(f, [x1, x2, x3, x4]) = f(f(f(x1, x2), x3), x4)
比方说对一个序列求和,就可以用reduce
实现,当然求和运算可直接用Python内建函数sum()
def add(x, y):
return x + y
from functools import reduce
reduce(add, [1, 2, 3, 4, 5])
15
# 当然还可以用lambda函数进一步简化成
reduce(lambda x, y: x + y, [1, 2, 3, 4, 5])
15
filter()
函数用于过滤序列。
和map()
类似,filter()
也接收一个函数和一个序列。和map()
不同的是,filter()
把传入的函数依次作用于每个元素,然后根据返回值是True
还是False
决定保留还是丢弃该元素。
# 在一个list中,只保留奇数,可以这么写
list(filter(lambda x: x % 2 == 1, [1, 2, 3, 4, 5]))
[1, 3, 5]
# 把一个序列中的空字符串删掉,可以这么写
list(filter(lambda x: x and x.strip(), ['ab','','c',' ','d', None]))
['ab', 'c', 'd']
可见用filter()
这个高阶函数,关键在于正确实现一个“筛选”函数。
注意到filter()
函数返回的是一个Iterator
,也就是一个惰性序列,所以要强迫filter()
完成计算结果,需要用list()
函数获得所有结果并返回list。
排序也是在程序中经常用到的算法。无论使用冒泡排序还是快速排序,排序的核心是比较两个元素的大小。如果是数字,我们可以直接比较,但如果是字符串或者两个dict
呢?直接比较数学上的大小是没有意义的,因此,比较的过程必须通过函数抽象出来。
# Python内置的sorted()函数就可以对list进行排序
sorted([36, 5, -12, 9, -21])
[-21, -12, 5, 9, 36]
此外,sorted()
函数也是一个高阶函数,它还可以接收一个key
函数来实现自定义的排序,例如按绝对值大小排序:
sorted([36, 5, -12, 9, -21], key=lambda x: x if x >= 0 else -x)
[5, 9, -12, -21, 36]
要进行反向排序,不必改动key
函数,可以传入第三个参数reverse=True
sorted([36, 5, -12, 9, -21], key=lambda x: x if x >= 0 else -x, reverse=True)
[36, -21, -12, 9, 5]
从上述例子可以看出,高阶函数的抽象能力是非常强大的,而且,核心代码可以保持得非常简洁。
高阶函数除了可以接受函数作为参数外,还可以把函数作为结果值返回。
# 比如返回一个数学上的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)
21
上面在函数f(a, b)
中又定义了函数y,并且,内部函数y可以引用外部函数的参数a, b,当f(a, b)
返回函数y时,相关参数和变量都保存在返回的函数中,这种称为闭包(Closure)
的程序结构拥有极大的威力。
请再注意一点,当我们调用f(a, b)
时,每次调用都会返回一个新的函数,即使传入相同的参数:
y1 = f(2, 1)
y2 = f(2, 1)
y1 == y2
False
闭包需要注意的问题是,返回的函数并没有立刻执行,而是直到被调用了才执行,下面看个例子。
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,但实际结果是:
f1()
9
f2()
9
f3()
9
全部都是9!原因就在于返回的函数引用了变量i,但它并非立刻执行。等到3个函数都返回时,它们所引用的变量i已经变成了3,因此最终结果为9。
返回闭包时牢记一点:返回函数不要引用任何循环变量,或者后续会发生变化的变量。
在传入函数时,有些时候,不需要显式地定义函数,直接传入匿名函数更方便。下面看一个例子。
list(map(lambda x: x * x, [1, 2, 3, 4, 5]))
[1, 4, 9, 16, 25]
关键字lambda
表示匿名函数,:
前面的x
表示函数参数。
匿名函数有个限制,就是只能有一个表达式,不用写return
,返回值就是该表达式的结果。
用匿名函数有个好处,因为函数没有名字,不必担心函数名冲突。
此外,匿名函数也是一个函数对象,也可以把匿名函数赋值给一个变量,再利用变量来调用该函数。
f = lambda x: x * x
f
<function __main__.<lambda>(x)>
f(6)
36
同样,也可以把匿名函数作为返回值返回,比如:
def f(a, b):
return lambda x: a * x + b
y = f(2, 1)
y
<function __main__.f.<locals>.<lambda>(x)>
y(3)
7
函数对象有一个__name__
属性,可以拿到函数的名字:
def hello():
print("My blog is https://www.cnblogs.com/iflyendless/.")
hello.__name__
'hello'
假设要增强hello()
函数的功能,比如,在函数调用前后自动打印日志,但又不希望修改hello()
函数的定义,这种在代码运行期间动态增加功能的方式,称之为“装饰器”(Decorator
)。
本质上,decorator
就是一个返回函数的高阶函数。所以,我们要定义一个能打印日志的decorator
,可以定义如下:
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
置于函数的定义处:
@log
def hello():
print("My blog is https://www.cnblogs.com/iflyendless/.")
hello()
***Before hello函数*** My blog is https://www.cnblogs.com/iflyendless/. ***After hello函数***
在介绍函数参数的时候,我们知道通过设定参数的默认值,可以降低函数调用的难度。而偏函数也可以做到这一点。
int()
函数可以把字符串转换为整数,当仅传入字符串时,int()
函数默认按十进制转换
int('11')
11
但int()
函数还提供额外的base
参数,默认值为10。如果传入base
参数,就可以做N进制的转换:
int('11', base = 2)
3
如果要转换大量的二进制字符串,每次都传入int(x, base=2)
非常麻烦,于是,可以定义一个int2()
的函数,默认把base=2
传进去,如下:
def int2(x, base=2):
return int(x, base)
# 此时转换二进制就比较方便了
int2('11')
3
functools.partial
就是帮助我们创建一个偏函数的,不需要我们自己定义int2()
,可以直接使用下面的代码创建一个新的函数int2
:
import functools
int2 = functools.partial(int, base=2)
int2('11')
3
所以,简单总结functools.partial
的作用就是,把一个函数的某些参数给固定住(也就是设置默认值),返回一个新的函数,调用这个新函数会更简单。
注意到上面的新的int2
函数,仅仅是把base
参数重新设定默认值为2,但也可以在函数调用时传入其他值:
int2('11', base = 10)
11
创建偏函数时,实际上可以接收函数对象
、*args
和**kw
这3个参数,当传入:
int2 = functools.partial(int, base=2)
实际上固定了int()
函数的关键字参数base,也就是,int2('11')
相当于:
kw = { 'base': 2 }
int('11', **kw)
3
再举个例子:
my_max = functools.partial(max, 10)
my_max(1, 3, 5)
10
实际上会把10
作为*args
的一部分自动加到左边,也就是,my_max(1, 3, 5)
相当于:
args = (10, 1, 3, 5)
max(*args)
10
小结:当函数的参数个数太多,需要简化时,使用functools.partial
可以创建一个新的函数,这个新函数可以固定住原函数的部分参数,从而在调用时更简单。
开发过程中,随着程序代码越写越多,在一个文件里代码就会越来越长,越来越不容易维护。
为了编写可维护的代码,把很多函数分组,分别放到不同的文件里,这样,每个文件包含的代码就相对较少,很多编程语言都采用这种组织代码的方式。在Python中,一个.py
文件就称之为一个模块(Module
)。
使用模块有什么好处?
- 大大提高了代码的可维护性。
- 编写代码不必从零开始。当一个模块编写完毕,就可以被其他地方引用。我们在编写程序的时候,也经常引用其他模块,包括Python内置的模块和来自第三方的模块。
- 避免函数名和变量名冲突。相同名字的函数和变量完全可以分别存在不同的模块中,因此,我们自己在编写模块时,不必考虑名字会与其他模块冲突。但是也要注意,尽量不要与内置函数名字冲突。
补充Python的所有内置函数:
https://docs.python.org/3/library/functions.html
如果不同的人编写的模块名相同怎么办?为了避免模块名冲突,Python又引入了按目录来组织模块的方法,称为包(Package
)。
举个例子,一个abc.py
的文件就是一个名字叫abc
的模块,一个xyz.py
的文件就是一个名字叫xyz
的模块。
现在,假设我们的abc
和xyz
这两个模块名字与其他模块冲突了,于是我们可以通过包来组织模块,避免冲突。方法是选择一个顶层包名,比如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.utils
和mycompany.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
),可以被直接引用,比如:abc
,x123
,PI
等;
类似__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
变量中:
import sys
sys.path
['/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:
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来捕获不同类型的错误:
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')
正常执行的情况如下:
f1('2')
try... result: 5.0 finally... END
参数s
转为int
失败的情况如下:
f1('a')
try... ValueError: invalid literal for int() with base 10: 'a' finally... END
除数为0的情况如下:
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
模块可以非常容易地记录错误信息:
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
语句抛出一个错误的实例:
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已有的内置的错误类型(比如ValueError
,TypeError
),尽量使用Python内置的错误类型。
与其他高级语言同样类似,如果不想处理异常,也可以抛出去让调用方去处理或者继续抛出。
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
)的概念。
来个快速入门面向对象编程的案例,感受一下什么是面向对象。
class Hero(object):
def __init__(self, name, skill):
self.name = name
self.skill = skill
def say_hi(self):
print(f'大家好,我是{self.name},我会{self.skill}')
hero1 = Hero('李寻欢', '小李飞刀')
hero1.say_hi()
大家好,我是李寻欢,我会小李飞刀
hero2 = Hero('令狐冲', '独孤九剑')
hero2.say_hi()
大家好,我是令狐冲,我会独孤九剑
如果用前面介绍的面向过程的思想来实现这一需求,是怎么做的呢?
def say_hi(name, skill):
print(f'大家好,我是{name},我会{skill}')
say_hi('李寻欢', '小李飞刀')
say_hi('令狐冲', '独孤九剑')
大家好,我是李寻欢,我会小李飞刀 大家好,我是令狐冲,我会独孤九剑
咦,好像面向过程的代码更简单一些。实际情况是不是这样呢?如果还有其他的需求呢,比如说Hero
要参加英雄大会,再比如说Hero
在英雄大会上交战过哪几个对手,赢了几场,输了几场。用面向过程来实现的话,且不说每次都需要把name
、skill
等参数传到函数里面去,然后外层还需要用list
数据结构存储交战过几个对手,外层也需要用2个变量存储该Hero
赢了几场,输了几场。另外,如果要记录多个Hero
的情况,用面向过程的思想,就需要在外部维护大量的描述信息与中间状态,程序的复杂度较大,扩展性也较差。而使用面向对象的思想来实现,外层不需要关心这些细节,只需要创建出来Hero
对象,然后到哪一步了,直接调用对象的方法,不需要关心对象中间状态的存储与维护,因为这些信息都统一封装在对象内部了,非常符合软件编程高内聚的设计思想!
物以类聚,人以群分。面向对象的设计思想是从自然界中来的,因为在自然界中,类(Class
)和实例(Instance
)的概念是很自然的。Class
是一种抽象概念,比如我们定义的Class——Hero
,是指武侠人物这个概念,而实例(Instance
)则是一个个具体的武侠人物,比如,李寻欢
和令狐冲
是两个具体的Hero
。
一个Class
既包含数据,又包含操作数据的方法。封装、继承和多态是面向对象的三大特性,随着程序学习的深入,你会自然理解。
面向对象最重要的概念就是类(Class
)和实例(Instance
),必须牢记类是抽象的模板,比如Hero
类,而实例是根据类创建出来的一个个具体的“对象”,每个对象都拥有相同的方法,但各自的数据可能不同。
在Python中,定义类是通过class
关键字:
class Hero(object):
pass
Hero
__main__.Hero
class
后面紧接着是类名,即Hero
,类名通常是大写开头的单词,紧接着是(object
),表示该类是从哪个类继承下来的,继承的概念后面再讲,通常,如果没有合适的继承类,就使用object
类,这是所有类最终都会继承的类。
定义好了Hero
类,就可以根据Hero
类创建出实例,创建实例是通过类名+()
实现的:
hero1 = Hero()
hero1
<__main__.Hero at 0x1136dd490>
可以自由地给一个实例变量绑定属性,比如,给实例hero1
绑定一个name
属性:
hero1.name = '张无忌'
hero1.name
'张无忌'
由于类可以起到模板的作用,因此,可以在创建实例的时候,把一些我们认为必须绑定的属性强制填写进去。通过定义一个特殊的__init__
方法,在创建实例的时候,就把name
,skill
等属性绑上去:
class Hero(object):
def __init__(self, name, skill):
self.name = name
self.skill = skill
注意:特殊方法__init__
前后分别有两个下划线!!!
__init__
方法的第一个参数永远是self
,表示创建的实例本身,因此,在__init__
方法内部,就可以把各种属性绑定到self
,因为self
就指向创建的实例本身。
有了__init__
方法,在创建实例的时候,就不能传入空的参数了,必须传入与__init__
方法匹配的参数,但self
不需要传,Python解释器自己会把实例变量传进去:
hero1 = Hero('张无忌', '乾坤大挪移')
hero1.name
'张无忌'
hero1.skill
'乾坤大挪移'
和普通的函数相比,在类中定义的函数只有一点不同,就是第一个参数永远是实例变量self
,并且,调用时,不用传递该参数。除此之外,类的方法和普通函数没有什么区别,所以,你仍然可以用默认参数、可变参数、关键字参数和命名关键字参数。
既然Hero
实例本身就拥有name
、skill
这些数据,要访问这些数据,就没有必要从外面的函数去访问,可以直接在Hero
类的内部定义访问数据的函数,这样,就把“数据”给封装起来了。这些封装数据的函数是和Hero
类本身是关联起来的,我们称之为类的方法
:
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
不用传递,其他参数正常传入:
hero1 = Hero('张无忌', '乾坤大挪移')
hero1.say_hi()
大家好,我是张无忌,我会乾坤大挪移
这样一来,从外部看Hero
类,就只需要知道,创建实例需要给出name
和skill
,而如何自我介绍,都是在Hero
类的内部定义的,这些数据和逻辑被封装
起来了,调用很容易,但却不用知道内部实现的细节。
小结:
- 类是创建实例的模板,而实例则是一个一个具体的对象,各个实例拥有的数据都互相独立,互不影响;
- 方法就是与实例绑定的函数,和普通函数不同,方法可以直接访问实例的数据;
和静态语言不同,Python允许对实例变量绑定任何数据,也就是说,对于两个实例变量,虽然它们都是同一个类的不同实例,但拥有的变量名称都可能不同:
hero1 = Hero('李寻欢', '小李飞刀')
hero2 = Hero('令狐冲', '独孤九剑')
hero2.wife = '任盈盈'
hero2.wife
'任盈盈'
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内部,可以有属性和方法,而外部代码可以通过直接调用实例变量的方法来操作数据,这样,就隐藏了内部的复杂逻辑。
外部代码还是可以自由地修改一个实例的属性
hero1 = Hero('张无忌', '乾坤大挪移')
hero1.skill
'乾坤大挪移'
hero1.skill = '九阳神功'
hero1.skill
'九阳神功'
如果要让内部属性不被外部访问,可以把属性的名称前加上两个下划线__
,在Python中,实例的变量名如果以__
开头,就变成了一个私有变量(private
),只有内部可以访问,外部不能访问,所以,我们把Hero
类改一改:
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
了:
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'
这样就确保了外部代码不能随意修改对象内部的状态,这样通过访问限制的保护,代码更加健壮。
但是如果外部代码要获取name
和skill
怎么办?可以给Hero
类增加get_name
和get_skill
这样的方法:
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
hero1 = Hero('张无忌', '乾坤大挪移')
hero1.get_skill()
'乾坤大挪移'
如果又要允许外部代码修改skill
怎么办?可以再给Hero
类增加set_skill
方法:
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
hero1 = Hero('张无忌', '乾坤大挪移')
hero1.set_skill('九阳神功')
hero1.get_skill()
'九阳神功'
需要注意的是,在Python中,变量名类似__xxx__
的,也就是以双下划线开头,并且以双下划线结尾的,是特殊变量,特殊变量是可以直接访问的,不是private
变量,所以,不能用__name__
、__skill__
这样的变量名。
有些时候,你会看到以一个下划线开头的实例变量名,比如_name
,这样的实例变量外部是可以访问的,但是,按照约定俗成的规定,当你看到这样的变量时,意思就是,“虽然我可以被访问,但是,请把我视为私有变量,不要随意访问”。
双下划线开头的实例变量是不是一定不能从外部访问呢?
其实也不是。不能直接访问__name
是因为Python解释器对外把__name
变量改成了_Hero__name
,所以,仍然可以通过_Hero__name
来访问__name
变量:
hero1._Hero__name
'张无忌'
但是强烈建议你不要这么干,因为不同版本的Python解释器可能会把__name
改成不同的变量名。
总的来说就是,Python本身没有任何机制阻止你干坏事,一切全靠自觉。
最后注意下面的这种错误写法:
hero1 = Hero('张无忌', '乾坤大挪移')
hero1.get_name()
'张无忌'
hero1.__name = '行无际'
hero1.__name
'行无际'
表面上看,外部代码“成功”地设置了__name
变量,但实际上这个__name
变量和class内部的__name
变量不是一个变量!内部的__name
变量已经被Python解释器自动改成了_Hero__name
,而外部代码给该对象新增了一个__name
变量。不信试试:
hero1.get_name()
'张无忌'
hero1._Hero__name = '行无际'
hero1.get_name()
'行无际'
在OOP
程序设计中,当我们定义一个class
的时候,可以从某个现有的class
继承,新的class称为子类(Subclass
),而被继承的class称为基类、父类或超类(Base class
、Super class
)。
比如,我们已经编写了一个名为Animal
的class,有一个run()
方法可以直接打印:
class Animal(object):
def run(self):
print('Animal is running...')
当我们需要编写Dog
和Cat
类时,就可以直接从Animal
类继承:
class Dog(Animal):
pass
class Cat(Animal):
pass
对于Dog
来说,Animal
就是它的父类,对于Animal
来说,Dog
就是它的子类。
继承有什么好处?最大的好处是子类获得了父类的全部功能。由于Animial
实现了run()
方法,因此,Dog
和Cat
作为它的子类,什么事也没干,就自动拥有了run()
方法:
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...
,因此,对Dog
和Cat
类改进如下:
class Dog(Animal):
def run(self):
print('Dog is running...')
class Cat(Animal):
def run(self):
print('Cat is running...')
再次运行,结果如下:
a1 = Dog()
a2 = Cat()
a1.run()
a2.run()
Dog is running... Cat is running...
当子类和父类都存在相同的run()
方法时,我们说,子类的run()
覆盖了父类的run()
,在代码运行的时候,总是会调用子类的run()
。这样,我们就获得了继承的另一个好处:多态。
判断一个变量是否是某个类型可以用isinstance()
判断:
isinstance(a1, Animal)
True
isinstance(a1, Dog)
True
对于一个变量,我们只需要知道它是Animal
类型,无需确切地知道它的子类型,就可以放心地调用run()
方法,而具体调用的run()
方法是作用在Animal
、Dog
、Cat
还是其他派生对象上,由运行时该对象的确切类型决定,这就是多态真正的威力:调用方只管调用,不管细节,而当我们新增一种Animal
的子类时,只要确保run()
方法编写正确,不用管原来的代码是如何调用的。这就是著名的开闭原则
。
其实,一般情况下,在有了一定的项目经验后才能真正理解多态、灵活运用多态,并配合常用的一些设计模式,能极大地提高程序的扩展性与可维护性。应用多态的关键在于抽象,能够捕捉到程序中变化的行为、可扩展的地方。这就是所谓的面向抽象编程
、面向接口编程
。本质上属于一种内功心法,平时项目中应该多锻炼抽象的能力。
继承还可以一级一级地继承下来,就好比从爷爷到爸爸、再到儿子这样的关系。而任何类,最终都可以追溯到根类object
,比如如下的继承树:
对于静态语言(例如Java
)来说,如果需要传入Animal
类型,则传入的对象必须是Animal
类型或者它的子类,否则,将无法调用run()
方法。
对于Python这样的动态语言来说,则不一定需要传入Animal
类型。我们只需要保证传入的对象有一个run()
方法就可以了。
这就是动态语言的“鸭子类型”,它并不要求严格的继承体系,一个对象只要“看起来像鸭子,走起路来像鸭子”,那它就可以被看做是鸭子。
当拿到一个对象的引用时,如何知道这个对象是什么类型、有哪些方法呢?
判断对象类型,使用type()
函数:
type(1024)
int
type('行无际')
str
type(None)
NoneType
type(abs)
builtin_function_or_method
type(a1)
__main__.Dog
type(a2)
__main__.Cat
type(1024) == int
True
import types
type(lambda x: x) == types.LambdaType
True
对于class
的继承关系来说,使用type()
就很不方便。我们要判断class的类型,可以使用isinstance()
函数。
isinstance(a2, Animal)
True
isinstance(a2, Cat)
True
isinstance(1024, int)
True
isinstance('行无际', str)
True
isinstance([], list)
True
还可以判断一个变量是否是某些类型中的一种:
isinstance([1, 2, 3], (list, tuple))
True
isinstance({1, 2, 3}, (set, dict))
True
isinstance(a1, (Dog, Cat))
True
使用isinstance()
判断类型,可以将指定类型及其子类“一网打尽”。
如果要获得一个对象的所有属性和方法,可以使用dir()
函数,它返回一个包含字符串的list
,比如,获得一个str
对象的所有属性和方法:
dir(a1)
['__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']
dir(Dog)
['__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__()
方法
len('行无际')
3
'行无际'.__len__()
3
自己写的类,如果也想用len(myObj)
的话,就自己写一个__len__()
方法
class MyDog(object):
def __len__(self):
return 100
len(MyDog())
100
仅仅把属性和方法列出来是不够的,配合getattr()
、setattr()
以及hasattr()
,我们可以直接操作一个对象的状态。看起来有点像Java或者Golang语言里面的反射哦。
hero = Hero('令狐冲', '独孤九剑')
# 有属性'_Hero__name'吗?
hasattr(hero, '_Hero__name')
True
# 有属性'wife'吗?
hasattr(hero, 'wife')
False
# 设置一个属性'wife'
setattr(hero, 'wife', '任盈盈')
# 有属性'wife'吗?
hasattr(hero, 'wife')
True
# 获取属性'wife'
getattr(hero, 'wife')
'任盈盈'
也可以获得对象的方法
hasattr(hero, 'say_hi')
True
hi = getattr(hero, 'say_hi')
hi
<bound method Hero.say_hi of <__main__.Hero object at 0x1136c6d30>>
hi()
大家好,我是令狐冲,我会独孤九剑
要注意的是,只有在不知道对象信息的时候,才会去获取对象信息然后尝试操作对象状态。一个正确的用法的例子如下:
def readImage(fp):
if hasattr(fp, 'read'):
return readData(fp)
return None
给实例绑定属性的方法是通过实例变量,或者通过self
变量。
如果Hero
类本身需要绑定属性呢?可以直接在class
中定义属性,这种属性是类属性,归Hero
类所有:
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}')
Hero.book
'射雕英雄传'
Hero.count
0
这个属性虽然归类所有,但类的所有实例都可以访问到。
hero = Hero('郭靖', '降龙十八掌')
hero.book
'射雕英雄传'
hero.count
1
# 给实例绑定book属性
hero.book = '神雕侠侣'
# 由于实例属性优先级比类属性高,会屏蔽掉类的book属性
hero.book
'神雕侠侣'
# 类属性没有被修改
Hero.book
'射雕英雄传'
# 删除实例的book属性
del hero.book
# 由于实例的book属性没有找到,类的book属性就显示出来了
hero.book
'射雕英雄传'
可以看出,在编写程序的时候,千万不要对实例属性和类属性使用相同的名字,因为相同名称的实例属性将屏蔽掉类属性,但是当你删除实例属性后,再使用相同的名称,访问到的将是类属性。程序的可读性非常差,给自己找麻烦。
创建了一个class
的实例后,我们可以给该实例绑定任何属性和方法,这就是动态语言的灵活性。
但是,如果我们想要限制实例的属性怎么办?比如,只允许对Hero
实例添加name
和skill
属性。
为了达到限制的目的,Python允许在定义class
的时候,定义一个特殊的__slots__
变量,来限制该class
实例能添加的属性:
class Hero(object):
# 用tuple定义允许绑定的属性名称
__slots__ = ('name', 'skill')
def __init__(self, name):
self.name = name
hero = Hero('杨过')
hero.skill = '黯然销魂掌'
hero.skill
'黯然销魂掌'
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__
定义的属性仅对当前类实例起作用,对继承的子类是不起作用的。
class ChineseHero(Hero):
pass
h = ChineseHero('中国人')
h.country = '中国'
h.country
'中国'
除非在子类中也定义__slots__
,这样,子类实例允许定义的属性就是自身的__slots__
加上父类的__slots__
。
在绑定属性时,如果直接把属性暴露出去,虽然写起来很简单,但是,没办法检查参数,导致可以把对象属性随便改:
hero = Hero('杨过')
hero.name = '郭靖'
hero.name
'郭靖'
这可能就不符合实际业务逻辑。
- 为了让某属性只读,可以不提供类似
set_xxx()
的方法; - 为了限制某属性的范围,则可以通过一个
set_xxx()
方法来设置属性,再通过一个get_xxx()
来获取属性,这样,在set_xxx()
方法里,可以检查参数。
但是,这样的调用方法又略显复杂,没有直接用属性这么直接简单。有没有既能检查参数,又可以用类似属性这样简单的方式来访问类的变量呢?
还记得装饰器(decorator
)可以给函数动态加上功能吗?对于类的方法,装饰器一样起作用。Python内置的@property
装饰器就是负责把一个方法变成属性
调用的。
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
方法变成属性赋值,于是,就拥有一个可控的属性操作。
hero = Hero('令狐冲')
# 实际转化为hero.get_name()
hero.name
'令狐冲'
try:
hero.name = '李寻欢'
except AttributeError as e:
print(e)
can't set attribute
name
就是一个只读属性。
# 实际转化为hero.set_wife('')
hero.wife = ''
wife不能为空
hero.wife = '任盈盈'
hero.wife
'任盈盈'
wife
是可读写属性。并且在设置属性时对参数做了非空检验。
@property
广泛应用在类的定义中,可以让调用者写出简短的代码,同时保证对参数进行必要的检查,这样,程序运行时就减少了出错的可能性。
继承是面向对象编程的一个重要的方式,因为通过继承,子类就可以扩展父类的功能。
class Animal(object):
def say_hi(self):
print('hi, Animal...')
现在,如果要给动物再加上Runnable
和Flyable
的功能,只需要先定义好Runnable
和Flyable
的类:
class Runnable(object):
def run(self):
print('Running...')
class Flyable(object):
def fly(self):
print('Flying...')
对于需要Runnable
功能的动物,就多继承一个Runnable
,例如Dog
:
class Dog(Animal, Runnable):
pass
dog = Dog()
dog.say_hi()
dog.run()
hi, Animal... Running...
对于需要Flyable
功能的动物,就多继承一个Flyable
,例如Bird
:
class Bird(Animal, Flyable):
pass
bird = Bird()
bird.say_hi()
bird.fly()
hi, Animal... Flying...
通过多重继承,一个子类就可以同时获得多个父类的所有功能。
在设计类的继承关系时,通常,主线都是单一继承下来的,例如,Bird
继承自Animal
。但是,如果需要混入
额外的功能,通过多重继承就可以实现,比如,让Bird
除了继承自Animal
外,再同时继承Flyable
。这种设计通常称之为MixIn
。
为了更好地看出继承关系,可以把Runnable
和Flyable
改为RunnableMixIn
和FlyableMixIn
。
MixIn
的目的就是给一个类增加多个功能,这样,在设计类的时候,我们优先考虑通过多重继承来组合多个MixIn
的功能,而不是设计多层次的复杂的继承关系。
Python自带的很多库也使用了MixIn
。举个例子,Python自带了TCPServer
和UDPServer
这两类网络服务,而要同时服务多个用户就必须使用多进程或多线程模型,这两种模型由ForkingMixIn
和ThreadingMixIn
提供。通过组合,我们就可以创造出合适的服务来。
比如,编写一个多进程模式的TCP
服务,定义如下:
from socketserver import TCPServer
from socketserver import ForkingMixIn
class MyTCPServer(TCPServer, ForkingMixIn):
pass
编写一个多线程模式的UDP
服务,定义如下:
from socketserver import UDPServer
from socketserver import ThreadingMixIn
class MyUDPServer(UDPServer, ThreadingMixIn):
pass
这样一来,我们不需要复杂而庞大的继承链,只要选择组合不同的类的功能,就可以快速构造出所需的子类。
看到类似__slots__
这种形如__xxx__
的变量或者函数名就要注意,这些在Python中是有特殊用途的。
__slots__
我们已经知道怎么用了,__len__()
方法我们也知道是为了能让class作用于len()
函数。
除此之外,Python的class中还有许多这样有特殊用途的函数,可以帮助我们定制类。
先定义一个Hero
类,打印一个实例:
class Hero(object):
def __init__(self, name):
self.name = name
print(Hero('张无忌'))
<__main__.Hero object at 0x113646a60>
打印出一堆<__main__.Hero object at 0x...>
,不好看。
怎么才能打印得好看呢?只需要定义好__str__()
方法,返回一个好看的字符串就可以了:
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
,打印出来的实例还是不好看:
Hero('张无忌')
<__main__.Hero at 0x113661700>
这是因为直接显示变量调用的不是__str__()
,而是__repr__()
,两者的区别是__str__()
返回用户看到的字符串,而__repr__()
返回程序开发者看到的字符串,也就是说,__repr__()
是为调试服务的。
解决办法是再定义一个__repr__()
。但是通常__str__()
和__repr__()
代码都是一样的,所以,有个偷懒的写法:
class Hero(object):
def __init__(self, name):
self.name = name
def __str__(self):
return f'Hero object (name: {self.name})'
__repr__ = __str__
Hero('张无忌')
Hero object (name: 张无忌)
如果一个类想被用于for ... in
循环,类似list
或tuple
那样,就必须实现一个__iter__()
方法,该方法返回一个迭代对象,然后,Python的for循环就会不断调用该迭代对象的__next__()
方法拿到循环的下一个值,直到遇到StopIteration
错误时退出循环。
以斐波那契数列为例,写一个Fib
类,可以作用于for
循环:
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
Fib
实例虽然能作用于for
循环,看起来和list
有点像,但是,把它当成list
来使用还是不行,比如,取第5
个元素:
try:
Fib()[5]
except TypeError as e:
print(e)
'Fib' object is not subscriptable
要表现得像list
那样按照下标取出元素,需要实现__getitem__()
方法:
class Fib(object):
def __getitem__(self, n):
a, b = 1, 1
for x in range(n):
a, b = b, a + b
return a
f = Fib()
f[0]
1
f[5]
8
f[6]
13
此外,如果把对象看成dict
,__getitem__()
的参数也可能是一个可以作key
的object
,例如str
。
与之对应的是__setitem__()
方法,把对象视作list
或dict
来对集合赋值。最后,还有一个__delitem__()
方法,用于删除某个元素。
总之,通过上面的方法,我们自己定义的类表现得和Python自带的list
、tuple
、dict
没什么区别,这完全归功于动态语言的“鸭子类型”,不需要强制继承某个接口。
正常情况下,当调用类的方法或属性时,如果不存在,就会报错。
要避免这个错误,除了可以加上这个属性外,Python还有另一个机制,那就是写一个__getattr__()
方法,动态返回一个属性。当调用不存在的属性时,比如friend
,Python解释器会试图调用__getattr__(self, 'friend')
来尝试获得属性,这样,我们就有机会返回friend
的值。
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('令狐冲')
当调用不存在的属性时:
hero.friend
'有朋自远方来,不亦乐乎!'
当调用不存在的方法时:
hero.count()
1024
注意,只有在没有找到属性的情况下,才调用__getattr__
,已有的属性不会在__getattr__
中查找。
此外,注意到任意调用如hero.xxx
都会返回None
,这是因为我们定义的__getattr__
默认返回就是None
。
print(hero.xxx)
None
这实际上可以把一个类的所有属性和方法调用全部动态化处理了,不需要任何特殊手段。
Python的class允许定义许多定制方法,可以让我们非常方便地生成特定的类。这里只介绍最常用的几个定制方法,还有很多可定制的方法,请参考Python的官方文档。
Python也提供了Enum
。
from enum import Enum
Month = Enum('Month', ('Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'))
这样我们就获得了Month
类型的枚举类,可以直接使用Month.Jan
来引用一个常量,或者枚举它的所有成员:
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
派生出自定义类:
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
访问这些枚举类型可以有若干种方法:
Weekday.Mon
<Weekday.Mon: 1>
Weekday['Mon']
<Weekday.Mon: 1>
Weekday.Mon.value
1
Weekday(1)
<Weekday.Mon: 1>
可见,既可以用成员名称引用枚举常量,又可以直接根据value
的值获得枚举常量。
动态语言和静态语言最大的不同,就是函数和类的定义,不是编译时定义的,而是运行时动态创建的。
比方说上面我们要定义一个Hero
的class,就写一个hero.py
模块。当Python解释器载入hero模块时,就会依次执行该模块的所有语句,执行结果就是动态创建出一个Hero
的class对象。
hero = Hero('令狐冲')
print(type(hero))
<class '__main__.Hero'>
print(type(Hero))
<class 'type'>
type()
函数可以查看一个类型或变量的类型,Hero
是一个class
,它的类型就是type
,而hero
是一个实例,它的类型就是class Hero
。
我们说class的定义是运行时动态创建的,而创建class的方法就是使用type()
函数。
type()
函数既可以返回一个对象的类型,又可以创建出新的类型,比如,我们可以通过type()
函数创建出Hello
类,而无需通过class Hello(object)...
的定义
# 先定义函数
def fn(self, name='world'):
print('Hello, %s.' % name)
# 创建Hello class
Hello = type('Hello', (object,), dict(hello=fn))
h = Hello()
h.hello()
Hello, world.
print(type(Hello))
<class 'type'>
print(type(h))
<class '__main__.Hello'>
要创建一个class
对象,type()
函数依次传入3个参数:
- class的名称;
- 继承的父类集合,注意Python支持多重继承,如果只有一个父类,别忘了tuple的单元素写法;
- class的方法名称与函数绑定,这里我们把函数fn绑定到方法名hello上
通过type()
函数创建的类和直接写class
是完全一样的,因为Python解释器遇到class
定义时,仅仅是扫描一下class
定义的语法,然后调用type()
函数创建出class
。
正常情况下,我们都用class Xxx...
来定义类,但是,type()
函数也允许我们动态创建出类来,也就是说,动态语言本身支持运行期动态创建类,这和静态语言有非常大的不同,要在静态语言运行期创建类,必须构造源代码字符串再调用编译器,或者借助一些工具生成字节码实现,本质上都是动态编译,会非常复杂。
除了使用type()
动态创建类以外,要控制类的创建行为,还可以使用metaclass
。
metaclass
,直译为元类,简单的解释就是:
当我们定义了类以后,就可以根据这个类创建出实例,所以:先定义类,然后创建实例。 但是如果我们想创建出类呢?那就必须根据
metaclass
创建出类,所以:先定义metaclass
,然后创建类。 连接起来就是:先定义metaclass
,就可以创建类,最后创建实例。
所以,metaclass
允许你创建类或者修改类。换句话说,你可以把类看成是metaclass
创建出来的“实例”。
先看一个简单的例子,这个metaclass
可以给我们自定义的MyList
增加一个add
方法:
定义ListMetaclass
,按照默认习惯,metaclass
的类名总是以Metaclass
结尾,以便清楚地表示这是一个metaclass
:
# 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
:
class MyList(list, metaclass=ListMetaclass):
pass
当我们传入关键字参数metaclass
时,魔术就生效了,它指示Python解释器在创建MyList
时,要通过ListMetaclass.__new__()
来创建,在此,我们可以修改类的定义,比如,加上新的方法,然后,返回修改后的定义。
__new__()
方法接收到的参数依次是:
- 当前准备创建的类的对象;
- 类的名字;
- 类继承的父类集合;
- 类的方法集合。
测试一下MyList
是否可以调用add()
方法:
L = MyList()
L.add(1)
L.add(0)
L.add(2)
L.add(4)
L
[1, 0, 2, 4]
动态修改有什么意义?直接在MyList
定义中写上add()
方法不是更简单吗?正常情况下,确实应该直接写,但是,总会遇到需要通过metaclass
修改类定义的。
其实,读到这里,如果熟悉Java的朋友应该能看出,这与Java中的字节码增强技术非常类似,不过因为Python动态语言的特性,比Java运行时修改类定义要容易许多。