Python中的非成员函数与成员函数

19

我对Python相对较新,并且很难将该语言的特性与我在C ++和Java背景下养成的习惯结合起来。

我目前遇到的问题与封装有关,具体而言是Meyer的“Effective C++”中第23项最好概括的一个想法:

优先使用非成员非友元函数而不是成员函数

暂时忽略缺少friend机制的问题,在Python中非成员函数是否被认为比成员函数更可取呢

以下是一些必要但愚蠢的示例:

class Vector(object):
    def __init__(self, dX, dY):
        self.dX = dX
        self.dY = dY

    def __str__(self):
        return "->(" + str(self.dX) + ", " + str(self.dY) + ")"

    def scale(self, scalar):
        self.dX *= scalar
        self.dY *= scalar

def scale(vector, scalar):
    vector.dX *= scalar
    vector.dY *= scalar

假设有 v = Vector(10, 20),现在我们可以调用 v.scale(2) 或者 scale(v, 2) 来使向量的大小加倍。

考虑到这里我们使用了属性,在这种情况下,这两个选项中的哪一个更好一些(如果有),为什么?


2
我认为这在Python中并不完全正确。这些论点并不适用于Python,因为你可以很容易地修改类。Python还注重可读性,我觉得“v.scale(2)”比“scale(v, 2)”更清晰明了。如果你查看标准库,除了最常见的函数外,其他函数都被保留为成员而不是内置函数。 - Gareth Latty
6个回答

18

有趣的问题。

你的出发点与大多数来自Java程序员的问题不同,他们倾向于认为你需要类,而你却很少需要。一般来说,在Python中,除非你特别要做数据封装,否则没有必要拥有类。

当然,这里在你的例子中,你实际上正在进行数据封装,因此使用类是合理的。个人而言,我会说既然你有了一个类,那么成员函数就是最好的选择:你在特定的向量实例上执行一个操作,所以让函数成为Vector的方法是有意义的。

如果你需要让它与多个不一定继承自彼此或公共基类的类一起工作,那么你可能希望将其作为独立的函数(我们不常用“成员”或“非成员”这个词)。由于鸭子类型,这是相当常见的做法:指定你的函数期望具有特定属性或方法的对象,并对其进行一些操作。


3
在这里提一下,与C++不同的是,Python没有“public”或“private”的概念。下划线语法是传达某些内容为私有的提示的惯用方式,但它并不是强制执行的 - Li-aung Yip
我不同意这个答案。它试图为方法解决方案辩护,却连试图找出为什么Scott Mayer的书推荐不使用方法解决方案都没有。 - habrewning
我不同意这个答案。它试图在甚至没有尝试找出为什么Scott Mayer的书建议不使用方法解决方案的情况下为方法解决方案辩护。 - undefined
我再次阅读了Scott Mayer的建议。他建议在这种情况下不要使用这些方法。但是这个答案却说它是“合理的”,“最好的方式”和“有道理的”。这与Scott Mayer的建议完全相反,而且没有任何理由。Scott Mayer之所以提出这个建议的原因是大多数开发人员有一个错误的个人直觉,导致他们做出错误的决定。正是因为这种回答,Scott Mayer才会提出这个建议。 - habrewning

4
看看你自己的例子——非成员函数必须访问Vector类的数据成员。这并不符合封装的原则。特别是,该函数会更改传入对象的数据成员。在这种情况下,最好返回一个经过缩放的向量,并保留原始向量不变。
此外,使用非成员函数时无法实现类多态性的任何优势。例如,在这种情况下,它仍然只能处理两个分量的向量。最好使用向量乘法功能或使用一种迭代组件的方法来进行操作。
总之:
1. 使用成员函数来操作控制的类的对象; 2. 使用非成员函数执行纯通用操作,这些操作是基于方法和运算符本身是多态的; 3. 最好将对象突变保留在方法中。

被访问的成员(x 和 y 组件)必须是公共的,以便对象有用。考虑到具有不同维度的向量,你对多态性有一些看法。 - user395760
1
@delnan,我并不认为独立函数违反封装的原则,而是它显然没有任何改善封装的作用。我不赞成它缺乏通用性——它只能处理两个元素的向量。我也有点不赞成它改变了向量的数据。 - Marcin
1
@Marcin - 示例中对象的名称并不重要,而且,为了辩护,它确实被标记为愚蠢的。该问题要求对成员/非成员函数的相对优点发表评论,而不是建议如何使任意示例更通用。 - JimmidyJoo
@JimmidyJoo,我并没有评论你的命名选择。请指出你认为这个答案中哪些部分不切题:每个部分都涉及到方法选择与通用函数的选择。 - Marcin
@Marcin - 没有任何答案是离题的 - 你提到的“缺乏通用性”正是我所指的。当我说名称不重要时,我的意思是我同样可以将类称为“Apple”,我的问题仍然存在。谢谢你的回答。 - JimmidyJoo

4

一个自由函数可以让你在第一个参数上使用鸭子类型,从而提供了更大的灵活性。

成员函数可以让你将功能与类关联起来,提供了更高的表达力。

根据需要选择。通常情况下,函数是平等的,因此它们都应该对类的接口有相同的假设。一旦你发布了一个自由函数 scale,你实际上在宣传 .dX.dYVector 的公共接口的一部分。这可能不是你想要的。你这样做是为了能够重复使用具有 .dX.dY 的其他对象的相同函数。这可能不会对你有价值。所以在这种情况下,我肯定更喜欢成员函数。

对于好的自由函数示例,我们无需查看标准库: sorted 是一个自由函数,而不是 list 的成员函数,因为在概念上,你应该能够创建结果列表来排序任何可迭代序列。


