Python 进阶语法

这篇是 Python 进阶语法的一份整理:从变量赋值和内置函数出发,走过序列、字典与推导式,再到函数进阶、特殊方法、鸭子类型,最后落在元编程(type 与元类)上,末尾附一组常用的优雅写法。所有示例都以 Python 3 为准。

1. 变量与赋值

先记住一句话:Python 中变量名指向的是对象的地址,不是对象的值。 变量只是贴在对象上的一个标签,赋值做的事情是让标签改指向另一个对象,而不是把数据搬进变量里。下面这些解包、交换的写法之所以成立,根子都在这里。

1.1 交换变量

不需要临时变量,右边先被打包成元组再解包给左边:

1
a, b = b, a

1.2 序列解包

等号右边是任意可迭代对象,左边的变量个数对得上就能一次性拆开:

1
2
p = '1', '2', '3', 4
a, b, c, d = p

变量个数不确定时可以用星号收集剩余部分:

1
2
first, *rest = [1, 2, 3, 4]
print(first, rest) # 1 [2, 3, 4]

关于命名、作用域和变量的更多讲究,推荐读 Python 工匠:善用变量改善代码质量

2. 内置函数与 lambda

2.1 lambda 匿名函数

没有函数名的函数,适合写一次就丢的小逻辑(例如 sortkey):

1
2
x = lambda a, b: a * b
print(x(2, 3)) # 6

2.2 map

map() 把函数逐个应用到可迭代对象的元素上,返回的是迭代器而不是列表。多个序列长度不一致时,以最短的为准:

1
2
3
x = map(lambda a, b: a + b, [1, 3, 5, 7, 9], [2, 4, 6, 8, 10, 11])
print(x) # <map object at 0x00000255474D24A8>
print(list(x)) # [3, 7, 11, 15, 19]

注意迭代器只能消费一次,list(x) 之后再取就是空的了。

2.3 filter

map() 类似,但只保留判断为真的元素:

1
2
3
4
5
6
7
8
9
10
numbers = [1, 2, 3, 4, 5, 6, 7, 8]


def is_even(num):
return num % 2 == 0


filtered = filter(is_even, numbers)
print(filtered) # <filter object at 0x00000237EFD62438>
print(list(filtered)) # [2, 4, 6, 8]

if num % 2 == 0: return True else: return False 这种写法可以直接简化成返回比较表达式本身。

2.4 any 与 all

all() 要求全部为真,any() 只要有一个为真。空序列的结果分别是 TrueFalse

1
2
3
x = [0, 2, 1]
print(all(x)) # False
print(any(x)) # True

2.5 zip

zip() 把多个可迭代对象里对应位置的元素打包成元组,同样以最短的为准,返回迭代器:

1
2
3
4
5
6
7
8
9
10
11
12
keys = ['a', 'b', 'c', 'd', 'e']
values = [1, 2, 3, 3]
zipped = dict(zip(keys, values))
print(zipped) # {'a': 1, 'b': 2, 'c': 3, 'd': 3}

# 键值互换,重复的值会互相覆盖,只留最后一个
d = {v: k for k, v in zipped.items()}
print(d) # {1: 'a', 2: 'b', 3: 'd'}

# 按字典的值从大到小排序
print(dict(sorted(d.items(), key=lambda item: item[1], reverse=True)))
# {3: 'd', 2: 'b', 1: 'a'}

3. 序列、字典与推导式

3.1 遍历时拿到下标:enumerate

普通版要靠下标去取值:

1
2
3
items = [8, 23, 45]
for index in range(len(items)):
print(index, '-->', items[index])

优雅版直接把下标和元素一起拿到:

1
2
3
4
5
6
for index, item in enumerate(items):
print(index, '-->', item)

# 0 --> 8
# 1 --> 23
# 2 --> 45

enumerate 还能指定起始序号,默认从 0 开始:

1
2
3
4
5
6
for index, item in enumerate(items, start=1):
print(index, '-->', item)

# 1 --> 8
# 2 --> 23
# 3 --> 45

3.2 append 与 extend 的区别

append 把参数当成一个新元素追加到末尾,参数可以是任意对象:

1
2
3
4
x = [1, 2, 3]
y = [4, 5]
x.append(y)
print(x) # [1, 2, 3, [4, 5]]

extend 的参数必须是可迭代对象,它把里面的元素逐个追加进去:

