Pythonic的几个办法
Pythonic的几个办法
这些年来,Python 开发者用Pythonic这个形容词来描述那种符合特定风格的代码。这种Pythonice风格,既不是非常严密的规范,也不是由编译器强加给开发者的规则,而是大家在使用Python语言协同工作的过程中逐渐形成的习惯。
确认python版本
1 python --version
1
2
3
4
5 import sys
print(sys.version)
3.7.6 (default, Jan 8 2020, 20:23:39) [MSC v.1916 64 bit (AMD64)]
print(sys.version_info)
sys.version_info(major=3, minor=7, micro=6, releaselevel='final', serial=0)有很多种流行的Python运行时环境,例如,CPython、 Jython、 IronPython 以及PyPy等。
enumerate迭代
enumerate可以把各种迭代器包装为生成器,以便稍后产生输出值。
可以给enumerate提供第二个参数,以指定开始计数时所用的值(默认为0)。
1 | 45, 22, 14, 65, 97, 72] numbers = [ |
列表递推式
列表推导是构建列表(
list
)的快捷方式,而生成器表达式则可以用来创建其他任何类型的序列。通常的原则是,只用列表推导来创建新的列表,并且尽量保持简短。如果列表推导的代码超过了两行,你可能就要考虑是不是得用
for
循环重写了。在 Python 3 中都有了自己的局部作用域,就像函数似的。表达式内部的变量和赋值只在局部起作用,表达式的上下文里的同名变量还可以被正常引用,局部变量并不会影响到它们。
列表推导可以帮助我们把一个序列或是其他可迭代类型中的元素过滤或是加工,然后再新建一个列表。
列表推导的作用只有一个:生成列表。如果想生成其他类型的序列,生成器表达式就派上了用场。
1 | 4, 2, 1, 6, 9, 7] numbers = [ |
1 | # 列表推导也支持多个if条件。处在同-循环级别中的多项条件,彼此之间默认形成and表达式。 |
生成器表达式
生成器表达式背后遵守了迭代器协议,可以逐个地产出元素,而不是先建立一个完整的列表,然后再把这个列表传递到某个构造函数里。
生成器表达式的语法跟列表推导差不多,只不过把方括号换成圆括号而已。
如果生成器表达式是一个函数调用过程中的唯一参数,那么不需要额外再用括号把它围起来。
生成器表达式就可以帮忙省掉运行
for
循环的开销
1 | # 前面提到,列表推导是方便的工具,但有时会导致不必要的内存使用。 |
列表展平
1 | def flat(deep_list, result): |
1 | def list_flat(deep_list, ignore_types=(str, bytes)) -> List: |
所以,当代码运行到
1 | [x for x in flat(a)] |
的时候,每一次循环都会进入到 flat
生成器里面。在 flat
里面,对传入的参数使用for循环进行迭代,如果拿到的元素不是列表,那么就直接抛出,送到上一层。如果当前已经是最上层了,那么就再一次抛出给外面的列表推导式。如果当前元素是列表,那么继续生成一个生成器,并对这个新的生成器进行迭代,并把每一个结果继续往上层抛出。
最终,每一个数字都会被一层一层往上抛出给列表推导式,从而获得需要的结果。
字典展平
1 | nest_dict = { |
使用yield
关键字来实现这个需求,在不炫技
的情况下,只需要8行代码。在炫技的情况下,只需要3行代码。
要快速地把这个嵌套字典压扁,我们需要从下向上来处理字段。例如对于b->e->f->4
这条路径,我们首先把最里面的{'f': 4}
转换为一个元组('f', 4)
。然后,把这个元组向上抛出,于是得到了元组('e', ('f', 4))
。我们把 e
拼接到f
的前面,变为:('e_f', 4)
,继续往上抛出,得到('b', ('e_f', 4))
。再把b
拼接到e_f
上面,得到('b_e_f', 4)
。完成一条线路的组装。
这个逻辑如果使用yield
关键字来实现,就是:
1 | def map_flat(deep_map, full_key: bool = True) -> Dict: |
1 | {k:v for k,v in map_flat(nest_dict)} |
通过使用 yield
关键字,字典的key
会像是在流水线上一样,一层一层从内向外进行组装,从而形成完整的路径。
找list的最值索引
1 | def max_idx(lst): |
list去重并保留顺序
1 | from collections import OrderedDict |
代码里面调用 pip
说到安装 Python 的第三方库,会 Python 的同学都知道,在终端使用pip install xxx
即可。
那么如果我想在代码里面安装第三方库怎么办呢?可能有人想到使用 os
模块:
1 | import os |
这种方法确实可行,并且即使你在虚拟环境中使用这种方式安装,也确实不会安装到系统的 Python 环境中。
但是这种方式总感觉有点奇怪。而且如果这个package_name
字符串经过精心构造,可以执行任意系统命令,例如:
1 | import os |
为了防止这种情况发生,我们可以直接调用pip
这个 Python 包:
1 | from pip._internal import main |
命令行下面的参数都可以通过转换为列表的形式执行,例如:
1 | from pip._internal import main |
使用f-Strings格式化字符串
1 | # f-strings支持使用字符串格式化迷你语言,以及强大的字符串插值。这些功能允许你添加变量甚至有效的Python表达式,并在添加到字符串之前在运行时对它们进行评估: |
集合论
集合的本质是许多唯一对象的聚集。因此,集合可以用于去重
集合中的元素必须是可散列的,
set
类型本身是不可散列的,但是frozenset
可以。因此可以创建一个包含不同frozenset
的set
。给定两个集合
a
和b
,a | b
返回的是它们的合集,a & b
得到的是交集,而a - b
得到的是差集。除空集之外,集合的字面量——
{1}
、{1, 2}
,等等——看起来跟它的数学形式一模一样。如果是空集,那么必须写成set()
的形式。在 Python 3 里面,除了空集,集合的字符串表示形式总是以
{...}
的形式出现。像
{1, 2, 3}
这种字面量句法相比于构造方法(set([1, 2, 3])
)要更快且更易读。后者的速度要慢一些,因为 Python 必须先从set
这个名字来查询构造方法,然后新建一个列表,最后再把这个列表传入到构造方法里。但是如果是像{1, 2, 3}
这样的字面量,Python 会利用一个专门的叫作BUILD_SET
的字节码来创建集合。
数学符号 | python运算符 | 方法 | 描述 |
---|---|---|---|
set
的实现以及导致的结果
set
和frozenset
的实现也依赖散列表,但在它们的散列表里存放的只有元素的引用(就像在字典里只存放键而没有相应的值)。在set
加入到 Python 之前,我们都是把字典加上无意义的值当作集合来用的。这些特点总结如下。
- 集合里的元素必须是可散列的。
- 集合很消耗内存。
- 可以很高效地判断元素是否存在于某个集合。
- 元素的次序取决于被添加到集合里的次序。
- 往集合里添加元素,可能会改变集合里已有元素的次序。
1 | import random |
使用字符串常量访问公共字符串组
1 | # 可以使用is_upper(),它返回字符串中的所有字符是否都是大写字母: |
使用Itertools生成排列和组合
1 | # itertools.permutations()构建所有排列的列表,这意味着它是输入值的每个可能分组的列表,其长度与count参数匹配。r关键字参数允许我们指定每个分组中有多少值: |
漂亮的打印出JSON
1 | import json |
with上下文管理
with模块
with 语句会设置一个临时的上下文,交给上下文管理器对象控制,并且负责清理上下文。这么做能避免错误并减少样板代码,因此 API 更安全,而且更易于使用。with的行为上下文管理器对象存在的目的是管理
with
语句,就像迭代器的存在是为了管理for
语句一样。
with
语句的目的是简化try/finally
模式。finally
子句中的代码通常用于释放重要的资源,或者还原临时变更的状态。上下文管理器协议包含
__enter__
和__exit__
两个方法。
with 语句开始运行时,会在上下文管理器对象上调用__enter__ 方法。
with 语句运行结束后,会在上下文管理器对象上调用__exit__方法,以此扮演
finally
子句的角色。
如果 `__exit__` 方法返回 `None`,或者 `True` 之外的值,`with` 块中的任何异常都会向上冒泡。执行
with
后面的表达式得到的结果是上下文管理器对象,不过,把值绑定到目标变量上(as
子句)是在上下文管理器对象上调用__enter__
方法的结果。
__enter__
方法除了返回上下文管理器之外,还可能返回其他对象。- 不管控制流程以哪种方式退出
with
块,都会在上下文管理器对象上调用__exit__
方法,而不是在__enter__
方法返回的对象上调用。with
语句的as
子句是可选的。对open
函数来说,必须加上as
子句,以便获取文件的引用。
可以手动调用 `__enter__` 和 `__exit__` 方法。解释器调用
__enter__
方法时,除了隐式的self
之外,不会传入任何参数。传给__exit__
方法的三个参数列举如下。
1 exc_type异常类(例如
ZeroDivisionError
)。
1 exc_value异常实例。有时会有参数传给异常构造方法,例如错误消息,这些参数可以使用
exc_value.args
获取。
1 traceback
traceback
对象。
with
不仅能管理资源,还能用于去掉常规的设置和清理代码,或者在另一个过程前后执行的操作
1 | with allocate_resource() as resource: |
上下文装饰器
contextlib 模块中的实用工具重点介绍下**@contextmanager**
closing: 如果对象提供了
close()
方法,但没有实现__enter__/__exit__
协议,那么可以使用这个函数构建上下文管理器。suppress: 构建临时忽略指定异常的上下文管理器。
@contextmanager: 这个装饰器把简单的生成器函数变成上下文管理器,这样就不用创建类去实现管理器协议了。
ContextDecorator: 这是个基类,用于定义基于类的上下文管理器。这种上下文管理器也能用于装饰函数,在受管理的上下文中运行整个函数。
ExitStack: 这个上下文管理器能进入多个上下文管理器。
with
块结束时,ExitStack
按照后进先出的顺序调用栈中各个上下文管理器的__exit__
方法。如果事先不知道with
块要进入多少个上下文管理器,可以使用这个类。例如,同时打开任意一个文件列表中的所有文件。显然,在这些实用工具中,使用最广泛的是
@contextmanager
装饰器,因此要格外留心。这个装饰器也有迷惑人的一面,因为它与迭代无关,却要使用
yield
语句。
with 块终止时,`__exit__` 方法会做以下几件事:
@contextmanager
装饰器能减少创建上下文管理器的样板代码量,因为不用编写一个完整的类,定义__enter__
和__exit__
方法,而只需实现有一个 yield 语句的生成器,生成想让__enter__
方法返回的值。在使用
@contextmanager
装饰的生成器中,yield
语句的作用是把函数的定义体分成两部分:yield
语句前面的所有代码在with
块开始时(即解释器调用__enter__
方法时)执行,yield
语句后面的代码在with
块结束时(即调用__exit__
方法时)执行。
1
2
3
4
5
6
7
8
9
10
11
12 import contextlib
def looking_glass():
import sys
original_write = sys.stdout.write ➋
def reverse_write(text): ➌
original_write(text[::-1])
sys.stdout.write = reverse_write ➍
yield 'JABBERWOCKY' ➎
sys.stdout.write = original_write ➏ # 这里相当于__exit__这个类的
__enter__
方法有如下作用。(1) 调用生成器函数,保存生成器对象(这里把它称为
gen
)。(2) 调用
next(gen)
,执行到yield
关键字所在的位置。(3) 返回
next(gen)
产出的值,以便把产出的值绑定到with/as
语句中的目标变量上。
检查有没有把异常传给
exc_type
;如果有,调用gen.throw(exception)
,在生成器函数定义体中包含yield
关键字的那一行抛出异常。否则,调用
next(gen)
,继续执行生成器函数定义体中yield
语句之后的代码。上文示例有一个严重的错误:如果在
with
块中抛出了异常,Python 解释器会将其捕获,然后在looking_glass
函数的yield
表达式里再次抛出。但是,那里没有处理错误的代码,因此
looking_glass
函数会中止,永远无法恢复成原来的sys.stdout.write
方法,导致系统处于无效状态。以下代码是基于生成器的上下文管理器,而且实现了异常处理——从外部看,行为与前文一样
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 import contextlib
def looking_glass():
import sys
original_write = sys.stdout.write
def reverse_write(text):
original_write(text[::-1])
sys.stdout.write = reverse_write
msg = '' ➊
try:
yield 'JABBERWOCKY'
except ZeroDivisionError: ➋
msg = 'Please DO NOT divide by zero!'
finally:
sys.stdout.write = original_write ➌
if msg:
print(msg) ➍前面说过,为了告诉解释器异常已经处理了,
__exit__
方法会返回True
,此时解释器会压制异常。如果__exit__
方法没有显式返回一个值,那么解释器得到的是None
,然后向上冒泡异常。使用@contextmanager
装饰器时,默认的行为是相反的:装饰器提供的__exit__
方法假定发给生成器的所有异常都得到处理了,因此应该压制异常。6 如果不想让@contextmanager
压制异常,必须在被装饰的函数中显式重新抛出异常。在
@contextmanager
装饰器装饰的生成器中,yield
与迭代没有任何关系。
@contextmanager
装饰器能把包含一个yield
语句的简单生成器变成上下文管理器——这比定义一个至少包含两个方法的类要更简洁。
for/else块
`else` 子句不仅能在 if 语句中使用,还能在 for、while 和 try 语句中使用。 `for/else`、`while/else` 和 `try/else` 的语义关系紧密,不过与 `if/else` 差别很大。在循环中,
else
的语义恰好相反:“运行这个循环,然后做那件事。”else
子句的行为如下。for: 仅当
for
循环运行完毕时(即for
循环没有被break
语句中止)才运行else
块。while: 仅当
while
循环因为条件为假值而退出时(即while
循环没有被break
语句中止)才运行else
块。try: 仅当
try
块中没有异常抛出时才运行else
块。官方文档还指出:“else
子句抛出的异常不会由前面的except
子句处理。”在所有情况下,如果异常或者
return
、break
或continue
语句导致控制权跳到了复合语句的主块之外,else
子句也会被跳过。在这些语句中使用
else
子句通常能让代码更易于阅读,而且能省去一些麻烦,不用设置控制标志或者添加额外的if
语句。
1 | # for else 是 Python 中特有的语法格式,else 中的代码在 for 循环遍历完所有元素之后执行 |
1 | # 方式一 |
1 | # 方式二 |
1 | # 判断质数/素数——我知道的最快的方法 |
合理使用列表
1 | from collections import deque |
- 列表对象(list)是一个查询效率高于更新操作的数据结构,删除和插入需要对剩下的元素做移动操作
- deque 是一个双向队列的数据结构,删除元素和插入元素会很快
序列解包
元组拆包可以应用到任何可迭代对象上,唯一的硬性要求是,被可迭代对象中的元素数量必须要跟接受这些元素的元组的空档数一致。除非我们用
*
来表示忽略多余的元素。os.path.split() 函数就会返回以路径和最后一个文件名组成的元组 (path, last_part)
在平行赋值中,
*
前缀只能用在一个变量名前面,但是这个变量可以出现在赋值表达式的任意位置
1 | p = 'vttalk', 'female', 30, 'python@qq.com' |
链式比较操作
1 | if 18 < age < 60: |
assert用法
1 | assert mul(2, 3) == 7, 'This statement is wrong!!!!!!' |
slots优化内存
使用slots使用了100M内存,比使用dict存储属性值节省了2倍。
其实使用collection模块的namedtuple也可以实现slots相同的功能。namedtuple其实就是继承自tuple,同时也因为slots的值被设置成了一个空tuple以避免创建dict
collection 和普通创建类方式相比,也节省了不少的内存。所在在确定类的属性值固定的情况下,可以使用slots方式对内存进行优化。但是这项技术不应该被滥用于静态类或者其他类似场合,那不是python程序的精神所在。
1 | # 未使用__slots__ |
1 | # 使用__slots__ |
1 | # 使用__slots__要注意,__slots__定义的属性仅对当前类起作用,对继承的子类是不起作用的 |