贫瘠之地

华北无浪漫,死海扬起帆
多少个夜晚,独自望着天

0%

Python 进阶语法

因为 Python 并非是工作内容需要使用的语言,所以对语法并没有系统地学习过,加上也不会使用 Python 编写什么复杂的逻辑,所以一直以来都是使用过程中遇到需要的逻辑语法就查一下

在学习 LangChain 的过程中,发现 Python 的语法糖很多,很多设计思想提供了语法层面的支持,使得阅读源码挺费劲的

所以在这里还是系统地了解下 Python 都提供了哪些语法,也了解下 Python 编写代码的主要思想

这里主要是整理下 Python 中比较特殊的、和 Java 有较大区别的操作

函数

多返回值

Python 允许函数返回多个值,并且调用时使用多个变量承接

1
2
3
4
5
6
7
def build():
return 1, 2, 3

a, b, c = build()
print(a) # 1
print(b) # 2
print(c) # 3

实际上真正的返回值是一个 Tuple 对象,Python 帮忙做了解压(unpack)操作

1
2
3
4
5
6
def build():
return 1, 2, 3

t = build()
print(t) # (1, 2, 3)
print(t.__class__) # <class 'tuple'>

默认参数

设置参数的默认值,默认参数可以简化函数的调用

需要注意:

  • 必选参数在前,默认参数在后
  • 实践中一般将变化大的参数放在前面,变化小的参数放在后面(简单理解,就是因为变化小才需要默认)
  • 当不按顺序提供部分默认参数时,需要把参数名写上;如 default_params('李四', city='上海')
1
2
3
4
5
6
7
8
9
def default_params(name, age=20, city='北京'):
print(name)
print(age)
print(city)


default_params('张三')
default_params('李四', city='上海')
default_params('王五', 21, '上海')

默认参数有一个需要注意的坑:默认参数必须指向不变对象

1
2
3
4
5
6
7
8
def default_params(L=[]):
L.append("END")
print(L)


default_params([1, 2, 3]) # [1, 2, 3, 'END']
default_params() # ['END']
default_params() # ['END', 'END']

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

实际上 PyCharm IDE 也会出现警告 Default argument value is mutable

可以使用 None 来解决上述问题

1
2
3
4
5
def default_params(L=None):
if L is None:
L = []
L.append("END")
print(L)

可变参数

可变参数指传入的参数个数是可变的,例如 0 个、1 个、2 个...

定义可变参数和定义一个 list 或 tuple 参数相比,只需要在参数前面加了一个 * 号,在函数内部实现逻辑可以完全不变

1
2
3
4
5
6
7
def sum(*nums):
sum = 0
for n in nums:
sum += n
return sum

print(sum(1, 2, 3, 4)) # 10

如果我们已经有了一个 list 或 tuple,则无法直接作为参数传入该方法,也可以使用 * 号将其转换为可变参数

1
2
3
4
5
6
7
8
9
10
def sum(*nums):
sum = 0
for n in nums:
sum += n
return sum


nums = [1, 2, 3, 4]
print(sum(nums)) # 报错;TypeError: unsupported operand type(s) for +=: 'int' and 'list'
print(sum(*nums)) # 10

关键字参数

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

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

使用 ** 表示希望接收的是个关键字参数

1
2
3
4
5
def build_student(name, age, **other):
print('name:', name, 'age:', age, 'other:', other)

build_student(name='张三', age=20, city='北京', birthday='2004-01-01')
# name: 张三 age: 20 other: {'city': '北京', 'birthday': '2004-01-01'}

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

1
2
3
4
5
def build_student(name, age, **other):
print('name:', name, 'age:', age, 'other:', other)

student = {name='张三', age=20, city='北京', birthday='2004-01-01'}
build_student(**student)

对于关键字参数,函数的调用方可以传入任意不受限制的关键字参数,对于参数值可以在函数内进行参数校验

对于参数名字的限制可以通过命名关键字参数来进行限制,通过 * 号分隔位置参数和关键字参数

1
2
3
4
5
def build_student(name, age, *, city):
print('name:', name, 'age:', age, 'city:', city)

build_student(name='张三', age=20, city='北京', birthday='2004-01-01')
# 报错 TypeError: build_student() got an unexpected keyword argument 'birthday'

命名关键字参数也可以有默认值

1
2
3
4
5
def build_student(name, age=20, *, city='北京'):
print('name:', name, 'age:', age, 'city:', city)

build_student(name='张三')
# name: 张三 age: 20 city: 北京

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