1
2
3
4
5
6
7
8
x = [1, 2, 3]
y = [4, 5]
x.extend(y)
print(x) # [1, 2, 3, 4, 5]

# 等价于
for i in y:
x.append(i)

3.3 判断列表是否为空

普通版:

1
2
3
4
5
if len(items) == 0:
print('空列表')

if items == []:
print('空列表')

优雅版,直接利用容器的真值判断:

1
2
if not items:
print('空列表')

3.4 切片

语法是 items[start:end:step],取的是 startend - 1 之间的元素,step 是步长,默认 1:

1
2
3
4
5
6
7
8
a = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

a[3:8] # [4, 5, 6, 7, 8] 下标 3 到 7
a[3:8:2] # [4, 6, 8] 隔一个取一个
a[1::2] # [2, 4, 6, 8, 10] 从下标 1 开始,步长 2
a[:5] # [1, 2, 3, 4, 5] 省略 start 表示从头开始
a[3:] # [4, 5, 6, 7, 8, 9, 10] 省略 end 表示到末尾
a[::] # 全部省略等于拷贝一份,注意这是浅拷贝

range 对象切片得到的仍是 range,想要列表就先 list(range(10))

3.5 拷贝列表

1
2
new_list = old_list[:]        # 切片
new_list = list(old_list) # 构造函数

以上两种都是浅拷贝,嵌套的子对象仍然共享。需要完全独立的副本就用 copy 模块:

1
2
3
4
import copy

new_list = copy.copy(old_list) # 浅拷贝
new_list = copy.deepcopy(old_list) # 深拷贝

3.6 取最后一个元素

下标支持负数,从右往左数:

1
2
3
a = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
a[len(a) - 1] # 10
a[-1] # 10

3.7 排序:sort 与 sorted

list.sort() 原地排序、返回 None;内置函数 sorted() 不动原列表、返回新列表。复杂结构靠 key 指定排序依据:

1
2
3
4
5
6
7
items = [{'name': 'Homer', 'age': 39},
{'name': 'Bart', 'age': 10},
{'name': 'cater', 'age': 20}]

items.sort(key=lambda item: item['age'])
print(items)
# [{'name': 'Bart', 'age': 10}, {'name': 'cater', 'age': 20}, {'name': 'Homer', 'age': 39}]

默认升序,加 reverse=True 变降序:

1
2
items.sort(key=lambda item: item['age'], reverse=True)
# [{'name': 'Homer', 'age': 39}, {'name': 'cater', 'age': 20}, {'name': 'Bart', 'age': 10}]

不想改动原列表就用 sorted()

1
2
3
4
5
6
7
items = [{'name': 'Homer', 'age': 39},
{'name': 'Bart', 'age': 10},
{'name': 'cater', 'age': 20}]

new_items = sorted(items, key=lambda item: item['age'])
print(items) # 原列表顺序不变
print(new_items) # 新列表按 age 升序

3.8 删除元素:remove、del、pop

remove 按值删除,只删第一次出现的那个;找不到会抛 ValueError

1
2
3
4
5
6
7
8
>>> a = [0, 2, 2, 3]
>>> a.remove(2)
>>> a
[0, 2, 3]
>>> a.remove(7)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ValueError: list.remove(x): x not in list

del 按位置删除,越界抛 IndexError

1
2
3
4
5
6
7
8
>>> a = [3, 2, 2, 1]
>>> del a[1]
>>> a
[3, 2, 1]
>>> del a[7]
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
IndexError: list assignment index out of range

popdel 类似,但会把被删掉的元素返回:

1
2
3
4
5
6
7
8
9
>>> a = [4, 3, 5]
>>> a.pop(1)
3
>>> a
[4, 5]
>>> a.pop(7)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
IndexError: pop index out of range

3.9 连接两个列表

1
2
3
4
5
listone = [1, 2, 3]
listtwo = [4, 5, 6]

merged = listone + listtwo
print(merged) # [1, 2, 3, 4, 5, 6]

列表能用 + 是因为它实现了 __add__。任何对象只要实现了 __add__ 都能支持 +,具体见后面”特殊方法(魔术方法)”一节。

3.10 随机取一个元素

random.choice() 从序列里随机返回一个元素,不需要自己算随机下标:

1
2
3
4
5
import random

items = [8, 23, 45, 12, 78]
random.choice(items) # 78
random.choice(items) # 45

3.11 频繁增删两端就换 deque

