Python基础

函数知识点汇总
调用函数
Python内置了很多有用的函数可以直接调用。
要掉用一个函数需要知道函数的名称与参数,可以从Python的官方网站查看文档,也可以通过help函数查询帮助信息,如help(abs)。
调用函数时,如果传入的参数数量不对,会报TypeError的错误,并且Python会明确地告诉你所调用的函数需要几个参数;如果传入参数数量正确,但是参数类型不被函数所接受,也会报TypeError错误。
也有函数可以接受任意多个参数,并返回最大的那个,如max函数:
max(1, 3) # 3
max(1, -3, 2) # 22
数据类型转换
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)
breakg: 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):
passclass后面紧跟着类名即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)中的strTrue
使用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: Jingyao Zhang)"
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。