组合参数

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

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

比如定义一个函数,包含上述若干种参数:

1
2
3
4
5
def f1(a, b, c=0, *args, **kw):
print('a =', a, 'b =', b, 'c =', c, 'args =', args, 'kw =', kw)

def f2(a, b, c=0, *, d, **kw):
print('a =', a, 'b =', b, 'c =', c, 'd =', d, 'kw =', kw)

最神奇的是通过一个 tuple 和 dict 也可以调用上述函数

1
2
3
4
args = (1, 2, 3, 4)
kw = {'d': 99, 'x': '#'}
f1(*args, **kw)
# a = 1 b = 2 c = 3 args = (4,) kw = {'d': 99, 'x': '#'}

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

集合特性

切片

l[0:3] 表示,从索引 0 开始取,直到索引 3 为止,但不包括索引 3(左闭右开,基本上所有的集合 API 都是这种设计)

1
2
3
4
l = [1, 2, 3, 4]

print(l[0:3])
# [1, 2, 3]

如果第一个索引是 0,还可以省略

1
2
print(l[:3])
# [1, 2, 3]

-1 表示取倒数第一个元素,即同样支持倒数切片

1
2
print(l[-3:-1])
# [2, 3]

前 10 个数,每 2 个取一个

1
2
3
4
l = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]

print(l[:10:2])
# [1, 3, 5, 7, 9]

所有数每 3 个取一个

1
2
3
4
l = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]

print(l[::3])
# [1, 4, 7, 10, 13]

甚至什么都不写,只写 [:] 就可以原样复制一个 list

1
2
3
4
l = [1, 2, 3, 4, 5]

print(l[:])
# [1, 2, 3, 4, 5]

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

1
2
3
4
5
6
t = (1, 2, 3, 4, 5)

print(t[:3])
# (1, 2, 3)
print(t[:3].__class__)
# <class 'tuple'>

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

1
2
3
4
5
6
s = 'Hello World!'

print(s[-6:])
# World!
print(s[-6:].__class__)
# <class 'str'>

迭代

只要是可迭代对象,无论有无下标,都可以迭代,比如 dict 就可以迭代

因为 dict 的存储不是按照 list 的方式顺序排列,所以迭代出的结果顺序很可能不一样

1
2
3
4
5
6
7
d = {'name': 'zhangsan', 'age': 20, 'city': 'beijing'}

for key in d:
print(key)
# name
# age
# city

默认情况下,dict 迭代的是 key;如果要迭代 value,可以用 for value in d.values(),如果要同时迭代 key 和 value,可以用 for k, v in d.items()

1
2
3
4
5
6
7
d = {'name': 'zhangsan', 'age': 20, 'city': 'beijing'}

for item in d.items():
print(item)
# ('name', 'zhangsan')
# ('age', 20)
# ('city', 'beijing')

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

1
2
3
4
5
6
7
from collections.abc import Iterable

print(isinstance('abc', Iterable)) # True
print(isinstance([1, 2, 3], Iterable)) # True
print(isinstance((1, 2, 3), Iterable)) # True
print(isinstance({1: 'a', 2: 'b', 3: 'c'}, Iterable)) # True
print(isinstance(123, Iterable)) # False

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

1
2
3
4
5
for i, value in enumerate(['A', 'B', 'C']):
print(i, value)
# 0 A
# 1 B
# 2 C

Python 中也可以同时引用多个变量进行 for 循环

1
2
3
4
5
for name, age in ('张三', 20), ('李四', 21), ('王五', 20):
print(age, name)
# 20 张三
# 21 李四
# 20 王五

列表生成式

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

如果要生成 [1x1, 2x2, 3x3, ..., 10x10] 的 list 方法一是循环

1
2
3
4
5
l = []
for x in range(1, 11):
l.append(x * x)
print(l)
# [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]

而列表生成式则可以用一行语句代替循环生成上面的 list

1
2
3
l = [x * x for x in range(1, 11)]
print(l)
# [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]

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

1
2
3
l = [m + n for m in ['草莓味', '柠檬味', '菠萝味'] for n in ['棒棒糖', '口香糖', '水果茶']]
print(l)
# ['草莓味棒棒糖', '草莓味口香糖', '草莓味水果茶', '柠檬味棒棒糖', '柠檬味口香糖', '柠檬味水果茶', '菠萝味棒棒糖', '菠萝味口香糖', '菠萝味水果茶']

if ... else ...

在生成式中,也可以使用 if ... else

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