list 的查询很快,但在头部插入或删除元素时要搬动后面所有元素,效率较低。collections.deque 是双向队列,两端的增删都是常数时间:

1
2
3
4
5
from collections import deque

names = deque([1, 2, 3, 4, 'haha'])
names.popleft() # 从左边弹出
names.appendleft('mark') # 从左边插入

3.12 字典的几个常用写法

遍历键值对用 items(),它返回视图对象,不会额外复制一份数据:

1
2
3
d = {1: 'haha', 2: 'xixi'}
for key, value in d.items():
print(key, '+', value)

取值时用 get() 可以指定默认值,键不存在也不会抛 KeyError

1
2
3
d = {'name': 'foo'}
d.get('name', 'unknown') # 'foo'
d.get('age', 'unknown') # 'unknown'

按键分组时,setdefault()defaultdict 都能免去”先判断键在不在”的样板代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
data = [('a', 1), ('a', 2), ('b', 3)]

groups = {}
for key, value in data:
groups.setdefault(key, []).append(value)

from collections import defaultdict

groups = defaultdict(list)
for key, value in data:
groups[key].append(value)

print(groups) # defaultdict(<class 'list'>, {'a': [1, 2], 'b': [3]})

3.13 推导式

列表推导式是最常用的一种:

1
[i for i in range(10)]

字典和集合也有对应写法:

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

my_dict = {number: number * 2 for number in numbers} # {1: 2, 2: 4, 3: 6, 4: 8}
my_set = {number % 3 for number in numbers} # {0, 1, 2}

把方括号换成圆括号得到的是生成器表达式,它不会一次性构建出全部元素:

1
squares = (i * i for i in range(10))   # 生成器,边迭代边算

4. 函数进阶

4.1 为什么要写函数

  1. 给一组语句起一个名字,表达意图;
  2. 减少重复代码;
  3. 把一长段程序拆成多个函数再组合起来;
  4. 一次书写,多次调用。

调用时实参会赋值给形参。没有 return 的函数返回 None,它只产生副作用:

1
2
3
4
5
6
7
def print_twice(x):
print(x)
print(x)


a = print_twice('haha')
print(a is None) # True

4.2 增量开发

每次只增加并测试一小部分代码,避免最后陷入长时间调试。例如已知圆心坐标 (x1, y1) 和圆上一点 (x2, y2),求圆的面积。

第一步,先写面积:

1
2
3
4
5
6
7
8
import math


def area(radius):
return math.pi * radius ** 2


print(area(2)) # 12.566370614359172

第二步,写两点间距离,也就是半径:

1
2
3
4
5
6
def distance(x1, y1, x2, y2):
"""计算两点之间的距离"""
dx = x2 - x1
dy = y2 - y1 # 临时变量,保存中间结果
dsquared = dx ** 2 + dy ** 2
return math.sqrt(dsquared)

函数体开头的字符串是文档字符串(docstring),用来简明地说明函数做什么。调试期间那些为了看中间值而写的临时变量属于”脚手架代码”,写完可以撤掉。

第三步,把两步串起来并封装:

1
2
3
def circle_area(x1, y1, x2, y2):
radius = distance(x1, y1, x2, y2)
return area(radius)

确认无误后再简化成一行:

1
2
3
4
5
def circle_area(x1, y1, x2, y2):
return area(distance(x1, y1, x2, y2))


circle_area(0, 0, 2, 2) # 25.132741228718352

4.3 递归

调用自己的函数称为递归函数。没有终止条件的递归会一直调下去,直到超出递归深度上限:

1
2
3
4
5
def recurse():
recurse()


recurse() # RecursionError: maximum recursion depth exceeded

正确的递归一定有一个能返回的出口:

1
2
3
4
5
6
7
8
def print_n(s, n):
if n <= 0:
return
print(s)
print_n(s, n - 1)


print_n('haha', 5)

参数可能过大时,可以在函数开头加一道”守卫”,先把非法输入挡回去:

1
2
3
4
5
6
7
8
9
10
11
12
def countdown(n):
if n > 100000: # 守卫,保护后面的代码不被撑爆
print('over limit')
return
elif n <= 0:
print('haha')
else:
print(n)
countdown(n - 1)


countdown(10000000000000) # over limit

4.4 递归还是循环

同样的逻辑用 while 写不占用调用栈,数据量大时更稳:

1
2
3
4
5
6
7
8
def countdown(n):
if n > 100000:
print('over limit')
return
while n > 0:
print(n)
n -= 1
print('haha')

