Following system colour scheme - Python 增强提案 Selected dark colour scheme - Python 增强提案 Selected light colour scheme - Python 增强提案

Python 增强提案

PEP 225 – 元素级/对象级运算符

作者:
Huaiyu Zhu <hzhu at users.sourceforge.net>,Gregory Lielens <gregory.lielens at fft.be>
状态:
已拒绝
类型:
标准跟踪
创建:
2000年9月19日
Python 版本:
2.1
历史记录:


目录

警告

此 PEP 已被拒绝。

×

后来在PEP 465 中采用的方法最终被接受,以代替此 PEP。该被拒绝的想法更详细地解释了基本原理。

引言

此 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 邮件列表上的讨论探讨了许多替代方案。这里列出了一些主要的替代方案,以乘法运算符为例。

  1. 使用函数 mul(a,b)

    优点

    • 不需要新运算符。

    缺点

    • 前缀形式对于复合公式来说很笨拙。
    • 目标用户不熟悉。
    • 对于目标用户来说过于冗长。
    • 无法使用自然优先级规则。
  2. 使用方法调用 a.mul(b)

    优点

    • 不需要新运算符。

    缺点

    • 对两个操作数来说都是不对称的。
    • 目标用户不熟悉。
    • 对于目标用户来说过于冗长。
    • 无法使用自然优先级规则。
  3. 使用影子类。对于矩阵类,定义一个可以通过方法 .E 访问的影子数组类,以便对于矩阵 aba.E*b 将是一个矩阵对象,即 elementwise_mul(a,b)

    同样,为数组定义一个影子矩阵类,可以通过方法 .M 访问,以便对于数组 aba.M*b 将是一个数组,即 matrixwise_mul(a,b)

    优点

    • 不需要新运算符。
    • 具有正确优先级规则的中缀运算符的优势。
    • 应用程序中的公式简洁。

    缺点

    • 在当前的 Python 中难以维护,因为普通数字不能具有用户定义的类方法;即如果 a 是纯数字,则 a.E*b 将失败。

    • 难以实现,因为这会干扰现有的方法调用,例如转置的.T等。
    • 对象创建和方法查找的运行时开销。
    • 影子类不能替换真正的类,因为它不返回自己的类型。因此需要一个具有影子E类的M类,以及一个具有影子M类的E类。
    • 对数学家来说不自然。
  4. 实现矩阵式和元素式类,并方便地转换为另一个类。因此,数组的矩阵式运算类似于a.M*b.M,矩阵的元素式运算类似于a.E*b.E。为了进行错误检测,a.E*b.M会引发异常。

    优点

    • 不需要新运算符。
    • 类似于具有正确优先级规则的中缀表示法。

    缺点

    • 由于纯数字缺乏用户方法,因此存在类似的困难。
    • 对象创建和方法查找的运行时开销。
    • 公式更加杂乱。
    • 为了方便运算符,对象的类型切换变得持久化。这在应用程序代码中引入了长距离上下文依赖关系,这将非常难以维护。
  5. 使用小型解析器来解析用任意扩展名编写的、放在引号字符串中的公式。

    优点

    • 纯 Python,没有新的运算符

    缺点

    • 实际语法在引号字符串内,这本身并没有解决问题。
    • 引入特殊语法区域。
    • 对小型解析器要求较高。
  6. 引入一个单独的运算符,例如@,用于矩阵乘法。

    优点

    • 引入较少的运算符

    缺点

    • + - **这样的运算符的区分同样重要。它们在面向矩阵或数组的包中的含义将被反转(见下文)。
    • 新的运算符占用一个特殊字符。
    • 这对于更一般的对象-元素问题效果不佳。

在这些备选方案中,第一和第二种在某种程度上被当前的应用程序使用,但被认为不足。第三种是应用程序最喜欢的,但它会带来巨大的实现复杂性。第四种会使应用程序代码非常依赖上下文,难以维护。这两种备选方案由于当前的类型/类划分也存在重大的实现困难。第五种似乎会制造比解决的问题更多的问题。第六种不涵盖相同的应用程序范围。

中缀运算符的替代形式