我不同意这个答案。自由函数并不揭示类的内部是真实的。那么为什么 Scott Mayer 推荐不使用该方法解决方案呢? - habrewning
我不同意这个答案。一个自由函数并不会暴露类的内部是不正确的。那么为什么Scott Mayer建议不要使用这种方法呢? - undefined

3

优先使用非成员函数而非成员函数

这是一种设计理念,可以并且应该扩展到所有面向对象编程语言。如果您理解了其本质,这个概念就很清晰。

如果您不需要访问类的私有/保护成员,则您的设计没有必要将函数包含为类成员。换句话说,在设计一个类时,在枚举了所有属性之后,您需要确定足够使类得以形成的最小行为集。所有可以通过任何可用的公共方法/成员函数编写的成员函数都应该是公共的。

这在Python中适用于多少程度?

在某些程度上,如果您非常小心,它是适用的。与其他面向对象编程语言(如Java/C++)相比,Python支持较弱的封装,尤其是因为没有私有成员。(程序员可以通过在变量名之前加上“_”来轻松地编写称为私有变量的内容。这通过名称混淆功能变为类私有)。所以如果我们完全采纳Scott Meyer的话,考虑到应该从类中访问什么和应该从外部访问什么之间的细微差别,最好由设计师/程序员决定一个函数是否应该成为类的组成部分或者不是。我们可以轻松采用一项设计原则:“除非您的函数需要访问类的任何属性,否则可以将其作为非成员函数”


2

由于 scale 依赖于向量的逐个成员相乘,因此我建议将乘法实现为一个方法,并将 scale 定义得更加通用:

class Vector(object):
    def __init__(self, dX, dY):
        self._dX = dX
        self._dY = dY

    def __str__(self):
        return "->(" + str(self._dX) + ", " + str(self._dY) + ")"

    def __imul__(self, other):
        if other is Vector:
            self._dX *= other._dX
            self._dY *= other._dY
        else:
            self._dX *= other
            self._dY *= other

        return self

def scale(vector, scalar):
    vector *= scalar

因此,类接口丰富而简化,同时保持了封装性。

我认为这绝对是对我的具体示例的一种有趣的方法。 - JimmidyJoo
如果你已经有可以使用的运算符,为什么还要定义这样一个比例函数呢? - habrewning
如果你已经有可以使用的运算符,为什么还要定义这样一个比例函数呢? - undefined
@habrewning真的只是为了完整性,以及展示__imul__函数的应用,不过坦率地说,我觉得我更喜欢@marcin的答案! - John McFarlane
@habrewning真的只是为了完整性,以及展示__imul__函数的应用,不过说实话,我觉得我更喜欢@marcin的答案! - undefined

0
还有一点尚未提到。假设你有一个类Vector和一个类Matrix,并且两者都应该是可扩展的。
使用非成员函数,你必须定义两个scale函数。一个用于Vector,一个用于Matrix。但是Python是动态类型的。这意味着,你必须给这两个函数不同的名称,比如scale_vectorscale_matrix,并且你总是必须显式地调用其中一个。如果它们的名称相同,Python就不知道你想调用哪一个。
这是两种选项之间的一个重要区别。如果你事先(静态地)知道你有哪些类型,那么使用不同的名称的解决方案将起作用。但是如果你事先不知道这一点,方法是Python提供“动态分派”的唯一方式。
通常,“动态分派”正是你想要的,以实现动态行为和所需的抽象级别。有趣的是,类和对象是面向对象语言(包括Python)提供“动态分派”的唯一机制。在这里,Scott Meyer的建议也适用于Python。可以这样理解:只有在真正需要动态分派时才使用方法。这不是一个关于你更喜欢哪种语法的问题。
虽然你经常需要方法(或者你觉得经常需要它们),但这个答案并不是对方法论的推荐。如果你不打算使用继承、多态或运算符等特性,那么非成员解决方案更好,因为它可以实现不可变性(如果你喜欢这种风格),测试更容易,复杂度较低(因为它没有引入所有面向对象机制),阅读起来更好,并且更灵活(因为它可以用于所有具有两个组件dx和dy的事物)。
然而,作为免责声明,我必须说,在讨论封装(例如私有数据)或静态类型时,总体上并不总是有助于使用Python。这是一个设计决策,这些概念在Python中不存在。这也是语言设计者的决定,你不必提前定义一切。将这些概念引入你的Python代码中有时会引起争议,并导致人们可能会说,这不符合Python的风格。并不是每个人都喜欢面向对象的编程风格,也并不总是适用。Python给了你很大的自由,只有在你想要的情况下才能使用面向对象的编程。没有非面向对象的动态分派机制并不意味着你没有其他选择。

你提到scale需要改名是正确的,因为scale这个名字太模糊了,但它的真正目的是非常具体的,只适用于二维向量。在我看来,这是独立函数的一个巨大问题——很少有函数能提供如此通用的功能,以至于需要一个独立的函数。如果你必须将类数据传递给一个函数,我认为这个函数很可能是专门针对该类进行调整的,而不适合使用独立的通用函数。 - undefined
当然可以。通常情况下,拥有scale_vectorscale_matrix两个功能并不是你想要的。在极端情况下,你可能会得到数百个函数名,用于处理所有参数的组合。我在回答中没有对此进行评估,因为这取决于读者的决定。总的来说,定义两个独立的函数更好。没错!但前提是编程语言能够根据类型签名选择正确的函数。而这并不是Python的工作方式。 - undefined
1
你说“通用函数不合适”。我完全同意。这就是其他答案存在的问题。当然,拥有一个通用的鸭子类型完美函数会很好。但在实践中,你通常会遇到具体的问题,而寻找通用机制的努力往往得不偿失。因此,在实践中,你需要的是一些能够直接运行而不需要过多思考和推测未来可能发生的事情的东西。 - undefined

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