Python 没有尾递归优化,默认递归深度只有一千层左右。所以”能自然写成循环的就写循环”,把递归留给树、图这类天生分叉的结构。循环里用 break 提前退出:

1
2
3
4
5
while True:
line = input('>')
if line == 'done':
break
print(line)

4.5 装饰器:把与业务无关的代码抽出去

装饰器可以把计时、缓存、日志这类和业务逻辑无关的代码抽离出来,让主逻辑保持干净,而且能重复利用。下面这个 cache 装饰器把抓过的网页存起来,同一个 URL 第二次就直接返回缓存:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import urllib.request


def cache(func):
saved = {} # 闭包变量,跟着 wrapper 一起活着

def wrapper(url):
if url in saved:
return saved[url]
page = func(url)
saved[url] = page
return page

return wrapper # 必须和 def wrapper 同级!


@cache
def web_lookup(url):
return urllib.request.urlopen(url).read()

最容易踩的坑是把 return wrapper 缩进到了 wrapper 函数体内部——那样 cache() 本身没有返回值,被装饰的函数会变成 None,调用时报 TypeError: 'NoneType' object is not callable。另外这种纯粹的结果缓存,标准库已经给好了 functools.lru_cache,不必自己写。

装饰器的完整玩法(functools.wraps 保留元信息、带参数的装饰器、用类的 __call__ 实现装饰器等)另有一篇专门讲:Python 装饰器

5. 特殊方法(魔术方法)

特殊方法就是以双下划线 __ 作前后缀的那些方法。它们是普通方法,只不过 Python 会在特定语法下自动调用它们——这就是”魔法”的来源:定义了 __str__str()print() 就知道怎么显示你的对象;定义了 __iter____next__for 循环就能迭代它;定义了 __bool__if obj 就走你自己的判断逻辑。

5.1 实例的创建与销毁

__new__() 创造实例,__init__() 初始化实例。 __init__() 常被叫作构造函数,但它并不是最先执行的那个:

  1. __new__(cls, *args, **kwargs):真正创建实例的方法,最先被调用;
  2. __init__(self, *args, **kwargs):初始化实例属性,绝大多数情况只需要写它;
  3. __del__(self):对象被垃圾回收时的行为,也叫析构方法。

需要澄清一点:参数不是由 __new__ “转交”给 __init__ 的,串起这两步的是元类上的 type.__call__Cls(*args) 实际展开成这样:

1
2
3
obj = Cls.__new__(Cls, *args)
if isinstance(obj, Cls):
obj.__init__(*args) # 收到的是同一份原始参数

也就是说 __new__ 内部怎么折腾参数,都不影响 __init__ 收到什么,两边各自从调用处拿了一份原样的参数。由此还能推出一个可以实测的结论:如果 __new__ 返回的不是本类(或其子类)的实例,那句 isinstance 判断不成立,__init__ 根本不会被调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class SimpleInit:
def __new__(cls, *args, **kwargs):
print('__new__ is called')
return super().__new__(cls)

def __init__(self, value=10):
print('__init__ is called')
print('self is:', self)
self._list = [value]

def __del__(self):
print('__del__ is called', self._list)
del self._list


a = SimpleInit()
a.__del__() # 手动调用只是执行了方法,对象并没有真的销毁
1
2
3
4
5
6
__new__ is called
__init__ is called
self is: <__main__.SimpleInit object at 0x0000017e8a132470>
__del__ is called [10]
Exception ignored in: <function SimpleInit.__del__ at 0x0000017e8a0c5378>
AttributeError: 'SimpleInit' object has no attribute '_list'

后面那段异常说明了两件事:__del__ 不该手动调用,因为对象真正被回收时它还会再执行一次;而 _list 已经被删掉了,第二次访问就抛 AttributeError。这类在 __del__ 里抛出的异常不会中断程序,只会被打印成 “Exception ignored”,非常难排查——所以 __del__ 里的逻辑越少越好。

5.2 算术运算与增量赋值

运算 方法 增量赋值
+ __add__ __iadd__
- __sub__ __isub__
* __mul__ __imul__
/ __truediv__ __itruediv__
// __floordiv__ __ifloordiv__
% __mod__ __imod__
** __pow__ __ipow__
& 按位与 __and__ __iand__
按位或 __or__ __ior__
^ 按位异或 __xor__ __ixor__
<< 左移 __lshift__ __ilshift__
>> 右移 __rshift__ __irshift__

