【问题标题】:Why return anything but `self` from `__iadd__`?为什么从`__iadd__`返回除`self`之外的任何东西?
【发布时间】:2013-12-31 00:30:03
【问题描述】:

Python 的 documentation on the methods related to the in-place operators+=*=(或者,正如它所说的那样,增强算术赋值)有以下说法:

这些方法应该尝试就地执行操作(修改 self)并返回结果(可以是,但不一定是,self)。如果未定义特定方法,则扩充分配回退到正常方法。

我有两个密切相关的问题:

  • 如果文档规定,如果实现了这些方法,那么为什么必须从这些方法中返回任何内容?在实现__iadd__ 的情况下,为什么增广赋值运算符不执行冗余赋值?
  • 在什么情况下从增强赋值方法返回 self 以外的东西才有意义?

一个小实验表明 Python 的不可变类型没有实现 __iadd__(这与引用的文档一致):

>>> x = 5
>>> x.__iadd__
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'int' object has no attribute '__iadd__'

当然还有它的可变类型的__iadd__方法,就地操作并返回self

>>> list1 = []
>>> list2 = list1
>>> list1 += [1,2,3]
>>> list1 is list2
True

因此,我无法弄清楚从__iadd__ 返回除self 以外的东西的能力是什么。在任何情况下,这似乎都是错误的事情。

【问题讨论】:

  • l = [1, 2, 3]; l = l.__iadd__([3, 2, 1]) + [1]; &gt;&gt;&gt; l [1, 2, 3, 3, 2, 1, 1]
  • @Smac89 很好 - 这是一个古怪但只是合法的理由 __iadd__ 有一个返回值,所以我想我可以检查我的第一个问题。不过,我仍然对第二个问题感到困惑 - 在什么情况下,从 __iadd__ 方法返回除 self 以外的其他内容是有用或正确的,因为文档表明是允许的?
  • 对于像x[[0, 1]] += 2 这样的情况,赋值是必要的,其中x 是一个 NumPy 数组,并且赋值将 2 添加到选定索引的列表中。不过,我想不出除了self 之外的任何情况。
  • @Smac89:实际上,我不确定这是一个很好的理由。您可以很容易地争辩说l = l.sort() + [1] 应该有效,而它(故意)无效。
  • 我想指出+= 没有值。例如,无论bar 是什么类型,foo = (bar += 1) 都是语法错误。因此,除非您希望您的客户直接致电 __iadd__(他们的头脑正常的人会这样做吗?!)它并不重要您从中返回什么。

标签: python


【解决方案1】:

如果文档规定,如果实现了这些方法,那么为什么有必要从这些方法中返回任何东西?在实现__iadd__ 的情况下,为什么增广赋值运算符不执行冗余赋值?

一个原因是强制它们成为语句而不是表达式。


更大的原因是分配并不总是多余的。在左侧只是一个变量的情况下,当然,在改变对象之后,通常不需要将该对象重新绑定到它已经绑定到的名称。

但是左侧是更复杂的赋值目标的情况呢?请记住 you can assign—and augmented-assign—to subscriptions, slicings, and attribute referencesa[1] += 2a.b -= 2。在这种情况下,您实际上是在一个对象上调用__setitem____setattr__,而不仅仅是绑定一个变量。


另外,值得注意的是,“冗余分配”并不是一项昂贵的操作。这不是 C++,在 C++ 中,任何赋值最终都可以在值上调用自定义赋值运算符。 (它最终可能会在一个对象上调用自定义 setter 运算符,该对象的值是元素、子切片或属性,这可能很昂贵……但在这种情况下,它不是多余的,如上所述。)


最后一个原因与您的第二个问题直接相关:您几乎总是想从__ispam__返回self,但几乎总是不是总是。如果__iadd__ 没有返回self,那么分配显然是必要的。


在什么情况下,从增强赋值方法中返回 self 以外的东西才有意义?

您在这里略过了一个重要的相关部分:

这些方法应该尝试就地执行操作(修改self

如果他们无法就地执行操作,但可以执行某些操作else,则返回 self 以外的其他内容可能是合理的。

想象一个使用写时复制实现的对象,如果它是唯一的副本,则在原地变异,否则创建一个新副本。你不能通过不实现__iadd__ 并让+= 回退到__add__ 来做到这一点;你只能通过实现一个__iadd__ 来做到这一点,它可以制作并返回一个副本,而不是变异并返回self。 (出于性能原因,您可能会这样做,但也可以想象,您会有一个具有两个不同接口的对象;“高级”接口看起来是不可变的,并且在写入时复制,而“低级”接口公开实际共享。)

因此,需要它的第一个原因是处理非就地情况。


但是还有其他原因吗?当然。

一个原因只是为了包装其他语言或库,这是一个重要的特性。

例如,在 Objective C 中,许多方法返回一个 self,它通常但并不总是与接收方法调用的对象相同。 “并不总是”是 ObjC 处理类集群之类的事情的方式。在 Python 中,有更好的方法来做同样的事情(即使在运行时更改你的类通常会更好),但在 ObjC 中,这是完全正常和惯用的。 (它仅用于 Apple 当前框架中的init 方法,但这是他们标准库的约定,即NSMutableFoo 添加的突变方法总是返回void,就像像list.sort 这样的突变方法总是返回@ 的约定987654342@ 在 Python 中,不是语言的一部分。)那么,如果你想用 Python 封装 ObjC 运行时,你会如何处理呢?

你可以在所有东西前面放置一个额外的代理层,这样你的包装器对象就可以改变它所包装的 ObjC 对象。但这意味着一大堆复杂的委托代码(尤其是如果您想通过包装器将 ObjC 反射工作备份到 Python 中)和内存管理代码,并且会影响性​​能。

相反,您可以只使用一个通用的瘦包装器。如果您返回一个与您开始时不同的 ObjC 对象,您将返回该对象的包装器,而不是您开始使用的对象的包装器。简单的代码,内存管理是自动的,没有性能成本。只要您的包装器的用户始终使用a += b 而不是a.__iadd__(b),他们就不会看到任何区别。

我意识到“围绕与 Apple 的 Foundation 不同的 ObjC 框架库编写 PyObjC 风格的包装器”并不是一个日常用例……但您已经知道这是一个您不会每天都使用的功能,那你还有什么期待?

惰性网络对象代理可能会做类似的事情——从一个很小的名字对象开始,当你第一次尝试对它做某事时,将它换成一个完整的代理对象。您可能会想到其他此类示例。你可能永远都不会写它们……但如果你不得不写,你可以。

【讨论】:

  • @abarnert: x[index_list] += 2,其中x 是一个 NumPy 数组。
  • @abarnert:实际上,它就是这么做的。这是一个临时数组,而不是列表,但它确实创建了一个很大的临时值数组,向它们添加 2,然后将它们分配回来。当人们期望 x[[0, 0]] += 2 将 4 添加到 x[0] 时,这有时会咬人。
  • @MarkAmery: (2) 一个写时复制对象,您可以在其中制作数百万份副本(可能在树中而不是平线中),这将是您可能想要的一个示例如果有其他引用,则复制并返回其他内容,但如果没有,则就地变异(节省大量复制时间)。
  • @MarkAmery: (3) ObjC does 现在限制了运算符重载,但这无关紧要;任何体面的 Python 包装器都可能希望将 ObjC 功能公开为运算符,因此您可以尽可能编写 a[3] 而不是 a.objectAtIndex_(3)
  • @MarkAmery:(4)ObjC 语言中没有任何东西只适用于-init 方法;这是 Foundation 中的约定——就像 Python 的 stdlib 中的约定一样——NSMutableFoo 添加的方法返回void。查看从appendString: 返回self 的早期可变字符串类,以便它们可以在需要时更改类;在某些时候,NeXT 认为这样做的好处不如在任何地方都遵循惯例的好处。
【解决方案2】:

要回答您的第二个问题,当您处理不可变对象时,您返回的不是 self 。

举个例子:

>>> foo = tuple([1, 2, 3])
>>> foo
(1, 2, 3)

现在我尝试就地改变对象:

>>> foo += (4,)
>>> foo
(1, 2, 3, 4)

不敢相信这有效!...现在我们尝试调用它的 __iadd__ 方法:

>>> foo.__iadd__((5,))
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'tuple' object has no attribute '__iadd__'

它没有!奇怪……现在你一定想知道,当我们做foo += (4,) 时发生了什么?

本质上发生的事情是,当尝试对不可变对象进行这种就地修改时,python 使用对象__add__ 方法来执行操作,即创建一个新对象。所以实际发生的是,当我们这样做时:

>>> foo += (4,)

发生了这样的事情:

>>> foo = foo + (4,)

See this

因此,当您尝试对不可变对象使用+= 或任何其他就地修改时,python 将使用该运算符的非变异等效项,您将获得一个新对象。另一个例子:

>>> foo += (5,) + (6,)
>>> foo
(1, 2, 3, 4, 5, 6)

以上翻译为:

foo = foo + (5,) + (6,)

希望能回答你的问题

【讨论】:

  • 不,当您处理不可变对象时,您根本没有实现__iadd__。只需实现 __add__ ,它将用于覆盖 +=
  • 感谢您的尝试-您之前对该问题的评论确实很有帮助-但由于wim给出的原因,对此答案的-1。通过诉诸甚至不实现 __iadd__ 的类型的情况来证明让 __iadd__ 返回 self 以外的东西是合理的,这是一个明显的错误。
  • 编辑后,此答案不再错误,但也不再相关 - 现在主要与 += 运算符有关,而不是 __iadd__ 方法。我问的是方法,而不是操作员。您没有提供一个示例,说明实际实现 __iadd__ 但让它返回self 以外的东西是有意义的。
  • @Smac89 是的 - 如您引用的文档中所述,__iadd__ 未实现且+= 依赖于__add__ 的情况。我的问题是,您何时想要编写一个自定义的 __iadd__ 方法,该方法返回 self 以外的内容,而不是当 += 可能分配 self 以外的内容时。
  • @Falmarri:这个答案对那里有什么帮助?当然,知道您可以通过使用返回 NotImplemented 的方法覆盖它来阻止 __iadd__ 或者(我认为仅在 2.x 中)通过插入一个额外的类来破坏基类属性查找,这很有用,但是这个答案不会帮助你找出答案,不是吗?
猜你喜欢
  • 2022-01-15
  • 1970-01-01
  • 2020-07-24
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多