1
2
3
4
# 在 [0,10) 范围中取出 x % 2 == 0 的数字
l = [x for x in range(0, 10) if x % 2 == 0]
print(l)
# [0, 2, 4, 6, 8]
1
2
3
4
# 如果 x % 2 != 0,则取 'x'
l = [x if x % 2 == 0 else 'x' for x in range(0, 10)]
print(l)
# [0, 'x', 2, 'x', 4, 'x', 6, 'x', 8, 'x']

生成器

在 Python 中,一边循环一边计算的机制,称为生成器 Generator

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

1
2
3
l = (x for x in range(0, 10) if x % 2 == 0)
print(l) # <generator object <genexpr> at 0x000002293F899630>
print(type(l)) # <class 'generator'>

获取生成器元素时,可以使用 next,也可以将其作为迭代器循环

1
2
3
4
5
6
7
8
l = (x for x in range(0, 10) if x % 2 == 0)

print(next(l))
print(next(l))
print('------------')

for i in l:
print(i)
1
2
3
4
5
6
0
2
------------
4
6
8

同理也可以将迭代器输出为 list

1
2
print(list(l.__iter__()))
# [0, 2, 4, 6, 8]

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

下面是一个斐波那契数列的函数

1
2
3
4
5
6
7
def fib(max):
n, a, b = 0, 0, 1
while n < max:
print(b)
a, b = b, a + b
n = n + 1
return 'done'

只需要把 print(b) 改为 yield b 就修改为了生成器

1
2
3
4
5
6
7
8
9
10
11
def fib(max):
n, a, b = 0, 0, 1
while n < max:
yield b
a, b = b, a + b
n = n + 1
return 'done'


print(list(fib(10).__iter__()))
# [1, 1, 2, 3, 5, 8, 13, 21, 34, 55]

需要注意:

  • 函数生成器遇到 yield 语句返回,再次执行时从上次返回的 yield 语句处继续执行
  • 没有 yield 执行时会报错 StopIteration
  • 调用 generator 函数会创建一个 generator 对象,多次调用 generator 函数会创建多个相互独立的 generator

此外,生成器函数中的 return 会被当做 StopIteration 异常抛出,如果想要获取返回值,需要解析异常;并且迭代器不会抛出异常,需要使用 next 获取

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def return_gen():
yield 1
yield 2
return 100


for i in return_gen():
print(i)

print("------------")
rg = return_gen()
while True:
try:
i = next(rg)
print(i)
except StopIteration as e:
print('return value:' + str(e.value))
break

1
2
3
4
5
6
1
2
------------
1
2
return value:100

迭代器

前面已经了解到可以直接作用于 for 循环的数据类型有以下几种

  • 集合数据类型,如 list、tuple、dict、set、str 等
  • 生成器,包括生成器和带 yield 的生成器函数

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

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

1
2
3
4
5
print(isinstance([], Iterable)) # True
print(isinstance({}, Iterable)) # True
print(isinstance((x for x in range(10)), Iterable)) # True
print(isinstance('a', Iterable)) # True
print(isinstance(1, Iterable)) # False

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

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

1
2
3
4
5
print(isinstance([], Iterator)) # False
print(isinstance({}, Iterator)) # False
print(isinstance((x for x in range(10)), Iterator)) # True
print(isinstance('a', Iterator)) # False
print(isinstance(1, Iterator)) # False

生成器都是 Iterator 对象,但 list、dict、str 虽然是 Iterable,却不是 Iterator

把 list、dict、str 等 Iterable 变成 Iterator 可以使用 iter 函数

1
2
3
print(isinstance(iter([]), Iterator)) # True
print(isinstance(iter({}), Iterator)) # True
print(isinstance(iter('a'), Iterator)) # True

为什么 list、dict、str 等数据类型不是 Iterator

因为 Python 的 Iterator 对象表示的是一个数据流,Iterator 对象可以被 next 函数调用并不断返回下一个数据,直到没有数据时抛出 StopIteration 错误

可以把这个数据流看做是一个有序序列,但我们却不能提前知道序列的长度,只能不断通过 next 函数实现按需计算下一个数据,所以 Iterator 的计算是惰性的,只有在需要返回下一个数据时它才会计算

函数式编程

高阶函数

一个函数接收另一个函数作为参数,这种函数就称之为高阶函数(Java 中的 Function)

1
2
3
4
5
def add(x, y, f):
return f(x) + f(y)

print(add(3, -5, abs))
# 8

map

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

