《深入理解Python特性》学习笔记之Python整洁之道

1. 用断言 assert 调试程序

开发人员在 Python 中使用断言来自动检测 Python 程序中的错误,让程序更可靠且更易于调试。

从根本上来说,Python 的断言语句是一种调试工具,用来测试某个断言条件。如果断言条件
为真,则程序将继续正常执行;但如果条件为假,则会引发 AssertionError 异常并显示相关
的错误消息。

1.1 示例:Python 中的断言

下面举一个断言能派上用场的简单例子。
假设你需要用 Python 构建在线商店。为了添加打折优惠券的功能,你编写了下面这个
apply_discount 函数:

def apply_discount(product, discount):
	price = int(product['price'] * (1.0 - discount)) 
 	assert 0 <= price <= product['price']
	return price 

注意到 assert 语句了吗?这条语句确保在任何情况下,通过该函数计算的折后价不低于 0,
也不会高于产品原价。
来看看调用该函数能否正确计算折后价。在这个例子中,商店中的产品用普通的字典表示。

>>> shoes = {'name': 'Fancy Shoes', 'price': 14900}
>>> apply_discount(shoes, 0.25) 
11175

接着再尝试使用一些无效的折扣,比如 200%的“折扣”会让商家向顾客付钱

>>> apply_discount(shoes, 2.0) 
Traceback (most recent call last): 
 File "<input>", line 1, in <module> 
 apply_discount(prod, 2.0) 
 File "<input>", line 4, in apply_discount 
 assert 0 <= price <= product['price'] 
AssertionError

从上面可以看到,当尝试使用无效的折扣时,程序会停止并触发一个 AssertionError。
发生这种情况是因为 200%的折扣违反了在 apply_discount 函数中设置的断言条件。

1.2 常见陷阱

注意事项1:不要使用断言验证数据,因为当全局禁用断言时,验证失效,可能存在严重的安全漏洞。
在 Python 中使用断言时要注意的一个重点是,若在命令行中使用-O 和-OO 标识,或修改
CPython 中的 PYTHONOPTIMIZE 环境变量,都会全局禁用断言。

注意事项2:永不失败的断言
在将一个元组作为 assert 语句中的第一个参数传递时,断言条件总为真,因此永远不会失败。
例如,这个断言永远不会失败:

assert(1 == 2, 'This should fail') 

这是因为在 Python 中非空元组总为真值。如果将元组传递给 assert 语句,则会导致断言
条件始终为真,因此上述 assert 语句毫无用处,永远不会触发异常。

1.3 Python 断言总结

尽管有这些需要注意的事项,但 Python 的断言依然是功能强大的调试工具,且常常得不到充分的利用。
了解断言的工作方式及使用场景有助于编写更易维护和调试的 Python 程序。
学习断言有助于将你的 Python 知识提升到新的水平,让你在调试过程中节省大量时间。

1.4 关键要点

Python 断言语句是一种测试某个条件的调试辅助功能,可作为程序的内部自检。
断言应该只用于帮助开发人员识别 bug,它不是用于处理运行时错误的机制。
设置解释器可全局禁用断言。


2. 巧妙地放置逗号

如果需要在 Python 中的列表、字典或集合常量中添加或移除项,记住一个窍门:在所有行
后面都添加一个逗号。
在 Python 中,可以在列表、字典和集合常量中的每一项后面都放置一个逗号,包括最后一
项。因此只要记住在每一行末尾都加上一个逗号,就可以避免逗号放置问题。

>>> names = [ 
... 'Alice', 
... 'Bob', 
... 'Dilbert', 
... ]

看到 Dilbert 后面的那个逗号了吗?现在能方便地添加或移除新的项,无须再修改逗号了。
这不仅让各行代码保持一致,而且源码控制系统生成的 diff 清晰整洁,让代码审阅者心情愉悦。

关键要点

 合理的格式化及逗号放置能让列表、字典和集合常量更容易维护。
 Python 的字符串字面值拼接特性既可能带来帮助,也可能引入难以发现的 bug。


3. 上下文管理器和 with 语句

with 语句究竟有哪些好处?它有助于简化一些通用资源管理模式,抽象出其中的功能,将
其分解并重用。
若想充分地使用这个特性,比较好的办法是查看 Python 标准库中的示例。内置的 open()函
数就是一个很好的用例:

with open('hello.txt', 'w') as f:
	f.write('hello, world!') 

打开文件时一般建议使用 with 语句,因为这样能确保打开的文件描述符在程序执行离开
with 语句的上下文后自动关闭。本质上来说,上面的代码示例可转换成下面这样:

f = open('hello.txt', 'w') 
try: 
	f.write('hello, world') 
finally: 
	f.close()

如果在调用 f.write()时发生异常,这段代码不能保证文件最后被关闭,因此程序可能会
泄露文件描述符。此时 with 语句就派上用场了,它能够简化资源的获取和释放。
with 语句不仅让处理系统资源的代码更易读,而且由于绝对不会忘记清理或释放资源,因
此还可以避免 bug 或资源泄漏。

3.1 在自定义对象中支持 with

无论是 open()函数和 threading.Lock 类本身,还是它们与 with 语句一起使用,这些都
没有什么特殊之处。只要实现所谓的上下文管理器(context manager),就可以在自定义的类和
函数中获得相同的功能。
上下文管理器是什么?这是一个简单的“协议”(或接口),自定义对象需要遵循这个接口来
支持 with 语句。总的来说,如果想将一个对象作为上下文管理器,需要做的就是向其中添加
__enter__和__exit__方法。Python 将在资源管理周期的适当时间调用这两种方法。
来看看实际代码,下面是 open()上下文管理器的一个简单实现:

class ManagedFile:
	def __init__(self, name):
		self.name = name 
	def __enter__(self):
		self.file = open(self.name, 'w')
		return self.file 
	def __exit__(self, exc_type, exc_val, exc_tb):
		if self.file: 
		self.file.close()

其中的 ManagedFile 类遵循上下文管理器协议,所以与原来的 open()例子一样,也支持 with
语句:

>>> with ManagedFile('hello.txt') as f: 
... f.write('hello, world!') 
... f.write('bye now') 

当执行流程进入 with 语句上下文时,Python 会调用__enter__获取资源;离开 with 上下
文时,Python 会调用__exit__释放资源。

在 Python 中,除了编写基于类的上下文管理器来支持 with 语句以外,标准库中的
contextlib 模块在上下文管理器基本协议的基础上提供了更多抽象。如果你遇到的情形正好
能用到 contextlib 提供的功能,那么可以节省很多精力。
例如,使用 contextlib.contextmanager 装饰器能够为资源定义一个基于生成器的工厂
函数,该函数将自动支持 with 语句。下面的示例用这种技术重写了之前的 ManagedFile 上下
文管理器:

from contextlib import contextmanager 
@contextmanager
def managed_file(name):
	try:
		f = open(name, 'w')
		yield f
	finally:
		f.close()
>>> with managed_file('hello.txt') as f: 
... 	f.write('hello, world!') 
... 	f.write('bye now') 

这个 managed_file()是生成器,开始先获取资源,之后暂停执行并产生资源以供调用者
使用。当调用者离开 with 上下文时,生成器继续执行剩余的清理步骤,并将资源释放回系统。
基于类的实现和基于生成器的实现基本上是等价的,选择哪一种取决于你的编码偏好。

3.2 用上下文管理器编写漂亮的 API

上下文管理器非常灵活,巧妙地使用 with 语句能够为模块和类定义方便的 API。
下面就是使用基于类的上下文管理器来实现的方法:

class Indenter:
	def __init__(self):
		self.level = 0 
	def __enter__(self):
		self.level += 1
		return self 
	def __exit__(self, exc_type, exc_val, exc_tb):
		self.level -= 1 
	def print(self, text):
		print(' ' * self.level + text) 

熟练地在自己的 Python 程序中使用上下文管理器和 with 语句,这两个功能很不错,可以用来
以更加有 Python 特色和可维护的方式处理资源管理问题。
如果你还想再找一个练习来加深理解,可以尝试实现一个使用 time.time 函数来测量代码
块执行时间的上下文管理器。一定要试着分别编写基于装饰器和基于类的变体,以此来彻底弄清
楚两者的区别。

3.3 关键要点

 with 语句通过在所谓的上下文管理器中封装 try…finally 语句的标准用法来简化异
常处理。
 with 语句一般用来管理系统资源的安全获取和释放。资源首先由 with 语句获取,并在
执行离开 with 上下文时自动释放。
 有效地使用 with 有助于避免资源泄漏的问题,让代码更加易于阅读。


4 下划线、双下划线及其他

单下划线和双下划线在 Python 变量名和方法名中都有各自的含义。有些仅仅是作为约定,
用于提示开发人员;而另一些则对 Python 解释器有特殊含义。

4.1 关键要点

 前置单下划线_var:命名约定,用来表示该名称仅在内部使用。一般对 Python 解释器没
