为什么从`__iadd__`返回除“self”之外的任何东西?

时间:2013-12-31 00:30:03

标签: python

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

因此,我无法弄清楚从self返回__iadd__以外的内容的能力是什么。在绝对所有情况下,这似乎都是错误的事情。

2 个答案:

答案 0 :(得分:6)

  

为什么有必要从这些方法中返回任何内容,如果文档指定,如果实现,他们应该只在原地进行操作?为什么增强赋值运算符在实现__iadd__的情况下根本不执行冗余赋值?

一个原因是迫使它们成为陈述而不是表达。


更大的原因是任务并不总是多余的。在左侧只是一个变量的情况下,确保在变换对象之后,将该对象重新绑定到它已绑定的名称通常是不必要的。

但是左手边是一个更复杂的任务目标呢?请注意you can assign—and augmented-assign—to subscriptions, slicings, and attribute references,例如a[1] += 2a.b -= 2。在这种情况下,您实际上是在对象上调用__setitem____setattr__,而不仅仅是绑定变量。


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


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


  

在什么情况下从增强的分配方法中返回除self之外的东西是否有意义?

你已经在这里浏览了一个重要的相关内容:

  

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

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

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

因此,需要的第一个原因是处理非就地案件。


但还有其他原因吗?肯定。

其中一个原因就是包装其他语言或库,这是一个重要的功能。

例如,在Objective C中,许多方法返回self,它通常但不总是接收方法调用的同一对象。 “并非总是”是ObjC处理类集群之类的事情。在Python中,有更好的方法来做同样的事情(甚至在运行时改变你的类通常更好),但在ObjC中,它是完全正常和惯用的。 (它仅用于Apple当前Framework中的init方法,但它是标准库的惯例,NSMutableFoo添加的mutator方法总是返回void,就像mutator方法之类的约定一样list.sort始终在Python中返回None,而不是语言的一部分。)因此,如果您想在Python中包装ObjC运行时,您将如何处理?

您可以在所有内容之前放置一个额外的代理层,因此您的包装器对象可以更改它包装的ObjC对象。但这意味着一大堆复杂的委托代码(特别是如果你想让ObjC反射工作通过包装器备份到Python中)和内存管理代码,以及性能损失。

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

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

懒惰的网络对象代理可能会做类似的事情 - 从一个小的名字对象开始,在你第一次尝试对它做一些事情时将其交换为一个完整的代理对象。你可能会想到其他这样的例子。你可能永远不会写任何一个......但如果你必须,你可以。

答案 1 :(得分:-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,)

希望能回答你的问题