PEP 335 – 可重载的布尔运算符
- 作者:
- Gregory Ewing <greg.ewing at canterbury.ac.nz>
- 状态:
- 已拒绝
- 类型:
- 标准跟踪
- 创建日期:
- 2004年8月29日
- Python 版本:
- 3.3
- 发布历史:
- 2004年9月5日,2011年9月30日,2011年10月25日
拒绝通知
此PEP已被拒绝。详情请参阅 https://mail.python.org/pipermail/python-dev/2012-March/117510.html
摘要
此PEP提议扩展,允许对象为布尔运算符“and”、“or”和“not”定义自己的含义,并提出了一种高效的实现策略。此实现的原型可供下载。
背景
Python目前没有提供与“and”、“or”和“not”布尔运算符对应的任何“__xxx__”特殊方法。对于“and”和“or”来说,最可能的原因是这些运算符具有短路语义,即如果结果可以从第一个操作数确定,则不评估第二个操作数。因此,为这些运算符提供特殊方法的常用技术将不起作用。
然而,对于“not”来说,没有这样的困难,为该运算符提供特殊方法将是直接的。因此,本提案的其余部分将主要集中于提供一种重载“and”和“or”的方法。
动机
在许多应用程序中,为Python运算符提供自定义含义是很自然的,在其中一些应用程序中,将布尔运算符排除在可自定义的运算符之外可能会带来不便。示例包括:
- NumPy,其中几乎所有运算符都在数组上定义,以便在相应元素之间执行适当的操作,并返回结果数组。为了保持一致性,人们会期望两个数组之间的布尔运算返回一个布尔数组,但这目前是不可能的。
此类扩展已有先例:比较运算符最初仅限于返回布尔结果,后来添加了富比较,以便 NumPy 数组的比较可以返回布尔数组。
- 符号代数系统,其中 Python 表达式在某个环境中进行评估,从而构建一个与表达式结构相对应的对象树。
- 关系数据库接口,其中 Python 表达式用于构建 SQL 查询。
通常建议的变通方法是使用按位运算符“&”、“|”和“~”代替“and”、“or”和“not”,但这有一些缺点:
- 这些运算符与其他运算符的优先级不同,并且可能已被用于其他目的(如示例 1)。
- 强迫用户使用非最明显语法来表达其意图,在美学上令人不悦。在示例 3 的情况下,考虑到布尔运算是 SQL 查询的常见操作,这将尤为突出。
- 按位运算符无法解决诸如“a < b < c”之类的链式比较问题,这些比较涉及隐式的“and”操作。此类表达式目前无法在 NumPy 数组等数据类型上使用,因为比较的结果不能被视为具有正常的布尔语义;它们必须扩展为类似 (a < b) & (b < c) 的形式,这会大大降低清晰度。
基本原理
成功解决允许自定义布尔运算符的问题所需的条件是:
- 在默认情况下(没有自定义时),必须保留现有的短路语义。
- 在默认情况下,速度不能有任何明显的损失。
- 理想情况下,自定义机制应允许对象自行选择提供短路或非短路语义。
一种显而易见的、以前曾提出过的策略是,将第一个参数和用于评估第二个参数的函数传递给特殊方法。这将满足要求 1 和 3,但不满足要求 2,因为它会在每次布尔操作时产生构造函数对象和可能的 Python 函数调用的开销。因此,此处将不再进一步考虑。
以下部分提出了一种满足所有三个要求的策略。该策略的原型实现可供下载。
规范
特殊方法
在 Python 层面,对象可以定义以下特殊方法。
| 一元 | 二元,阶段 1 | 二元,阶段 2 |
|---|---|---|
|
|
|
__not__ 方法(如果已定义)实现“not”运算符。如果未定义,或者返回 NotImplemented,则使用现有语义。
为了允许短路,对“and”和“or”运算符的处理分为两个阶段。阶段 1 在评估第一个操作数之后但在第二个操作数之前发生。如果第一个操作数定义了相关的阶段 1 方法,则以第一个操作数作为参数调用它。如果该方法无需第二个操作数即可确定结果,则它返回结果,并跳过进一步的处理。
如果阶段 1 方法确定需要第二个操作数,则返回特殊值 NeedOtherOperand。这将触发第二个操作数的评估以及相关阶段 2 方法的调用。在阶段 2 期间,__and2__/__rand2__ 和 __or2__/__ror2__ 方法对像其他二元运算符一样工作。
如果在任何阶段未找到相关特殊方法或返回 NotImplemented,处理将回退到现有语义。
作为特例,如果第一个操作数定义了阶段 2 方法但没有相应的阶段 1 方法,则始终评估第二个操作数并调用阶段 2 方法。这允许不希望短路语义的对象简单地实现阶段 2 方法并忽略阶段 1。
字节码
该补丁增加了四个新的字节码:LOGICAL_AND_1、LOGICAL_AND_2、LOGICAL_OR_1 和 LOGICAL_OR_2。作为其用法的示例,“and”表达式生成的字节码如下所示:
.
.
.
evaluate first operand
LOGICAL_AND_1 L
evaluate second operand
LOGICAL_AND_2
L: .
.
.
LOGICAL_AND_1 字节码执行阶段 1 处理。如果它确定需要第二个操作数,它会将第一个操作数留在栈上并继续执行以下代码。否则,它会弹出第一个操作数,推入结果并分支到 L。
LOGICAL_AND_2 字节码执行阶段 2 处理,弹出两个操作数并推入结果。
类型槽
在 C 语言层面,新的特殊方法在类型对象中体现为五个新的槽位。在补丁中,它们被添加到 tp_as_number 子结构中,因为这允许利用一些处理一元和二元运算符的现有代码。它们的存在由一个新的类型标志 Py_TPFLAGS_HAVE_BOOLEAN_OVERLOAD 表示。
新的类型槽位是
unaryfunc nb_logical_not;
unaryfunc nb_logical_and_1;
unaryfunc nb_logical_or_1;
binaryfunc nb_logical_and_2;
binaryfunc nb_logical_or_2;
Python/C API 函数
还有五个对应新操作的新的 Python/C API 函数:
PyObject *PyObject_LogicalNot(PyObject *);
PyObject *PyObject_LogicalAnd1(PyObject *);
PyObject *PyObject_LogicalOr1(PyObject *);
PyObject *PyObject_LogicalAnd2(PyObject *, PyObject *);
PyObject *PyObject_LogicalOr2(PyObject *, PyObject *);
替代方案和优化
本节讨论了该提案的一些可能变体,以及如何优化布尔表达式生成的字节码序列。
精简的特殊方法集
为了完整起见,此提案的完整版本包含了一种机制,允许类型定义其自己的自定义短路行为。然而,完整的机制对于此处提出的主要用例并非必需,并且可以定义一个仅包含阶段 2 方法的简化版本。这样就只有 5 个新的特殊方法(__and2__、__rand2__、__or2__、__ror2__、__not__),以及 3 个相关的类型槽和 3 个 API 函数。
如果需要,此简化版本可以在以后扩展为完整版本。
附加字节码
如本文所定义,针对基于布尔表达式结果进行分支的代码的字节码序列将比目前略长。例如,在 Python 2.7 中,
if a and b:
statement1
else:
statement2
生成
LOAD_GLOBAL a
POP_JUMP_IF_FALSE false_branch
LOAD_GLOBAL b
POP_JUMP_IF_FALSE false_branch
<code for statement1>
JUMP_FORWARD end_branch
false_branch:
<code for statement2>
end_branch:
根据目前描述的此提案,它将变成类似以下内容:
LOAD_GLOBAL a
LOGICAL_AND_1 test
LOAD_GLOBAL b
LOGICAL_AND_2
test:
POP_JUMP_IF_FALSE false_branch
<code for statement1>
JUMP_FORWARD end_branch
false_branch:
<code for statement2>
end_branch:
这涉及在短路情况下执行一个额外的字节码,在非短路情况下执行两个额外的字节码。
然而,通过引入结合逻辑运算与结果测试和分支的额外字节码,它可以减少到与原始字节码相同的数量。
LOAD_GLOBAL a
AND1_JUMP true_branch, false_branch
LOAD_GLOBAL b
AND2_JUMP_IF_FALSE false_branch
true_branch:
<code for statement1>
JUMP_FORWARD end_branch
false_branch:
<code for statement2>
end_branch:
在这里,AND1_JUMP 执行上述阶段 1 处理,然后检查结果。如果存在结果,它会从栈中弹出,测试其真值并分支到两个位置之一。
否则,第一个操作数留在栈上,执行继续到下一个字节码。AND2_JUMP_IF_FALSE 字节码执行阶段 2 处理,弹出结果并在其测试为假时分支。
对于“or”运算符,将有相应的 OR1_JUMP 和 OR2_JUMP_IF_TRUE 字节码。
如果使用没有阶段 1 方法的简化版本,则只有在第一个操作数对于“and”为假,对于“or”为真时,才能发生早期退出。因此,双目标 AND1_JUMP 和 OR1_JUMP 字节码可以替换为 AND1_JUMP_IF_FALSE 和 OR1_JUMP_IF_TRUE,这些都是只有单个目标的普通分支指令。
“not”的优化
Python 的最新版本实现了一个简单的优化,即通过反转分支的方向来实现对否定布尔表达式的分支,从而节省了一个 UNARY_NOT 操作码。
严格来看,这种优化不应再执行,因为“not”运算符可能会被重载以产生与通常情况截然不同的结果。然而,在典型的用例中,不设想涉及自定义布尔运算的表达式会用于分支——结果更有可能以其他方式使用。
因此,指定编译器被允许使用布尔代数定律来简化直接出现在布尔上下文中的任何表达式,可能不会造成太大危害。如果这不方便,结果总是可以先分配给一个临时名称。
这将允许保留现有的“not”优化,并允许将来对其进行扩展,例如使用德摩根定律将其深入扩展到表达式中。
使用示例
示例 1: NumPy 数组
#-----------------------------------------------------------------
#
# This example creates a subclass of numpy array to which
# 'and', 'or' and 'not' can be applied, producing an array
# of booleans.
#
#-----------------------------------------------------------------
from numpy import array, ndarray
class BArray(ndarray):
def __str__(self):
return "barray(%s)" % ndarray.__str__(self)
def __and2__(self, other):
return (self & other)
def __or2__(self, other):
return (self & other)
def __not__(self):
return (self == 0)
def barray(*args, **kwds):
return array(*args, **kwds).view(type = BArray)
a0 = barray([0, 1, 2, 4])
a1 = barray([1, 2, 3, 4])
a2 = barray([5, 6, 3, 4])
a3 = barray([5, 1, 2, 4])
print "a0:", a0
print "a1:", a1
print "a2:", a2
print "a3:", a3
print "not a0:", not a0
print "a0 == a1 and a2 == a3:", a0 == a1 and a2 == a3
print "a0 == a1 or a2 == a3:", a0 == a1 or a2 == a3
示例 1 输出
a0: barray([0 1 2 4])
a1: barray([1 2 3 4])
a2: barray([5 6 3 4])
a3: barray([5 1 2 4])
not a0: barray([ True False False False])
a0 == a1 and a2 == a3: barray([False False False True])
a0 == a1 or a2 == a3: barray([False False False True])
示例 2: 数据库查询
#-----------------------------------------------------------------
#
# This example demonstrates the creation of a DSL for database
# queries allowing 'and' and 'or' operators to be used to
# formulate the query.
#
#-----------------------------------------------------------------
class SQLNode(object):
def __and2__(self, other):
return SQLBinop("and", self, other)
def __rand2__(self, other):
return SQLBinop("and", other, self)
def __eq__(self, other):
return SQLBinop("=", self, other)
class Table(SQLNode):
def __init__(self, name):
self.__tablename__ = name
def __getattr__(self, name):
return SQLAttr(self, name)
def __sql__(self):
return self.__tablename__
class SQLBinop(SQLNode):
def __init__(self, op, opnd1, opnd2):
self.op = op.upper()
self.opnd1 = opnd1
self.opnd2 = opnd2
def __sql__(self):
return "(%s %s %s)" % (sql(self.opnd1), self.op, sql(self.opnd2))
class SQLAttr(SQLNode):
def __init__(self, table, name):
self.table = table
self.name = name
def __sql__(self):
return "%s.%s" % (sql(self.table), self.name)
class SQLSelect(SQLNode):
def __init__(self, targets):
self.targets = targets
self.where_clause = None
def where(self, expr):
self.where_clause = expr
return self
def __sql__(self):
result = "SELECT %s" % ", ".join([sql(target) for target in self.targets])
if self.where_clause:
result = "%s WHERE %s" % (result, sql(self.where_clause))
return result
def sql(expr):
if isinstance(expr, SQLNode):
return expr.__sql__()
elif isinstance(expr, str):
return "'%s'" % expr.replace("'", "''")
else:
return str(expr)
def select(*targets):
return SQLSelect(targets)
#-----------------------------------------------------------------
dishes = Table("dishes")
customers = Table("customers")
orders = Table("orders")
query = select(customers.name, dishes.price, orders.amount).where(
customers.cust_id == orders.cust_id and orders.dish_id == dishes.dish_id
and dishes.name == "Spam, Eggs, Sausages and Spam")
print repr(query)
print sql(query)
示例 2 输出
<__main__.SQLSelect object at 0x1cc830>
SELECT customers.name, dishes.price, orders.amount WHERE
(((customers.cust_id = orders.cust_id) AND (orders.dish_id =
dishes.dish_id)) AND (dishes.name = 'Spam, Eggs, Sausages and Spam'))
版权
本文档已置于公共领域。
来源: https://github.com/python/peps/blob/main/peps/pep-0335.rst
最后修改: 2025-02-01 08:59:27 GMT