Python Basics

Python Basics

December 29, 2021·Jensen
Jensen

image

函数知识点汇总


调用函数


Python内置了很多有用的函数可以直接调用。 要掉用一个函数需要知道函数的名称与参数,可以从Python的官方网站查看文档,也可以通过help函数查询帮助信息,如help(abs)

调用函数时,如果传入的参数数量不对,会报TypeError的错误,并且Python会明确地告诉你所调用的函数需要几个参数;如果传入参数数量正确,但是参数类型不被函数所接受,也会报TypeError错误。

也有函数可以接受任意多个参数,并返回最大的那个,如max函数:

max(1, 3)  # 3
max(1, -3, 2)  # 2
2

数据类型转换

Python内置的函数还包括数据类型转换函数,如intstr等。

函数名其实就是指向一个函数对象的引用,因此完全可以把函数名赋给一个变量,相当于给这个函数起了一个“别名”:

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 Nonereturn

空函数

如果定义一个啥都不做的空函数可以在其函数体部分使用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函数便可以计算任意次方数,而且此时它拥有两个位置参数xn,调用时,需要按位置顺序依次给这两个参数赋值。

默认参数

但是似乎日常开发中用到二次方计算的情况远大于其他次方,此时可以使用默认参数,将第二个参数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'}

命名关键字参数

若要限制关键字参数的名字,只接收cityjob作为关键字参数,需要在函数的参数表中添加一个分隔符*,分隔符后面的参数被视为命名关键字参数:

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 - 1num * 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。即索引为012正好三个元素,如果开始的索引是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一旦初始化就不能修改。)

迭代


若给定一个listtuple,可以通过循环来遍历它们,这种遍历就是迭代。在Python中使用for ... in来完成迭代,不仅可以用在listtuple上,还可以作用在其他可迭代对象(如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错误,返回值就包含在StopIterationvalue中:

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循环的数据类型有:

  • 集合数据类型:如listtupledictsetstr等;
  • 生成器;

上述这些可直接作用于for循环的对象被统称为可迭代(Iterable)对象,可以使用isinstance函数判断一个对象是否是可迭代对象:isinstance([], Iterable)

可以被next函数调用并不断返回下一个值的对象成为迭代器(Iterator),也可以使用isinstance函数判断一个对象是否是迭代器对象:isinstance([], Iterator)

因此,生成器即是可迭代对象,也是迭代器。但是listdictstr等虽然是可迭代对象,但不是迭代器,可以使用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)时,参数xyf分别接收-65abs

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函数也接收一个函数和一个可迭代序列,将传入的函数依次作用于序列中的每个元素,根据返回值是TrueFalse来决定保留还是丢弃该元素。例如删掉一个列表中的奇数,只保留偶数:

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)

调用上述f1f2f3函数返回的结果均为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类中,每个实例就拥有各自的namescore这些数据,可以通过函数来访问这些数据:

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的代码中可以发现,在继承关系中,如果一个实例的数据类型是某个子类,那他的数据类型也可被看作是基类。但是反过来却不行,如上面的bAnimal类型但却不是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方法是作用在AnimalDog还是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()即返回大写的字符串。

可以通过getattrsetattrhasattr等函数查看一个对象的状态:

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实例添加nameage属性,可以在定义类的时候定义一个特殊的_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类可能会有DogBird等子类,但是DogBird又能向下细分出好几类,Dog可能会有HoundsHerdings等子类、Bird可能有FlyableRunnable等子类。

鸵鸟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__方法,可以将对象视作listdict来对其赋值。最后还有一个__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中定义的metaclassModelMetaclass来创建User类。

ModelMetaclass中,一共做了如下几件事:

  • 排除对Model类的修改;
  • 在当前类User中查找定义类的所有属性,如果找到一个Field属性就把它保存在一个__mappings__的字典中,同时从类属性中删除该属性,否则容易在运行时出错;
  • 把表名保存到__table__中,这里简化将类名作为默认表名。

Model类中可以定义各种操作数据库的方法,如savedeletefindupdate等。

错误、调试和测试


错误处理


在程序运行的过程中,如果发生了错误可以事先约定返回一个错误代码,这样就知道是否有错以及出错的原因。

在操作系统提供的调用中,返回错误码非常常见,比如打开文件的函数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语句块就一定会被执行。

错误有很多种类,如ZeroDivisionErrorValueErrorAttributeError等,若发生了不同的错误,应由不同的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语句如果不带任何参数就会把当前错误原样抛出。此外,在exceptraise一个错误还可以把一种类型的错误转化为另一种类型:

try:
    10 / 0
except ZeroDivisionError:  # 捕获到一个ZeroDivisionError
    raise ValueError('Input Error!')  # 将错误转换为ValueError

只要是合理的转换逻辑就可以,但是,决不应该把一个IOError转换成毫不相干的ValueError

Last updated on