PEP 335 – 可重载的布尔运算符
- 作者:
- Gregory Ewing <greg.ewing at canterbury.ac.nz>
- 状态:
- 已拒绝
- 类型:
- 标准跟踪
- 创建:
- 2004-08-29
- Python 版本:
- 3.3
- 历史记录:
- 2004-09-05, 2011-09-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