讨论了两种主要形式和几种次要变体的新的中缀运算符

  • 括号形式
    (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符号已经被使用。仅有的三个未使用的符号是@ $ ?

新运算符的语义

有一些令人信服的论点支持使用任一组运算符作为对象式或元素式。其中一些列在下面

  1. op用于元素,~op用于对象
    • 与Numeric包的当前多维数组接口一致。
    • 与其他一些语言一致。
    • 认为元素式运算更自然。
    • 认为元素式运算使用频率更高
  2. op用于对象,~op用于元素
    • 与MatPy包的当前线性代数接口一致。
    • 与其他一些语言一致。
    • 认为对象式运算更自然。
    • 认为对象式运算使用频率更高。
    • 与列表上运算符的当前行为一致。
    • 允许~在未来的扩展中成为通用的元素式元字符。

人们普遍认为

  • 没有绝对的理由偏袒一方或另一方。
  • 很容易在一大块代码中从一种表示形式转换为另一种表示形式,因此其他类型的运算符始终是少数。
  • 即使运算符统一,也存在其他语义差异有利于数组式和矩阵式包的存在。
  • 无论做出什么决定,使用现有接口的代码都应该在很长一段时间内不会被破坏。

因此,如果这两组运算符的语义类型不受核心语言的支配,那么损失不大,并且保留了很大的灵活性。应用程序包负责做出最合适的选择。对于使用相反语义的NumPy和MatPy来说,情况已经如此。添加新的运算符不会破坏这一点。另请参阅下面示例中第2小节后的观察结果。

提出了数值精度的议题,但如果语义留给应用程序,则实际精度也应该留给应用程序。

示例

以下是使用上面描述的各种运算符或其他表示形式将出现的实际公式示例。

  1. 矩阵求逆公式
    • 使用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
      

    观察:命名运算符不适合数学公式。

  2. 绘制3D图形
    • 使用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运算符只需要出现在其他类型占主导地位的代码块中,同时保持代码的语义一致。

  3. 使用+-以及自动广播
    a = b - c;  d = a.T*a
    

    观察:如果bc之一是行向量而另一个是列向量,这将默默地产生难以追踪的错误。

其他问题

  • 需要~+ ~-运算符。对象式+ -很重要,因为它们根据线性代数提供了重要的完整性检查。元素式+ -很重要,因为它们允许在应用程序中非常高效的广播。
  • 左除(求解)。对于矩阵,a*x不一定等于x*a。因此,a*x==b的解,记为x=solve(a,b),与x*a==b的解不同,记为x=div(b,a)。关于为solve找到一个新符号的讨论。[背景:MatLab使用b/a表示div(b,a),使用a\b表示solve(a,b)。]

    人们认识到Python提供了一个更好的解决方案,而无需引入新的符号:可以使inverse方法.I延迟,以便a.I*bb*a.I分别等效于Matlab的a\bb/a。实现非常简单,生成的应用程序代码简洁。

  • 幂运算符。Python 使用 a**b 作为 pow(a,b) 有两个明显的缺点。
    • 大多数数学家更熟悉使用 a^b 来表示幂运算。
    • 它会导致较长的增强赋值运算符 ~**=

    但是,这个问题与这里的主要问题不同。

  • 额外的乘法运算符。(多)线性代数中使用了多种形式的乘法。大多数可以看作是线性代数意义上乘法的变体(例如克罗内克积)。但两种形式似乎更基本:外积和内积。然而,它们的规范包括索引,索引可以是
    • 与运算符相关联,或者
    • 与对象相关联。

    后者(爱因斯坦求和约定)在纸面上被广泛使用,并且更容易实现。通过实现一个带有索引的张量类,一种通用的乘法形式将涵盖外积和内积,并专门用于线性代数乘法。索引规则可以定义为类方法,例如

    a = b.i(1,2,-1,-2) * c.i(4,-2,3,-1)   # a_ijkl = b_ijmn c_lnkm
    

    因此,一个面向对象的乘法就足够了。

  • 按位运算符。
    • 提议的新数学运算符使用符号 ~,它是按位取反运算符。这不会造成兼容性问题,但会稍微复杂化实现。
    • 符号 ^ 可能比按位 xor 更适合用于 pow。但这取决于按位运算符的未来发展。它不会立即影响提议的数学运算符。
    • 符号 | 曾被建议用于矩阵求解。但使用延迟 .I 的新解决方案在几个方面都更好。
    • 当前的提案适合于一个更大、更通用的扩展,该扩展将消除对特殊按位运算符的需求。(参见下面的元素化)。
  • 定义中使用的特殊运算符名称的替代方案,
    def "+"(a, b)      in place of       def __add__(a, b)
    

    这似乎需要更大的语法更改,并且仅在允许任意其他运算符时才有用。

对一般元素化的影响

对象级操作和元素级操作之间的区别在其他上下文中也很有意义,在这些上下文中,对象可以在概念上被视为元素的集合。重要的是,当前提案不排除可能的未来扩展。

一个通用的未来扩展是使用 ~ 作为元运算符来元素化给定的运算符。这里列出了几个示例

  1. 按位运算符。目前 Python 将六个运算符分配给按位运算:与 (&)、或 (|)、异或 (^)、补码 (~)、左移 (<<) 和右移 (>>),它们有自己的优先级。

    其中,& | ^ ~ 运算符可以被视为应用于被视为位字符串的整数的格运算符的元素级版本。

    5 and 6                # 6
    5 or 6                 # 5
    
    5 ~and 6               # 4
    5 ~or 6                # 7
    

    这些可以被视为通用的元素级格运算符,而不限于整数中的位。

    为了为 xor ~xor 提供命名运算符,必须将 xor 设为保留字。

  2. 列表算术。
    [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)]
    
  3. 列表推导式。
    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)]
    
  4. 元组生成(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)]
    
  5. 使用 ~ 作为通用元素级元字符来替换 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)
    ...
    
  6. 元素级格式运算符(带广播)
    a = [1,2,3,4,5]
    print ["%5d "] ~% a
    a = [[1,2],[3,4]]
    print ["%5d "] ~~% a
    
  7. 丰富比较
    [1, 2, 3]  ~< [3, 2, 1]  # [1, 0, 0]
    [1, 2, 3] ~== [3, 2, 1]  # [0, 1, 0]
    
  8. 丰富索引
    [a, b, c, d] ~[2, 3, 1]  # [c, d, b]
    
  9. 元组展平
    a = (1,2);  b = (3,4)
    f(~a, ~b)                # f(1,2,3,4)
    
  10. 复制运算符
    a ~= b                   # a = b.copy()
    