1
2
3
4
o_list = [-1, 2, -3, -4, 5, 6]
n_list = map(abs, o_list)
print(list(n_list))
# [1, 2, 3, 4, 5, 6]

reduce

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

1
reduce(f, [x1, x2, x3, x4]) = f(f(f(x1, x2), x3), x4)
1
2
3
4
5
def fn(x, y):
return x * 10 + y

print(reduce(fn, [1, 3, 5, 7, 9]))
# 13579
1
2
3
4
5
6
7
8
9
10
11
DIGITS = {'0': 0, '1': 1, '2': 2, '3': 3, '4': 4, '5': 5, '6': 6, '7': 7, '8': 8, '9': 9}

def char2num(s):
return DIGITS[s]

def str2int(s):
return reduce(lambda x, y: x * 10 + y, map(char2num, s))

i = str2int("1234")
print(i) # 1234
print(i.__class__) # <class 'int'>

filter

filter 也接收一个函数和一个序列

把传入的函数依次作用于每个元素,然后根据返回值是 True 还是 False 决定保留还是丢弃该元素

1
2
3
4
5
def not_empty(s):
return s and s.strip()

print(list(filter(not_empty, ['A', '', 'B', None, 'C', ' '])))
# ['A', 'B', 'C']

需要注意 filter 使用了惰性计算,所以只有在取结果的时候,才会真正筛选并每次返回下一个筛出的元素

sort

如果是字符串或者两个 dict 直接比较数学上的大小是没有意义的,因此比较的过程必须通过函数抽象出来

直接排序

1
2
3
list = sorted(["zhangsan", "lisi", "wangwu", "zhaoliu"])
print(list)
# ['lisi', 'wangwu', 'zhangsan', 'zhaoliu']

sorted 函数也是一个高阶函数,它还可以接收一个 key 函数来实现自定义的排序

1
2
3
list = sorted([10, -5, 1, 0, -7, 3], key=abs)
print(list)
# [0, 1, 3, -5, -7, 10]

reverse 可以控制顺逆序,逆序为由大到小

1
2
3
list = sorted(["zhangsan", "lisi", "wangwu", "zhaoliu", "a"], key=str.__len__, reverse=True)
print(list)
# ['zhangsan', 'zhaoliu', 'wangwu', 'lisi', 'a']

函数返回值

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

1
2
3
4
5
6
7
8
9
10
11
12
def lazy_sum(*args):
def sum():
ax = 0
for n in args:
ax = ax + n
return ax

return sum


lazy_sum = lazy_sum(1, 2, 3, 4)
print(lazy_sum()) # 10

调用函数 lazy_sum 时,才真正计算求和的结果

当调用lazy_sum()时,每次调用都会返回一个新的函数,即使传入相同的参数

1
2
3
lazy_sum1 = lazy_sum(1, 2, 3, 4)
lazy_sum2 = lazy_sum(1, 2, 3, 4)
print(lazy_sum1 == lazy_sum2) # False

闭包

在上述例子中,我们在函数lazy_sum中又定义了函数sum,并且,内部函数sum可以引用外部函数lazy_sum的参数和局部变量,当lazy_sum返回函数sum时,相关参数和变量都保存在返回的函数中

这种程序结构就被称为闭包(Closure)

使用闭包时需要注意

  • 返回闭包函数不要引用任何循环变量,或者后续会发生变化的变量
  • 使用闭包时,对外层变量赋值前,需要先使用 nonlocal 声明该变量不是当前函数的局部变量
  • 返回的函数并不是立刻执行,而是直到调用才执行

这是一个问题示例

1
2
3
4
5
6
7
8
9
10
11
12
13
def count():
fs = []
for i in range(1, 4):
def f():
return i * i

fs.append(f)
return fs


f1, f2, f3 = count()
print([f1(), f2(), f3()])
# [9, 9, 9]

实际上调用 f1 等函数会发现,结果都是 9,原因是返回的函数引用了变量 i,但它并非立刻执行,等到 3 个函数都返回时,它们所引用的变量 i 已经变成了 3,因此最终结果为 9

如果一定要引用循环变量怎么办?方法是再创建一个函数,用该函数的参数绑定循环变量当前的值,无论该循环变量后续如何更改,已绑定到函数参数的值不变

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def count():
def f(j):
def g():
return j * j

return g

fs = []
for i in range(1, 4):
fs.append(f(i)) # f(i)立刻被执行,因此i的当前值被传入f()
return fs


f1, f2, f3 = count()
print([f1(), f2(), f3()])
# [1, 4, 9]

