PEP 225 – 逐元素/逐对象操作符
- 作者:
- Huaiyu Zhu <hzhu at users.sourceforge.net>, Gregory Lielens <gregory.lielens at fft.be>
- 状态:
- 已拒绝
- 类型:
- 标准跟踪
- 创建日期:
- 2000年9月19日
- Python 版本:
- 2.1
- 发布历史:
引言
本 PEP 描述了一项提议,旨在向 Python 中添加新的操作符,这些操作符对于区分逐元素操作和逐对象操作非常有用,并总结了新闻组 comp.lang.python 上关于此主题的讨论。请参阅末尾的“致谢和档案”部分。此处讨论的问题包括:
- 背景。
- 提议的操作符描述和实现问题。
- 对新操作符替代方案的分析。
- 对替代形式的分析。
- 兼容性问题
- 更广泛的扩展和其他相关想法的描述。
本 PEP 的很大一部分描述了未纳入提议扩展的想法。之所以提出这些想法,是因为该扩展本质上是语法糖,因此必须权衡其采用与各种可能的替代方案。尽管许多替代方案在某些方面可能更好,但目前的提案似乎总体上更具优势。
逐元素-逐对象操作的问题延伸到数值计算以外的更广泛领域。本文档还描述了如何将当前提案与更通用的未来扩展相结合。
背景
Python 提供了六个二元中缀数学操作符:+ - * / % **,下文统称为 op。它们可以为用户定义的类重载新语义。然而,对于由同质元素组成的对象,例如数值计算中的数组、向量和矩阵,存在两种本质上不同的语义风格。逐对象操作将这些对象视为多维空间中的点。逐元素操作将它们视为单个元素的集合。这两种风格的操作经常在相同的公式中混合使用,因此需要语法上的区分。
许多数值计算语言提供了两组数学操作符。例如,在 MatLab 中,普通的 op 用于逐对象操作,而 .op 用于逐元素操作。在 R 中,op 表示逐元素操作,而 %op% 表示逐对象操作。
在 Python 中,还有其他表示方法,其中一些已被现有的数值包使用,例如:
- 函数:mul(a,b)
- 方法:a.mul(b)
- 类型转换:a.E*b
在几个方面,这些都不如中缀操作符合适。更多细节将在稍后展示,但关键点是:
- 可读性:即使对于中等复杂的公式,中缀操作符也比替代方案清晰得多。
- 熟悉度:用户熟悉普通的数学操作符。
- 实现:新的中缀操作符不会过度扰乱 Python 语法。它们将极大地简化数值包的实现。
虽然可以将当前数学操作符分配给一种语义风格,但中缀操作符的数量不足以重载另一种风格。如果其中一种风格不包含普通数学操作符的符号,也无法在这两种风格之间保持视觉对称性。
提议的扩展
- 核心 Python 中新增了六个二元中缀操作符
~+~-~*~/~%~**。它们与现有操作符+-*/%**并行。 - 核心 Python 中新增了六个增广赋值操作符
~+=~-=~*=~/=~%=~**=。它们与 Python 2.0 中可用的操作符+=-=*=/=%=**=并行。 - 操作符
~op保留操作符op的语法属性,包括优先级。 - 操作符
~op在内置数字类型上保留操作符op的语义属性。 - 操作符
~op在非数字内置类型上引发语法错误。这是暂时的,直到商定合适的行为。 - 这些操作符可以在类中重载,其名称在普通数学操作符名称前加上 t(表示波浪号)。例如,
__tadd__和__rtadd__对~+的作用就像__add__和__radd__对+的作用一样。 - 与现有操作符一样,当左操作数不提供适当的方法时,会调用
__r*__()方法。
旨在将 op 或 ~op 中的一组用于逐元素操作,另一组用于逐对象操作,但未指定哪个版本的操作符代表逐元素或逐对象操作,将决定权留给应用程序。
拟议的实现是修补与分词器、解析器、语法和编译器相关的几个文件,以根据需要复制相应现有操作符的功能。所有新语义都将在重载它们的类中实现。
符号 ~ 在 Python 中已用作一元 按位非 操作符。目前它不允许用于二元操作符。新操作符完全向后兼容。
原型实现
Greg Lielens 将中缀 ~op 作为 Python 2.0b1 源代码的补丁实现 [1]。
为了允许 ~ 成为二元操作符的一部分,分词器将把 ~+ 视为一个标记。这意味着当前有效的表达式 ~+1 将被分词为 ~+ 1 而不是 ~ + 1。解析器随后会将 ~+ 视为 ~ + 的复合体。这种影响对应用程序是不可见的。
关于当前补丁的说明
- 它尚未包含
~op=操作符。 ~op在列表上的行为与op相同,而不是引发异常。
这些应在本提案的最终版本准备就绪时修复。
- 它保留
xor作为中缀操作符,其语义等同于def __xor__(a, b): if not b: return a elif not a: return b else: 0
这尽可能地保留了真值,否则尽可能保留左侧值。
这样做是为了将来可以将按位操作符视为逐元素逻辑操作符(见下文)。
新增操作符的替代方案
comp.lang.python 和 python-dev 邮件列表上的讨论探讨了许多替代方案。下面列出了一些主要的替代方案,以乘法操作符为例。
- 使用函数
mul(a,b)。优点
- 无需新操作符。
缺点
- 前缀形式对于复合公式来说很麻烦。
- 目标用户不熟悉。
- 对目标用户来说过于冗长。
- 无法使用自然的优先级规则。
- 使用方法调用
a.mul(b)。优点
- 无需新操作符。
缺点
- 对两个操作数不对称。
- 目标用户不熟悉。
- 对目标用户来说过于冗长。
- 无法使用自然的优先级规则。
- 使用 影子类。对于矩阵类,定义一个可通过方法
.E访问的影子数组类,以便对于矩阵 a 和 b,a.E*b将是一个矩阵对象,即elementwise_mul(a,b)。同样,为数组定义一个可通过方法
.M访问的影子矩阵类,以便对于数组 a 和 b,a.M*b将是一个数组,即matrixwise_mul(a,b)。优点
- 无需新操作符。
- 具有正确优先级规则的中缀操作符的优点。
- 应用程序中清晰的公式。
缺点
- 在当前 Python 中难以维护,因为普通数字不能有用户定义的类方法;即,如果 a 是纯数字,
a.E*b将失败。 - 难以实现,因为这会干扰现有方法调用,例如转置的
.T等。 - 对象创建和方法查找的运行时开销。
- 影子类不能替换真正的类,因为它不返回其自己的类型。因此,需要一个带有影子
E类的M类,以及一个带有影子M类的E类。 - 对数学家来说不自然。
- 实现矩阵式和逐元素式类,并易于转换为另一种类。因此,数组的矩阵式操作将类似于
a.M*b.M,矩阵的逐元素式操作将类似于a.E*b.E。对于错误检测,a.E*b.M将引发异常。优点
- 无需新操作符。
- 类似于具有正确优先级规则的中缀表示法。
缺点
- 由于纯数字缺乏用户方法,存在类似的困难。
- 对象创建和方法查找的运行时开销。
- 公式更混乱。
- 为了方便操作符而切换对象风格会变得持久。这会在应用程序代码中引入长期的上下文依赖,这将非常难以维护。
- 使用迷你解析器解析引用字符串中放置的任意扩展编写的公式。
优点
- 纯 Python,没有新操作符
缺点
- 实际的语法在引用字符串内,这本身并不能解决问题。
- 引入特殊语法的区域。
- 对迷你解析器的要求很高。
- 引入一个单一操作符,例如
@,用于矩阵乘法。优点
- 引入的操作符更少
缺点
- 对于
+-**等操作符的区别同样重要。它们在矩阵或数组导向包中的含义将被颠倒(见下文)。 - 新操作符占用一个特殊字符。
- 这与更一般的对象-元素问题配合不佳。
在这些替代方案中,第一种和第二种在当前应用程序中或多或少地使用,但被发现不足。第三种是应用程序中最喜欢的,但它会带来巨大的实现复杂性。第四种会使应用程序代码对上下文非常敏感且难以维护。这两种替代方案还由于当前的类型/类划分而存在显著的实现困难。第五种似乎制造了比解决的更多的问题。第六种不涵盖相同范围的应用程序。
中缀操作符的其他形式
讨论了两种主要形式和几种次要变体的新中缀操作符
- 括号形式
(op) [op] {op} <op> :op: ~op~ %op%
- 元字符形式
.op @op ~op
或者将元字符放在操作符之后。
- 这些主题的变化不太一致。这些被认为是不利的。为了完整起见,这里列出了一些
- 使用
@/和/@表示左除法和右除法 - 使用
[*]和(*)表示外积和内积 - 使用单一操作符
@进行乘法。
- 使用
- 使用
__call__模拟乘法a(b) or (a)(b)
选择表示形式的标准包括
- 与现有操作符没有语法歧义。
- 在实际公式中具有更高的可读性。这使得括号形式不受欢迎。参见下面的示例。
- 与现有数学操作符视觉相似。
- 语法简单,不阻碍未来可能的扩展。
根据这些标准,括号形式的总体胜者似乎是 {op}。元字符形式的明显胜者是 ~op。比较这些,似乎 ~op 是所有形式中最受欢迎的。
一些分析如下
.op形式是模糊的:1.+a将与1 .+a不同。- 括号类型操作符在单独使用时最受欢迎,但在公式中则不然,因为它们会干扰对括号的视觉解析,以确定优先级和函数参数。对于
(op)和[op]尤其如此,对于{op}和<op>则略微不那么严重。 <op>形式有可能与<>和=混淆。@op不受青睐,因为@视觉上较重(密集,更像一个字母):a@+b更容易被读作a@ + b而不是a @+ b。- 关于元字符的选择:大多数现有 ASCII 符号已被使用。仅有三个未使用的符号是
@$?。
新操作符的语义
有令人信服的论据支持将任一组操作符用作逐对象或逐元素操作符。其中一些如下:
op用于元素,~op用于对象- 与 Numeric 包的当前多数组接口一致。
- 与其他一些语言一致。
- 认为逐元素操作更自然。
- 认为逐元素操作使用更频繁
op用于对象,~op用于元素- 与 MatPy 包的当前线性代数接口一致。
- 与其他一些语言一致。
- 认为逐对象操作更自然。
- 认为逐对象操作使用更频繁。
- 与操作符在列表上的当前行为一致。
- 允许
~在未来的扩展中成为通用的逐元素元字符。
人们普遍认为
- 没有绝对的理由偏爱其中之一。
- 在大量的代码中,从一种表示形式转换到另一种表示形式很容易,所以另一种操作符风格总是少数。
- 即使操作符统一,也存在其他语义差异,有利于存在面向数组和面向矩阵的包。
- 无论做出何种决定,使用现有接口的代码在很长一段时间内都不应被破坏。
因此,如果这两种操作符集的语义风格不由核心语言规定,则损失不大,且保留了很大的灵活性。应用程序包负责做出最合适的选择。对于 NumPy 和 MatPy 而言,它们使用相反的语义,情况已经如此。添加新的操作符不会破坏这一点。另请参阅下面的示例中第 2 小节之后的观察。
提出了数值精度的问题,但如果语义留给应用程序,实际精度也应该在那里处理。
示例
以下是使用上述各种操作符或其他表示形式的实际公式示例。
- 矩阵求逆公式
- 使用
op表示对象,~op表示元素b = a.I - a.I * u / (c.I + v/a*u) * v / a b = a.I - a.I * u * (c.I + v*a.I*u).I * v * a.I
- 使用
op表示元素,~op表示对象b = a.I @- a.I @* u @/ (c.I @+ v@/a@*u) @* v @/ a b = a.I ~- a.I ~* u ~/ (c.I ~+ v~/a~*u) ~* v ~/ a b = a.I (-) a.I (*) u (/) (c.I (+) v(/)a(*)u) (*) v (/) a b = a.I [-] a.I [*] u [/] (c.I [+] v[/]a[*]u) [*] v [/] a b = a.I <-> a.I <*> u </> (c.I <+> v</>a<*>u) <*> v </> a b = a.I {-} a.I {*} u {/} (c.I {+} v{/}a{*}u) {*} v {/} a
观察:对于线性代数,使用
op表示对象更可取。观察:在复杂的公式中,
~op类型操作符比(op)类型更好看。- 使用命名操作符
b = a.I @sub a.I @mul u @div (c.I @add v @div a @mul u) @mul v @div a b = a.I ~sub a.I ~mul u ~div (c.I ~add v ~div a ~mul u) ~mul v ~div a
观察:命名操作符不适合数学公式。
- 使用
- 绘制三维图
- 使用
op表示对象,~op表示元素z = sin(x~**2 ~+ y~**2); plot(x,y,z)
- 使用 op 表示元素,~op 表示对象
z = sin(x**2 + y**2); plot(x,y,z)
观察:具有广播功能的逐元素操作比 MatLab 效率更高。
观察:拥有两个语义互换的
op和~op相关类是有用的。使用这些类,~op操作符只需出现在另一种风格占主导地位的代码块中,同时保持代码语义的一致性。 - 使用
- 使用
+和-并自动广播a = b - c; d = a.T*a
观察:如果 b 或 c 是行向量而另一个是列向量,这会悄无声息地产生难以追踪的错误。
杂项
- 需要
~+~-操作符。逐对象的+-很重要,因为它们根据线性代数提供重要的健全性检查。逐元素的+-很重要,因为它们允许在应用程序中非常高效的广播。 - 左除法(解)。对于矩阵,
a*x不一定等于x*a。因此,a*x==b的解(表示为x=solve(a,b))与x*a==b的解(表示为x=div(b,a))不同。有人讨论为解寻找一个新的符号。[背景:MatLab 使用b/a表示div(b,a),使用a\b表示solve(a,b)。]公认的是,Python 提供了一种无需新符号的更好解决方案:
inverse方法.I可以延迟执行,使得a.I*b和b*a.I等同于 Matlab 的a\b和b/a。实现非常简单,生成的应用程序代码也很简洁。 - 幂操作符。Python 将
a**b用作pow(a,b)存在两个明显的缺点:- 大多数数学家更熟悉
a^b用于此目的。 - 它导致增广赋值操作符
~**=变得很长。
然而,这个问题与这里的主要问题无关。
- 大多数数学家更熟悉
- 额外的乘法操作符。在(多)线性代数中使用了几种形式的乘法。大多数可以看作是线性代数意义上乘法的变体(例如 Kronecker 积)。但两种形式似乎更基本:外积和内积。然而,它们的规范包括索引,这些索引可以是
- 与操作符关联,或
- 与对象关联。
后者(爱因斯坦记法)在纸上广泛使用,也是更容易实现的一种。通过实现一个带索引的张量类,一种通用形式的乘法将涵盖外积和内积,并专门用于线性代数乘法。索引规则可以定义为类方法,例如
a = b.i(1,2,-1,-2) * c.i(4,-2,3,-1) # a_ijkl = b_ijmn c_lnkm
因此,一个逐对象乘法就足够了。
- 按位操作符。
- 拟议的新数学操作符使用符号 ~,它是 按位非 操作符。这不会造成兼容性问题,但会使实现稍微复杂化。
- 符号
^可能更适合用于pow而不是按位xor。但这取决于按位操作符的未来。它不直接影响拟议的数学操作符。 - 符号
|被建议用于矩阵求解。但是使用延迟的.I的新解决方案在几个方面更好。 - 目前的提案符合一个更大、更通用的扩展,该扩展将消除对特殊按位操作符的需求。(参见下面的元素化。)
- 替代定义中使用的特殊操作符名称,
def "+"(a, b) in place of def __add__(a, b)
这似乎需要更大的语法更改,并且仅在允许任意附加操作符时才有用。
对一般元素化(elementization)的影响
逐对象和逐元素操作之间的区别在其他上下文中也很有意义,即对象可以概念上被视为元素的集合。重要的是,当前提案不会排除未来可能的扩展。
一个通用的未来扩展是使用 ~ 作为元操作符来 元素化 给定的操作符。以下列出了一些示例
- 按位操作符。目前 Python 为按位操作分配了六个操作符:与 (
&)、或 (|)、异或 (^)、补码 (~)、左移 (<<) 和右移 (>>),它们有各自的优先级。其中,
&|^~操作符可以被视为将格操作符应用于被视为位串的整数的逐元素版本。5 and 6 # 6 5 or 6 # 5 5 ~and 6 # 4 5 ~or 6 # 7
这些可以被视为通用的逐元素格操作符,不限于整数中的位。
为了拥有
xor~xor的命名操作符,需要将xor设为保留字。 - 列表算术。
[1, 2] + [3, 4] # [1, 2, 3, 4] [1, 2] ~+ [3, 4] # [4, 6] ['a', 'b'] * 2 # ['a', 'b', 'a', 'b'] 'ab' * 2 # 'abab' ['a', 'b'] ~* 2 # ['aa', 'bb'] [1, 2] ~* 2 # [2, 4]
这与笛卡尔积也是一致的
[1,2]*[3,4] # [(1,3),(1,4),(2,3),(2,4)]
- 列表推导式。
a = [1, 2]; b = [3, 4] ~f(a,b) # [f(x,y) for x, y in zip(a,b)] ~f(a*b) # [f(x,y) for x in a for y in b] a ~+ b # [x + y for x, y in zip(a,b)]
- 元组生成(Python 2.0 中的 zip 函数)
[1, 2, 3], [4, 5, 6] # ([1,2, 3], [4, 5, 6]) [1, 2, 3]~,[4, 5, 6] # [(1,4), (2, 5), (3,6)]
- 使用
~作为通用逐元素元字符来替换 map~f(a, b) # map(f, a, b) ~~f(a, b) # map(lambda *x:map(f, *x), a, b)
更一般地,
def ~f(*x): return map(f, *x) def ~~f(*x): return map(~f, *x) ...
- 逐元素格式化操作符(带广播)
a = [1,2,3,4,5] print ["%5d "] ~% a a = [[1,2],[3,4]] print ["%5d "] ~~% a
- 富比较
[1, 2, 3] ~< [3, 2, 1] # [1, 0, 0] [1, 2, 3] ~== [3, 2, 1] # [0, 1, 0]
- 富索引
[a, b, c, d] ~[2, 3, 1] # [c, d, b]
- 元组展平
a = (1,2); b = (3,4) f(~a, ~b) # f(1,2,3,4)
- 复制操作符
a ~= b # a = b.copy()
可以有特定级别的深拷贝a ~~= b # a = b.copy(2)
备注
- 可能还有许多其他类似的情况。这种通用方法似乎非常适合大多数情况,可以取代针对每种情况的几个独立扩展(并行和交叉迭代、列表推导式、富比较等)。
- “逐元素”的语义取决于应用程序。例如,从列表套列表的角度来看,矩阵的一个元素是两层向下。这需要比当前提案更根本的改变。无论如何,当前提案不会对这种性质的未来可能性产生负面影响。
请注意,本节描述了一种与当前提案一致的未来扩展类型,但可能会带来额外的兼容性或其他问题。它们与当前提案无关。
对命名操作符的影响
讨论普遍明确指出,中缀操作符在 Python 中是一种稀缺资源,不仅在数值计算中如此,在其他领域也一样。提出了几个提案和想法,旨在以类似于命名函数的方式引入中缀操作符。我们在此表明,当前的扩展不会对未来在这方面的扩展产生负面影响。
- 命名中缀操作符。
选择一个元字符,例如
@,以便对于任何标识符opname,组合@opname将是一个二元中缀操作符,并且a @opname b == opname(a,b)
提到的其他表示形式包括
.name ~name~ :name: (.name) %name%
和类似的变体。纯粹基于括号的操作符不能以这种方式使用。
这需要更改解析器以识别
@opname,并将其解析为与函数调用相同的结构。所有这些操作符的优先级都必须固定在一个级别,因此实现将与保持现有数学操作符优先级的附加数学操作符不同。当前拟议的扩展不以任何方式限制此类形式的未来扩展。
- 更通用的符号操作符。
未来扩展的一种额外形式是使用元字符和操作符符号(不能在除操作符之外的语法结构中使用的符号)。假设
@是元字符。那么a + b, a @+ b, a @@+ b, a @+- b
都将是具有优先级层次结构的操作符,由
def "+"(a, b) def "@+"(a, b) def "@@+"(a, b) def "@+-"(a, b)
与命名操作符相比,一个优点是基于元字符或普通操作符符号的优先级具有更大的灵活性。这也允许操作符组合。缺点是它们更像“行噪声”。无论如何,当前提案不影响其未来的可能性。
当 Unicode 普遍可用时,这些类型的未来扩展可能不再是必要的。
请注意,本节讨论了拟议扩展与未来可能扩展的兼容性。此处未特别考虑这些其他扩展本身的适宜性或兼容性。
致谢和档案
讨论主要发生在2000年7月至8月的新闻组 comp.lang.python 和邮件列表 python-dev 上。总共有数百篇帖子,大多数可以从这两个页面(并搜索关键词“operator”)检索:
贡献者的名字太多,无法在此提及,只需说此处讨论的大部分想法并非我们自己的。
一些关键帖子(从我们的角度来看)可能有助于浏览讨论,包括
https://pythonlang.cn/pipermail/python-list/2000-July/108893.html https://pythonlang.cn/pipermail/python-list/2000-July/108777.html https://pythonlang.cn/pipermail/python-list/2000-July/108848.html https://pythonlang.cn/pipermail/python-list/2000-July/109237.html https://pythonlang.cn/pipermail/python-list/2000-July/109250.html https://pythonlang.cn/pipermail/python-list/2000-July/109310.html https://pythonlang.cn/pipermail/python-list/2000-July/109448.html https://pythonlang.cn/pipermail/python-list/2000-July/109491.html https://pythonlang.cn/pipermail/python-list/2000-July/109537.html https://pythonlang.cn/pipermail/python-list/2000-July/109607.html https://pythonlang.cn/pipermail/python-list/2000-July/109709.html https://pythonlang.cn/pipermail/python-list/2000-July/109804.html https://pythonlang.cn/pipermail/python-list/2000-July/109857.html https://pythonlang.cn/pipermail/python-list/2000-July/110061.html https://pythonlang.cn/pipermail/python-list/2000-July/110208.html https://pythonlang.cn/pipermail/python-list/2000-August/111427.html https://pythonlang.cn/pipermail/python-list/2000-August/111558.html https://pythonlang.cn/pipermail/python-list/2000-August/112551.html https://pythonlang.cn/pipermail/python-list/2000-August/112606.html https://pythonlang.cn/pipermail/python-list/2000-August/112758.htmlhttps://pythonlang.cn/pipermail/python-dev/2000-July/013243.html https://pythonlang.cn/pipermail/python-dev/2000-July/013364.html https://pythonlang.cn/pipermail/python-dev/2000-August/014940.html
这些是本 PEP 的早期草稿
Greg Wilson 还有另一个 PEP(官方名称为 PEP 211),标题为“Adding New Linear Algebra Operators to Python”。
它的第一个(也是当前)版本位于
其他参考文献
来源: https://github.com/python/peps/blob/main/peps/pep-0225.rst
最后修改: 2024-04-14 20:08:31 GMT