Chapter 1. 变量与注释
变量解包
变量解包(unpacking)是 Python 里的一种特殊赋值操作,允许我们把一个可迭代对象(比如列表)的所有成员,一次性赋值给多个变量
- 左侧变量的个数必须和待展开的列表长度相等,否则会报错
user_id, (username, score) = [1, ['piglei', 100]]Python 还支持更灵活的动态解包语法。只要用星号表达式(*variables)作为变量名,它便会贪婪地捕获多个值,并将捕获到的内容作为列表赋值给 variables
username, *fruits, score = ['piglei', 'apple', 'orange', 'banana', 100]单下划线变量名 _
_ 本身没什么特别之处,这算是大家约定俗成的一种用法
- 假如你想在解包赋值时忽略某些变量,就可以使用
_作为变量名 - 在 Python 交互式命令行里,
_变量默认保存我们输入的上个表达式的返回值
类型注解
- “类型注解”只是一种有关类型的注释,不提供任何校验功能
from typing import List
def remove_invalid(items: List[int]):
"""剔除 items 里面无效的元素"""几条变量命名的基本原则
- 遵循 PEP 8
- 普通变量,蛇形,
max_value - 函数名,蛇形,
bar_function - 类名,驼峰,
FooClass - 常量,全大写下划线
MAX_VALUE - 如果变量标记为“仅内部使用”,为其增加下划线前缀,比如
_local_var - 当名字与 Python 关键字冲突时,在变量末尾追加下划线,比如
class_
- 普通变量,蛇形,
- 描述性强,
process不如extract_username - 尽量短
- 匹配类型
- 布尔值:
is_xxxhas_xxxallow_xxx - int/float
- 能表示数字的单词
portageradius xxx_id- 以
lengthcount开头或结尾 - 最好别拿一个名词的复数形式来作为 int 类型的变量名,可使用
number_of_apples
- 能表示数字的单词
- 布尔值:
注释
- 先写注释,后写代码
- 对于不再需要的代码,我们应该直接把它们删掉,而不是注释掉
- 如果未来有人真的需要用到这些旧代码,去 Git 仓库历史里就能找到,毕竟版本控制就是专门干这个的
- 描述为什么要这么做,而不要复述代码本身
- 指引性注释,简明扼要地概括代码功能,降低代码的认知成本
Chapter 2. 数值与字符串
字符串
- 在拼接字符串时,
+=和join同样好用 - 拼接字符串:创建一个空列表,然后把需要拼接的字符串都放进列表,最后调用
str.join来获得大字符串'\n'.join(words) - 字符串串格式化首选 f-string Python 字符串格式化
- 删掉
open(...)里的encoding参数
数字
在 Python 中,一共存在三种内置数值类型:整型(int)、浮点型(float)和复数类型(complex)。
- 无穷
float("-inf") < 任意数值 < float("inf") - 不必预计算字面量表达式
if delta_seconds < 11 * 24 * 3600- 当我们需要用到复杂计算的数字字面量时,请保留整个算式。这样做对性能没有任何影响
- 解释器除了会预计算数值表达式以外,还会对字符串、列表执行类似的操作
- bool 也是数字,
True和False这两个布尔值可以直接当作 1 和 0 来使用- 如计算列表中的偶数数量
count = sum(i % 2 == 0 for i in numbers)
- 如计算列表中的偶数数量
- 浮点数精度问题
- Python 提供了一个内置模块:decimal。假如你的程序需要==精确的浮点数计算==,请考虑使用
decimal.Decimal对象来替代普通浮点数Decimal('0.1') + Decimal('0.2') - 使用 Decimal 必须用字符串来表示数字
- Python 提供了一个内置模块:decimal。假如你的程序需要==精确的浮点数计算==,请考虑使用
- int 与 float 可以通过内置方法进行转换
int(22.2)
Chapter 3. 容器类型
在 Python 中,最常见的内置容器类型有四种:列表 list、元组 tuple、字典 dict、集合 set
| 特性 | 元组 (tuple) | 列表 (list) | 集合 (set) |
|---|---|---|---|
| 有序性 | 有序 | 有序 | 无序 |
| 可变性 | ==不可变== | 可变 | 可变;有不可变版本 frozenset |
| 元素是否可重复 | 允许重复 | 允许重复 | ==不允许重复== |
Python 数据 - 可变类型 mutable 与不可变类型 immutable
可变(mutable):列表、字典、集合
不可变(immutable):整数、浮点数、字符串、字节串、元组
列表
- 遍历时获得下标
for index, s in enumerate(names):
- 使用列表推导式(list comprehension)创建列表 - 修改已有成员的值,或根据规则剔除某些成员
results = [expression for item in iterable if condition]
Python 函数调用传参
Python 的函数调用不能简单归类为“值传递”或者“引用传递”,一切行为取决于对象的可变性
Python 函数调用传参时,采用的既不是值传递,也不是引用传递,而是传递了“变量所指对象的引用”(==pass-by-object-reference==)
当你调用 func(orig_obj) 后,Python 只是新建了一个函数内部变量 in_func_obj,然后让它和外部变量 orig_obj 指向同一个对象,相当于做了一次变量赋值
基本等于执行了 in_func_obj = orig_obj
比如我们在函数内执行一个 += 操作:
在对字符串进行
+=操作时,因为字符串是不可变类型,所以程序会生成一个新对象,并让 in_func_obj 变量指向这个新对象PURESCRIPT┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ orig_obj ├─────────────▶ "foo" │ │ orig_obj ├───────────▶ "foo" │ └──────────┘ └────▲─────┘ └──────────┘ └──────────┘ ┌─────────────────┐ │ ┌─────────────────┐ │ add_str() │ │ │ add_str() │ ┌────────────────┐ │ ┌─────────────┐ │ │ │ ┌─────────────┐ │ ┌────▶ "foo suffix" │ │ │ in_func_obj ├─┼──────────────┘ │ │ in_func_obj ├─┼──┘ └────────────────┘ │ └─────────────┘ │ │ └─────────────┘ │ └─────────────────┘ └─────────────────┘如果对象是可变的(比如列表),
+=操作就会直接原地修改 in_func_obj 变量所指向的值PURESCRIPT┌──────────┐ ┌─────┬─────┐ ┌──────────┐ ┌─────┬─────┬─────┐ │ orig_obj ├────────────▶"foo"│"bar"│ │ orig_obj ├──────────▶"foo"│"bar"│"bz" │ └──────────┘ └──▲──┴─────┘ └──────────┘ └──▲──┴─────┴─────┘ ┌─────────────────┐ │ ┌─────────────────┐ │ │ add_str() │ │ │ add_str() │ │ │ ┌─────────────┐ │ │ │ ┌─────────────┐ │ │ │ │ in_func_obj ├─┼────────────┘ │ │ in_func_obj ├─┼──────────┘ │ └─────────────┘ │ │ └─────────────┘ │ └─────────────────┘ └─────────────────┘
深拷贝与浅拷贝
浅拷贝,最通用的办法是使用 copy.copy() 方法 - nums_copy = copy.copy(nums)
有些类型自身就提供了浅拷贝方法
对于一些层层嵌套的复杂数据来说,浅拷贝仍然无法解决嵌套对象被修改的问题,就需要用 copy.deepcopy() 函数来进行深拷贝操作
元组
- 元组和列表非常类似,但不能被修改
- 元组经常用来存放结构化数据,可以存不同类型的值
- Python 中,函数一次返回多个结果,其实就是返回了一个元组
- 将函数返回值一次赋值给多个变量时,其实就是对元组做了一次解包操作
- 没有「元组推导式」,
(expression for item in iterable if condition)返回的是一个迭代器对象,可以用tuple()转为一个元组
具名元组 namedtuple
- 具名元组在保留普通元组功能的基础上,允许为元组的每个成员命名,这样你便能通过名称而不止是数字索引访问成员
- 和普通元组一样,具名元组是不可变的
Python 3.6 后可以使用 typing.NamedTuple,可读性更好:
>>> from typing import NamedTuple
>>> class Rectangle(NamedTuple):
… width: int
… height: int # 并不会真的做类型校验
…
>>> rect = Rectangle(20, 35.5)较旧的方法:
>>> from collections import namedtuple
>>> Rectangle = namedtuple('Rectangle', ['width', 'height'])
>>> rect = Rectangle(20, 35.5)
>>> rect.width
20
>>> rect[1]
35.5字典
遍历字典
PYTHON# 默认只遍历 key for key in movie: ... # 遍历 key: value 键值对 for key, value in movie.items(): ...字典推导式
PYTHON{key: value * 10 for key, value in d1.items() if key == 'foo'}dict[key]访问不存在的 key,程序会抛出 KeyErrorPYTHONtry: rating = movie['rating'] except KeyError: rating = 0dict.get(key, default)方法接收一个 default 参数,当访问的键不存在时,方法会返回 default 作为默认值PYTHONmovie.get('rating', 0)修改某个 key 的值,但这个 key 可能不存在
dict.setdefault(key, default)会产生两种结果:当 key 不存在时,该方法会把 default 值写入字典的 key 位置,并返回该值;假如 key 已经存在,该方法就会直接返回它在字典中的对应值(如果这个返回值可变,则可直接修改)PYTHONd.setdefault('items', []).append('foo')
使用 pop 方法删除不存在的 key
del d[key]语句,删除的键不存在会抛出 KeyErrord.pop(key, None)在调用 pop 方法时传入默认值 None,在键不存在的情况下也不会产生任何异常
collections 中的的
defaultdict,当操作不存在的 key 时,会直接初始化一个PYTHON>>> from collections import defaultdict >>> int_dict = defaultdict(int) >>> int_dict['foo'] += 1
字典元素的顺序
Python 里的字典在底层使用了哈希表。因此,内容插入顺序,在哈希过程中被自然丢掉了,字典里的内容顺序变得仅与哈希值相关,与写入顺序无关。在很长一段时间里,字典的这种无序性一直被当成一个常识为大家所接受。
但 Python 3.6 为字典类型引入了一个改进:优化了底层实现,同样的字典相比 3.5 版本可节约多达 25% 的内存。而这个改进同时带来了一个有趣的副作用:字典变得有序了。
到了 3.7 版本,它成了语言规范的一部分。==遍历字典的顺序与插入的顺序是一样的==
集合
集合是一种无序的可变容器类型,它最大的特点就是成员不能重复
PYTHON>>> fruits = {'apple', 'orange', 'apple', 'pineapple'} >>> fruits >>> {'pineapple', 'apple', 'orange'}- 使用
.add().remove()方法可以向集合追加/移除成员
- 使用
不可变集合,可以用
frozenset({'apple', 'orange', 'apple'})初始化一个空集合,只能调用
empty_set = set(),因为{}表示的是一个空字典集合推导式
{n for n in nums if n < 3}集合只能存放可哈希对象
Python 对象的可哈希性
- 只有可哈希的对象,才能放进集合,或是作为字典的 Key
- 适用内置函数
hash(obj),如果对象是可哈希的,hash 函数会返回一个整型结果,否则将会报 TypeError 错误 - 不可变的内置类型都是可哈希的,比如 str、int、tuple、frozenset 等
- 整型的 hash 是其自身
hash(-1) == -2- 不可变容器类型 (tuple, frozenset),仅当它的所有成员都不可变时,它自身才是可哈希的
- 可变内置类型,都是不可哈希的,比如 dict、list 等
- 用户定义的对象默认都是可哈希的
集合运算
- 所有操作都可以用两种方式来进行:方法和运算符
- 交集
&set1.intersection(set2) - 并集
|set1.union(set2) - 差集
-set1.difference(set2)
- 交集
生成器 - 按需生成,不是一次性返回
定义一个生成器,需要用到生成器函数与 yield 关键字
- return 的返回是一次性的,而 yield 可以逐步给调用方生成结果
- 调用
next()可以逐步从生成器对象里拿到结果 - 因为生成器是可迭代对象,可以使用
list()等函数方便地把它转换为各种其他容器类型
def generate_even(max_number):
"""一个简单生成器,返回 0 到 max_number 之间的所有偶数"""
for i in range(0, max_number):
if i % 2 == 0:
yield i
for i in generate_even(10):
print(i)
>>> i = generate_even(10)
>>> next(i)
0
>>> next(i)
2使用 deque 在头部追加成员
Python 列表底层使用了 array 数据结构,当你在数组中间插入新成员时,该成员之后的其他成员都需要移动位置,该操作的平均时间复杂度是 O(n)
因此,在列表的头部插入成员,比在尾部追加要慢得多
deque 底层使用了双端队列,无论在头部还是尾部追加成员,时间复杂度都是 O(1)
from collections import deque
l = deque()
l.appendleft(i)使用集合判断成员是否存在
- 要判断某个容器是否包含特定成员,用集合比用列表更合适
- 在列表中查询的时间复杂度是
O(n) - 集合底层是哈希,查询的时间复杂度是
O(1) - 如果需要进行 in 判断,可以考虑把目标容器转换成集合类型,作为查找时的索引使用
快速合并字典
- Python 3.9 中,字典类型新增了对
|运算符的支持。只要执行d1 | d2,就能快速拿到两个字典合并后的结果 - 调用
d1.update(d2),d1 会变成合并的结果- 它会修改字典 d1 的原始内容,因此并不算无副作用的合并
- 如果有相同的 Key,d2 的值回覆盖 d1
- 解包过程会默认进行浅拷贝操作,所以我们可以用它方便地合并两个字典
>>> d1 = {'name': 'apple'}
>>> d2 = {'price': 10}
# d1、d2 原始值不会受影响
>>> {**d1, **d2}
{'name': 'apple', 'price': 10}还可以使用单星号 * 来解包任何可迭代对象
>>> [1, 2, *range(3)]
[1, 2, 0, 1, 2]
>>> l1 = [1, 2]
>>> l2 = [3, 4]
# 合并两个列表
>>> [*l1, *l2]
[1, 2, 3, 4]别把推导式当作代码量更少的循环
推导式的核心意义在于它会返回值——一个全新构建的列表,如果你不需要这个新列表,就失去了使用表达式的意义。==直接编写循环并不会多出多少代码量,而且代码更直观。==
让返回多个值的函数返回 NamedTuple
对于==未来可能会变动的多返回值函数==来说,如果使用 NamedTuple 类型对返回结果进行建模,已有的函数调用代码也不用进行任何适配性修改
from typing import NamedTuple
class Address(NamedTuple):
"""地址信息结果"""
country: str
province: str
city: str
def latlon_to_address(lat, lon):
return Address(
country=country,
province=province,
city=city,
)
addr = latlon_to_address(lat, lon)
# 通过属性名来使用addr
# addr.country / addr.province / addr.cityChapter 4. 条件分支
- 让三元表达式保持简单
# true_value if <expression> else false_value
language = "python" if you.favor("dynamic") else "golang"- 分支语句不要显式地和 布尔值/0/空 做比较
# 绝大多数情况下,在分支判断语句里写 == True 都没有必要
# if user.is_active_member() == True:
if user.is_active_member():
# 省略零值判断
# if containers_count == 0:
if not containers_count:
# if fruits_list != []:
if fruits_list:- 与 None 比较时使用 is 运算符
==对比两个对象的值是否相等,行为可被__eq__方法重载is判断两个对象是否是内存里的同一个东西(更严格),无法被重载- 除了 None、True 和 False 这三个内置对象以外,其他类型的对象在 Python 中并不是严格以单例模式存在的。==因此仅当你需要判断某个对象是否是 None、True、False 时,使用 is,其他情况下,请使用== ==
====
- 使用 all()/any() 函数构建条件表达式
- and 运算符的优先级高于 or
- or 的短路求值:
True or (1 / 0)中,1/0 永远不会被执行- 使用 or 来替代一些简单的条件判断语句
- a 为空时用 b 代替
context.update(extra_context or {})
- a 为空时用 b 代替
- 使用 or 来替代一些简单的条件判断语句
Python 布尔值规则
- 布尔值为假:None、0、False、[]、()、{}、set()、frozenset(),等等
- 布尔值为真:非 0 的数值、True,非空的序列、元组、字典,用户定义的类和实例,等等
修改对象的布尔值
所有用户自定义的类和类实例的计算结果都是 True,如果我们稍微改动一下这个默认行为,就能写出更优雅的代码。
可以通过定义 __bool__ 或 __len__ 魔法方法来修改对象的布尔值判断行为。
class Account:
def __init__(self, balance=0):
self.balance = balance
def __bool__(self):
# 当账户余额为正时返回 True,否则返回 False
return self.balance > 0class TodoList:
def __init__(self):
self.items = []
def add(self, item):
self.items.append(item)
def __len__(self): # len 应返回非负整数;在布尔判断中只关心是否为 0,非零一律为 True
# 返回待办事项的数量
return len(self.items)- 如果同时定义了
__bool__和__len__,Python 会优先使用__bool__ - 如果两者都没有定义,则对象默认为 True
整型驻留 Integer Interning
- 永远使用
==而不是is来比较整数值
Python 语言使用了一种名为“整型驻留”(integer interning)的底层优化技术。
对于从 -5 到 256 的这些常用小整数,Python 会将它们缓存在内存里的一个数组中。当你的程序需要用到这些数字时,Python 不会创建任何新的整型对象,而是会返回缓存中的对象。这样能为程序节约可观的内存。
- 即使超出驻留范围,同一行中的赋值可能也会引用同一对象
>>> a = 100
>>> b = 100
>>> a is b
True
>>> a = 1000
>>> b = 1000
>>> a is b
False
>>> a = b = 1000
>>> a is b
TrueChapter 5. 异常与错误处理
LBYL(look before you leap)编程风格,常被翻译成「三思而后行」
- 如果天气预报说会下雨,那么我就不出门
EAFP(easier to ask for forgiveness than permission)风格,在 Python 世界里,EAFP 指不做任何事前检查,直接执行操作,==在外层用 try 来捕获可能发生的异常==
- 出门前不看天气预报,如果淋雨了,就回家后洗澡吃感冒药
和 LBYL 相比,EAFP 编程风格更为简单直接,它总是直奔主流程而去,把意外情况都放在异常处理 try/except 块内消化掉。
try/except
def safe_int(value):
"""尝试把输入转换为整数"""
try:
return int(value)
except TypeError:
# 当某类异常被抛出时,将会执行对应 except 下的语句
print(f'type error: {type(value)} is invalid')
except ValueError:
# 你可以在一个 try 语句块下写多个 except
print(f'value error: {value} is invalid')
finally:
# finally 里的语句,无论如何都会被执行,哪怕已经执行了return
print('function completed')- 一个 try 语句支持多个 except 子句,把更精确的 except 语句放在前面
- 使用 else 分支:有时程序需要仅在一切正常时做某件事,只有成功才执行
- 仅当 try 语句块里没抛出任何异常时,才执行 else 分支下的内容
- 假如程序在执行 try 代码块时碰到了 return 或 break 等跳转语句,中断了本次异常捕获,else 分支内的逻辑不会被执行
- 如果 try 中有 return,会在执行 finally 之后 return
try:
sync_profile(user.profile, to_external=True)
except Exception as e:
print("Error while syncing user profile")
else:
send_notification(user, 'profile sync succeeded')- 在 except 中使用空 raise 语句,抛出当前异常
- 除非有意静默,否则不要无故忽视异常
- 不要手动做数据校验,用 pydantic
抛出异常,而不是返回错误
返回错误并非解决此类问题的最佳办法,Python 有完善的异常机制
使用上下文管理器 with
with 是一个神奇的关键字,它可以在代码中开辟一段由它管理的上下文,并控制程序在进入和退出这段上下文时的行为。
- 只有满足上下文管理器(context manager)协议的对象才可以配合 with 使用
- 要创建一个上下文管理器,只要实现
__enter__和__exit__两个魔法方法即可。
- 要创建一个上下文管理器,只要实现
- 用 with 替代 finally 语句清理资源,比如关闭已创建的网络连接
- 用于忽略异常
==使用 @contextmanager 装饰器==
在日常工作中,我们用到的大多数上下文管理器,可以直接通过“生成器函数 +@contextmanager”的方式来定义,这比创建一个符合协议的类要简单得多
- yield 前相当于 enter,yield 后的 finally 相当于 exit
自定义异常类
- 继承 Exception 而不是 BaseException
- 异常类名最好以 Error 或 Exception 结尾等
- 调用方能清晰区分各种异常
Chapter 6. 循环与可迭代对象
iter() 与 next() 内置函数
- 调用
iter()会尝试返回一个迭代器对象- 对不可迭代的类型执行
iter()会抛出TypeError异常 - 当你对迭代器执行
iter()函数,返回的结果是迭代器本身
- 对不可迭代的类型执行
- 迭代器最鲜明的特征是:不断对它执行
next()函数会返回下一次迭代结果- 当
next()没有更多值可以返回时,便会抛出StopIteration异常
- 当
- 当你使用
for循环遍历某个可迭代对象时,其实是先调用了iter()拿到它的迭代器,然后不断地用next()从迭代器中获取值
>>> l = ['foo', 'bar']
>>> iter_l = iter(l)
>>> next(iter_l)
'foo'
>>> next(iter_l)
'bar'
>>> next(iter_l)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration自定义迭代器
实现下面这两个魔法方法
__iter__:调用 iter() 时触发,迭代器对象总是返回自身__next__:调用 next() 时触发,通过 return 来返回结果,没有更多内容就抛出 StopIteration 异常,会在迭代过程中多次触发。
class Range7:
"""生成某个范围内可被 7 整除或包含 7 的整数"""
def __init__(self, start, end):
self.start = start
self.end = end
# 使用 current 保存当前所处的位置
self.current = start
def __iter__(self):
return self
def __next__(self):
while True:
# 当已经到达边界时,抛出异常终止迭代
if self.current >= self.end:
raise StopIteration
if self.num_is_valid(self.current):
ret = self.current
self.current += 1
return ret
self.current += 1
def num_is_valid(self, num):
"""判断数字是否满足要求"""
if num == 0:
return False
return num % 7 == 0 or '7' in str(num)>>> r = Range7(0, 20)
>>> for num in r:
… print(num)
…
7
14
17- 每个新 Range7 对象只能被完整遍历一次,假如做二次遍历,就会拿不到任何结果
>>> r = Range7(0, 20)
>>> tuple(r)
(7, 14, 17)
>>> tuple(r) # 第二次只能得到一个空元组区分迭代器与可迭代对象
- 迭代器是可迭代对象的一种
- 一个合法的迭代器,必须同时实现
__iter__和__next__两个魔法方法 - 每个迭代器都对应一次完整的迭代过程,因此它自身必须保存与当前迭代相关的状态——迭代位置(就像 Range7 里面的 current 属性)
- 可迭代对象不一定是迭代器
- 判断一个对象是否可迭代的唯一标准,就是调用
iter(obj),然后看结果是不是一个迭代器 - 可迭代对象只需要实现
__iter__方法,不一定得实现__next__方法
如果想让 Range7 对象在每次迭代时都返回完整结果,我们必须把现在的代码拆成两部分:可迭代类型 Range7 和迭代器类型 Range7Iterator,每次遍历 Range7 对象时,都会创建出一个全新的迭代器对象 Range7Iterator
class Range7:
"""生成某个范围内可被 7 整除或包含 7 的数字"""
def __init__(self, start, end):
self.start = start
self.end = end
def __iter__(self):
# 返回一个新的迭代器对象
return Range7Iterator(self)
class Range7Iterator:
def __init__(self, range_obj):
self.range_obj = range_obj
self.current = range_obj.start
def __iter__(self):
return self
def __next__(self):
while True:
if self.current >= self.range_obj.end:
raise StopIteration
if self.num_is_valid(self.current):
ret = self.current
self.current += 1
return ret
self.current += 1
def num_is_valid(self, num):
if num == 0:
return False
return num % 7 == 0 or '7' in str(num)__getitem__
- 如果一个类没有定义
__iter__,但是定义了__getitem__方法,那么 Python 也会认为它是可迭代的 - 在遍历它时,解释器会不断使用数字索引值 (0, 1, 2, …) 来调用
__getitem__方法获得返回值,直到抛出 IndexError 为止 __getitem__可遍历的这个特点这个特点不属于目前主流的迭代器协议,更多是对旧版本的一种兼容行为
生成器是迭代器
生成器(generator)利用其简单的语法,大大降低了迭代器的使用门槛,是优化循环代码时最得力的帮手。
生成器是一种“懒惰的”可迭代对象,使用它来替代传统列表可以节约内存,提升执行效率
但除此之外,==生成器还是一种简化的迭代器实现==,使用它可以大大降低实现传统迭代器的编码成本。因此在平时,我们基本不需要通过 __iter__ 和 __next__ 来实现迭代器,只要写上几个 yield 就行。
def range_7_gen(start, end):
"""生成器版本的 Range7Iterator"""
num = start
while num < end:
if num != 0 and (num % 7 == 0 or '7' in str(num)):
yield num
num += 1
>>> nums = range_7_gen(0, 20)
>>> iter(nums)
<generator object range_7_gen at 0x10404b2e0>
>>> iter(nums) is nums
True
>>> next(nums)
7
>>> next(nums)
14enumerate()
enumerate() 是 Python 的一个内置函数,它接收一个可迭代对象作为参数,返回一个不断生成 (当前下标,当前元素) 的新可迭代对象
itertools
itertools 是一个和迭代器有关的标准库模块
使用 product() 扁平化多层嵌套循环
product()接收多个(不止两个)可迭代对象作为参数,然后根据它们的==笛卡儿积==不断生成结果
>>> from itertools import product
>>> list(product([1, 2], [3, 4]))
[(1, 3), (1, 4), (2, 3), (2, 4)]使用 islice() 实现隔行处理,islice(seq, start, end, step)
from itertools import islice
def parse_titles_v2(filename):
with open(filename, 'r') as fp:
# 原文本每行之间隔了一个空行
# 设置 step=2,跳过空行
for line in islice(fp, 0, None, 2):
yield line.strip()使用 takewhile() 替代 break 语句,==在每次开始执行循环体代码时,决定是否需要提前结束循环==
takewhile(predicate, iterable) 会在迭代第二个参数的过程中,不断使用当前值作为参数调用 predicate() 函数
- 如果为 True,则返回当前值并继续迭代
- 否则立即中断本次迭代
for user in users:
# 当第一个不合格的用户出现后,不再进行后面的处理
if not is_qualified(user):
break
...
⬇️
from itertools import takewhile
for user in takewhile(is_qualified, users):在循环中使用 else 关键字
for 循环和 while 循环后的 else 关键字,代表如果循环正常结束(没有碰到任何 break),便执行该分支内的语句
- 如果有
continue语句,else还是会执行
numbers = [1, 2, 3, 4, 5]
for num in numbers:
if num == 6:
print("找到数字 6")
break
else:
print("没有找到数字 6") # 会执行这个中断嵌套循环
- 当程序需要从一个多层嵌套循环里中断时,需要用多个 break 跳出多层循环
- 一个更好的做法,是把循环代码拆分为一个新函数,然后直接使用 return
Chapter 7. 函数
- 函数在 Python 中是一等对象,这意味着我们可以把函数自身作为函数参数来使用。
别将可变类型作为参数默认值
- Python 函数的==参数默认值只会在函数定义阶段被创建一次==,之后不论再调用多少次,函数内拿到的默认值都是同一个对象
def append_value(value, items=[]):
items.append(value)
return items
>>> append_value('foo')
['foo']
>>> append_value('bar')
['foo', 'bar']- 为了规避这个问题,==使用 None 来替代可变类型默认值==是比较常见的做法
def append_value(value, items=None):
if items is None:
items = []
items.append(value)
return itemfunctools
functools 是一个专门用来处理函数的内置模块
functools.partial允许你「冻结」一个函数的部分参数,返回一个新的函数对象,这个新函数在调用时只需要提供剩余的参数
from functools import partial
def multiply(x, y):
return x * y
double = partial(multiply, 2) # x 固定位 2
double_2 = partial(multiply, y = 2)
print(double(5))functools.lru_cache()为了提高效率,给慢函数加上缓存是比较常见的做法- 被装饰的函数应该是纯函数(相同输入总是产生相同输出,且没有副作用,纯函数是一种无状态的函数)
- 参数必须可哈希
- 缓存失效,手动调用
function_to_cache.cache_clear() - maxsize 代表当前函数最多可以保存多少个缓存结果。当缓存的结果数量超过 maxsize 以后,程序就会基于“最近最少使用”(least recently used,LRU)算法丢掉旧缓存,释放内存。默认情况下,maxsize 的值为 128
- 把 maxsize 设置为 None,函数就会保存每一个执行结果,不再剔除任何旧缓存。这时如果被缓存的内容太多,就会有占用过多内存的风险
from functools import lru_cache
@lru_cache(maxsize=128)
def function_to_cache(arg1, arg2, ...):
...
return result给函数加状态
函数的状态(State)指的是函数在运行过程中保存的内部数据,可以让函数每次调用的输出不同
方法一:在函数内使用 global 关键字声明一个全局变量
用全局变量保存状态,其实是写代码时最应该避开的事情之一
方法二:闭包
方法三:类 ✅
- 在一个类中,状态和行为可以被很好地封装在一起,因此它天生适合用来实现有状态对象
- 状态一般都在 init 函数里初始化
Chapter 8. 装饰器
装饰器把影响函数的装饰行为移到了函数头部,降低了代码的阅读与理解成本,装饰器特别适合用来实现以下功能:
- 运行时校验:在执行阶段进行特定校验,当校验通不过时终止执行。如 Django 框架中的用户登录态校验装饰器
@login_required - 注入额外参数:在函数被调用时自动注入额外的调用参数。如
unittest.mock模块的装饰器@patch - 缓存执行结果:通过调用参数等输入信息,直接缓存函数执行结果。如
functools模块的缓存装饰器@lru_cache - 注册函数:将被装饰函数注册为某个外部流程的一部分。如 Flask 框架的路由注册装饰器
@app.route - 替换为复杂对象:将原函数(方法)替换为更复杂的对象,比如静态类方法装饰器
@staticmethod
Chapter 9. 面向对象编程
- 为了区分,我们常把类里定义的函数称作方法
- 除了普通方法外,你还可以使用
@classmethod、@staticmethod等装饰器来定义特殊方法
私有属性是「君子协定」
在 Python 里,所有的类属性和方法默认都是公开的,不过你可以通过添加双下划线前缀 __ 的方式把它们标示为私有。
- 当你使用
__{var}的方式定义一个私有属性时,Python 解释器只是重新给了它一个包含当前类名的别名_{class}__{var},因此你仍然可以在外部用_{class}__{var}来访问和修改它 - 日常编程中,我们极少使用双下划线来标示一个私有属性。如果你认为某个属性是私有的,直接给它加上单下划线
_{var}前缀就够了
内置类方法装饰器
在编写类时,除了普通方法以外,我们还常常会用到一些特殊对象,比如类方法、静态方法等。要定义这些对象,得用到特殊的装饰器
==类方法==:可以用 @classmethod 装饰器定义一种特殊的方法:类方法(class method),它属于类但是无须实例化也可调用
>>> Duck.quack()
TypeError: quack() missing 1 required positional argument: 'self'
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
@classmethod
def from_birth_year(cls, name, birth_year):
age = datetime.date.today().year - birth_year
return cls(name, age)
person1 = Person('Alice', 25)
person2 = Person.from_birth_year('Bob', 1990)- 普通方法接收类实例
self作为参数,但类方法的第一个参数是类本身,通常使用名字cls - 也可以通过实例来调用类方法
- ==类方法最常见的使用场景,是定义工厂方法来生成新实例==
==静态方法==:如果你发现某个方法不需要使用当前实例里的任何内容,那可以使用 @staticmethod
- 静态方法不接收当前实例作为第一个位置参数
- 静态方法不需要访问实例的任何状态,是一种与状态无关的方法,可以改写成脱离于类的外部普通函数
属性装饰器
在一个类里,属性代表状态,方法代表行为。属性可以通过 inst.attr 的方式直接访问,而方法需要通过 inst.method() 来调用
@property装饰器模糊了属性和方法间的界限,可以把方法变成一个虚拟属性,然后像使用普通属性一样使用它
class FilePath:
...
@property
def basename(self):
"""获取文件名"""
return self.path.rsplit(os.sep, 1)[-1]
>>> p = FilePath('/tmp/foo.py')
>>> p.basename
'foo.py'多重继承与 MRO
在复杂的继承关系下,如何确认子类的某个方法会用到哪个父类?
在解决多重继承的方法优先级问题时,Python 使用了一种名为 MRO(method resolution order)的算法。该算法会遍历类的所有基类,并将它们按优先级从高到低排好序
- 调用类的
mro()方法,你可以看到按MRO算法排好序的基类列表 - 当你调用子类的某个方法时,Python 会按照 MRO 列表从前往后寻找这个方法,假如某
个类实现了这个方法,就直接返回
MRO 与 super()
- Python 里的多重继承是一个相当复杂的特性,尤其在配合
super()时 super()使用的其实不是当前类的父类,而是它在 MRO 链条里的上一个类
大多数情况下,你需要的并不是多重继承,而也许只是一个更准确的抽象模型,在该模型下,最普通的继承关系就能完美解决问题