nonlocal

使用闭包,就是内层函数引用了外层函数的局部变量,可以读取值,但是赋值时 Python 解释器会认为对局部变量赋值,就会报错

可以加上 nonlocal 声明,声明后解释器会把变量看作外层函数的局部变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def count():
count = 0

def f():
nonlocal count
count += 1
print(count)

return f


f = count()
f() # 1
f() # 2
f() # 3

匿名函数

1
list(map(lambda x: x * x, [1, 2, 3, 4, 5, 6, 7, 8, 9]))

如上,关键字 lambda 表示匿名函数,冒号前面的 x 表示函数参数

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

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

1
2
f = lambda: print("Hello World!")
f()

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

1
2
3
4
5
def add(x, y):
return lambda: x + y

f = add(3, 4)
print(f()) # 7

装饰器

偏函数

functools.partial 可以帮助创建一个偏函数

把一个函数的某些参数给固定住(也就是设置默认值),返回一个新的函数,使得调用这个新函数会更简单

1
2
3
4
5
6
def hello(name):
print("Hello " + name + "!")

fun2 = functools.partial(hello, name='World')
fun2() # Hello World!
fun2(name='Tomorrow') # Hello Tomorrow!

创建偏函数时,实际上可以接收函数对象、*args**kw 3 个参数

1
max2 = functools.partial(max, 10)

实际上会把 10 作为 *args 的一部分自动加到左边,也就是

1
2
3
max2(5, 6, 7)
# >> 实际上等同于 args = (10, 5, 6, 7) max(*args) 的调用
# 结果为 10

面向对象

访问限制

在 Python 中,实例的变量名如果以 __ 开头,就变成了一个私有变量(private)

1
2
3
4
5
6
7
8
9
10
11
12
13
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))


s = Student("zhangsan", 100)
print(s.score) # 100
print(s.__name) # AttributeError: 'Student' object has no attribute '__name'

需要注意

  • 在 Python 中,变量名类似 __xxx__ ,也就是以双下划线开头,并且以双下划线结尾的,是特殊变量;特殊变量是可以直接访问的,不是私有变量
  • 以一个下划线开头的实例变量名,比如 _name,这样的实例变量外部是可以访问的,但是,按照约定俗成的规定,当你看到这样的变量时,意思为“虽然我可以被访问,但是请把我视为私有变量,不要随意访问”
  • 在外部手动设置私有属性和对象真正的私有属性无关(因为已经被改名)
  • 本质上私有的实现原理是解释器变更了属性名

继承和多态

Python 的继承和多态和 Java 类似,这里只列举一些 API 和明显区别的点

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

1
2
3
print(isinstance(123, int)) # True
print(isinstance('123', int)) # False
print(isinstance('123', str)) # True

对于静态语言(例如 Java)来说,如果需要传入 Animal 类型,则传入的对象必须是 Animal 类型或者它的子类

对于 Python 这样的动态语言来说,则不一定需要传入 Animal 类型,只需要具有相同的方法即可

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Animal(object):
def run(self):
print('Animal is running...')


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


class Human(object):
def run(self):
print('Human is running...')


def run_twice(animal):
animal.run()
animal.run()


# 都可以调用成功
run_twice(Dog())
run_twice(Human())

对象信息

Python 中对象的属性可以不用定义在类结构中

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

  • type 获取对象类型
  • isinstance 判断对象是否等于 or 继承于某类
  • dir 返回一个包含字符串的 list,包含一个对象的所有属性和方法
1
2
3
4
5
6
7
8
9
10
11
12
13
class Animal(object):
def run(self):
print('Animal is running...')


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


print(dir(Animal()))
print(dir(Dog()))
# ['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'run']

配合 getattrsetattr 以及 hasattr,我们可以直接操作一个对象的属性;类似 Java 的反射

  • hasattr 是否具有某属性
  • getattr 获取某属性
  • setattr 设置某属性
1
2
3
4
5
6
7
8
9
10
11
12
class Student(object):
def __init__(self) -> None:
self.name = 'zhangsan'
self.age = 20


student = Student()
if hasattr(student, 'age'):
print(getattr(student, 'age')) # 20
setattr(student, 'age', 25)

print(student.age) # 25

实例属性和类属性

对应 Java 中的静态变量和成员变量

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

1
2
3
4
5
6
class Student(object):
def __init__(self, name):
self.name = name

s = Student('Bob')
s.score = 90

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

1
2
class Student(object):
name = 'Student'

