一尘不染

在Python中,什么时候应该使用函数而不是方法?

python

Python的Zen指出,只有一种方法可以做事情-但我经常遇到决定何时使用函数以及何时使用方法的问题。

让我们举一个简单的例子-
ChessBoard对象。假设我们需要某种方式使董事会上所有合法的King举动均可用。我们写ChessBoard.get_king_moves()还是get_king_moves(chess_board)?

我得到的答案基本上没有定论:

为什么Python使用方法来实现某些功能(例如list.index()),却使用其他方法的功能(例如len(list))?

主要原因是历史。函数用于那些对一组类型通用的操作,即使对于根本没有方法的对象(例如,元组),这些操作也可以使用。使用Python的功能部件(map(),apply()等)时,具有可以轻松应用于对象的不定形集合的函数也很方便。

实际上,将len(),max(),min()实现为内置函数实际上比将它们实现为每种类型的方法要少。人们可能会质疑个别情况,但这是Python的一部分,现在进行这样的基本更改为时已晚。必须保留功能以避免大量代码损坏。

有趣的是,以上内容并未真正说明采用哪种策略。

这是原因之一-
使用自定义方法,开发人员可以自由选择其他方法名称,例如getLength(),length(),getlength()或其他名称。Python强制执行严格的命名,以便可以使用通用函数len()。

稍微有趣一点。我认为函数在某种意义上是接口的Pythonic版本。

最后,来自Guido本人

谈论能力/接口使我想到了一些“流氓”特殊方法名称。
它在《语言参考》中说:“类可以通过定义具有特殊名称的方法来实现某些由特殊语法调用的操作(例如算术运算或下标和切片)。”
但是,所有这些方法都有特殊的名称,例如__len____unicode__,它们似乎是为内置函数的目的提供的,而不是为了支持语法。大概在基于接口的Python中,这些方法将在ABC上变成常规命名的方法,因此
__len__将成为

class container:

  ...

  def len(self):

    raise NotImplemented

虽然,再想一想,我不明白为什么 所有的 句法运算都不会仅仅在特定的ABC上调用适当的通常命名的方法。 “ <”举例来说,大概会调用“
object.lessthan”(或者是“
comparable.lessthan“)。因此,另一个好处是能够使Python摆脱这种乱七八糟的名字的怪异,在我看来这似乎是对HCI的改进

嗯 我不确定我是否同意(图:-)。

我首先要解释“ Python基本原理”的两个方面。

首先,出于HCI的原因,我选择了len(x)而不是x.len()(def __len__()后来出现了)。实际上,HCI有两个相互交织的原因:

(a)对于某些运算,前缀表示法比后缀读得更好-
前缀(和infix!)运算符在数学中具有悠久的传统,喜欢在视觉上帮助数学家思考问题的表示法。比较与我们改写像公式简单x*(a+b)x*a + x*b使用原始OO符号做同样的事情的笨拙。

(b)当我读到说的代码时,len(x)知道
那是在问某物的长度。这告诉我两件事:结果是整数,参数是某种容器。相反,当我阅读时x.len(),我必须已经知道这x是一种实现接口或从具有standard的类继承的容器len()。当未实现映射的类具有get()keys()
方法,或者不是文件的某些具有方法时,我们有时会感到困惑write()

用另一种方式说同样的事情,我将’len’视为内置 操作 。我不想失去那个。我不能肯定地说出您的意思,但是“ def
len(self):…”听起来确实像是您想将其降级为普通方法。我对此坚决为-1。

我答应解释的Python基本原理的第二点是为什么我选择了特殊的外观__special__而不是仅仅
选择外观的原因special。我期待类可能要覆盖的许多操作,一些标准(例如__add____getitem__),某些不那么标准(例如,泡菜__reduce__很长一段时间完全不支持C代码)。我不希望这些特殊操作使用普通的方法名称,因为那样的话,预先存在的类或用户没有为所有特殊方法存储百科全书的用户编写的类可能会意外地定义它们本不打算实现的操作,可能会造成灾难性的后果。伊万·科斯蒂奇(IvanKrstić)在他的信息中对此进行了更为简洁的解释,在我将所有内容写完之后,这些信息才得以体现。

--Guido van Rossum(主页:http
://www.python.org/~guido/ )

我对此的理解是,在某些情况下,前缀表示法更有意义(即,从语言的角度来看,Duck.quack比quack(Duck)更有意义。)而且,该函数还允许使用“接口”。

在这种情况下,我的猜测是仅基于Guido的第一点实现get_king_moves。但这仍然存在很多悬而未决的问题,例如,使用类似的push和pop方法实现堆栈和队列类-
它们应该是函数还是方法?(在这里我会猜测函数,因为我真的很想发信号通知push-pop接口)

TLDR:有人可以解释决定何时使用函数还是方法的策略是什么?


阅读 267

收藏
2020-12-20

共1个答案

一尘不染

我的一般规则是- 是在对象上执行操作还是由对象执行操作?

如果是由对象完成的,则应该是成员操作。如果它也可以应用于其他事物,或者由对象的其他事物完成,那么它应该是一个函数(或其他事物的成员)。

引入编程时,传统上(尽管实现不正确)以现实世界中的对象(例如汽车)来描述对象。您提到一只鸭子,让我们开始吧。

class duck: 
    def __init__(self):pass
    def eat(self, o): pass 
    def crap(self) : pass
    def die(self)
    ....

在“对象是真实的事物”类比的上下文中,为对象可以执行的任何操作添加类方法是“正确的”。所以说我想杀死一只鸭子,是否要在鸭子上添加.kill()?不,据我所知,动物不会自杀。因此,如果我想杀死一只鸭子,我应该这样做:

def kill(o):
    if isinstance(o, duck):
        o.die()
    elif isinstance(o, dog):
        print "WHY????"
        o.die()
    elif isinstance(o, nyancat):
        raise Exception("NYAN "*9001)
    else:
       print "can't kill it."

远离这种类比,为什么我们要使用方法和类?因为我们要包含数据并希望以某种方式构造我们的代码,以便将来可以重用和扩展它。这使我们想到了面向对象设计非常重要的封装概念。

封装原理实际上就是它的含义:作为设计人员,您应该隐藏有关实现和类内部的所有内容,对于任何用户或其他开发人员而言,都不一定要访问它。因为我们处理类的实例,所以这简化为“什么操作对于
该实例 至关重要”。如果操作不是实例特定的,则它不应是成员函数。

TL; DR :@Bryan说了什么。如果它在实例上运行并且需要访问类实例内部的数据,则它应该是成员函数。

2020-12-20