模块函数vs静态方法vs类方法vs没有修饰符:哪个成语更适合pythonic?


71

我是一个Java开发人员,他喜欢开和关Python。我最近偶然发现了这篇文章,其中提到了Java程序员在选择Python时常犯的错误。第一个吸引了我的眼球:

Java中的静态方法不会转换为Python类方法。哦,可以肯定,它或多或少会产生相同的效果,但是类方法的目标实际上是要做通常在Java中甚至是不可能的事情(例如继承非默认构造函数)。Java静态方法的惯用翻译通常是模块级别的函数,而不是类方法或静态方法。(静态的final字段应转换为模块级常量。)

这并不是性能问题,但必须使用此类Java惯用语代码的Python程序员会因键入Foo.Foo.someMethod而应为Foo.someFunction而感到恼火。但请注意,调用类方法涉及额外的内存分配,而调用静态方法或函数则不会。

哦,所有那些Foo.Bar.Baz属性链也不是免费提供的。在Java中,那些点名是由编译器查找的,因此在运行时,实际上有多少个都没有关系。在Python中,查找是在运行时进行的,因此每个点都很重要。(请记住,在Python中,“扁平比嵌套更好”,尽管它与“可读性计数”和“简单胜于复杂”更多地与性能无关)。

我发现这有点奇怪,因为staticmethod的文档说:

Python中的静态方法类似于Java或C ++中的静态方法。另请参见classmethod()以获取对创建备用类构造函数有用的变体。

更令人费解的是,这段代码:

class A:
    def foo(x):
        print(x)
A.foo(5)

未能按预期在Python 2.7.3中失败,但在3.2.3中工作正常(尽管您不能仅在类上调用A实例的方法)。

因此,有三种方法可以实现静态方法(如果使用类方法,则有四种方法),每种方法都有细微的差别,其中一种似乎没有记载。这似乎与Python的口头禅不一致,即应该有一种-最好只有一种-显而易见的方法。哪个成语是最Python化的?各自的优缺点是什么?

到目前为止,这是我的理解:

模块功能:

  • 避免Foo.Foo.f()问题
  • 污染模块的名称空间比其他方法更多
  • 没有继承

静态方法:

  • 将与类相关的函数保留在类内部,并保留在模块名称空间之外。
  • 允许在类的实例上调用该函数。
  • 子类可以覆盖该方法。

类方法:

  • 与staticmethod相同,但也将类作为第一个参数传递。

常规方法(仅适用于Python 3)

  • 与staticmethod相同,但不能在类的实例上调用该方法。

我在想这个吗?这不是问题吗?请帮忙!

Answers:


90

考虑它的最直接方法是根据方法需要什么类型的对象来进行工作。如果您的方法需要访问实例,请使其成为常规方法。如果需要访问该类,请将其设为类方法。如果不需要访问类或实例,则使其成为函数。几乎没有必要将某种静态方法变为静态方法,但是我想如果您希望将某个函数与一个类“分组”(例如,因此可以覆盖它),即使它不需要访问该类,您可以将其设为静态方法。

我要补充一点,将功能放在模块级别不会“污染”名称空间。如果要使用这些函数,则它们并不污染名称空间,而是按应使用的方式使用它。函数是模块中的合法对象,就像类或其他任何东西一样。如果没有任何理由,没有理由将函数隐藏在类中。


当我写关于污染名称空间的文章时,我在想如果有人要“从Foo import *”来处理更可能发生的名称冲突。我想这是每个程序员的责任。
2012年

14
这就是为什么from Foo import *不鼓励。如果这些功能是要在模块内部而不是公共API的一部分,则可以将其命名为前导下划线,并且这样做时不会将其导入from Foo import *
BrenBarn

4
为+1If the functions are meant to be used, they're not polluting the namespace, they're using it just as it should be used.表示污染,那么为什么要避免使用命名空间呢?
rickcnagy 2014年

以下是这些装饰器之间差异的全面概述:stackoverflow.com/a/1669524/1080804
ecoe

30