真除法在 Python 2 里叫 __div__,Python 3 已经改名为 __truediv__,写老代码时容易搞错。

实现 __add__ 之后,自定义对象就能用 +;顺手实现 __repr__ 还能让它在打印时有个像样的样子:

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

def __repr__(self):
return 'User(%d)' % self.age

def __add__(self, other):
return User(self.age + other.age)


user_a = User(10)
user_b = User(20)
print(user_a + user_b) # User(30)

5.3 比较运算

__eq__==)、__ne__!=)、__lt__<)、__gt__>)、__le__<=)、__ge__>=)。

Python 3.7 起,@dataclass(order=True) 能自动生成这一整套比较方法,@dataclass 也会顺带生成 __init____repr__,不必手写。

5.4 类型转换

  1. __int__(self)int()
  2. __float__(self)float()
  3. __complex__(self)complex()
  4. __bool__(self):真值判断
  5. __index__(self):转成整数,对象被用作切片下标时调用

Python 2 里的 __long____oct____hex__ 在 Python 3 中已经移除:long 类型不存在了,oct()hex() 转而依赖 __index__

5.5 最常用的一批

  1. __str__(self):给人看的字符串
  2. __repr__(self):给开发者看的字符串,交互式环境和容器打印时用它
  3. __hash__(self)hash() 的行为,决定对象能否作字典键
  4. __len__(self):容器长度
  5. __getitem__ / __setitem__ / __delitem__:下标读、写、删
  6. __iter__(self):返回一个迭代器,让对象能被 for 遍历
  7. __contains__(self, item)in 运算
  8. __call__(self):让实例可以像函数一样被调用

__call__() 不影响实例的构造和析构,但可以在调用时读写实例内部的状态:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from dataclasses import dataclass


@dataclass
class X:
a: int
b: int
limit: int

def __call__(self):
print('__call__ with ({}, {})'.format(self.a, self.b))


x = X(1, 2, 3)
print(x) # X(a=1, b=2, limit=3)
x() # 把实例直接当函数调用:__call__ with (1, 2)

迭代相关的 __iter__ / __next__ 以及生成器,另见迭代对象,迭代器,生成器

6. 鸭子类型与协议

“如果它走起来像鸭子、叫起来像鸭子,那它就是鸭子。” 鸭子类型说的是:调用方不关心对象属于哪个类,只关心它有没有需要的方法。下面三个类之间毫无继承关系,但都有 fly(),于是都能被 lift_off() 使唤——不管传进来的是类本身还是实例:

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
class Duck:
@classmethod
def fly(cls):
print('Duck flying')


class Airplane:
@staticmethod
def fly():
print('Airplane flying')


class Bird:
def fly(self):
print('Bird is flying')


def lift_off(entity):
entity.fly()


if __name__ == '__main__':
lift_off(Duck) # 传类,靠 classmethod
lift_off(Airplane) # 传类,靠 staticmethod
lift_off(Bird()) # 传实例,普通方法

换成静态类型语言,这三者得先抽出一个公共接口或基类才能统一调用。Python 把这件事交给了运行时:能调通就行,调不通再抛 AttributeError

上一节的特殊方法正是 Python 内置协议的实现方式——实现 __len__ 就能被 len() 调用,实现 __iter__ 就是可迭代对象,实现 __getitem__ 就能用下标取值。所谓”像序列一样的对象”,指的就是它实现了序列协议要求的那几个魔术方法,而不是它继承自 list

7. 元编程:type 与元类

7.1 type 既能查类型,也是内置元类

元类是”类的类”:普通类描述实例的行为,元类描述类本身的行为。type 就是 Python 内置的元类,想改变类的行为(而不是实例的行为),就继承 type 定义自己的元类——这是 Python 里做元编程的主要方式。

1
2
3
4
5
6
7
8
class SomeClass:
pass


some_object = SomeClass()

type(some_object) # <class '__main__.SomeClass'>
type(SomeClass) # <class 'type'>

inspect.isclass() 可以看得更清楚:实例不是类,而实例的类型是类。

1
2
3
4
5
6
7
>>> import inspect
>>> inspect.isclass(SomeClass)
True
>>> inspect.isclass(some_object)
False
>>> inspect.isclass(type(some_object))
True

再往上追溯,type 的类型就是 type 自己,这条链到这里闭合:

1
2
3
4
5
6
>>> type(type(SomeClass))
<class 'type'>
>>> inspect.isclass(type(type(SomeClass)))
True
>>> type(type(type(SomeClass)))
<class 'type'>

type 之外,Python 中的一切都是对象,它们要么是类的实例,要么是元类的实例:

1
2
3
4
5
6
>>> isinstance(some_object, SomeClass)
True
>>> isinstance(SomeClass, type) # 类是元类 type 的实例
True
>>> isinstance(some_object, type) # 实例不是
False

7.2 用 type 动态创建类

用一个参数调用 type 是查询类型,用三个参数调用则是创建一个新的类对象,参数依次是类名、基类元组、以及包含全部字段和方法的命名空间字典。

1
SomeClass = type('SomeClass', (), {})

它等价于最普通的类定义:

1
2
class SomeClass:
pass

带基类和成员的版本:

1
2
3
4
5
6
7
8
9
def some_function(self):
print('Hello!')


ParentClass = type('ParentClass', (), {})
SomeClass = type('SomeClass',
(ParentClass,),
{'some_function': some_function,
'some_var': 5})

等价于:

1
2
3
4
5
6
7
8
9
class ParentClass:
pass


class SomeClass(ParentClass):
some_var = 5

def some_function(self):
print('Hello!')

区别在于前者的类名、基类、成员都可以在运行时算出来,这正是框架做代码生成时需要的能力。

7.3 自定义元类的用途

编写自定义元类分两步:先写一个 type 的子类,再通过 metaclass= 把它挂到类的创建流程上。

由于元类会被子类继承,它天然适合消除重复代码(DRY);同时它能在生成类对象的过程中插入额外操作,把与类创建有关的复杂逻辑收拢到一处。常见用例有:

  1. 抽象基类;
  2. 类的自动注册;
  3. 在库和框架中构建 API。

7.4 抽象基类

抽象基类只能被继承,不能被实例化,用来约定子类必须实现哪些方法:

1
2
3
4
5
6
7
8
9
10
11
12
from abc import ABCMeta, abstractmethod


class Vehicle(metaclass=ABCMeta):

@abstractmethod
def refill_tank(self, litres):
pass

@abstractmethod
def move_ahead(self):
pass

子类把抽象方法全部实现之后才能正常实例化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Truck(Vehicle):
def __init__(self, company, color, wheels):
self.company = company
self.color = color
self.wheels = wheels

def refill_tank(self, litres):
print('refill %d litres' % litres)

def move_ahead(self):
print('%s is moving' % self.company)


mini_truck = Truck('Tesla Roadster', 'Black', 4)
mini_truck.move_ahead() # Tesla Roadster is moving

Vehicle() # TypeError: Can't instantiate abstract class Vehicle ...

metaclass=ABCMeta 也可以简写成继承 abc.ABC,两者等价。

本节原先参考的 IBM developerWorks 文章链接已失效(developerWorks 已于 2019 年前后下线)。相同主题可改看官方文档 Metaclassesabc 抽象基类

8. 优雅写法小结

8.1 字符串拼接优先用 join

1
2
names = ['tom', 'jerry']
print('. '.join(names))

join 全程只产生一个字符串对象,而每次 + 都会生成一个新的字符串。不过现在 CPython 对循环里的 += 做了优化,少量拼接直接用加号并不会拖慢程序,不必被过时的经验绑住手脚;拼接大量片段时 join 依然是首选。

值得知道这个优化的成立条件,因为它随时会失效。它的原理是:当左侧字符串的引用计数为 1(除了这个变量没人再持有它),解释器就可以直接 realloc 原地扩容,不必新建对象再整体拷贝。一旦这个字符串还被别处引用住,就只能退回”每次新建 + 全量拷贝”,复杂度立刻变成 O(n²):

1
2
3
4
5
6
7
8
9
10
11
# 快:s 只有一个引用,能原地扩容
s = ''
for i in range(20000):
s += 'x'

# 慢:每轮的 s 都被 list 存了一份,引用计数不再是 1
s = ''
keep = []
for i in range(20000):
s += 'x'
keep.append(s)

两万次拼接实测(已经对齐 list append 的开销):前者 0.85 ms,后者 24.31 ms,差 28.7 倍。同理,self.s += x 这种写在实例属性上的也命中不了优化。

另外两个限制:这是 CPython 的实现细节,不是语言保证,PyPy 上完全没有这回事;所以在库代码里还是老老实实收集片段再 join,别把性能押在解释器的实现上。

