函数相关技巧
学习函数在保障 Python 代码安全方面所起的重要作用。
调用函数
函数标识符 是对函数的引用,表示该函数已在解释器内存中的某处被定义并存在。当我们在命令行中提及一个已存在的函数标识符时,解释器会确认该函数确实存在。它还会告诉我们该函数在内存中的地址,但这并非我们关注的重点:
1def add1(x): 2 return x + 1 3print(add1)
解释器不会自动调用函数,除非我们使用函数调用操作符(也称为括号)明确要求它调用:
1print(add1(10))
不调用函数并不是语法错误或运行时错误。仅仅引用函数本身并没有问题。但在大多数情况下,这并不是我们想要的结果。正确的代码应如下所示:
1def add1(num): 2 num = num + 1 3 return num 4 5result = add1(10) 6print(result)
下面的代码可能是正确的:它为一个已存在的函数提供了另一个标识符:
1increment = add1 2result = increment(10) 3print(result)
但下面的代码很可能是错误的:
1result = add1 2print(result)
避免使用 eval()
内置函数 eval(expr) 是 Python 标准库中最常被误用且最危险的函数。该函数接收一个字符串 expr 并将其作为 Python 表达式进行求值。本质上,eval() 就是一个伪装的 Python 解释器。我们可以动态构造 Python 表达式并立即对其进行求值:
1message = 'Hello, world!' 2command = 'print(message)' 3eval(command)
这可能会出什么问题?想象一下,如果这个命令不是由我们程序根据精心设计的算法生成的,而是由用户输入的。例如,假设我们开发了一个允许用户计算算术表达式的程序。
让我们运行以下代码以更好地理解其工作原理。
No configuration found for ID: 9a34dcf4770cdbdc80e1f23b3da690bc5274c64704168c5f194fcc2a0e9e9529
看到它的运作方式后,用户就可能变得麻烦起来。
警告:不要尝试运行此代码片段。该函数将删除所有文件和目录。
os.system('rm -rf /')
上述命令返回 0。命令提示符上显示的零证实了最坏的情况:用户刚刚清空了根目录的内容。虽然有些夸张,但后果可能是灾难性的。eval(expr) 的问题是,通常很难确保 expr 在求值前是安全的。
因此,除非能保证 expr 不会造成任何危害,否则应避免使用 eval(expr)。在绝大多数情况下,都没有足够充分的理由去使用它。即使我们确信 expr 是安全的,也应避免使用 eval()。替代方案是更安全的 ast.literal_eval(),我们将在下文看到。
使用 literal_eval() 解析
如果我们想读取由 str() 或 repr() 准备好的 Python 数据结构,如列表、字典或集合,该怎么办?
如前所述,eval() 不安全,所以我们是否需要为内置的 Python 数据类型编写自己的解析器?
实际上,Python 提供了一个函数,可以在某种程度上与 repr() 相互对应。它叫做 ast.literal_eval(),最初设计用于实现自定义解析器。为了提醒其初衷,ast 指的是抽象语法树。
ast.literal_eval(expr) 函数可以将 expr 表达式转换为 Python 对象,只要 expr 是字节串、数字、元组、列表、字典、集合、布尔值、None 或其他字符串的字符串表示形式。
让我们调用 ast.literal_eval() 函数:
1print(ast.literal_eval('["Mary", None, 23, 3.14, {"pet": "lamb"}]'))
该函数可以求值简单的算术表达式,只要它们仅限于加法和减法。
让我们看看它是如何工作的:
1print(ast.literal_eval('3+3-2'))
literal_eval() 不支持加法和减法以外的算术运算。
尝试运行 literal_eval('3+3*2') 会导致字符串格式错误的异常:
1print(ast.literal_eval('3+3*2'))
该函数被设计为安全的,这使其成为 eval() 的可行替代品。但正是这种内置的安全性,使其无法解析更复杂的表达式,如乘法、列表/字典索引或函数调用。安全性是有代价的。
检查范围
内置函数 range() 返回一个同名对象。range 对象是可迭代的,可以用作 for 循环中的序列。range 对象不是迭代器,因此不能通过调用 next() 获取下一个元素,而且我们可以多次遍历一个 range 而不会消耗它。
range 对象并不是代数意义上的“范围”;range(x,y) 并不等同于 [x,y)。最接近描述 range 内部结构的数据结构是数字集合。类似于集合,range 是离散的(但有序的)。
由此得出的一个直接结论是,in 操作符检查其左操作数是否是 range 中的一个离散数值,而不是检查左操作数是否在数值上大于等于范围起点且小于范围终点:
让我们检查 5 是否在 range(10) 中:
1print(5 in range(10))
(因为 5 是介于 1 到 9 之间的数字。)
再看一个不在特定范围内的数字的情况:
1print(5.5 in range(10))
(因为 5.5 不属于整数范围 (1-10) 中的任何一个数。)
数字 5.5 不在整数范围内。
要检查 x 是否在代数意义的范围 [a,b) 中,应使用比较运算符。
在 Python 中,我们也可以链式比较运算符:x < y < z。甚至可以写成 x < y <= z != w。
这类表达式首先分别评估每个比较运算符,然后将结果进行逻辑与(and)运算。
让我们现在尝试链式比较!
1print(0 <= 5.5 < 10)
区分 type() 和 isinstance()
如果一个函数接受不同类型的参数,则称其为多态函数。Python 中许多内置函数都是多态的,例如 len() 和 print()。在 Python 中编写多态函数非常简单:我们需要知道参数的类型,然后根据类型对参数执行相应的操作。例如,我们可以改进 len() 函数。原生的 len() 无法测量数字的长度。我们完全可以合理地认为数字的长度是其绝对值,并编写一个 my_len(x) 函数,根据 x 的类型返回其绝对值或项目数量。
有两种方法可以获取对象的类型:调用 type(obj) 或 isinstance(obj, type_s) 函数。type() 函数返回参数的类型:
让我们看看它是如何工作的!
1print(type(int)) 2print(type(type(int))) 3print(type(type(type(int))))
整数(int())也是一种类。事实上,Python 中的每个对象都属于某个类。甚至连对象的类型本身也是一个类。
注意:
int类的实例具有int.numerator(等于该数本身)和int.denominator(始终为 1)属性,因为 Python 也支持有理数。
有理数由fractions模块提供。有理数 可表示为Fraction(p,q)。
isinstance() 函数接收一个对象和一个类型或类型列表,检查该对象是否属于列出的某个类型。它还有一个姐妹函数 issubclass(),用于检查一个类是否是另一个类或一组类的子类(记住,在 Python 中一切皆为 object 的后代):
1print(isinstance(1, int)) 2print(isinstance(1, float)) 3print(isinstance(1, object)) 4print(isinstance(1, numbers.Integral))
整数字面量(如 1)不仅是 int,还是 numbers.Integral。这只有在 int 是 numbers.Integral 的直接或间接子类时才可能成立。
注意:
int类是numbers.Integral的子类,但反过来不成立。
1print(issubclass(int, numbers.Integral)) 2print(issubclass(numbers.Integral, int))
type() 和 isinstance() 的主要区别在于:前者报告参数的最直接类,而后者检查参数是否属于指定类或其任何父类(直接或间接)。假设我们要实现 my_len() 函数:
1def my_len(x): 2 if type(x) in (str, list, tuple, map, dict): 3 return len(x) 4 if type(x) in (bool, int, float, complex): 5 return abs(x) 6 raise Exception('x 无长度')
我们创建一个 MyList,它是 list 的简单子类,并将此函数应用于 MyList 对象。从实际用途来看,MyList 就是一个列表:
1class MyList(list): 2 def __init__(self, x): 3 super().__init__(x) 4l = MyList([1, 2, 3]) 5print(len(l))
现在调用 my_len(l) 并观察输出:
1def my_len(x): 2 if type(x) in (str, list, tuple, map, dict): 3 return len(x) 4 if type(x) in (bool, int, float, complex): 5 return abs(x) 6 raise Exception('x 无长度') 7 8class MyList(list): 9 def __init__(self, x): 10 super().__init__(x) 11l = MyList([1, 2, 3]) 12print(my_len(l))
这是因为 MyList 是一种列表,但不是列表本身;它是子类。
更好的 my_len() 版本能够处理类层次结构:
1def my_len(x): 2 if isinstance(x, (str, list, tuple, map, dict)): 3 return len(x) 4 if isinstance(x, (bool, int, float, complex)): 5 return abs(x) 6 raise Exception('x 无长度') 7 8class MyList(list): 9 def __init__(self, x): 10 super().__init__(x) 11l = MyList([1, 2, 3]) 12 13print(my_len(l)) # 再次调用
当需要判断对象是否属于某个类或其任何父类时,应使用 isinstance(),因为后续操作可能依赖于父类可能具有的某些特性。当需要确切知道对象的最终类型时,应使用 type()。
避免调用 list 列表
Python 自带约 150 个内置函数。它们并无特殊之处,只是过去不属于任何模块而已。如今它们属于 builtins 模块,尽管技术上该模块是核心 Python 的一部分,且是虚拟的。
不幸的是,Python 的内置函数并未受到保护,容易被篡改。最常见的篡改方式是重新定义它们的标识符。比如,为什么不能把新列表命名为 list?
list = [2, 4, 6, 8, 10]
只要我们或他人在重用代码时不尝试使用类构造器创建另一个列表,这条语句就能正常工作:
1anotherList = list(1, 2, 3)
list 已不再是构造器。事实上,它甚至不再是一个函数,只是一个 list,正如我们更改代码前所期望的那样。
为了避免麻烦,不要重新定义 builtins 模块中的任何标识符。不确定某个标识符是否为内置函数或变量?导入该模块并检查:
1import builtins 2print('list' in dir(builtins)) 3print('mylist' in dir(builtins))
通常,给标识符加上前缀 “my” 会使它成为我们的专属名称,因为没有任何内置标识符以 “my” 开头:
1print([x for x in dir(builtins) if x.startswith('my')])
函数测验
Quiz
该代码的输出是什么?