有特殊含义(通配符导入除外),只能作为对程序员的提示。
 后置单下划线 var_:命名约定,用于避免与 Python 关键字发生命名冲突。
 前置双下划线__var:在类环境中使用时会触发名称改写,对 Python 解释器有特殊含义。
 前后双下划线__var__:表示由 Python 语言定义的特殊方法。在自定义的属性中要避免
使用这种命名方式。
 单下划线_:有时用作临时或无意义变量的名称(“不关心”)。此外还能表示 Python REPL
会话中上一个表达式的结果。

4.2 前置双下划线:__var

迄今为止,我们介绍的命名模式只有约定的意义,但使用以双下划线开头的 Python 类属性
(变量和方法)就不一样了。
双下划线前缀会让 Python 解释器重写属性名称,以避免子类中的命名冲突。
这也称为名称改写(name mangling),即解释器会更改变量的名称,以便在稍后扩展这个类
时避免命名冲突。
听起来很抽象,下面用代码示例来实验一下:

class Test: 
	def __init__(self): 
		self.foo = 11 
		self._bar = 23 
		self.__baz = 42 

接着用内置的 dir() 函数来看看这个对象的属性:

>>> t = Test() 
>>> dir(t) 
['_Test__baz', '__class__', '__delattr__', '__dict__',
 '__dir__', '__doc__', '__eq__', '__format__', '__ge__',
 '__getattribute__', '__gt__', '__hash__', '__init__',
 '__le__', '__lt__', '__module__', '__ne__', '__new__',
 '__reduce__', '__reduce_ex__', '__repr__',
 '__setattr__', '__sizeof__', '__str__',
 '__subclasshook__', '__weakref__', '_bar', 'foo']

该函数返回了一个包含对象属性的列表。在这个列表中尝试寻找之前的变量名称 foo、_bar 和 __baz,你会发现一些有趣的变化。
首先,self.foo 变量没有改动,在属性列表中显示为 foo。
接着,self._bar 也一样,在类中显示为_bar。前面说了,在这种情况下前置下划线仅仅是一个约定,是对程序员的一个提示。
然而 self.__baz 就不一样了。在该列表中找不到__baz 这个变量。
__baz 到底发生了什么?
仔细观察就会看到,这个对象上有一个名为_Test__baz 的属性。这是 Python 解释器应用
名称改写之后的名称,是为了防止子类覆盖这些变量。

下面这个名称改写的示例:

_MangledGlobal__mangled = 23 

class MangledGlobal:
	def test(self):
		return __mangled 
		
>>> MangledGlobal().test() 
23 

这个例子先声明_MangledGlobal__mangled 为全局变量,然后在名为 MangledGlobal 的
类环境中访问变量。由于名称改写,类中的 test()方法仅用 __mangled 就能引用_MangledGlobal__mangled 全局变量。
__mangled 以双下划线开头,因此 Python 解释器自动将名称扩展为_MangledGlobal__ mangled
这表明名称改写不专门与类属性绑定,而是能够应用于类环境中所有以双下划线开头的名称。

4.3 补充内容:什么是 dunder —— “双下划线方法”

在 Python 社区中通常称双下划线为 dunder。因为 Python 代码中经常出现双下划线,所以为
了简化发音,Python 高手通常会将“双下划线”(double underscore)简称为 dunder。
__init__读作 dunderinit,但这只是命名约定中的一个癖好,就像是 Python 开发人员的暗号。


5 字符串格式化中令人震惊的真相

本节将介绍这四种字符串格式化方法的工作原理以及它们各自的优缺点。除此之外,还会介
绍简单的“经验法则”,用来选择最合适的通用字符串格式化方法。

下面用一个简单的示例来实验,假设有以下变量
(或常量)可以使用:

>>> errno = 50159747054 
>>> name = 'Bob' 

基于这些变量,我们希望生成一个输出字符串并显示以下错误消息:

'Hey Bob, there is a 0xbadc0ffee error!' 
5.1 第一种方法:“旧式”字符串格式化

Python 内置了一个独特的字符串操作:通过%操作符可以方便快捷地进行位置格式化。如果
你在 C 中使用过 printf 风格的函数,就会立即明白其工作方式。这里有一个简单的例子:

>>> 'Hello, %s' % name 
'Hello, Bob'

这里使用%s 格式说明符来告诉 Python 替换 name 值的位置。这种方式称为“旧式”字符串
格式化。
在旧式字符串格式化中,还有其他用于控制输出字符串的格式说明符。例如,可以将数转换
为十六进制符号,或者填充空格以生成特定格式的表格和报告。
下面使用%x 格式说明符将 int 值转换为字符串并将其表示为十六进制数:

>>> '%x' % errno 
'badc0ffee'

