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


118

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方法实现堆栈和队列类-它们应该是函数还是方法?(在这里我会猜测功能,因为我真的很想发信号通知推送界面)

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


2
嗯,我一直认为那完全是武断的。鸭子类型允许使用隐式“接口”,无论您具有X.frob还是X.__frob__独立的,都没有太大区别frob
Cat Plus Plus

2
虽然我大体上同意您的观点,但原则上您的回答不是Pythonic。回想一下,“面对模棱两可,拒绝猜测的诱惑。” (当然,截止日期会改变这一点,但是我这样做是出于娱乐/自我完善的目的。)
Ceasar Bautista

这是我不喜欢python的一件事。我觉得如果您要强制将像整数一样的类型强制转换为字符串,则只需将其设置为方法即可。必须将它封装在括号中并且很费时间,这很烦人。
Matt

1
这是我不喜欢Python的最重要原因:您永远不知道要实现某些目标时是否必须寻找函数或方法。当您将其他库与矢量或数据帧等新数据类型一起使用时,它甚至变得更加令人费解。
vonjd

1
“ Python的Zen指出应该只有一种做事的方式”,除了没有。
gented '18

Answers:


84

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

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

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

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说了什么。如果它在实例上运行并且需要访问类实例内部的数据,则它应该是成员函数。


简而言之,非成员函数在不可变对象上运行,可变对象使用成员函数吗?(或者这太严格了吗?对某些作品而言,这仅仅是因为不可变类型没有状态。)
Ceasar Bautista

1
从严格的OOP角度来看,我认为这是公平的。由于Python同时具有公共变量和私有变量(名称以__开头的变量)并且提供与Java不同的零保证访问保护,因此没有绝对值,仅仅是因为我们正在讨论一种允许的语言。但是,在像Java这样的宽松语言中,请记住,getFoo()和setFoo()函数是规范,因此不变性不是绝对的。客户代码只是不允许分配给成员。
arrdem

1
@Ceasar事实并非如此。不可变的对象具有状态。否则,将没有任何其他整数可以区分。不可变的对象不会更改其状态。总的来说,这使得所有州的公共问题变得更少。在这种情况下,使用功能有意义地操纵一个不变的所有公共对象要容易得多。没有成为方法的“特权”。

1
@CeasarBautista是的,Ben有观点。有3个“主要”的代码设计学派:1)未设计的,2)OOP和3)功能的。在功能风格上,根本没有状态。这是我看到的大多数python代码设计的方式,它接受输入并生成几乎没有副作用的输出。OOP 的要点是,一切都有状态。类是状态的容器,因此“成员”函数是状态相关的和基于副作用的代码,该代码会在调用时加载类中定义的状态。Python倾向于精益功能,因此倾向于非成员代码。
arrdem

1
吃(自己),胡扯(自己),死(自己)。哈哈哈哈
Wapiti 2015年

27

在需要以下情况时,请使用课程:

1)从实现细节中隔离调用代码-利用抽象封装

2)当您想替代其他对象时-利用多态性

3)当您想为相似的对象重用代码时-利用继承

将函数用于对许多不同的对象类型有意义的调用-例如,内置的lenrepr函数适用于多种对象。

话虽如此,选择有时取决于口味。考虑一下最适合常规通话的方式和可读性。例如,这将是更好的(x.sin()**2 + y.cos()**2).sqrt()还是sqrt(sin(x)**2 + cos(y)**2)


8

这是一条简单的经验法则:如果代码作用于对象的单个实例,请使用一种方法。甚至更好:除非有充分的理由将其编写为函数,否则请使用一种方法。

在您的特定示例中,您希望它看起来像这样:

chessboard = Chessboard()
...
chessboard.get_king_moves()

不要过度考虑。始终使用方法,直到您对自己说“将此方法定义为没有意义”为止,在这种情况下,您可以创建函数。


2
您能否解释为什么默认使用方法而不是函数?(您能解释一下该规则在堆栈,队列/弹出和推送方法的情况下是否仍然有意义?)
Ceasar Bautista