8.2 打开文件用 with

1
2
with open('xxx.xxx', 'r') as f:
data = f.read()

离开 with 代码块时文件一定会被关闭,中途抛异常也一样。

8.3 链式比较

1
2
3
age = 18
if 18 <= age < 60: # 写成 18 < age 的话,18 岁这个边界就被漏掉了,什么都不会打印
print('young man')

链式比较会被展开成用 and 连接的多个比较,所以下面这行的结果是 False——它等价于 False == True and True == True and True == True

1
False == True == True == True   # False

8.4 条件表达式

简单的 if/else 赋值可以压成一行:

1
text = '男' if gender == 'male' else '女'

8.5 直接做真值判断

1
2
if a:
do_something()

空字符串、空容器、0None 都是假值,非空即真,不需要写 if len(a) != 0 或者 if a is not None and a != ''

8.6 f-string 格式化

1
2
3
4
5
a, b = 'haha', [1, 2, 3]
s = f'str is {a}, list is {b}'

PI = 3.141592653
print(f'PI is {PI:.4f}') # PI is 3.1416

f-string 需要 Python 3.6 及以上,冒号后面跟的是格式说明符,数字的位数、对齐、千分位都在这里控制。

8.7 for/else

Python 特有的写法:else 分支只在 for 正常跑完(没有被 break 打断)时执行,适合表达”找遍了也没找到”。

1
2
3
4
5
6
7
mylist = [1, 2, 3, 'theflag', 4, 5, 6]
for i in mylist:
if i == 'theflag':
break
print(i)
else:
raise ValueError('list argument missing terminal flag')

有了 for/else,就不用再额外维护一个 flag_found 变量了。

8.8 生成器

生成器不会一次性把所有元素加载进内存,只有迭代到某个元素时才计算并返回它,而列表是预先把全部元素都建好。函数里出现 yield 就会让它变成生成器函数,执行到 yield 处暂停并交出控制权,下次迭代再从这里继续:

1
2
3
4
5
6
7
8
def fib(n):
a, b = 0, 1
while a < n:
yield a
a, b = b, a + b


print(list(fib(50))) # [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

概念上的细分(容器、可迭代对象、迭代器、生成器)见迭代对象,迭代器,生成器

8.9 数字里的下划线分隔符

Python 3.6 起,数字字面量中可以插入 _ 来分组,不影响实际数值:

1
2
3
a = 11_22_33_44        # 11223344
big = 1_000_000 # 一百万,比数零好使
error = 0xbad_c0ffee # 十六进制也能分组

8.10 类型注解

给参数和返回值加上类型注解,能让编辑器和 mypy 这类工具帮你查错:

1
2
def my_add(a: str, b: int) -> str:
return a + b

注意注解只是提示,解释器并不做检查,my_add(1, 2) 照样会返回 3 而不报错。真正的校验要靠静态检查工具或运行时的显式判断。

9. 模块与导入

9.1 给 import 添加搜索路径

import 只在 sys.path 列出的目录里找模块。要导入的模块不在其中时,可以在运行时把目录追加进去:

1
2
3
4
import sys

sys.path.append('..') # 将上层目录加入路径
sys.path.append('/home') # 将 /home 加入路径

这属于临时手段,路径写死在代码里不利于迁移;长期方案是把项目做成包,或者用 PYTHONPATH、虚拟环境来管理。

9.2 一个模块只会被导入一次

同一个模块无论被 import 多少次,模块里的代码只在第一次导入时执行,之后都直接复用 sys.modules 中的缓存。所以在交互式环境里改完模块源码,再 import 一遍是没有效果的,得显式重新加载:

1
2
3
4
import importlib
import some_module

importlib.reload(some_module) # 重新导入某模块

⚠️ 注:老代码里常见的 from imp import *reload(...) 已经不能照抄了——imp 模块自 Python 3.4 起被废弃,并在 Python 3.12 中彻底移除,请改用 importlib.reload()。另外 reload(sys) 是 Python 2 时代为了调 sys.setdefaultencoding('utf-8') 才有的偏方,Python 3 里这个函数已经不存在,也不需要这么做。

参考

  • 序列与容器一节的部分内容整理自微信公众号文章《Python 列表操作》
  • 优雅写法一节是阅读《Python Cookbook》时的笔记
  • 元编程一节参考的 IBM developerWorks 原文已随站点下线,替代资料见该节末尾的官方文档链接