在原地操作并返回对象是一个不好的想法吗?

28

我主要是在谈论 Python,但我认为这可能适用于大多数编程语言。如果我有一个可变对象,做原地操作并同时返回该对象是否是一个不好的主意?似乎大多数示例只是修改该对象并返回 None,例如 list.sort


我认为一切都在于一致性。Python 在可变对象的方法上非常一致,这些方法是原地操作。只要你保持一致,原地操作返回一个对象或对象引用都不应该成为问题。 - Joel Cornett
但首先为什么会这样呢? - asmeurer
2
我不是100%确定,但大多数情况下,就没有必要进行原地操作来返回一个对象。毕竟,你并没有创建一个需要分配的新对象。此外,每个原地操作都有类似的操作,以明确表明你正在返回某些东西以进行进一步的操作(例如list.sortsorted(list)list.reversereversed(list))。 - Joel Cornett
4个回答

35

是的,这是个不好的主意。原因是如果原地操作和非原地操作有着看似相同的输出结果,那么程序员就会频繁地混淆原地操作和非原地操作(例如 List.sort()sorted()),这将导致难以检测的错误。

返回自身的原地操作允许你执行 "方法串联",然而这是不好的实践,因为你可能会在一条链的中间不小心嵌入具有副作用的函数。

为了防止这样的错误,方法链应该只有一个具有副作用的方法,并且该函数应该处于链的末尾。链中之前的函数应该在没有副作用的情况下转换输入(例如,导航树、切片字符串等)。如果原地操作返回自己,则程序员很可能会无意中使用它来替代另一个返回副本且没有副作用的备选函数(再次提到 List.sort()sorted()),这可能会导致难以调试的错误。

这就是为什么 Python 标准库函数总是返回副本或返回 None 并在原地修改对象,但从不在原地修改对象并返回自身的原因。其他像 Django 这样的 Python 库也遵循这个做法(请参见有关 Django 的 这个非常相似的问题)。


一般情况下同意这个规则,但我认为在某些特定情况下有例外,这种情况并不罕见。例如1:当方法的语义明确是原地操作时,比如 jQuery 的 .empty()。例如2:当 API 如此常用以至于每个人都从一开始就知道它,并且没有返回副本的版本,比如 jQuery 的 .append() - Samuel Rossille
仅仅因为方法名是一个现在时动词,并不意味着人们会发现它很明显地表示该操作是原地进行的。在学习 Python 之后,我花了很长时间才始终记得 list.sort 是原地排序,尽管名称听起来应该这样做。 - asmeurer
在以原地操作结束链式调用时,难道还会有混淆吗? a.sort()a[:2].sort() 将执行完全不同的操作(我猜如果使用像 numpy 的 array 这样使用视图的东西,则情况会有所不同)。也许重点是 sort 返回 None 保护您免受认为 a[:2].sort() 有任何用处的影响? - asmeurer
1
是的,关键在于当sort用于其返回值时(因为它返回None),它会立即失败,而不是悄悄地引起开发人员可能不想引起的副作用。 - Andrew Gorcester

10
从修改它的方法中返回已修改的对象可能有一些好处,但不建议在Python中这样做。在修改操作之后返回self将允许您对该对象执行method chaining,这是一种在同一对象上执行多个方法的便捷方式,它是面向对象编程中非常常见的习语。反过来,方法链接允许直接实现fluent interfaces。此外,它使一些函数式编程习惯更容易表达。
举几个例子:在Python中,Moka库使用方法链接。在Java中,StringBuilder类允许在同一对象上多次调用append()。在JavaScript中,JQuery广泛使用方法链接。Smalltalk将这个想法提升到了下一个级别:默认情况下,所有方法返回self,除非另有说明(因此鼓励方法链接) - 与Python相比,它默认返回None
这个习语在Python中并不常见,因为Python遵循Command/Query Separation Principle,该原则规定“每个方法应该只是一个执行操作的命令或者返回数据给调用者的查询,但不能兼备二者”。
总的来说,是否在结尾处返回self是编程文化和惯例以及个人品味的问题。如上所述,有些编程语言鼓励这种做法(如Smalltalk),而有些则反对(如Python)。每种观点都有优点和缺点,并且存在激烈的讨论。如果你是一个严格遵守Python标准的程序员,最好不要返回self - 只需知道有时打破这个规则可能会有用。

1
感谢您的出色回答,特别是提供了命令/查询分离原则的链接,这有助于我将一些设计折衷方案进行标记。 - FMc

1

这里关于就地操作不返回的答案让我有点混乱,直到我发现this other SO post链接到Python documentation(我以为我已经读过了,但可能只是略读了一下)。文档中提到就地运算符时说:

这些方法应尝试就地执行操作(修改 self)并返回结果(可以是 self,但不必如此)。

当我尝试使用非返回 self 的就地操作时,它变成了 None。 在这个例子中,它会说 vars 需要一个带有 __dict__ 的对象。 查看 self 的类型显示为 None

# Skipping type enforcement and such.
from copy import copy
import operator
import imported_utility # example.
class A:
    def __init__(self, a, b):
        self.a = a
        self.b = b
    def one(self, scaler):
        self *= scaler
        return imported_utility(vars(self))
    def two(self, scaler):
        tmp = self * scaler
        return imported_utility(vars(tmp))
    def three(self, scaler):
        return imported_utility(vars(self * scaler))
    # ... addition, subtraction, etc.; as below.
    def __mul__(self, other):
        tmp = copy(self)
        tmp._inplace_operation(other, operator.imul)
        return tmp
    def __imul__(self, other): # fails.
        self._inplace_operation(other, operator.imul)
    # Fails for __imul__.
    def _inplace_operation(self, other, op):
        self.a = op(self.a, other)
        self.b = op(self.b, other)

*可以使用(两个和三个),但是*=(一个)在self返回之前不起作用。

    def __imul__(self, other):
        return self._inplace_operation(other, operator.imul)
    def _inplace_operation(self, other, op):
        self.a = op(self.a, other)
        self.b = op(self.b, other)
        return self

我并不完全理解这种行为,但是在引用的帖子中有一条跟进评论说,如果没有返回self,就会真正地修改该对象的就地方法,但是将其名称重新绑定到None。除非返回self,否则Python不知道要重新绑定什么。通过保留对对象的单独引用,可以看到这种行为。


0

我想这取决于使用情况。我不认为从原地操作返回一个对象会有什么问题,除非你不使用结果,但如果你不是特别注重纯函数式编程,那这并不是真正的问题。我喜欢调用链模式,比如jQuery使用的那种,所以当函数返回它们所作用的对象时,我很感激,因为这样我可以进一步使用它。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接