Python Basics
函数知识点汇总
调用函数
Python内置了很多有用的函数可以直接调用。
要掉用一个函数需要知道函数的名称与参数,可以从Python的官方网站查看文档,也可以通过help函数查询帮助信息,如help(abs)
。
调用函数时,如果传入的参数数量不对,会报TypeError
的错误,并且Python会明确地告诉你所调用的函数需要几个参数;如果传入参数数量正确,但是参数类型不被函数所接受,也会报TypeError
错误。
也有函数可以接受任意多个参数,并返回最大的那个,如max
函数:
max(1, 3) # 3
max(1, -3, 2) # 2
2
数据类型转换
Python内置的函数还包括数据类型转换函数,如int
、str
等。
函数名其实就是指向一个函数对象的引用,因此完全可以把函数名赋给一个变量,相当于给这个函数起了一个“别名”:
m = max # 变量m指向max函数
m(1, 3, 5) # 因此可以通过变量m调用max函数
5
定义函数
Python中定义函数需要使用def
语句,再在其后依次写出函数名、左括号、参数、右括号和冒号,然后再在缩进块中编写函数体,最后使用return
语句返回。例子:求绝对值函数my_abs
:
def my_abs(x):
if x >= 0:
return x
else:
return -x
函数体内部语句在执行时一旦执行到return
语句就执行完毕,并返回结果。如果函数体中没有return
语句,函数执行到最后也会返回,只是返回的结果为None
,也可以写成return None
或return
。
空函数
如果定义一个啥都不做的空函数可以在其函数体部分使用pass
语句。比如:
if height < 180:
pass
参数检查
正如上文所述,调用函数时若参数个数不对,Python解释器会抛出TypeError
错误并提示,但是如果参数类型不正确,Python解释器是不会像调用内置函数那样自动检查的,这可能会导致函数体执行过程中出现问题,可以使用Python的内置函数isinstance
检查参数类型:
def my_abs(x):
if not isinstance(x, (int, float)):
raise TypeError("bad operand type") # 若传入错误的参数类型,函数则会抛出错误
if x >= 0:
return x
else:
return -x
返回多个值
可以使用return A, B
语句返回两个值,当然也可以拓展到多个值。但其实这只是一种假象,Python函数返回的依然是单一值,但是当返回值不止一个时,返回值会被自动包装成一个tuple。
函数的参数
定义函数时,参数的名字和位置确定下来,函数的接口定义就完成了。虽然Python函数定义非常简答,但其却异常灵活。
位置参数
例子:计算二次方的函数:
def power(x):
return x ** x
对于函数power
,参数x
就是一个位置参数,当调用power
函数时,必须传入有且仅有的一个参数x
。
但是上面的函数实际用起来却不合适,倘若要计算三次方、四次方…岂不是要写很多函数。OK,可以稍微修改一下power
函数:
def power(x, n):
s = 1
while n > 0:
n -= 1
s *= x
return s
这个修改后的power
函数便可以计算任意次方数,而且此时它拥有两个位置参数x
和n
,调用时,需要按位置顺序依次给这两个参数赋值。
默认参数
但是似乎日常开发中用到二次方计算的情况远大于其他次方,此时可以使用默认参数,将第二个参数n
的默认值设为2:
def power(x, n = 2):
s = 1
while n > 0:
n -= 1
s *= x
return s
于是调用power(5)
时就相当于调用power(5, 2)
,而对于n ≠ 2
的情况,则需明确地传入n
的值。
使用默认参数可以简化函数的调用,但是有诸多坑:
- 默认参数必须放在必选参数的后面;
- 有多个默认参数时,既可以按顺序提供部分默认参数,也可以不按顺序提供部分默认参数,但是不按顺序提供时,需要把参数名字写上;
(如add_student('Jensen', 'Male', city = 'Hefei')
,意思为city
用传进去的值,其他默认参数继续使用默认值)
- 默认参数必须指向不变的对象;
(如例子:
def add_end(L = []):
L.append('END')
return L
连续调用两次add_end()
后,返回['END', 'END']
,这是因为默认参数是变量,指向[]
,每次调用默认参数的值都会发生改变。)
可变参数
Python中还可以定义可变参数,即传入的参数个数是可变的,仅需在参数前加上一个*
号即可:
def calc(seed, *nums):
sum = 0
for n in nums:
sum += n * n
return seed, sum
calc(1, 2, 3, 4) # 1对应参数seed,后面几个参数都对应可变参数nums
(1, 29)
在calc
函数内部,参数nums
接收到的是一个tuple,调用该函数时,可以传入任意个参数,包括1个参数(即必选参数seed
)。
如果已经有了一个list或者tuple,可以在list或tuple前面加一个*
来将其中的元素变为可变参数传入:
alist = [2, 5, 6, 7]
calc(1, *alist) # 将list变成转变为可变参数
(1, 114)
关键字参数
关键字参数允许传入0个或任意个含参数名的参数,这些关键字参数在函数内部自动封装为一个dict。与可变参数类似,也可以先组装一个dict,再在dict前加上**
来降关键字参数传入:
def person(name, age, **kw):
print("name: ", name, ", age: ", age, ", otr: ", kw)
info = {'city': 'HFE', 'major': 'Computer Science'}
person('Jensen', 22) # 可以只传入必选参数
person('Jensen', 22, city = 'Hefei', uni = 'HFUT') # 传入两个含参数名的参数
person('Jensen', 22, **info) # 将提前组装好的info解析传入
name: Jensen , age: 22 , otr: {}
name: Jensen , age: 22 , otr: {'city': 'Hefei', 'uni': 'HFUT'}
name: Jensen , age: 22 , otr: {'city': 'HFE', 'major': 'Computer Science'}
命名关键字参数
若要限制关键字参数的名字,只接收city
和job
作为关键字参数,需要在函数的参数表中添加一个分隔符*
,分隔符后面的参数被视为命名关键字参数:
def person(name, age, *, city, uni): # 分隔符*后面的参数即为命名关键字参数
print("name: ", name, ", age: ", age, ", city: ", city, ", uni: ", uni)
如果函数参数表中已经有了一个可变参数,则后面跟着的命名关键字参数就不再需要加一个分隔符*
了:
def person(name, age, *args, city, uni): # 已有可变参数,不需要再加分隔符
print("name: ", name, ", age: ", age, ", city: ", city, ", uni: ", uni)
print("\nargs: ", args)
命名关键字参数必须传入参数名,否则Python解释器将会将其视为位置参数,而出现报错。
命名关键字参数也可以有缺省值(默认值),调用时可不传入。
参数组合
在Python中定义函数,上述几种参数都可以组合使用,顺序必须遵循:必选参数、默认参数、可变参数、命名关键字参数和关键字参数。但是并不建议使用太多组合,否则函数接口的可读性很差:
def final(a, b, c = 0, *args, d, **kw):
print("a: ", a, ", b: ", b , ", d: ", d, ", args: ", args, ", d: ", d, ", kw: ", kw)
final(1, 2, 3, 4, d = 5, name = 'final function')
a: 1 , b: 2 , d: 5 , args: (4,) , d: 5 , kw: {'name': 'final function'}
递归函数
在Python函数体内部调用自身本身,这个函数即为递归函数。例子:阶乘函数:
def fact(n):
if n == 1: # 只有当n==1时需要特殊处理
return 1
return n * fact(n - 1)
fact(1), fact(10)
(1, 3628800)
使用递归函数需要注意防止栈溢出。在计算机中,函数调用是通过内存中的栈这种数据结构实现的,每进入一个函数调用,栈就会加一层栈帧,每当函数返回,栈中就会减一层栈帧。由于栈的大小有限,因此递归次数过多会导致栈溢出。比如运行fact(10000)
,Python解释器会抛错RecursionError: maximum recursion depth exceeded in comparison
。
可以通过尾递归优化解决递归调用栈溢出。尾递归是指函数返回时调用自身本身,并且return语句不能包含表达式。这样无论递归本身调用多少次,都只占用一个栈帧,不会出现栈溢出的问题。上面的fact
函数的return语句使用了乘法表达式,因此不是尾递归:
def fact(n):
return fact_iter(n, 1)
def fact_iter(num, product):
if num == 1:
return product
return fact_iter(num - 1, num * product)
fact(10) # 执行fact(10000)依旧会报错
3628800
可以发现,上面改进的fact
函数仅返回递归函数本身,num - 1
和num * product
在函数调用前就会被计算,不影响函数调用。遗憾的是,包括Python在内的大多数编程语言都没有对尾递归做优化,因此即便有改进的fact
函数也会导致栈溢出,比如执行fact(10000)
,Python解释器依旧会抛出RecursionError
的错误。
Python高级特性
切片
取一个list或tuple的前三个元素,使用Python提供的切片功能,一行就可以完成:
names = ['Jensen', 'Eric', 'Jerry', 'Anderson', 'Taylor']
names[0:3] # 输出 ['Jensen', 'Eric', 'Jerry']
names[0:3]
表示从索引0
处开始取直到索引3
为止,但却不包括索引3
。即索引为0
,1
,2
正好三个元素,如果开始的索引是0
则可以省略,如names[:3]
。(小技巧:可以通过names[:]
原样复制整个列表)
类似的,Python也支持取倒数切片,如取最后一个元素names[-1]
,取后三个元素names[-3:]
。
切片操作很有用,先创建一个0-99的列表:
L = list(range(100))
可以通过切片操作轻松去除某一段子列表:
L[:10] # 取前十个元素;输出:[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
L[-10:] # 取后十个元素;输出:[90, 91, 92, 93, 94, 95, 96, 97, 98, 99]
L[25:35] # 取第25-35个元素;输出:[25, 26, 27, 28, 29, 30, 31, 32, 33, 34]
L[:10:2] # 前十个元素间隔一个取一个;输出:[0, 2, 4, 6, 8]
L[::10] # 所有元素,每间隔10个取一个;输出:[0, 10, 20, 30, 40, 50, 60, 70, 80, 90]
[0, 10, 20, 30, 40, 50, 60, 70, 80, 90]
字符串也可以视作一种list,其中的每个元素就是字符串中的每个字符。因此,字符串也可以使用切片操作,得到的子列表依然是字符串。
(上述操作,对于元组Tuple均使用,Tuple和List非常类似,但是Tuple一旦初始化就不能修改。)
迭代
若给定一个list
或tuple
,可以通过循环来遍历它们,这种遍历就是迭代。在Python中使用for ... in
来完成迭代,不仅可以用在list
或tuple
上,还可以作用在其他可迭代对象(如dict
)上:
d = {'a': 1, 'b': 2, 'c': 3}
for key in d:
print(key) # 输出:a, b, c,默认迭代dict的key
迭代dict
时,默认是迭代key,若要迭代value可以用for value in d.values()
,若要同时迭代key和value,可以用for k, v in d.items()
。上文提到了字符串也可以视作一种列表,因此也可作用于for
循环。简而言之无论是list
还是其他数据类型,只要是可迭代对象,for
循环就可以正常运行。
可以通过collections.abs
模块的Iterable
类型判断:
from collections.abc import Iterable
isinstance('abc', Iterable) # str是否可迭代? True
isinstance(123, Iterable) # int是否可迭代? False
isinstance([1, 2, 3], Iterable) # list是否可迭代? True
可以使用Python内置的enumerate
函数把一个list
变成索引-元素对,从而可以在for
循环中同时迭代索引和元素本身,这种用法在PyTorch的train loop
中十分常见:
L = [100, 'Jensen', 99, 'Eric', 98] # 不仅仅作用于list,其他可迭代对象如字符串、元组都适用
for idx, value in enumerate(L):
print(idx, value) # 会同时打印索引和列表中的元素值
0 100
1 Jensen
2 99
3 Eric
4 98
上面的for
循环同时引用了两个变量,这在Python中十分常见:
for x, y in ((1, 2), (2, 3), (3, 4)):
print(x, y)
1 2
2 3
3 4
列表生成器
列表生成式是Python内置的简单却强大的创建list的生成器,例子:生成list([1, 2, 3, 4, 5, 6, 7, 8, 9])
可以用list(range(1, 10))
简单生成。
若要生成[1*1, 2*2, ..., 100*100]
的列表怎么办?可以使用列表生成式[x * x for x in range(1, 101)]
。
for
循环后面还可以加上if
判断,可以筛选出仅偶数的平方[x * x for x in range(1, 101) if x % 2 == 0]
。
还可以使用两层循环,生成全排列:
[m + n for m in 'ABC' for n in 'XYZ'] # 两层全排列,三层及以上的循环就很少用到了
['AX', 'AY', 'AZ', 'BX', 'BY', 'BZ', 'CX', 'CY', 'CZ']
运用列表生成式可以写出非常简洁的代码,例子:列出当前目录下的所有文件和目录名:
import os
[dir for dir in os.listdir('.')]
['Functions.ipynb', '.ipynb_checkpoints', 'Advanced Feature.ipynb']
上文提到for
循环可以同时引用两个变量,因此列表生成式也可以使用多个变量来生成list:
d = {'a': 1, 'b':2 , 'c': 3}
[k + '=' + v for k, v in d.items()] # 输出:['a=1', 'b=2', 'c=3']
if…else
在一个列表生成式中,for
前面的if ... else
是表达式必须要带else
子句,而for
后面的if
是过滤条件,不能带else
。
如执行[x for x in range(1, 11) if x % 2 == 0 else 0]
会报错,执行[x if x % 2 == 0 for x in range(1, 11)]
也会报错,然而执行下面这行则运行正常:
[x if x % 2 == 0 else 0 for x in range(1, 11)] # for前面的if ... else是表达式必须要带else子句
[0, 2, 0, 4, 0, 6, 0, 8, 0, 10]
生成器
若创建了一个十分庞大的列表,但仅仅需要访问其中的几个元素,那么这个列表占用的很多空间都白白浪费了。在Python中可以使用一种边循环边计算的机制:生成器。生成器可以使得列表元素按照某种算法推算出来,不必创建完整的list。
创建生成器的方法有多种,首先把一个列表生成器的[]
换成()
就创建了一个生成器g = (x * x for x in range(1, 10))
。可以通过next
函数将生成器中的每个元素依次打印出来。
因为生成器保存的是算法,每次调用next
函数就计算生成器的下一个元素值,直到计算到最后一个元素后抛出StopIteration
的错误。但是一直调用next
的方式实在是太傻了,由于生成器也是可迭代对象,可以使用for
循环:
g = (x * x for x in range(1, 10))
for i in g:
print(i)
1
4
9
16
25
36
49
64
81
若需要推算的算法十分复杂,无法使用列表生成式实现时,可以使用函数来实现,例如斐波那契数列函数:
def fib(max):
n, a, b = 0, 0, 1
while n < max:
print(b)
a, b = b, a + b
n += 1
return 'done'
上面的函数可以输出斐波那契数列的前N个数,可以从第一个元素开始推算出后续任意元素,这种逻辑非常类似生成器。要把fib
函数变成生成器,只需要把print(b)
改成yield b
就好了:
def fib(max):
n, a, b = 0, 0, 1
while n < max:
yield b # 将print(b)改成yield b
a, b = b, a + b
n += 1
return 'done'
fib(10) # 输出: <generator object fib at 0xXXXXX>,说明fib(10)是一个生成器
<generator object fib at 0x7f38c9d9ade0>
但是生成器与普通函数执行流程不一样,普通函数是顺序执行,执行到return
或者最后一行后返回,而生成器在每次调用next
函数时执行,遇到yield
语句返回,下次执行时再从上次返回的yield
处继续执行,例子:
(注意:多次调用生成器会生成多个相互独立的生成器对象。)
def odd():
print('step 1')
yield 1
print('step 2')
yield 3
print('step 3')
yield 5
o = odd()
next(o) # 输出:step 1;返回:1
next(o) # 输出:step 2;返回:3
next(o) # 输出:step 3;返回:5 此时后面已经没有yield可以执行了,再调用就会抛“StopIteration”错
step 1
step 2
step 3
5
生成器对象创建好后,可以使用for
循环迭代,但会发现for
循环拿不到生成器return
返回的值,因此必须捕获StopIteration
错误,返回值就包含在StopIteration
的value
中:
g = fib(10)
while True:
try:
x = next(g)
print('g: ', x)
except StopIteration as e:
print('Generator return value: ', e.value)
break
g: 1
g: 1
g: 2
g: 3
g: 5
g: 8
g: 13
g: 21
g: 34
g: 55
Generator return value: done
迭代器
根据上文可发现可以直接作用于for
循环的数据类型有:
- 集合数据类型:如
list
、tuple
、dict
、set
、str
等; - 生成器;
上述这些可直接作用于for
循环的对象被统称为可迭代(Iterable)对象,可以使用isinstance
函数判断一个对象是否是可迭代对象:isinstance([], Iterable)
。
可以被next
函数调用并不断返回下一个值的对象成为迭代器(Iterator),也可以使用isinstance
函数判断一个对象是否是迭代器对象:isinstance([], Iterator)
。
因此,生成器即是可迭代对象,也是迭代器。但是list
、dict
、str
等虽然是可迭代对象,但不是迭代器,可以使用iter
函数把它们变成迭代器。
Iterator
的计算是惰性的,只有在需要返回下一个数据时它才会计算,本质上Python的for
循环就是通过不断调用next
函数实现的。
函数式编程
高阶函数
变量可以指向函数
如果把函数本身赋值给变量f = abs
,此时变量f
已经指向了abs
函数本身,调用f()
和调用abs()
完全相同。
函数名也是变量
函数名其实就是指向函数的变量,对于abs
函数,完全可以把其函数名看作是变量,只是指向一个可以计算绝对值的函数。如果abs = 100
即将abs
指向100
,那就无法再通过abs
调用求绝对值函数了,实际代码中绝不允许这样写。
传入函数
既然变量可以指向函数,函数的参数能够接收变量,那么一个函数就可以接收另一个函数作为参数,这种函数就叫做高阶函数:
def add(x, y ,f):
return f(x) + f(y)
当调用abs(-6, 5, abs)
时,参数x
、y
、f
分别接收-6
、5
和abs
。
map & reduce
map
函数接收两个参数,一个是函数另一个时可迭代对象,map
将传入的函数依次作用在可迭代对象的每个元素上,并把结果作为新的迭代器对象返回。例子:将一个求二次方的函数作用在一个列表上:
def f(x):
return x * x
m = map(f, [1, 2, 3, 4 ,5, 6, 7, 8]) # 求二次方函数作用在列表中的每一个元素上
list(m)
[1, 4, 9, 16, 25, 36, 49, 64]
map
作为一个高阶函数,将运算规则抽象化,不但可以简单地求二次方,还可以计算任意复杂的函数,如把列表中的元素全都转换为字符list(map(str, [1, 2, 3, 4, 5, 6, 7, 'A', 'B']))
。
reduce
函数也是接收一个函数和一个可迭代对象。reduce
把函数作用在可迭代对象上,这个函数必须接收两个参数,reduce
把结果继续和序列的下一个元素做累积计算,如:reduce(f, [x, y, z, k]) = f(f(f(x, y), z), k)
。
例子:对一个列表序列求和:
from functools import reduce
def add(x, y):
return x + y
reduce(add, [2, 5, 6, 7, 5]) # 相当于 add(add(add(add(2, 5), 6), 7), 5)
25
例子:将字符串转换为整数:
from functools import reduce
DIGITS = {'0': 0, '1': 1, '2': 2, '3': 3, '4': 4, '5': 5, '6': 6, '7': 7, '8': 8, '9': 9}
def str2int(s):
def fn(x, y):
return x * 10 + y
def char2num(s):
return DIGITS[s]
return reduce(fn, map(char2num, s))
str2int('2021111052')
2021111052
上述str2int
函数还可以进一步使用lambda函数简化为:
from functools import reduce
DIGITS = {'0': 0, '1': 1, '2': 2, '3': 3, '4': 4, '5': 5, '6': 6, '7': 7, '8': 8, '9': 9}
def str2int(s):
return reduce(lambda x, y: x * 10 + y, map(lambda x: DIGITS[x], s)) # 运用lambda表达式可以简化函数
str2int('13579')
13579
filter
Python内置的filter
函数用于过滤可迭代对象中的元素,与上文类似,filter
函数也接收一个函数和一个可迭代序列,将传入的函数依次作用于序列中的每个元素,根据返回值是True
或False
来决定保留还是丢弃该元素。例如删掉一个列表中的奇数,只保留偶数:
def odd(n):
return n % 2 == 0
list(filter(odd, [2, 5, 6, 7, 5, 0, 1, 3, 9])) # 结果: [2, 6, 0]
例子:删去一个列表中的空字符串:
def not_empty(s):
return s and s.strip()
list(filter(not_empty, ['A', ' ', 'C', None])) # 与map/reduce类似,filter返回的是迭代器对象,需要用list()获得所有结果
['A', 'C']
sorted
Python内置的sorted
函数可以对可迭代对象进行排序,与上述几种高姐函数不同,sorted
函数直接返回一个列表。
sorted
一般接收一个可迭代对象作为参数,还可以再接收一个key
函数来实现自定义的排序,如按绝对值大小排序:sorted([2, 5, 6 ,7, 9, 0, -3, -11], key = abs)
。key
指定的函数将作用于列表的每一个元素上,sorted
根据key
指定的函数返回的结果进行排序。
例子:对字符串进行排序:
sorted(['Jensen', 'Bob', 'eric', 'yiming']) # 默认依照ASCII的大小顺序排列,ASCII码中,'J' < 'B' < 'e' < 'y'
['Bob', 'Jensen', 'eric', 'yiming']
有些时候按照ASCII
码排序不太直观,按照字母序排序更合适:
sorted(['Jensen', 'Bob', 'eric', 'yiming'], key = str.lower) # 把字符串全变成小写或者大写(str.upper)即可
若要按照字母序逆序排列,不必改动key
函数,仅需传入第三个参数reverse = True
即可:sorted(['Jensen', 'Bob', 'eric', 'yiming'], key = str.lower, reverse = True)
。
d = {'A': 'America', 'C': 'China', 'R': 'Russia'}
sorted(d.values()) # 对字典的值进行排序,返回列表:['America', 'China', 'Russia']
['America', 'China', 'Russia']
返回函数
函数作为返回值
高阶函数除了可以接受函数作为参数之外,还可以把参数作为结果值返回。通常情况下,求和函数:
def calc_sum(*nums):
ax = 0
for n in nums:
ax += n
return ax
如若不需要立即求和,而是像Swift
语言中的延迟加载,需要用到的时候再计算,则可以不返回求和的结果,而是返回求和函数:
def lazy_sum(*nums):
def sum():
ax = 0
for n in nums:
ax += n
return ax
return sum
f = lazy_sum(1, 3, 4, 5, 7 ,9) # 此时调用lazy_sum函数返回的并不是求和结果,而是求和函数
f() # 执行求和函数得到结果
29
上面的例子中需要注意,每次调用lazy_sum
函数时都会返回一个新的独立的求和函数,它们的调用互不影响。内部函数sum
可以引用外部函数lazy_sum
的参数和局部变量,当lazy_sum
函数返回sum
时,相关参数和变量都保存在返回的sum
函数中,这种程序结构被称为“闭包”。
闭包
当一个函数返回一个函数后,其内部的局部变量还被返回的函数所引用,这种模式就称为“闭包”。需要注意的是,返回的函数并没有立刻执行,而是直到被调用才执行。返回闭包时请牢记一点,即返回函数不要引用任何循环变量,或者后续会发生变化的变量:
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(), f2(), f3() # 但实际上三个返回的均是9
(9, 9, 9)
调用上述f1
、f2
和f3
函数返回的结果均为9
,其原因是返回函数引用了变量i
,但是并没有立即执行,而是等三个函数都返回时再被调用执行,此时所引用的变量i
已经变成了3
,所以最终结果变成了9
。若一定要引用循环变量,可以再在创建一个函数,用该函数的参数绑定循环变量当前的值:
def count():
def f(j):
return lambda: j * j # 绑定此时的j值
fs = []
for i in range(1, 4):
fs.append(f(i)) # f(i)立即执行,因此当前的i值被传入被绑定
return fs
f1, f2, f3 = count()
f1(), f2(), f3()
(1, 4, 9)
nonlocal
使用闭包时,如果内层函数只是读外层函数的局部变量,那似乎没有什么问题:
def inc():
x = 0
def fn():
return x + 1 # 仅读取外层局部变量x
return fn
f = inc()
print(f()) # 输出:1
print(f()) # 输出:1
但是如果对外层变量赋值,Python解释器会把x
视作fn
函数的局部变量,但又因x
作为内层函数的局部变量并没有进行初始化,所以会报错。
若对外层变量赋值,实际上是想引用外层函数的局部变量x
,所以需要在fn
函数内部加一个nolocal x
的声明:
def inc():
x = 0
def fn():
nonlocal x # 如果注释这行,Python解释器则会将x视为fn函数内部的局部变量
x += 1
return x
return fn
f = inc()
print(f())
print(f())
1
2
匿名函数 Lambda
上文已经多次用到了匿名函数即Lambda
表达式,有些时候传入函数时,不需要显式地定义函数,直接传入匿名函数更方便。比如匿名函数lambda x: x * x
实际上就是:
def f(x):
return x * x
关键字lambda
表示匿名函数,冒号前的x
表示函数参数。匿名函数有个限制,就是只能有一个表达式,不用写return
,返回值就是该表达式的结果。
匿名函数也是函数对象,也可以像函数一样,将匿名函数赋值给一个变量,再利用变量来调用该函数;同样,也可以把匿名函数作为返回值返回。
装饰器
装饰器可以增强函数的功能,比如在调用某个函数前后自动打印日志,但又不希望修改函数的本体,这种在代码运行期间动态增加功能的方式被称为装饰器。例子:定义一个能打印日志的装饰器:
def log(func):
def wrapper(*args, **kw):
print('call %s():' % func.__name__) # .__name__属性存储了函数的名称
return func(*args, **kw)
return wrapper
@log # 装饰器
def date():
print('2021-12-11')
date() # 调用date函数不仅会运行函数本身,还会在函数输出前打印一行日志
call date():
2021-12-11
观察上面的log
函数,因其是一个装饰器,所以接收一个函数作为参数,并返回一个函数。可以借助Python的@
语法讲装饰器置于函数date
的定义处,调用date
时,不仅会允许其本身,还会在其输出前打印一行日志。
把@log
放到date
函数的定义处相当于执行了语句date = log(date)
。
wrapper
函数的参数定义是(*args, **kw)
,因此wrapper
函数可以接受任意参数的调用。在wrapper
函数内首先打印日志,再紧接着调用传入的原始函数。
如果装饰器本身需要传入参数,那就要用上述提到的闭包方法返回一个装饰器函数,比如一个自定义log文本的装饰器函数:
def log(text): # 接收自定义log文本
def decorator(func):
# @functools.wraps(func) # 取消注释可以将原始func函数中的属性复制到wrapper函数中,下文介绍
def wrapper(*args, **kw):
print('%s %s(): ' % (text, func.__name__))
return func(*args, **kw)
return wrapper # 闭包
return decorator # 闭包
@log('excute') # 传入参数的装饰器
def date():
print('2021-12-11')
date() # 相当于执行了date = log('excute')(date),即参数是date函数,返回wrapper函数
excute date():
2021-12-11
上述两种装饰器的定义都没有问题,上文提到__name__
属性存储了函数的名称,但是经过装饰器装饰的函数,其__name__
属性都发生了变化,执行date.__name__
会发现输出不再是date
而是wrapper
,因为上述代码返回的是wrapper
函数。
因此,需要使用Python内置的functools.wraps
函数把原始date
函数的属性复制到wrapper
函数中,否则一些依赖函数签名的代码执行就会出错:
import functools
def log(func):
@functools.wraps(func) # 可以将原始func函数中的属性复制到wrapper函数中
def wrapper(*args, **kw):
print('call %s(): ' % func.__name__)
return func(*args, **kw)
return wrapper
@log
def date():
print('2021-12-11')
date.__name__
'date'
偏函数
所谓偏函数即把一个函数的某些参数给固定住(设置默认值),返回一个新的函数,使得函数调用更加简单,在Python中可以使用内置的functools.partial
轻松实现。当函数的参数个数太多时,可以通过此方法固定部分参数简化调用。
众所周知int
函数可以将字符串转换为整数,当仅传入字符串时,int
函数默认按照十进制转换。但是还可以给int
函数传入额外的base
参数(其默认值为10)来实现进制转换:
int('25675', base=16) # 将‘25675’转换为十六进制整数
153205
假如要经常使用十六进制转换,上述写法不免显得多余,可以使用functools.partial
函数创建一个偏函数:
import functools
int16 = functools.partial(int, base=16) # int16函数直接可以将字符串转换为十六进制整数
int16('25675')
153205
Python内置的functools.partial
函数实际上可以接收函数对象、可变参数和关键字参数三种类型的参数,所以上述functools.partial(int, base=16)
中的base
参数实际上是关键字参数。
下面是使用可变参数的max
函数的偏函数:
import functools
max2 = functools.partial(max, 10, -11) # 10和-11组成了可变参数
max2(19) # 相当于执行max(10, -11, 19)
# 也可以如下写法,具体参考函数那一节:
# nums = [10, -11]
# max2 = functools.partial(max, *nums)
19
面向对象编程
类和实例
面向对象最重要的概念就是类和实例,以Student
类为例,在Python中,定义类是通过class
关键字:
class Student(object):
pass
class
后面紧跟着类名即Student
,类名通常是大写开头的单词,紧接着是(object)
,表示该类是从哪个类继承下来的。通常如果没有合适的继承类就默认继承object
类,这是所有类最终都会继承的类。
定义好一个类之后就可以根据类创建出相应的实例,创建实例是通过类名+()
实现的:
class Student(object): # 默认继承Object类
pass
stu = Student() # stu是Student类的一个实例
stu, Student
(<__main__.Student at 0x7ff5c8788d30>, __main__.Student)
执行上一个block语句会发现,变量stu
指向的就是一个Student
实例,后面的0x......
是实例在内存中的地址,每一个实例的地址都不一样,而Student
本身则是一个类。
可以自由的给一个实例变量绑定属性如stu.name = 'Jensen'
。通常情况下会使用__init__
构造函数在创建实例的时候就把一些必须绑定的属性强制传入:
class Student(object):
def __init__(self, name, score): # 第一个参数永远是self即代表实例本身,不需要手动传入
self.name = name
self.score = score
stu = Student('Jensen', 100) # 构造实例时会自动调用构造函数,又构造函数后创建实例就不能传入空参数,必须传入构造函数匹配的参数(self不用传)
stu.name, stu.score
('Jensen', 100)
数据封装
面向对象编程中一个非常重要的概念就是数据封装。在上面的Student
类中,每个实例就拥有各自的name
和score
这些数据,可以通过函数来访问这些数据:
def print_score(stu):
print('%s: %s' % (stu.name, stu.score))
可以将print_score
函数封装在Student
内部,这样就把数据封装起来了,这些封装数据的函数和类本身是相关联的被称为累的方法:
class Student(object):
def __init__(self, name, score):
self.name = name
self.score = score
def print_score(self): # 类的方法必须要有self参数
print('%s: %s' % (self.name, self.score))
stu = Student('Jensen', 20)
stu.print_score()
Jensen: 20
访问限制
上一个block中,虽然数据已经封装在类内部,但是外部代码还是可以直接调用实例变量修改数据的值:stu.name = 'Shen Yiming'
,这样会使得数据变得不可靠。
如果要让内部属性不可以被外部代码直接访问,可以在属性的名称前加上两个下划线__
,使属性变成类的私有属性,只可以被类的方法访问,外部代码无法访问,通过访问限制保护,使得代码更加健壮:
class Student(object):
def __init__(self, name, score):
self.__name = name # 属性变量名前加两个下划线__,即私有属性
self.__score = score
def print_score(self):
print('%s: %s' % (self.__name, self.__score))
stu = Student('Jensen', 20)
# stu.__name # 若执行此代码,则会报错没有__name属性
stu.print_score()
Jensen: 20
如果此时又要允许外部代码有限制的修改内部数据,可以给Student
类再添加一个set
方法:
def Student(object):
...
def set_name(self, name): # set方法,可以在方法体里添加一些参数检查,避免传入无效参数
self.__name = name
def set_score(self, score):
self.__score = score
需要注意的是,Python中还有很多变量名类似__XXX__
,这些不是私有变量,而是特殊变量,是可以直接通过类名.XXX
等方式直接访问的,所以不可以使用这些作为变量名。
还有一些形如_name
的变量名,这样的实例变量在外部是可以直接访问的,但这种写法希望你将其视为私有变量,不要随意访问。
实际上,私有变量如__name
在实例内部被改成了_Student__name
,所以可以在外部通过stu._Student__name
访问私有变量__name
,但是强烈不建议这样做。
请注意下面的错误用法:
stu = Student('Jensen', 20)
stu.__name = 'Yiming' # 设置__name变量
上面的写法只是给实例变量stu
绑定了一个__name
属性,与类内部的私有变量__name
没有任何关系,因为其已经被修改为_Student__name
。
继承和多态
在面向对象的程序设计中,定义一个类可以从某个现有的类继承,新定义的类被称为子类,而被继承的类被称为基类或父类、超类。
继承最主要的用处就是子类获得了父类的全部功能:
class Animal(object): # 基类
def run(self):
print('Animal is running...')
class Dog(Animal): # 子类,继承自Animal类,自动获得Animal类的run方法
pass
class Cat(Animal):
pass
但是猫和狗继承自基类的run
函数指代的范围太广泛了,子类可以重写基类的方法,仅需在子类中重新定义改方法即可:
class Animal(object):
def run(self):
print('Animal is running...')
class Dog(Animal):
def run(self): # 重写基类的run方法
print('Dog is running...')
class Cat(Animal):
def run(self): # 重写基类的run方法
print('Cat is running...')
dog, cat = Dog(), Cat()
dog.run() # 子类的run方法覆盖了父类的run,代码运行时总会调用子类的run方法
cat.run()
Dog is running...
Cat is running...
多态
多态即子类实例即属于子类本身,也属于子类所继承的基类:
a, b, c = list(), Animal(), Dog()
print(isinstance(a, list)) # a是list类型
print(isinstance(b, Animal)) # b是Animal类型
print(isinstance(c, Dog)) # c是Dog类型
print(isinstance(c, Animal)) # c不仅是Dog类型,也是Animal类型
print(isinstance(b, Dog)) # b不是Dog类型
True
True
True
True
False
从上一个block的代码中可以发现,在继承关系中,如果一个实例的数据类型是某个子类,那他的数据类型也可被看作是基类。但是反过来却不行,如上面的b
是Animal
类型但却不是Dog
类型。
再看一个多态的例子:
class Animal(object):
def run(self):
print('Animal is running...')
class Dog(Animal):
def run(self):
print('Dog is running...')
class Cat(Animal):
def run(self):
print('Cat is running...')
def run_twice(animal):
animal.run()
animal.run()
run_twice(Animal()) # 正常打印
run_twice(Dog()) # 不需要对run_twice,任何依赖Animal作为参数的函数都可以不加修改正常运行,原因就在于多态
run_twice(Cat()) # 实际上,任何具有run方法的类的实例都可以传入正常运行
Animal is running...
Animal is running...
Dog is running...
Dog is running...
Cat is running...
Cat is running...
对于一个变量,只需要知道它是Animal
类型,无需确切地知道它的子类型就可以放心地调用run
方法,而具体调用的run
方法是作用在Animal
、Dog
还是Cat
对象上,由运行时该对象的确切类型决定。调用方只管调用不管细节,而当新增一种Animal
子类时,只需确保run
方法编写正确,不用管原来的代码是如何调用的,这就是著名的开闭原则:
- 对扩展开放:允许新增
Animal
子类; - 对修改封闭:不需要修改依赖
Animal
类型的run_twice
等函数;
继承还可以一级一级地继承下来,好比爷爷到爸爸、再到儿子这样的关系。而任何类最终都可以追溯到object
类,这些继承关系看上去就像一颗倒着的树。
静态语言 vs 动态语言
对于静态语言如Java来说,如果需要传入Animal
类型,则传入的对象必须是Animal
类型或者它的子类,否则,将无法调用run
方法。
对于Python这样的动态语言来说则不一定要传入Animal
类型,如上文所说只需保证传入的对象有一个run
方法就可以了。
获取对象信息
可以用type
函数来判断对象类型,基本类型都可以通过type
函数判断:
print(type(123))
print(type('Jensen'))
print(type(None))
# 如果一个变量指向函数或类,也可以用type函数判断
a = abs
print(type(abs))
print(type(a)) # 变量指向函数
<class 'int'>
<class 'str'>
<class 'NoneType'>
<class 'builtin_function_or_method'>
<class 'builtin_function_or_method'>
type
返回的是对应的Class类型,在if
语句中可以比较两个变量的type
类型来进行判断:
# type返回的是对应Class的类型,如str、int
type(123) == type(456), type('jensen') == str, type(123) == str
(True, True, False)
若需要判断一个对象是否是函数则可以使用Python内置的types
模块中定义的常量:
import types
def func():
pass
type(func) == types.FunctionType # True. 自定义函数类型
type(abs) == types.BuiltinFunctionType # True. 内置函数类型
type(lambda x: x) == types.LambdaType # True. 匿名函数类型
type((x for x in range(10))) == types.GeneratorType # True. 生成器类型
使用isinstance()
对于类的继承关系来说,使用type
函数就很不方便,如果要判断类的类型,可以使用isinstance
函数:
a = Animal() # 基类
d = Dog() # 子类,基类是Animal
h = Husky() # 子类,基类是Dog
isinstance(h, Dog) # True
isinstance(h, Animal) # True
isinstance(d, Animal) # True
isinstance(d, Husky) # False,子类属于基类类型,反过来不正确
能用type
函数判断的基本类型也可以用isinstance
函数判断:
isinstance('a', str) # True
isinstance(123, int) # True
isinstance(func, types.FunctionType) # True,(详见上一个block)
isinstance(b'a', bytes) # True
还可以判断一个变量是否属于某些类型中的一种:
isinstance([1, 2, 3], (list, tuple)) # True,[1, 2 ,3]属于(list, tuple)中的list
isinstance('jensen', (str, int)) # True,'jensen'属于(str, int)中的str
True
使用dir()
若想要获得一个对象的所有属性和方法可以使用dir
函数,它返回一个包含字符串的list:
dir('Jensen')
['__add__',
'__class__',
'__contains__',
'__delattr__',
'__dir__',
'__doc__',
'__eq__',
'__format__',
'__ge__',
'__getattribute__',
'__getitem__',
'__getnewargs__',
'__gt__',
'__hash__',
'__init__',
'__init_subclass__',
'__iter__',
'__le__',
'__len__',
'__lt__',
'__mod__',
'__mul__',
'__ne__',
'__new__',
'__reduce__',
'__reduce_ex__',
'__repr__',
'__rmod__',
'__rmul__',
'__setattr__',
'__sizeof__',
'__str__',
'__subclasshook__',
'capitalize',
'casefold',
'center',
'count',
'encode',
'endswith',
'expandtabs',
'find',
'format',
'format_map',
'index',
'isalnum',
'isalpha',
'isascii',
'isdecimal',
'isdigit',
'isidentifier',
'islower',
'isnumeric',
'isprintable',
'isspace',
'istitle',
'isupper',
'join',
'ljust',
'lower',
'lstrip',
'maketrans',
'partition',
'replace',
'rfind',
'rindex',
'rjust',
'rpartition',
'rsplit',
'rstrip',
'split',
'splitlines',
'startswith',
'strip',
'swapcase',
'title',
'translate',
'upper',
'zfill']
前面提到形如__XXX__
的属性和方法在Python中是有特殊用途的,比如__len__
方法返回长度,在Python中可以使用len
函数试图获取一个对象的长度,实际上就是调用该对象的__len__
方法,所以len('Jensen')
和'Jensen'.__len__()
是等价的。
当自己写类时若也想用len
函数的话可以在类中写一个__len__
方法:
class TestLen(object):
def __len__(self): # 拥有这个方法后就可以使用len(TestLen)
return 120
test = TestLen()
len(test)
120
除此之外都是普通属性或方法,如'Jensen'.upper()
即返回大写的字符串。
可以通过getattr
、setattr
、hasattr
等函数查看一个对象的状态:
class TestClass(object):
def __init__(self, x):
self.x = x
def power(self):
return self.x * self.x
test = TestClass(9)
hasattr(test, 'x') # True,是否有x属性
setattr(test, 'y', 10) # True,设置一个属性y,值为10
hasattr(test, 'y') # True,是否有y属性
getattr(test, 'y') # 10,获得y属性的值
# getattr(test, 'z') # 若取消注释,则会抛出AttributeError错误
getattr(test, 'z', 404) # 404,获取z属性的值,如果没有z属性则创建z属性并赋值404,再返回z属性的值
hasattr(test, 'power') # True,是否有power方法
func = getattr(test, 'power') # 将power方法赋给test变量
func() #81,调用func指向的函数,等价于test.power()
81
实例属性与类属性
由于Python是动态语言,所以根据类创建的实例可以任意绑定属性,可以通过实例变量或者self
变量:
class Student(object):
def __init__(self, name):
self.name = name # 通过self变量绑定属性
s = Student('Jensen')
s.score = 99 # 通过实例变量绑定属性
如果Student
类本身需要绑定一个属性,可以在类中直接定义属性,这种属性称为类属性,归类所有:
class Student(object):
name = 'Students'
类属性可以通过类名直接访问Student.name
,当类的实例没有name
属性时也会调用类的name
属性。当类的实例中有了和类属性同名的属性,那么实例属性则会覆盖类属性。所以编写程序时切勿将类属性和实例属性使用相同的名字。
面向对象高级编程
正常情况下,创建一个类实例后可以给改实例绑定任何属性和方法,这就是动态语言的灵活性优势:
class Student(object):
pass
s = Student()
s.name = 'Jensen' # 动态地给实例绑定一个属性
还可以给实例动态地绑定一个方法:
from types import MethodType
def set_score(self, score): # 定义一个函数作为实例方法
self.score = score
s.set_name = MethodType(set_name, s) # 动态地给实例绑定一个方法
当时上述方法很明显存在局限性即属性和方法只是绑定在实例s
上,对于其他Student
的实例是不起作用的,为了给所有的实例都绑定相应地属性和方法,可以选择绑定类属性和类方法:
def set_score(self, score):
self.score = score
Student.name = 'NAME' # 给类绑定属性
Student.set_score = set_score # 给类绑定方法
s = Student(), s.set_score(99) # 给类绑定方法和属性后,所有类的实例都可调用
s.name # 输出'NAME'
s.score # 输出99
动态语言允许在程序运行过程中动态的给类加上一些功能,这在Java等静态语言中是不可想象的。
使用__slots__
若想要限制实例的属性如只允许Student
实例添加name
和age
属性,可以在定义类的时候定义一个特殊的_slots__
变量以限制类实例能添加的属性:
class Student(object):
__slots__ = ('name', 'age') # 用元祖定义允许绑定的属性名称
s = Student()
s.name = 'Eric' # 绑定属性name
s.age = '22' # 绑定属性age
# s.score = 99 # 若取消注释则报AttributeError错
上个block中由于score
没有被放置到__slots__
中,所以实例无法绑定score
属性,若强行绑定则会得到AttributeError
错误。
需要注意的是,__slots__
定义的属性仅对当前类的实例起作用,而对于继承的子类是不起作用的,除非在子类中也定义__slots__
,这样,子类实例允许定义的属性就是自身的__slots__
加上基类的__slots__
。
使用@property
在绑定属性时,如果直接把属性暴露出去,虽然写起来十分的简单,但是,没有办法检查参数,比如s.score = 10000
就显得十分不合理。可以在Student
类的set_score
方法中添加if
语句来做判断,将score
设置在0到100之间,但每次给score
赋值都要调用set_score
未免显得过于麻烦,不如直接给属性赋值来得方便。
还记得前面提到的装饰器吗,可以使用Python内置的@property
装饰器可以将一个方法变成属性调用:
class Student(object):
@property # 将一个getter方法变成属性,此时@property本身又创建了一个装饰器@score.setter
def score(self):
return self._score
@score.setter # 将一个setter方法变成属性赋值
def score(self, value):
if not isinstance(value, int):
raise ValueError('Score must be an integer!')
if value < 0 or value > 100:
raise ValueError('Score must between 0 ~ 100!')
self._score = value
s = Student()
# s.score = 101 # 若取消注销,则会报错ValueError
s.score = 90 # 实际上转化为调用s.set_score(90)
s.score # 实际上转化为调用s.get_score()
90
还可以只定义只读属性,只定义getter
方法而不定义setter
方法就是一个只读属性,例如下面这个例子中,birth
是可读写属性然而age
就是一个只读属性:
class Student(object):
@property
def birth(self):
return self._birth
@birth.setter
def birth(self, value):
self._birth = value
@property
def age(self):
return 2021 - self.birth
需要注意的是,属性的方法名不要和实例变量名重名:
class Student(object):
@property
def birth(self):
return self.birth # 实例变量名和方法名重名
上面的代码会报RecursionError错,因为调用s.birth
会默认转换为s.get_birth()
调用,然而return self.birth
也会转换为调用get_birth
方法,因此会一直迭代下去,最终报错。
多重继承
继承是面向对象编程的重要方式,因为通过继承,子类就可以拓展基类的功能。
比如Animal
类可能会有Dog
和Bird
等子类,但是Dog
和Bird
又能向下细分出好几类,Dog
可能会有Hounds
和Herdings
等子类、Bird
可能有Flyable
和Runnable
等子类。
鸵鸟Ostrich
既属于Bird
类又属于Runnable
类,若要一层一层写继承关系实在太复杂,在Python中允许有多重继承关系即MixIn
:
class Ostrich(Bird, Runnable): # Ostrich类同时继承了Bird类和Runnable类
pass
这样一来,不需要复杂而庞大的继承链,只要选择组合不同的类的功能,就可以快速构造出所需的子类。
定制类
前面已经提到了__slots__
和__len__
等特殊方法的用法了,除此之外,Python的类中还内置能很多其他的特殊函数可以帮助定制类。
__str__
正常在打印一个类的实例时,总会打印出来一串看起来不好看的字符如<__main__.Student object at 0xXXXXX>
,如何才能将输出格式化,只需在类的内部定义好__str__
方法就可以了:
class Student(object):
def __init__(self, name):
self.name = name
def __str__(self):
return 'Student object (name: %s)' % self.name
print(Student('Jensen')) # 打印输出"Student object (name: Jensen)"
Student('Jensen') # 在交互界面下打印出来的还是和之前的一样,这是因为交互界面调用的是__repr__方法
上述代码在交互界面下直接打印还是会和之前一样,得到的结果非常不美观,此时在类中__str__
方法块后面加上__repr__ = __str__
就可以了。
__iter__
若想一个类被用于for...in
循环中,类似列表那样,可以在类中实现一个__iter__
方法,让类实例变成可迭代对象,然后Python的for
循环就会不断调用该对象的__next__
方法拿到循环的下一个值,直到遇到StopIteration
退出循环,如斐波那契数列:
class Fib(object):
def __init__(self):
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 > 100000: # 退出循环的条件
raise StopIteration()
return self.a # 返回下一个值
for n in Fib(): # 在循环中迭代Fib实例
print(n)
1
1
2
3
5
8
13
21
34
55
89
144
233
377
610
987
1597
2584
4181
6765
10946
17711
28657
46368
75025
__getitem__
Fib
实例虽然可以作用于for
循环,看起来有点像list
,但是list
可以根据索引取值,然而Fib
实例还不行,需要在类中实现__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[10] # 输出89,可以通过下标索引获得第11个元素的值
但是上面的代码并不支持list
中的切片语法即f[5:10]
,因为__getitem__
传入的参数可能是int
也可能是slice
,要对这两种参数做判断:
class Fib(object):
def __getitem__(self, n):
if isinstance(n, int): # 若参数为整型
a, b = 1, 1
for x in range(n):
a, b = b, a + b
return a
if isinstance(n, slice): # 若参数为切片
start = n.start # 获取切片的起点索引
end = n.stop # 获取切片的终点索引
if start is None:
start = 0 # 若起点索引为空,则设置为0
L = []
a, b = 1, 1
for x in range(end):
if x >= start:
L.append(a)
a, b = b, a + b
return L
f = Fib()
f[10] # 输出 89
f[:5] # 输出 [1, 1, 2, 3, 5]
f[5:10] # 输出 [8, 13, 21, 34, 55]
[8, 13, 21, 34, 55]
但是上面block的代码还是不够完善,比如没有对step参数作处理即f[:10:2]
,也没有对负数索引作处理,所以要用__getitem__
完整地复刻list
的功能还有很多工作要做。
若想把对象视为dict
,那么__getitem__
的参数也可能是一个作为key
的对象如str
,与之对应的是__setitem__
方法,可以将对象视作list
或dict
来对其赋值。最后还有一个__delitem__
方法,用于删除某个元素。
__getattr__
上文提到,当调用的类的方法或属性不存在时就会报AttirbuteError
错,避免这个错误除了给类添加一个score
属性之外,还可以写一个__getattr__
方法动态地返回一个属性:
class Student(object):
def __init__(self):
self.name = 'Jensen'
def __getattr__(self, attr):
if attr == 'score': # 若参数名为score
return 99 # 返回 99
if attr == 'age': # 若参数名为age
return lambda: 22 # 返回匿名函数
raise AttributeError('\'Student\' object has no attribute \'%s\'' % attr) # 调用其他参数均报错
s = Student()
s.name # 输出 'Jensen'
s.score # 输出 99
s.age() # 输出 22,调用匿名函数
__call__
一个对象实例可以有自己的属性和方法,可通过instance.method()
的形式来调用,若想直接用instance()
的形式来调用,在Python中可以在类中重写__call__
方法来实现:
class Student(object):
def __init__(self, name):
self.name = name
def __call__(self):
print('My name is %s.' % self.name)
s = Student('Jensen')
s(). # 打印输出 'My name is Jensen.'
__call__
方法还可以定义参数,对实例进行直接调用就像对一个函数进行调用一样,所以完全可以把对象看成函数,因为函数和对象二者之间本来就没啥根本的区别。
可以通过callable
方法判断一个变量是否是可以被调用的:
callable(max) # True,max函数可以被调用
callable(None) # False,None不可以被调用
callable('hello') # False,字符串不可以被调用
callable(Student()) # True,若去掉上面Student类代码中的__call__方法后就会变成不可调用即False
使用枚举类
当需要定义常量时,一般会用大写变量通过整数来定义比如JAN = 1, FEB = 2, ...
,虽然定义起来十分简单但缺点是类型依然是int
变量。在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(): # 可以枚举其所有成员,value是默认自动赋给成员的int常量,默认从1开始计数
print(name, ' ==> ', member, ', ', member.value)
也可以从Enum
类派生出自定义类:
from enum import Enum, unique
@unique # @unique可以检查确保没有重复值
class Weekday(Enum):
Sun = 0 # Sun的value被设置为0
Mon = 1
Tue = 2
Wed = 3
Thu = 4
Fri = 5
Sat = 6
# 访问这些枚举类型有若干种方法:
day1 = Weekday.Mon
print(day1)
print(Weekday.Tue)
print(Weekday['Wed'])
print(Weekday.Sun.value)
print(Weekday(6))
print(day1 == Weekday.Mon)
print(Weekday.Mon == Weekday['Fri'])
# Weekday(7) # 若取消注释会报ValueError错,因为没有枚举类型的value为7
for name, member in Weekday.__members__.items():
print(name, ' ==> ', member, ', ', member.value)
Weekday.Mon
Weekday.Tue
Weekday.Wed
0
Weekday.Sat
True
False
Sun ==> Weekday.Sun , 0
Mon ==> Weekday.Mon , 1
Tue ==> Weekday.Tue , 2
Wed ==> Weekday.Wed , 3
Thu ==> Weekday.Thu , 4
Fri ==> Weekday.Fri , 5
Sat ==> Weekday.Sat , 6
使用元类
type()
动态语言和静态语言最大的区别就是函数和类的定义,不是在编译时定义的而是在运行时动态创建的。
比如要定义一个Hello
类,可以写一个hello.py
模块:
class Hello(object):
def hello(self, name = 'world'):
print('Hello, %s.' % name)
当Python解释器载入hello
模块时就会依次执行该模块的所有语句,结果就是动态创建出一个Hello
的类对象:
from hello import Hello
h = Hello()
h.hello() # 打印输出"Hello, world."
print(type(Hello)) # 打印输出"<class 'type'>"
print(type(h)) # 打印输出"<class 'hello.Hello'>"
类的定义是运行时动态创建的,type
函数除了可以查看一个类型或变量的类型,也是能在运行时动态创建类的函数。type
函数既可以返回一个对象的类型,又可以创建出新的类型,如可以通过type
函数创建出Hello
类,无需通过class Hello(object)...
的定义:
def func(self, name = 'world'): # 先定义类的方法
print('Hello, %s.' % name)
Hello = type('Hello', (object,), dict(hello = func)) # 创建Hello类
h = Hello()
h.hello()
print(type(Hello))
print(type(h))
Hello, world.
<class 'type'>
<class '__main__.Hello'>
要创建一个类对象,type
函数需依次传入3个参数:
- 类的名称
- 继承的基类集合,支持多重继承
- 类的方法名与函数绑定(在上一个block中,将函数func绑定到方法名hello上)
通过type
函数创建的类和直接用class
定义的类本质上是完全一样的,但正常情况下都使用class
定义类。
metaclass
除了上文提到type
可以动态创建类之外,还可以使用metaclass
(元类)控制类的创建行为。正常情况下都是先定义类,再根据类的定义创建相应的实例,元类的意思即:但如何创建出类,则需要根据metaclass
创建类。
先定义元类 ==> 创建类 ==> 创建实例
所以,元类允许创建类或者修改类,也就是说可以把类看成是依据元类创建出来的“实例”。元类是Python面向对象里最难理解最难使用的魔术代码。
例子:使用metaclass
给自定义类MyList
增加一个add
方法:
# 先定义 ListMetaclass,按照默认习惯,元类的类名总是以Metaclass结尾,便于辨别
class ListMetaclass(type): # metaclass是类的模板,所以必须从`type`类型派生
# __new__方法接收的参数依次是:准备创建的类的对象;类的名字;类继承的基类集合,类的方法集合
def __new__(cls, name, bases, attrs):
attrs['add'] = lambda self, value: self.append(value)
return type.__new__(cls, name, bases, attrs)
# 有了 ListMetaclass,在定义类的时候还要指示使用ListMetaclass来定制类,传入关键字参数metaclass
# 当传入关键字参数metaclass时,魔术就生效了,它指示在创建MyList时通过ListMetaclass.__new__()来创建
class MyList(list, metaclass = ListMetaclass):
pass
L = MyList()
L.add(1) # add方法,普通的list没有add方法
L # 输出 [1]
[1]
正常情况下没有人会用上面这么复杂的方法,直接在MyList
定义中写上add
方法显然更简单。但总会遇到需要通过元类修改类定义的,ORM
就是一个典型的例子。
ORM(Object Relational Mapping 对象关系映射),就是指将关系型数据库的一行映射为一个对象即一个类对应一个表,这种方式可可大大简化代码编写。
要编写一个ORM
框架,所有的类都只能动态定义,因为只有使用者才能根据表的结构定义出对应的类。例子:编写一个ORM
框架:
# User接口的基类Model和属性类型StringField、IntegerField由ORM框架提供,魔法方法等如save全部由基类Model自动完成。接下来实现ORM框架
# 首先定义Field类,负责保存数据库表的字段名和字段类型
class Field(object):
def __init__(self, name, column_type):
self.name = name
self.column_type = column_type
def __str__(self):
return '<%s: %s>' % (self.__class__.__name__, self.name)
# 在Field的基础上进一步定义各种类型的Field
class StringField(Field):
def __init__(self, name):
super(StringField, self).__init__(name, 'varchar(100)')
class IntegerField(Field):
def __init__(self, name):
super(IntegerField, self).__init__(name, 'bigint')
# 接下来编写ModelMetaclass
class ModelMetaclass(type):
def __new__(cls, name, bases, attrs):
if name == 'Model':
return type.__new__(cls, name, bases, attrs)
print('Found model: %s' % name)
mappings = dict()
for k, v in attrs.items():
if isinstance(v, Field):
print('Found mapping: %s ==> %s' % (k, v))
mappings[k] = v
for k in mappings.keys():
attrs.pop(k) # 退栈
attrs['__mappings__'] = mappings
attrs['__table__'] = name
return type.__new__(cls, name, bases, attrs)
# 以及基类Model
class Model(dict, metaclass=ModelMetaclass):
def __init__(self, **kw):
super(Model, self).__init__(**kw)
def __getattr__(self, key):
try:
return self[key]
except KeyError:
raise AttributeError(r"'Model' object has no attribute '%s'" % key)
def __setattr__(self, key, value):
self[key] = value
def save(self):
fields = []
params = []
args = []
for k, v in self.__mappings__.items():
fields.append(v.name)
params.append('?')
args.append(getattr(self, k, None))
sql = 'insert into %s (%s) values (%s)' % (self.__table__, ','.join(fields), ','.join(params))
print('SQL: %s' % sql)
print('ARGS: %s' % str(args))
# 编写接口,定义一个User类操作对应的数据库User表
class User(Model):
# 类属性到列的映射
id = IntegerField('id')
name = StringField('username')
email = StringField('email')
password = StringField('password')
u = User(id = 12345, name = 'Jensen', email = 'jensen.acm@gmail.com', password = 'test123')
u.save() # 运行正常,只需真正连接到数据库上,执行SQL,就可以完成真正的功能
Found model: User
Found mapping: id ==> <IntegerField: id>
Found mapping: name ==> <StringField: username>
Found mapping: email ==> <StringField: email>
Found mapping: password ==> <StringField: password>
SQL: insert into User (id,username,email,password) values (?,?,?,?)
ARGS: [12345, 'Jensen', 'jensen.acm@gmail.com', 'test123']
当定义一个class User(Model)
时,Python解释器首先在当前类User
的定义中查找metaclass
,如果没有找到,就继续在基类Model
中查找,找到后使用Model
中定义的metaclass
的ModelMetaclass
来创建User
类。
在ModelMetaclass
中,一共做了如下几件事:
- 排除对
Model
类的修改; - 在当前类
User
中查找定义类的所有属性,如果找到一个Field
属性就把它保存在一个__mappings__
的字典中,同时从类属性中删除该属性,否则容易在运行时出错; - 把表名保存到
__table__
中,这里简化将类名作为默认表名。
在Model
类中可以定义各种操作数据库的方法,如save
、delete
、find
、update
等。
错误、调试和测试
错误处理
在程序运行的过程中,如果发生了错误可以事先约定返回一个错误代码,这样就知道是否有错以及出错的原因。
在操作系统提供的调用中,返回错误码非常常见,比如打开文件的函数open
,成功时返回文件描述符即一个整数,出错时返回-1
。
但是用错误码表示是否出错十分麻烦,函数本身应该返回的正常结果和错误码混在一起,需要调用者编写很多代码判断是否出错。一旦出错,需要一级一级上报直到某个函数可以处理该错误。
高级语言通常都内置了一套try...except...finally...
的错误处理机制,Python也不例外。
try
先来看一个try
的例子:被除数不可以为0
:
try: # 编写代码时认为某些代码可能会出错,可以用try来包裹这段代码
print('try...')
r = 10 / 0 # 被除数不可以为0,此处出错后续代码不会执行,直接跳到except处即错误处理部分
print('result: ', r)
except ZeroDivisionError as e: # except捕获到ZeroDivisionError
print('except: ', e) # 打印错误,如果后面有finally语句块则执行finally语句块,没有就结束
finally:
print('finally...')
print('END')
try...
except: division by zero
finally...
END
若将上面block中的r = 10 / 0
修改为r = 10 / 2
,则执行结果如下:
try...
result: 5
finally...
END
由于ZeroDivisionError
错误没有发生,因此except
语句块不会执行,但是无论是否出现错误,只要存在finally
语句块,finally
语句块就一定会被执行。
错误有很多种类,如ZeroDivisionError
、ValueError
、AttributeError
等,若发生了不同的错误,应由不同的except
语句块处理即可以有多个except
来捕获不同类型的错误:
try:
print('try...')
r = 10 / int('a') # int函数会抛出ValueError
print('result: ', r)
except ValueError as e: # except捕获ValueError
print('ValueError: ', e)
except ZeroDivisionError as e:
print('ZeroDivisionError: ', e)
else: # 若没有任何错误发生,则会自动执行else语句
print('No ERROR!')
finally:
print('finally...')
print('END')
Python中的错误也是类,所有的错误类型都继承自BaseException
,所以在使用except
时要注意它不仅捕获该类型的错误,同时也把它的子类都捕获了:
try:
foo() # 会抛出UnicodeError错误
excpet ValueError as e: # 错误被该except捕获,因为UnicodeError是ValueError的子类
print('ValueError: ', e)
except UnicodeError as e: # 该except永远捕获不到UnicodeError,错误已经被上一个except捕获了
print('UnicodeError: ', e)
使用try...except
结构捕获错误还有一个巨大的优势,即可以跨越多层调用,比如main
函数调用bar
函数,bar
函数调用foo
函数,若foo
函数出错了,仅需最外层的main
捕获到了就可以进行处理:
def foo(s):
return 10 / int(s)
def bar(s):
return foo(s) * 2
def main():
try: # 毋需在每个可能出错的地方捕获错误,只要在合适的层次捕获就好了,简化代码
bar('0')
except Exception as e: # except会捕获到错误 division by zero
print('Error: ', e)
finally:
print('finally...')
main()
Error: division by zero
finally...
调用栈
如果错误没有被捕获,则会一直上抛,最后被Python解释器所捕获,最后打印一个错误信息后程序退出,例如:
Traceback (most recent call last):
File "error.py", line 11, in <module>
main()
File "error.py", line 9, in main
bar('0')
File "error.py", line 6, in bar
return foo(s) * 2
File "error.py", line 3, in foo
return 10 / int(s)
ZeroDivisionError: division by zero
可以根据上述错误跟踪信息依次分析,最终定位到错误的源头发生在error.py
文件的第三行return 10 / int(s)
,因为最后打印了ZeroDivisionError: division by zero
,根据错误类型以及错误信息可以判断int(s)
本身没有错,只是int(s)
返回了0
,在计算10 / 0
时出了错。
记录错误
若不捕获错误,Python解释器自然会打印出错误堆栈,但是程序也终止了,既然可以捕获错误,就可以把错误堆栈打印记录下来,同时让程序继续执行下去,后来再根据需要分析错误原因。Python内置的logging
模块可以很好的实现这一点:
import logging
def foo(s):
return 10 / int(s)
def bar(s):
return foo(s) * 2
def main():
try:
bar('0')
except Exception as e:
logging.exception(e) # 通过适当的配置,logging还可以把错误信息保存在日志文件里,方便日后排查
main() # 同样是出错,但程序打印完错误堆栈后会继续执行,并正常退出
print('END')
ERROR:root:division by zero
Traceback (most recent call last):
File "/tmp/ipykernel_15563/3467350787.py", line 11, in main
bar('0')
File "/tmp/ipykernel_15563/3467350787.py", line 7, in bar
return foo(s) * 2
File "/tmp/ipykernel_15563/3467350787.py", line 4, in foo
return 10 / int(s)
ZeroDivisionError: division by zero
END
抛出错误
错误是类,捕获错误就是捕获到该类的一个实例,Python内置函数会抛出很多的类型的错误,也可以自己编写函数抛出相应的错误。
若要抛出错误,可以根据需要定义一个错误的类,选择好继承关系,最后用raise
语句抛出错误实例:
class FooError(ValueError): # 自定义错误类,继承自ValueError
pass
def foo(s):
n = int(s)
if n == 0:
raise FooError('invalid value: %s' % s) # 抛出自定义错误
return 10 / n
foo('0')
---------------------------------------------------------------------------
FooError Traceback (most recent call last)
/tmp/ipykernel_15563/3824456867.py in <module>
8 return 10 / n
9
---> 10 foo('0')
/tmp/ipykernel_15563/3824456867.py in foo(s)
5 n = int(s)
6 if n == 0:
----> 7 raise FooError('invalid value: %s' % s) # 抛出自定义错误
8 return 10 / n
9
FooError: invalid value: 0
如果可以选择Python已有的内置错误类型,尽量使用Python内置的错误类型。只有在必要的时候才自定义错误类型。
有时候底层代码不清楚如何处理错误,可以将错误上抛,抛给高层调用者处理:
def foo(s):
n = int(s)
if n == 0:
raise ValueError('invalid value: %s' % s) # 抛出ValueError
return 10 / n
def bar():
try:
foo('0')
except ValueError as e: # 底层调用者捕获到ValueError
print('ValueError!') # 打印错误只是单纯的记录一下
raise # 底层调用者不知道如何处理错误,继续上抛给高层调用者处理
bar()
ValueError!
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
/tmp/ipykernel_15563/1057959981.py in <module>
12 raise # 底层调用者不知道如何处理错误,继续上抛给高层调用者处理
13
---> 14 bar()
/tmp/ipykernel_15563/1057959981.py in bar()
7 def bar():
8 try:
----> 9 foo('0')
10 except ValueError as e: # 底层调用者捕获到ValueError
11 print('ValueError!') # 打印错误只是单纯的记录一下
/tmp/ipykernel_15563/1057959981.py in foo(s)
2 n = int(s)
3 if n == 0:
----> 4 raise ValueError('invalid value: %s' % s) # 抛出ValueError
5 return 10 / n
6
ValueError: invalid value: 0
raise
语句如果不带任何参数就会把当前错误原样抛出。此外,在except
中raise
一个错误还可以把一种类型的错误转化为另一种类型:
try:
10 / 0
except ZeroDivisionError: # 捕获到一个ZeroDivisionError
raise ValueError('Input Error!') # 将错误转换为ValueError
只要是合理的转换逻辑就可以,但是,决不应该把一个IOError
转换成毫不相干的ValueError
。