11
那种经验法则是没有道理的。标准库本身可以作为何时使用类与函数的指南。 heapqmath是函数,因为它们对普通的python对象(浮点数和列表)进行操作,并且因为它们不需要像random模块那样维护复杂的状态。
Raymond Hettinger

1
您是否说该规则没有意义,因为标准库违反了该规则?我认为这个结论没有道理。一方面,经验法则仅是这些,而不是绝对的法则。另外,标准库的很大一部分确实遵循这一经验法则。OP正在寻找一些简单的指南,我认为我的建议对刚起步的人来说是非常好的。不过,我感谢您的观点。
布莱恩·奥克利

STL这样做有一些充分的理由。对于数学和co来说,拥有一个类显然是没有用的,因为我们没有任何状态(在其他语言中,这些是仅具有静态函数的类)。对于可以在多个不同容器上运行的事物,例如说它len(),也可以说设计很有意义,尽管我个人认为函数在那里也不会太糟糕-我们只有另一种约定,len()总是必须返回一个整数(但对于backcomp的所有其他问题,我也不主张使用python)
Voo

-1,因为这答案完全是任意的:你基本上是试图证明x比y更好地假设 X是优于Y.
gented

7

我通常认为一个物体像一个人。

属性是人物的姓名,身高,鞋子的大小等。

方法功能是人可以执行的操作。

如果该操作只能由任何其他人完成,而又不需要该特定人独有的任何东西(并且无需更改该特定人的任何东西),那么它就是一个函数,应该这样编写。

如果某项操作正在对该人进行操作(例如进餐,散步等),或者需要该人独特的操作(例如跳舞,写书等),则应采用一种方法

当然,将其转换为您正在使用的特定对象并不总是一件容易的事,但是我发现这是思考它的好方法。


但你可以测量任何旧人的身高,所以通过逻辑应该是height(person),不person.height
endolith 2014年

@endolith可以,但是我想说身高作为一个属性更好,因为您不需要执行任何花哨的工作就可以检索它。编写一个函数来检索数字似乎无济于事。
2014年

@endolith另一方面,如果这个人是一个长大且会改变身高的孩子,那么让方法而不是功能来照顾就很明显了。
2014年

这并不成立。如果有的话怎么are_married(person1, person2)办?此查询非常笼统,因此应为函数而不是方法。
Pithikos

@Pithikos当然可以,但这不仅涉及对一个对象(人)执行的属性或动作,还涉及两个对象之间的关系。
2014年

4

通常,我使用类来为某件事实现一组逻辑功能,以便在程序的其余部分中,我可以对事物进行推理,而不必担心构成其实现的所有小问题。

凡是是那核心抽象的一部分“你可以用做什么事情 ”通常应该是一个方法。这通常包括可以改变一切的事情,作为内部数据状态通常被认为是私人,而不是“你可以用做什么逻辑思想的一部分的事情 ”。

当您进行更高级别的操作时,特别是如果它们涉及多个事物,我发现它们通常最自然地表示为函数,前提是它们可以从事物的公共抽象中构建而无需特殊的内部访问(除非它们re方法)。这具有很大的优势,当我决定完全重写我工作方式的内部结构(无需更改接口)时,我只有一小部分核心方法可以重写,然后使用这些方法编写所有外部函数将工作。我发现坚持认为与类X有关的所有操作都是类X上的方法会导致类过于复杂。

这取决于我正在编写的代码。对于某些程序,我将它们建模为对象的集合,这些对象的相互作用引起了程序的行为。在这里,最重要的功能紧密耦合到单个对象,因此是在方法中实现的,其中包含实用功能。对于其他程序,最重要的东西是一组操作数据的函数,而类仅用于实现由这些函数操纵的自然“鸭子类型”。


0

您可能会说,“面对模棱两可,拒绝猜测的诱惑”。

但是,这甚至不是猜测。您绝对可以确保两种方法的结果相同,因为它们可以解决您的问题。

我相信,采用多种方式实现目标只是一件好事。与其他用户一样,我要谦虚地告诉您,就语言而言,采用“味道更好” /感觉更直观的方法。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.