对象可以覆盖同名类属性,也可以通过类名加属性名访问类属性

1
2
3
4
5
6
7
8
9
10
11
class Student(object):
name = "Student"


zhangsan = Student()
zhangsan.name = 'zhangsan'
print(zhangsan.name) # zhangsan
lisi = Student()
print(lisi.name) # Student

print(Student.name) # Student

__slots__

正常情况下,当定义了一个 class,创建了一个 class 的实例后可以给该实例绑定任何属性和方法,这就是动态语言的灵活性

也可以给 class 绑定方法

1
2
3
4
5
6
7
8
9
10
11
12
class Student(object):
name = "Student"


def set_score(self, score):
self.score = score


student = Student()
Student.set_score = set_score
student.set_score(100)
print(student.score) # 100

如果想要限制实例的属性,Python 允许在定义 class 的时候,定义一个特殊的 __slots__ 变量,来限制该 class 实例能添加的属性

1
2
3
4
5
6
7
8
9
class Student(object):
# 用tuple定义允许绑定的属性名称
__slots__ = ('name', 'age')


student = Student()
student.name = 'zhangsan'
student.score = 50
# AttributeError: 'Student' object has no attribute 'score'

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

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

@property

Python 内置的 @property 装饰器就是负责把一个方法变成属性调用(和 Java 的 Lombok 相反)

把一个 getter 方法变成属性,只需要加上 @property 就可以了

@property 本身又创建了另一个装饰器 @score.setter,负责把一个 setter 方法变成属性赋值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Student(object):
@property
def score(self):
return self._score

@score.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


student = Student()
student.score = 100
student.score = -1
# ValueError: score must between 0 ~ 100!

要特别注意:属性的方法名不要和实例变量重名

例如以下的错误代码

1
2
3
4
5
6
class Student(object):

# 方法名称和实例变量均为birth:
@property
def birth(self):
return self.birth

这是因为调用 s.birth 时,首先转换为方法调用,在执行 return self.birth 时,又视为访问 self 的属性,于是又转换为方法调用,造成无限递归,最终导致栈溢出报错 RecursionError

多重继承

Python 支持多重继承,如果需要“混入”额外的功能,通过多重继承就可以实现,比如,让 Ostrich 除了继承自 Bird 外,再同时继承 Runnable,这种设计通常称之为 MixIn

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

类似的,你还可以定义出肉食动物 CarnivorousMixIn 和草食动物 HerbivoresMixIn,让某个动物同时拥有好几个 MixIn

1
2
class Dog(Mammal, RunnableMixIn, CarnivorousMixIn):
pass

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

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

1
2
3
4
5
6
7
# 多进程模式的 TCP 服务
class MyTCPServer(TCPServer, ForkingMixIn):
pass

# 多线程模式的 UDP 服务
class MyUDPServer(UDPServer, ThreadingMixIn):
pass

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

定制方法

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

__str__

调用 print 时,会调用对象的该方法进行打印

1
2
3
4
5
6
7
8
9
10
11
12
13
class Student(object):

def __init__(self) -> None:
self.name = 'zhangsan'
self.age = 20

def __str__(self) -> str:
return 'name:' + self.name + ", age:" + str(self.age)


s = Student()
print(s)
# name:zhangsan, age:20

__iter__

如果一个类想被用于 for ... in 循环,类似 list 或 tuple 那样,就必须实现一个 __iter__() 方法

该方法返回一个迭代对象,然后 Python 的 for 循环就会不断调用该迭代对象的 __next__() 方法拿到循环的下一个值,直到遇到 StopIteration 错误时退出循环

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class PrimeNumber(object):
def __init__(self):
self.current = 0

def __iter__(self):
return self # 实例本身就是迭代对象,故返回自己

def __next__(self):
for i in range(self.current + 1, 100):
count = 0
for j in range(1, i + 1):
if i % j == 0:
count += 1
# 是质数
if count <= 2:
self.current = i
return i
raise StopIteration()


print(list(PrimeNumber().__iter__()))
# [1, 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97]

上面的示例实现了一个 100 以内素数的迭代器

__getitem__

上面的 PrimeNumber 虽然可以迭代,但是还是不能当作 list 使用

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class PrimeNumber(object):
def __getitem__(self, n):
current_index = -1
for i in range(1, 100):
count = 0
for j in range(1, i + 1):
if i % j == 0:
count += 1
# 是质数
if count <= 2:
current_index += 1
if current_index == n:
return i
return current_index


pn = PrimeNumber()
print(pn[1]) # 2
print(pn[10]) # 29