BrenBarn的好答案,但我将“如果不需要访问类或实例,则使其成为函数”可以更改为:

'如果不需要访问类或实例...但是主题上与类相关(典型示例:其他类方法使用或替代构造方法使用的辅助函数和转换函数),请使用staticmethod

否则使其成为模块功能


好吧,make_from_XXX()听起来像是一个替代构造函数,所以它不适合作为静态方法。
加尔

@加尔,您是对的,我更改了该评论。在python
smci

静态方法实际上只是一种修饰。静态方法和函数之间几乎没有区别,因为静态方法仅从args获取所有信息,就像普通函数一样。您可以使用任何func并将其作为静态对象粘贴到类上,反之亦然,除了调用它们的方式之外,它绝对不会改变任何东西。

1
@hayavuk:有一个概念上的区别,即我们发出信号说,静态方法“属于”特定的类,并且其代码,文档字符串,文档,相关的常量等应一起保存在同一文件中。如果我们不这样做,请考虑例如datetime模块:将有更少的提示,该提示datetime.fromtimestamp()不应在任何旧整数上调用。
smci

@smci我仍然会提交“我们发出信号”作为装饰性的东西。它并没有真正改变fromtimestamp()行为方式。我认为fromtimestamp()这里确实应该是一种类方法,而不是静态方法。

15

这实际上不是答案,而是冗长的评论:

更令人费解的是,这段代码:

        class A:
            def foo(x):
                print(x)
        A.foo(5)

未能按预期在Python 2.7.3中失败,但在3.2.3中工作正常(尽管您不能仅在类上调用A实例的方法)。

我将尝试解释这里发生的情况。

严格来说,这是对“常规”实例方法协议的滥用。

您在此处定义的是一个方法,但是第一个(也是唯一一个)参数的名称不是self,而是x。当然,您可以在的实例中调用方法A,但是您必须像这样调用它:

A().foo()

要么

a = A()
a.foo()

因此将实例作为第一个参数提供给函数。

通过类调用常规方法的可能性一直存在,并且通过

a = A()
A.foo(a)

在这里,当您调用类的方法而不是实例时,它不会自动获得其第一个参数,但是您必须提供它。

只要这是的实例A,一切都可以。给予IMO其他东西就是协议的滥用,因此Py2和Py3之间的区别是:

在Py2中,A.foo被转换为未绑定方法,因此要求其第一个参数是其“存在”的类的实例。用其他方法调用它将失败。

在Py3中,此检查已被删除,A.foo只是原始功能对象。因此,您可以将所有内容作为第一个参数来调用它,但是我不会这样做。方法的第一个参数应始终命名,self并具有的语义self


0

最佳答案取决于该函数的使用方式。就我而言,我编写了将在Jupyter笔记本电脑中使用的应用程序包。我的主要目标是使用户易于使用。

函数定义的主要优点是,用户可以使用“ as”关键字导入其定义文件。这允许用户以与调用numpy或matplotlib中的函数相同的方式调用函数。

Python的缺点之一是不能防止名称被进一步分配。但是,如果“ import numpy as np导入”出现在笔记本顶部,则强烈暗示“ np”不应用作公用变量名称。显然,您可以使用类名来完成相同的事情,但是用户的熟悉度很重要。

但是,在软件包内部,我更喜欢使用静态方法。我的软件体系结构是面向对象的,并且我使用Eclipse进行编写,并且将其用于多种目标语言。打开源文件并在顶层查看类定义,缩进一层的方法定义等非常方便。此级别的代码的读者主要是其他分析师和开发人员,因此最好避免使用特定于语言的习惯用法。

我对Python名称空间管理没有足够的信心,尤其是在使用设计模式时(例如,一个对象将一个引用传递给自身),以便被调用对象可以调用在调用方上定义的方法。所以我尽量不要强迫它太远。我使用了很多完全限定的名称和显式实例变量(带有self),在其他语言中,我可以依靠解释器或编译器来更紧密地管理范围。使用类和静态方法更容易做到这一点,这就是为什么我认为它们是抽象和信息隐藏最有用的复杂软件包的更好选择。

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.