在Python中何时使用函数而非方法?

134 投票
6 回答
64222 浏览
提问于 2025-04-17 06:11

Python的哲学告诉我们,做事情应该有一种方式。然而,我常常遇到一个问题,就是在使用函数和方法时该如何选择。

我们来看看一个简单的例子——一个棋盘对象。假设我们需要一种方法来获取棋盘上所有合法的国王移动。我们是写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()。

这稍微有点意思。我认为函数在某种意义上是Python版本的接口。

最后,来自Guido本人的观点

谈到能力/接口让我想到了我们一些“流氓”特殊方法名称。在语言参考中提到,“一个类可以通过定义具有特殊名称的方法来实现某些操作,这些操作通过特殊语法(如算术操作或下标和切片)被调用。”但是有很多像__len____unicode__这样的特殊名称的方法,似乎是为了内置函数的便利,而不是为了支持语法。可以推测,在一个基于接口的Python中,这些方法会变成ABC上的常规命名方法,这样__len__就会变成

class container:
  ...
  def len(self):
    raise NotImplemented

不过,想得更多一点,我不明白为什么所有的语法操作不直接调用特定ABC上的常规命名方法。例如,“<”可能会调用“object.lessthan”(或者“comparable.lessthan”)。所以另一个好处是能够让Python摆脱这种混乱的名称,这在我看来是人机交互的改善。

嗯。我不太同意(你觉得呢 :-)。

我想先解释两点“Python的理由”。

首先,我选择len(x)而不是x.len()是出于人机交互的考虑(def __len__()是在后来的事情)。实际上有两个交织在一起的原因,都是人机交互:

(a) 对于某些操作,前缀表示法比后缀表示法更易读——前缀(和中缀!)操作在数学中有着悠久的传统,数学家喜欢那些视觉上能帮助他们思考问题的符号。比较一下我们将公式x*(a+b)重写为x*a + x*b的轻松程度,与使用原始面向对象表示法做同样事情的笨拙。

(b) 当我看到代码len(x)时,我知道这是在询问某个东西的长度。这告诉我两件事:结果是一个整数,参数是某种容器。相反,当我看到x.len()时,我必须已经知道x是某种实现了接口或继承了具有标准len()的类的容器。想想我们偶尔会遇到的困惑,比如一个没有实现映射的类却有get()keys()方法,或者某个不是文件的对象却有write()方法。

换句话说,我把'len'视为一个内置的操作。我不想失去这个功能。我不能确定你是否是这个意思,但'def len(self): ...'听起来确实像是你想把它降级为普通方法。我对此强烈反对。

我承诺要解释的第二个Python理由是,为什么我选择特殊方法看起来像__special__而不仅仅是special。我预见到许多类可能想要重写的操作,有些是标准的(例如__add____getitem__),有些则不那么标准(例如,pickle的__reduce__在很长一段时间内在C代码中没有任何支持)。我不想让这些特殊操作使用普通的方法名,因为那样的话,已有的类或者那些没有对所有特殊方法有百科全书般记忆的用户编写的类,可能会意外地定义他们并不想实现的操作,可能会导致灾难性的后果。Ivan Krstić在他的消息中更简洁地解释了这一点,他的消息在我写完这些内容后到达。

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

我对这一点的理解是,在某些情况下,前缀表示法更有意义(例如,从语言学的角度来看,Duck.quack比quack(Duck)更合理)。而且,函数允许“接口”的存在。

在这种情况下,我的猜测是根据Guido的第一个观点来实现get_king_moves。但这仍然留下了很多开放性问题,比如实现一个堆栈和队列类,使用类似的push和pop方法——它们应该是函数还是方法?(在这里我猜是函数,因为我确实想传达一个推送-弹出接口)

总结一下:有人能解释一下在决定使用函数还是方法时应该采用什么策略吗?

6 个回答

9

我通常把对象想象成一个人。

属性就像这个人的名字、身高、鞋码等等。

方法函数是这个人可以执行的操作。

如果这个操作是任何人都能做的,不需要这个人有什么特别之处(而且也不改变这个人本身),那么它就是一个函数,应该这样写。

如果这个操作是对这个人进行的(比如吃东西、走路等),或者需要这个人独特的东西来参与(比如跳舞、写书等),那么它就应该是一个方法

当然,把这些概念具体应用到你正在处理的对象上并不总是简单,但我觉得这样想是个不错的方式。

27

当你想要使用类的时候,可以考虑以下几点:

1) 想要把调用代码和具体实现分开,这样可以利用抽象封装的好处。

2) 当你希望这个类可以替代其他对象时,可以利用多态的特性。

3) 当你想要为相似的对象重用代码时,可以利用继承的功能。

而对于那些在多种对象类型中都适用的调用,使用函数会更合适。例如,内置的lenrepr函数就适用于很多不同类型的对象。

不过,有时候选择使用类还是函数其实是个人喜好的问题。可以考虑什么样的写法对一般的调用来说更方便、更易读。例如,你觉得(x.sin()**2 + y.cos()**2).sqrt()好,还是sqrt(sin(x)**2 + cos(y)**2)更好呢?

102

我的一般原则是——这个操作是由对象来执行,还是在对象上执行?

如果是由对象自己来做的,那就应该是一个成员操作。如果这个操作也可以适用于其他东西,或者是由其他东西对这个对象进行的,那就应该是一个函数(或者可能是其他东西的成员)。

在介绍编程的时候,通常会用现实生活中的物体来比喻,比如汽车。你提到了鸭子,那我们就用鸭子来举例。

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."

离开这个比喻,我们为什么要使用方法和类呢?因为我们想要把数据封装起来,并且希望我们的代码结构能够在将来被重复使用和扩展。这就引出了面向对象设计中非常重要的一个概念——封装。

封装原则其实就是这个意思:作为设计者,你应该隐藏所有实现细节和类内部的信息,只有在绝对必要的情况下,用户或其他开发者才能访问这些信息。因为我们处理的是类的实例,这就简化为“哪些操作对这个实例是关键的”。如果一个操作不是特定于某个实例的,那它就不应该是一个成员函数。

总结一下: 就是@Bryan说的。如果操作是针对一个实例,并且需要访问这个类实例内部的数据,那它就应该是一个成员函数。

撰写回答