如果进一步希望其支持切片方法,需要判断入参是否是 slice,再进行不同的操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class PrimeNumber(object):
def __getitem__(self, n):
if isinstance(n, int): # n是索引
current_index = -1
for i in range(1, 100):
count = 0
for j in range(1, i + 1):
if i % j == 0:
count += 1
# 是质数
if count <= 2:
current_index += 1
if current_index == n:
return i
return current_index
if isinstance(n, slice): # n是切片
start = n.start
stop = n.stop
if start is None:
start = 0
res = []
current_index = -1
for i in range(1, 100):
count = 0
for j in range(1, i + 1):
if i % j == 0:
count += 1
# 是质数
if count <= 2:
current_index += 1
if start <= current_index < stop:
res.append(i)
return res


pn = PrimeNumber()
print(pn[1:5]) # [2, 3, 5, 7]

(只是举个例子,实现的很粗糙)

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

__getattr__

当调用不存在的属性时,比如 score,Python 解释器会试图调用 __getattr__(self, 'score') 来尝试获得属性

1
2
3
4
5
6
7
8
9
10
11
class Student(object):

def __init__(self):
self.name = 'Michael'

def __getattr__(self, attr):
if attr == 'score':
return 99


print(Student().score) # 99

也可以返回函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Student(object):

def __init__(self):
self.name = 'Michael'

def __getattr__(self, attr):
def get_score():
return 99

if attr == 'score':
return get_score


f = Student().score
print(f())

__call__

一个对象实例可以有自己的属性和方法,当我们调用实例方法时,我们用 instance.method() 来调用

而任何类,只需要定义一个 __call__ 方法,就可以直接对实例进行调用

1
2
3
4
5
6
7
8
9
class Student(object):
def __init__(self, name):
self.name = name

def __call__(self):
print('My name is %s.' % self.name)

s = Student('Michael')
s() # My name is Michael.

完全可以把对象看成函数,把函数看成对象

那么怎么判断一个变量是对象还是函数呢,能被调用的对象就是一个 Callable 对象,比如函数和上面定义的带有 __call__ 的类实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Student(object):
def __init__(self, name):
self.name = name

def __call__(self):
print('My name is %s.' % self.name)


print(callable(Student('zhangsan'))) # True
print(callable(Student('zhangsan').name)) # False
print(callable(Student('zhangsan').__str__)) # True
print(callable(Student('zhangsan').__str__())) # False
print(callable(123)) # False
print(callable('123')) # False
print(callable(lambda x: print(x))) # True

枚举类

Python 提供了 Enum 类来实现枚举类的功能

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

for name, member in Month.__members__.items():
print(name, '=>', member, ',', member.value)

可以直接使用 Month.Jan 来引用一个常量,或者使用 __members__ 列举所有成员

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

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

1
2
3
4
5
6
7
8
9
@unique
class Weekday(Enum):
Sun = 0 # Sun的value被设定为0
Mon = 1
Tue = 2
Wed = 3
Thu = 4
Fri = 5
Sat = 6

@unique 装饰器可以帮助我们检查保证没有重复值

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

1
2
3
4
print(Weekday.Sat)
print(Weekday['Sat'])
print(Weekday(6))
# Weekday.Sat

元类

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

type 函数可以查看一个类型或变量的类型,一个 class 类型就是 type

创建 class 的方法就是使用 type 函数

type 函数既可以返回一个对象的类型,又可以创建出新的类型

1
2
3
4
5
6
7
8
9
10
# 定义出函数
def hello(self, name: str):
print("Hello " + name + "!")

# 使用 type 创建出 Student class
Student = type('Student', (object,), dict(hello=hello))
print(Student) # <class '__main__.Student'>
# 创建实例
s = Student()
s.hello('zhangsan') # Hello zhangsan!

type 的参数:

  • class 的名称
  • 继承的父类集合(注意 Python 支持多重继承,如果只有一个父类,别忘了 tuple 的单元素写法)
  • class 的方法名称与函数绑定;示例中将 hello 方法名绑定上面定义的 hello 函数

动态语言本身支持运行期动态创建类,这和静态语言有非常大的不同,要在静态语言运行期创建类,必须构造源代码字符串再调用编译器,或者借助一些工具生成字节码实现,本质上都是动态编译,会非常复杂(Java 中的 cglib、bytebuddy)

tuple 单元素写法

Python 中单元素的 tuple 应在元素后加上 ,,若括号中没有 ,,则会被认为是其元素类型(忽略 () tuple 表达)