如果要在单个字符串中进行多次替换,需要对“旧式”字符串格式化语法稍作改动。由于%
操作符只接受一个参数,因此需要将字符串包装到右边的元组中,如下所示:

>>> 'Hey %s, there is a 0x%x error!' % (name, errno) 
'Hey Bob, there is a 0xbadc0ffee error!'
5.2 第二种方法:“新式”字符串格式化

Python 3 引入了一种新的字符串格式化方式,后来又移植到了 Python 2.7 中。“新式”字符串
格式化可以免去%操作符这种特殊语法,并使得字符串格式化的语法更加规整。新式格式化在字
符串对象上调用 format()函数。
与“旧式”格式化一样,使用 format()函数可以执行简单的位置格式化:

>>> 'Hello, {}'.format(name) 
'Hello, Bob'

你还可以用别名以任意顺序替换变量。这是一个非常强大的功能,不必修改传递给格式函数
的参数就可以重新排列显示顺序:

>>> 'Hey {name}, there is a 0x{errno:x} error!'.format( 
... name=name, errno=errno) 
'Hey Bob, there is a 0xbadc0ffee error!'

从上面可以看出,将 int 变量格式化为十六进制字符串的语法也改变了。现在需要在变量
名后面添加:x 后缀来传递格式规范。
总体而言,这种字符串格式化语法更加强大,也没有额外增加复杂性。阅读 Python 文档对
字符串格式化语法的描述是值得的。
在 Python 3 中,这种“新式”字符串格式化比%风格的格式化更受欢迎。但从 Python 3.6 开
始,出现了一种更好的方式来格式化字符串。

5.3 第三种方法:字符串字面值插值(Python 3.6+)

Python 3.6 增加了另一种格式化字符串的方法,称为格式化字符串字面值(formatted string
literal)。采用这种方法,可以在字符串常量内使用嵌入的 Python 表达式。我们通过下面这个简单
的示例来体验一下该功能:

>>> f'Hello, {name}!' 
'Hello, Bob!'

这种新的格式化语法非常强大。因为其中可以嵌入任意的 Python 表达式,所以甚至能内联
算术运算,如下所示:

>>> a = 5 
>>> b = 10 
>>> f'Five plus ten is {a + b} and not {2 * (a + b)}.' 
'Five plus ten is 15 and not 30.'

本质上,格式化字符串字面值是 Python 解析器的功能:将 f 字符串转换为一系列字符串常
量和表达式,然后合并起来构建最终的字符串。
假设有如下的 greet()函数,其中包含 f 字符串:

>>> def greet(name, question): 
... return f"Hello, {name}! How's it {question}?" 
... 
>>> greet('Bob', 'going') 
"Hello, Bob! How's it going?"

字符串字面值也支持 str.format()方法所使用的字符串格式化语法,因此可以用相同的方
式解决前两节中遇到的格式化问题:

>>> f"Hey {name}, there's a {errno:#x} error!" 
"Hey Bob, there's a 0xbadc0ffee error!"
5.4 第四种方法:模板字符串

Python 中的另一种字符串格式化技术是模板字符串(template string)。这种机制相对简单,
也不太强大,但在某些情况下可能正是你所需要的。
来看一个简单的问候示例:

>>> from string import Template 
>>> t = Template('Hey, $name!') 
>>> t.substitute(name=name) 
'Hey, Bob!'

从上面可以看到,这里需要从 Python 的内置字符串模块中导入 Template 类。模板字符串
不是核心语言功能,而是由标准库中的模块提供。
另一个区别是模板字符串不能使用格式说明符。因此,为了让之前的报错字符串示例正常工
作,需要手动将 int 错误码转换为一个十六进制字符串:

>>> templ_string = 'Hey $name, there is a $error error!' 
>>> Template(templ_string).substitute( 
... 	name=name, error=hex(errno)) 'Hey 
Bob, there is a 0xbadc0ffee error!'

最佳使用场景是用来处理程序用户生成的格式字符串。因为模板字符串较为简单,所以是更安全
的选择。
其他字符串格式化技术所用的语法更复杂,因而可能会给程序带来安全漏洞。例如,格式字
符串可以访问程序中的任意变量。

5.5 达恩的 Python 字符串格式化经验法则

如果格式字符串是用户提供的,使用模板字符串来避免安全问题。如果不是,再考虑
Python版本:Python 3.6+使用字符串字面值插值,老版本则使用“新式”字符串格式化。

5.6 关键要点

 每种方式都有其优缺点,使用哪一种取决于具体情况。
 如果难以选择,可以试试我的字符串格式化经验法则。

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值