模块函数 vs 静态方法 vs 类方法 vs 无装饰器:哪个成语更 Pythonic?
Posted
技术标签:
【中文标题】模块函数 vs 静态方法 vs 类方法 vs 无装饰器:哪个成语更 Pythonic?【英文标题】:Module function vs staticmethod vs classmethod vs no decorators: Which idiom is more pythonic? 【发布时间】:2016-06-19 04:34:16 【问题描述】:我是一名 Java 开发人员,经常玩弄 Python。我最近偶然发现了this article,它提到了 Java 程序员在使用 Python 时常犯的错误。第一个引起了我的注意:
Java 中的静态方法不会转换为 Python 类方法。哦,当然,它或多或少会产生相同的效果,但类方法的目标实际上是做一些在 Java 中通常甚至不可能的事情(比如继承非默认构造函数)。 Java 静态方法的惯用翻译通常是模块级函数,而不是类方法或静态方法。 (静态最终字段应转换为模块级常量。)
这不是什么性能问题,但是对于必须使用此类 Java 习惯代码的 Python 程序员来说,当它应该只是 Foo.someFunction 时键入 Foo.Foo.someMethod 会相当恼火。但请注意,调用类方法涉及额外的内存分配,而调用静态方法或函数则不会。
哦,所有这些 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 的实例上调用该方法,只能在类上调用。)
所以有三种方法可以实现静态方法(如果使用 classmethod 算的话,有四种方法),每一种都有细微的差别,其中一种似乎没有记录。这似乎与 Python 的“应该有一种——最好只有一种——明显的方式来做到这一点”的口头禅不符。 哪个成语是最 Pythonic 的?各有什么优缺点?
这是我目前所理解的:
模块功能:
避免 Foo.Foo.f() 问题 比替代品更污染模块的命名空间 无继承静态方法:
将与类相关的函数保留在类内部和模块命名空间之外。 允许在类的实例上调用函数。 子类可以覆盖该方法。类方法:
与 staticmethod 相同,但也将类作为第一个参数传递。常规方法(仅限 Python 3):
与 staticmethod 相同,但不能在类的实例上调用该方法。我是不是想多了?这不是问题吗?请帮忙!
【问题讨论】:
【参考方案1】:最佳答案取决于函数的使用方式。就我而言,我编写了将在 Jupyter 笔记本中使用的应用程序包。我的主要目标是让用户更轻松。
函数定义的主要优点是用户可以使用“as”关键字导入他们的定义文件。这允许用户以与调用 numpy 或 matplotlib 中的函数相同的方式调用函数。
Python 的一个缺点是名称无法防止进一步赋值。但是,如果笔记本顶部出现“import numpy as np”,则强烈暗示不应将“np”用作通用变量名。很明显,你可以用类名来完成同样的事情,但用户熟悉度很重要。
但是,在包内部,我更喜欢使用静态方法。我的软件架构是面向对象的,我用 Eclipse 编写,我用它来编写多种目标语言。打开源文件可以很方便的看到顶层的类定义,缩进一级的方法定义等等。此级别代码的受众主要是其他分析师和开发人员,因此最好避免使用特定语言的习语。
我对 Python 命名空间管理没有多少信心,尤其是在使用设计模式时(比如说)一个对象传递一个对自身的引用,以便被调用的对象可以调用在调用者上定义的方法。所以我尽量不要强迫它太远。我使用了很多完全限定名称和显式实例变量(使用 self),而在其他语言中,我可以依靠解释器或编译器更紧密地管理范围。使用类和静态方法更容易做到这一点,这就是为什么我认为它们是抽象和信息隐藏最有用的复杂包的更好选择。
【讨论】:
好点。同样在 Jupyter 中,定义一个单独的函数更方便;特别是如果您想为其添加一些示例或测试(特别是如果使用nbdev
)【参考方案2】:
BrenBarn 的回答很好,但我会更改'如果它不需要访问类或实例,请将其设为函数' 为:
'如果它不需要访问类或实例...但是主题相关的类(典型示例:其他类方法使用或使用的辅助函数和转换函数通过备用构造函数),然后使用 staticmethod
否则将其设为模块函数
【讨论】:
好吧,make_from_XXX()
听起来像是一个替代构造函数,所以它不适合作为静态方法。
@Gall 你是对的,我改变了那个评论。 Using static methods in python - best practice有很好的讨论
静态方法实际上只是一个装饰性的东西。静态方法和函数之间几乎没有区别,因为静态方法仅从 args 获取所有信息,就像普通函数一样。您可以获取任何 func 并将其作为静态对象粘贴到类上,反之亦然,除了调用它们的方式之外,它不会改变任何东西。
@hayavuk:概念上的区别在于我们表示静态方法“属于”特定类,并且它的代码、文档字符串、文档、相关常量等应该放在一起,在同一个文件。如果我们不这样做,请考虑例如datetime
模块:暗示不应该在任何旧整数上调用 datetime.fromtimestamp()
。
@smci 我仍然会将“我们的信号”归档为装饰性的东西。它并没有真正改变fromtimestamp()
的行为方式。我认为fromtimestamp()
应该是一个类方法,而不是一个静态方法。【参考方案3】:
这不是一个真正的答案,而是一个冗长的评论:
更令人费解的是这段代码:
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
的语义。
【讨论】:
【参考方案4】:考虑它的最直接的方法是考虑该方法需要什么类型的对象才能完成其工作。如果您的方法需要访问实例,请将其设为常规方法。如果它需要访问该类,请将其设为类方法。如果它不需要访问类或实例,请将其设为函数。很少需要将某些东西设为静态方法,但如果你发现你希望一个函数与一个类“分组”(例如,这样它可以被覆盖),即使它不需要访问该类,我猜你可以让它成为一个静态方法。
我要补充一点,将函数放在模块级别不会“污染”命名空间。如果要使用这些函数,它们不会污染命名空间,它们会按照应该使用的方式使用它。函数是模块中的合法对象,就像类或其他任何东西一样。如果没有任何理由存在,就没有理由在类中隐藏函数。
【讨论】:
当我写关于污染命名空间的文章时,如果有人要“从 Foo 导入 *”,我会更多地考虑更多潜在的名称冲突。我想每个程序员都有责任注意这一点。 这就是不鼓励from Foo import *
的原因。如果这些函数是模块内部的而不是公共 API 的一部分,您可以使用前导下划线命名它们,并且在您执行 from Foo import *
时它们不会被导入。
+1 for If the functions are meant to be used, they're not polluting the namespace, they're using it just as it should be used.
如果这会造成污染,那么如果您只是要避免使用命名空间,那为什么还要使用命名空间呢?
这里对这些装饰器之间的区别进行了很好的概述:***.com/a/1669524/1080804以上是关于模块函数 vs 静态方法 vs 类方法 vs 无装饰器:哪个成语更 Pythonic?的主要内容,如果未能解决你的问题,请参考以下文章