1
2
3
4
5
6
7
tuple1 = ('a')
print(tuple1) # a
print(type(tuple1)) # <class 'str'>

tuple2 = ('a',)
print(tuple2) # ('a',)
print(type(tuple2)) # <class 'tuple'>

metaclass

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

metaclass 直译为元类,简单的解释就是:先定义 metaclass,就可以创建类,最后创建实例

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

举一个例子,给我们自定义的 MyList 增加一个 add 方法

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

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

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

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

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

1
2
class MyList(list, metaclass=ListMetaclass):
pass

当我们传入关键字参数 metaclass 时,魔术就生效了,它指示 Python 解释器在创建 MyList 时,要通过 ListMetaclass.__new__() 来创建

1
2
3
4
5
6
7
8
my_list = MyList()
my_list.add(1)
my_list.add(2)
print(my_list) # [1, 2]

# 普通的 list 没有 add 方法
list = []
list.add(1) # AttributeError: 'list' object has no attribute 'add'

这里使用元类实现一个 ORM Model 作为练习

首先来定义 Field 类,它负责保存数据库表的字段名和字段类型

1
2
3
4
5
6
7
8
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,比如 StringFieldIntegerField 等等

1
2
3
4
5
6
7
8
9
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class ModelMetaclass(type):

def __new__(mcs, name, bases, attrs):
# 如果是 Model,则直接创建 class 返回
if name == 'Model':
return type.__new__(mcs, name, bases, attrs)

# 非 Module
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 # 假设表名和类名一致
# 创建对应的 class
return type.__new__(mcs, name, bases, attrs)

基类 Model

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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 = []
# 从 __mappings__ 获取解析出的属性
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))

ModelMetaclass 中,一共做了几件事情

  • 排除掉对 Model 类的修改
  • 在当前类(比如 User)中查找定义的类的所有属性,如果找到一个 Field 属性,就把它保存到一个 __mappings__ 的 dict 中,同时从类属性中删除该 Field 属性,否则,容易造成运行时错误(实例的属性会遮盖类的同名属性)
  • 把表名保存到 __table__ 中,这里简化为表名默认为类名

定义实体类

1
2
3
4
class Student(Model):
id = IntegerField('id')
name = StringField('name')
email = IntegerField('email')
1
2
3
4
s = Student(id=12345, name='zhangsan', email='zhangsan@xxx.com')
s.save()
# SQL: insert into Student (id,name,email) values (?,?,?)
# ARGS: [12345, 'zhangsan', 'zhangsan@xxx.com']

这里在看的时候有一个问题,定义的 Student 属性必须和 Student(id=12345, name='zhangsan', email='zhangsan@xxx.com') 参数一致

不然就会出现如下结果 ARGS: [None, None, None]

这是为什么呢,其次如果一致的话不应该实例属性影响类属性吗

  • Q:为什么名称必须一致
    • 因为后续是通过 getattr(self, k, None) 操作来找到对应 value 的,如果不一致 self 将找不到该属性对应的 value
  • Q:名称如果一致的话不应该实例属性影响类属性吗
    • 不影响,因为 Model 重写了 __getattr__ 方法,实现为 self[key]
    • Model 继承了 dict,实际上讲数据以 KV 形式保存在自己 dict
    • ModelMetaclass 定义中,在保存了属性后使用 attrs.pop(k) 将实例同名属性删除了
1
2
3
4
# 实例化后,s 对象其实已经没有了 id\name\email 属性
s = Student(id=12345, name='zhangsan', email='zhangsan@xxx.com')
print(s.__dir__())
# ['__module__', '__mappings__', '__table__', '__doc__', '__init__', '__getattr__', '__setattr__', 'save', '__dict__', '__weakref__', '__repr__', '__hash__', '__getattribute__', '__lt__', '__le__', '__eq__', '__ne__', '__gt__', '__ge__', '__iter__', '__or__', '__ror__', '__ior__', '__len__', '__getitem__', '__setitem__', '__delitem__', '__contains__', '__new__', '__sizeof__', 'get', 'setdefault', 'pop', 'popitem', 'keys', 'items', 'values', 'update', 'fromkeys', 'clear', 'copy', '__reversed__', '__class_getitem__', '__str__', '__delattr__', '__reduce_ex__', '__reduce__', '__subclasshook__', '__init_subclass__', '__format__', '__dir__', '__class__']

参考

Python教程 - 廖雪峰的官方网站 (liaoxuefeng.com)