Python 中的运算符 += return 是什么意思

What does the operator += return in Python

原问题

当我试图在Whosebug上回答另一个人关于Python中=+=之间的区别的问题时,我遇到了以下问题:

class Foo:
    def __init__(self, value, name="default"):
        self.value = value
        self.name = name
        
    def __add__(self, that):
        return Foo(self.value + that.value)
    
    def __iadd__(self, that):
        self.value = self.value + that.value
        return self
    
    def __str__(self):
        return "name: {}, value: {:d}".format(self.name, self.value)
    
a = Foo(1, 'alice')
b = Foo(2, 'bob')
print(a+=b)

上次 print 调用不成功,给了我这个:

File "<ipython-input-8-0faa82ba9e4a>", line 3
    print(a+=b)
            ^
SyntaxError: invalid syntax

我不知道为什么这不起作用。也许它与关键字参数传递机制有关?我只是找不到关于这个主题的任何资源,因为重载的 __iadd__ 方法已经 returns 一个 Foo 对象。

************** 更新 ******************

如果我像这样更改 __iadd__ 方法(只需删除 return 语句):

...
    def __iadd__(self, that):
        print("__iadd__ was called")
        self.value = self.value + that.value

a = Foo(1, 'alice')
b = Foo(2, 'bob')
a += b
print(a)  # Outputs: None

因此,__iadd__ 中最后的 return 语句确实是必需的。但它并没有像我想的那样起作用(我来自 C/C++ 背景,所以这种行为对我来说有点奇怪)

*************************** 第二次更新 ****************** **************

我差点忘了 Python 中的 = 构成了语句而不是表达式。 __iadd__ 中的 return 语句和我使用其他语言的经验让我产生一种错觉,认为 += 可以用作表达式。

如 Python 文档中所述,__add__ 用于构造新对象。 __iadd__ 专为就地修改而设计。但这一切都取决于实施。虽然 __iadd__ returns 是一个对象,但是这个对象不知何故被语言“截获”了,重新赋值给了左边的运算符,最后的效果是,__iadd__ 仍然是一个语句,而不是一种表达。如果我错了,请纠正我。另外,我没有找到任何资源来确认 += 是一个声明。

总结

  1. 一个明码,比如说a = 1是一个赋值语句。它不允许用作函数参数。但是,关键字参数传递不受此限制:print('Hello world', end='') 仍然有效。
    • x = x + y等同于x = x.__add__(y),
    • x += y 等同于 x = x.__iadd__(y),查看文档了解更多详情。
  2. 一个例子:
class Foo:
    def __init__(self, value, name="default"):
        self.value = value
        self.name = name

    def __add__(self, that):
        return Foo(self.value + that.value)

    def __iadd__(self, that):
        self.value = self.value + that.value
        return self

    def __str__(self):
        return "name: {}, value: {:d}".format(self.name, self.value)

a = Foo(1, 'alice')
b = Foo(2, 'bob')
c = a + b # objects a and b are unchanged
a += b    # object a is changed
print(c) # name: default, value: 3
print(a) # name: alice, value: 3

这是使用赋值语句作为表达式导致的语法错误。这是另一个 SO post 解释了 difference between statements and expressions.

这不是运算符重载实现的错误。

语法如何在另一个上下文中导致错误的示例(使用 int 而不是 Foo):

a = 2
b = 3

# STILL AN ERROR!
print(a += b) # Error while using the standard int += operator

如何解决语法问题:

a = 2
b = 3

a += b      # Separate the assignment statement...
print(a)    #     ...and give the print function an expression

+= 运算符是一组多个 扩充赋值运算符 的一部分。您可以将这些视为赋值语句的快捷方式,其中分配的变量也在分配给变量的值中。所以,i += 1 等同于 i = i + 1.

因此,您遇到的问题是因为只能将 (整数、浮点数、字符串等)或表达式传递给 print()功能。 a += b 不是一个值(它是一个语句)也不是一个表达式(它不会 评估 为单个值),所以你不能将它传递给 print().

解决这个问题:

a = Foo(1, 'alice')
b = Foo(2, 'bob')
a += b
print(a)

Assignments/in-place 操作不能被视为可以传递的表达式,这是设计使然。

除其他外,Guido 希望避免臭名昭著的 C 错字

if (a=b)  // user actually wanted to test a==b

但是 python 3.8 提供了一种新的赋值方法,称为 assignment expression,允许将赋值作为参数传递(这在列表理解中非常方便)。

你仍然不能进行就地添加,但你可以先添加然后赋值:

>> a=2
>> b=3
>> print(a:=a+b)
5

Python 的语义可能令人困惑。作为说明性示例,请尝试完成以下示例:

class Foo:
    def __init__(self, value):
        self.value = value

    def __iadd__(self, other):
        self.value = self.value + other.value
        return 'did iadd'

a = Foo(1)
c = a
c += Foo(2)

print((c, a.value))

应该导致 ('did iadd', 3) 被打印

根据 docs:

x += y is equivalent to x = x.__iadd__(y)

__iadd__ 的约定是您应该以 x = x + y 的语义评估为与 x += y“相同”的方式来实现它。 “相同”取决于问题的细节,例如对于不可变对象(例如整数或字符串),这将需要分配一个新对象并更新指向的引用,例如:

a = 500  # move out of the "interned" number space
c = a
a += 1
print(id(a), id(c))

应该给你两个不同的数字,在 CPython 中这将是两个 int 对象的地址,既不同又不可变。对于其他数据类型,允许 mutable/inplace 更新发生可能很有用,例如出于效率或其他原因。 __iadd__ 的语义允许 class 实现选择默认值。

每种语言都有自己的特点,因此我建议不要试图将它与 C++ 进行过于字面的比较。特别是因为 C++ 有很多历史包袱,它被迫随身携带。