>>> from env_helper import info; info()
页面更新时间: 2024-04-04 20:59:11
运行环境:
Linux发行版本: Debian GNU/Linux 12 (bookworm)
操作系统内核: Linux-6.1.0-18-amd64-x86_64-with-glibc2.36
Python版本: 3.11.2
2.12. 三剑客¶
在结束本章前,我们再来看下 Python 语句的三位身怀绝技的剑客: pass
、
del
和 exec
。
2.12.1. 剑客1:pass语句——什么都不做¶
有时候,python可能什么都不用做。这种情况虽然不多,但是一旦遇到,你立刻就会明白使用
pass
语句大有裨益。
>>> pass
如你所见,这里什么都没发生。 那么为何还要一条什么都不做的语句呢?
因为在你编写代码时,可将其用作占位符。例如,你可能编写了一条 if
语句并想尝试运行它,但其中缺少一个代码块,如下所示:
if name == 'Ralph Auldus Melish':
print('Welcome!')
elif name == 'Enid':
# 还未完成……
elif name == 'Bill Gates':
print('Access Denied')
这些代码不能运行,因为在Python中代码块不能为空。要修复这个问题,只需在中间的代码块中添加一条pass语句即可。
当然可以随便写点什么,比如一条不会使用的赋值语句。
姑且不论其可能会引入的 Bug , 使用 pass
这个有语意的语句更 Pythonic
。
if name == 'Ralph Auldus Melish':
print('Welcome!')
elif name == 'Enid':
# 还未完成……
pass
elif name == 'Bill Gates':
print('Access Denied')
注意
也可不使用注释和pass语句,而是插入一个字符串。这种做法尤其适用于未完成的函数和类,因为这种字符串将充当文档字符串。
2.12.2. 剑客2:del 语句——删除¶
对于你不再使用的对象,Python通常会将其删除(因为没有任何变量或数据结构成员指向它)。
>>> scoundrel = {'age': 42, 'first name': 'Robin', 'last name': 'of Locksley'}
>>> robin = scoundrel
>>> scoundrel
{'age': 42, 'first name': 'Robin', 'last name': 'of Locksley'}
>>> robin
{'age': 42, 'first name': 'Robin', 'last name': 'of Locksley'}
>>> scoundrel = None
>>> robin
{'age': 42, 'first name': 'Robin', 'last name': 'of Locksley'}
>>> robin = None
最初,robin和scoundrel指向同一个字典,因此将None赋给scoundrel后,依然可以通过robin 来访问这个字典 。但将robin也设置为None之后,这个字典就漂浮在计算机内存中,没有任何名称与之相关联,再也无法获取或使用它了。 因此,智慧无穷的Python解释器直接将其删除。这被称为垃圾收集。 请注意,在前面的代码中,也可将其他任何值(而不是None)赋给两个变量,这样字典也将消失。
另一种办法是使用del语句。 这不仅会删除到对象的引用,还会删除名称本身。
>>> x = 1
>>> del x
>>> x
Traceback (most recent call last):
File "<pyshell#255>", line 1, in ?
x
NameError: name 'x' is not defined
这看似简单,但有时不太好理解。例如,在下面的示例中,x和y指向同一个列表:
>>> x = ["Hello", "world"]
>>> y = x
>>> y[1] = "Python"
>>> x
['Hello', 'Python']
你可能认为通过删除x,也将删除y,但情况并非如此。
>>> del x
>>> y
['Hello', 'Python']
这是为什么呢? x
和 y
指向同一个列表,但删除 x
对 y
没有任何影响,因为你只删除名称 x
,而没有删除列表本身(值)。
事实上,在Python中,根本就没有办法删除值,而且你也不需要这样做,因为对于你不再使用的值,Python解释器会立即将其删除。
2.12.3. 剑客3:exec 语句——执行字符串及计算其结果¶
有时候,你可能想动态地编写Python代码,并将其作为语句进行执行或作为表达式进行计算。
请注意,这犹如黑暗魔法,一定要慎用。 exec
和 eval
现在都是函数,但exec以前是一种语句,而eval与它紧密相关。
警告
本节介绍如何执行存储在字符串中的Python代码,这样做可能带来严重的安全隐患。 如果将部分内容由用户提供的字符串作为代码执行,将无法控制代码的行为。 在网络应用程序,如后面的章节中将介绍的通用网关接口(CGI)脚本中,这样做尤其危险。
函数exec将字符串作为代码执行。
>>> exec("print('Hello, world!')")
Hello, world!
然而,调用函数 exec
时只给它提供一个参数绝非好事。
在大多数情况下,还应向它传递一个命名空间——用于放置变量的地方;
否则代码将污染你的命名空间,即修改你的变量。 例如,假设代码使用了名称
sqrt
,结果将如何呢?
>>> from math import sqrt
>>> exec("sqrt = 1")
>>> sqrt(4)
Traceback (most recent call last):
File "<pyshell#18>", line 1, in ?
sqrt(4)
TypeError: object is not callable: 1
既然如此,为何要将字符串作为代码执行呢? 函数 exec
主要用于动态地创建代码字符串。
如果这种字符串来自其他地方(可能是用户),就几乎无法确定它将包含什么内容。
因此为了安全起见,要提供一个字典以充当命名空间。
注意
命名空间(作用域)是个重要的概念,将在以后深入讨论,但就目前而言,你可将命名空间视为放置变量的地方,类似于一个看不见的字典。 因此,当你执行赋值语句
x = 1
时,将在当前命名空间存储键x
和值1
。 当前命名空间通常是全局命名空间(到目前为止, 我们使用的大都是全局命名空间),但并非必然如此。
为此,你添加第二个参数——字典,用作代码字符串的命名空间。
实际上,可向exec提供两个命名空间:一个全局的和一个局部的。 提供的全局命名空间必须是字典,而提供的局部命名空间可以是任何映射。这一点也适用于eval。
>>> from math import sqrt
>>> scope = {}
>>> exec('sqrt = 1', scope)
>>> sqrt(4)
2.0
>>> scope['sqrt']
1
如你所见,可能带来破坏的代码并非覆盖函数 sqrt
。 函数 sqrt
该怎样还怎样,而通过exec执行赋值语句创建的变量位于scope中。
请注意,如果你尝试将 scope
打印出来,将发现它包含很多内容,
这是因为自动在其中添加了包含所有内置函数和值的字典 builtins 。
>>> len(scope)
2
>>> scope.keys()
dict_keys(['__builtins__', 'sqrt'])
2.eval
eval是一个类似于exec的内置函数。exec执行一系列Python语句,而eval计算用字符串表示的Python表达式的值,并返回结果(exec什么都不返回,因为它本身是条语句)。例如,你可使 用如下代码来创建一个Python计算器:
>>> eval(input("Enter an arithmetic expression: "))
Enter an arithmetic expression: 4+5
9
与exec一样,也可向eval提供一个命名空间,虽然表达式通常不会像语句那样给变量重新赋值。
警告
虽然表达式通常不会给变量重新赋值,但绝对能够这样做,如调用给全局变量重新赋值的函数。 因此,将
eval
用于不可信任的代码并不比使用exec
安全。 当前,在Python中执行 不可信任的代码时,没有安全的办法。 一种替代解决方案是使用 Jython 等 Python实现,以使用Java沙箱等原生机制。
浅谈作用域¶
向exec或eval提供命名空间时,可在使用这个命名空间前在其中添加一些值。
>>> scope = {}
>>> scope['x'] = 2
>>> scope['y'] = 3
>>> eval('x * y', scope)
6
同样,同一个命名空间可用于多次调用exec或eval。
>>> scope = {}
>>> exec('x = 2', scope)
>>> eval('x * x', scope)
4 警告 采用这种做法可编写出非常复杂的程序,但你也许不应这样做。