可以有特定的深复制级别
a ~~= b                  # a = b.copy(2)

备注

  1. 可能还有许多其他类似的情况。这种通用方法似乎非常适合其中大多数情况,可以替代为每个情况单独扩展(并行和交叉迭代、列表推导式、丰富比较等)。
  2. 元素级的语义取决于应用程序。例如,从列表的列表的角度来看,矩阵的元素位于两级之下。这需要比当前提案更根本的改变。无论如何,当前提案不会对这种性质的未来可能性产生负面影响。

请注意,本节描述了一种与当前提案一致的未来扩展类型,但可能会带来额外的兼容性或其他问题。它们与当前提案无关。

对命名运算符的影响

讨论结果普遍表明,中缀运算符在 Python 中是一种稀缺资源,不仅在数值计算中,而且在其他领域也是如此。提出了几个提案和想法,这些提案和想法将允许以类似于命名函数的方式引入中缀运算符。我们在这里表明,当前的扩展不会对这方面的未来扩展产生负面影响。

  1. 命名中缀运算符。

    选择一个元字符,例如 @,以便对于任何标识符 opname,组合 @opname 将是一个二元中缀运算符,并且

    a @opname b == opname(a,b)
    

    提到的其他表示形式包括

    .name ~name~ :name: (.name) %name%
    

    以及类似的变体。纯基于括号的运算符不能以这种方式使用。

    这需要更改解析器以识别 @opname,并将其解析成与函数调用相同的结构。所有这些运算符的优先级都必须固定在一个级别,因此实现将不同于保留现有数学运算符优先级的其他数学运算符。

    当前提出的扩展不会以任何方式限制此类形式的未来扩展。

  2. 更通用的符号运算符。

    未来扩展的一种附加形式是使用元字符和运算符符号(不能在除运算符之外的语法结构中使用的符号)。假设 @ 是元字符。那么

    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.html

https://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),标题为“向 Python 添加新的线性代数运算符”。

其第一个(也是当前的)版本位于

其他参考


来源:https://github.com/python/peps/blob/main/peps/pep-0225.rst

上次修改:2024-04-14 20:08:31 GMT