类方法的目的是什么?


250

我正在自学Python,最近的课程是Python不是Java,因此我花了一段时间将所有Class方法都转换为函数。

我现在意识到我不需要使用Class方法来完成staticJava中的方法,但是现在我不确定何时使用它们。我可以找到的有关Python类方法的所有建议都是像我这样的新手提出的,应该避免使用它们,并且在讨论它们时,标准文档最为模糊。

有没有人有在Python中使用Class方法的好例子,或者至少有人可以告诉我何时可以明智地使用Class方法?

Answers:


178

类方法用于需要不特定于任何特定实例但仍以某种方式涉及到类的方法。关于它们的最有趣的事情是它们可以被子类覆盖,这在Java的静态方法或Python的模块级函数中根本是不可能的。

如果您有一个类MyClass,以及一个在MyClass上运行的模块级函数(工厂,依赖项注入存根等),请将其设置为classmethod。然后它将对子类可用。


我懂了。但是,如果我只想对不需要MyClass的方法进行分类,但是如果我将其他MyClass分组,那从某种意义上还是有意义的呢?我可以想到将其移到辅助模块中
。.– swdev

我有一个与原始问题有关的问题,也许您可​​以同时回答?调用对象的类方法的哪种方式是“更好”或“更惯用”:obj.cls_mthd(...)type(obj).cls_mthd(...)
Alexey '18

63

工厂方法(替代构造函数)确实是类方法的经典示例。

基本上,类方法适合任何您想自然地适合于类的名称空间但不与类的特定实例相关联的方法。

例如,在出色的unipath模块中:

当前目录

  • Path.cwd()
    • 返回实际的当前目录;例如Path("/tmp/my_temp_dir")。这是一个类方法。
  • .chdir()
    • 使自己成为当前目录。

由于当前目录是进程范围的,因此该cwd方法没有应与之关联的特定实例。但是,将更cwd改为给定Path实例的目录确实应该是一个实例方法。

嗯... Path.cwd()确实确实返回了一个Path实例,我想它可以被认为是工厂方法...


1
是的...工厂方法是我想到的第一件事。同样遵循这些原则的是可以实现Singleton模式的事物,例如:getAndOptionallyCreateSomeSingleInstanceOfSomeResource()
Jemenake 2013年

3
在python中,这些是静态方法,而不是类方法。
Jaykul 2014年

46

这样考虑:普通方法对隐藏调度细节很有用:您可以键入myobj.foo()而不必担心该foo()方法是由myobj对象的类或其父类之一实现的。类方法与此完全类似,但是使用类对象:它们使您可以调用,MyClass.foo()而不必担心是否由于需要它自己的专用版本foo()而特别实现MyClass,或者是否让其父类处理该调用。

当您在创建实际实例之前进行设置或计算时,类方法必不可少,因为在该实例存在之前,您显然不能将实例用作方法调用的分配点。可以在SQLAlchemy源代码中查看一个很好的示例。dbapi()在以下链接中查看类方法:

https://github.com/zzzeek/sqlalchemy/blob/ab6946769742602e40fb9ed9dde5f642885d1906/lib/sqlalchemy/dialects/mssql/pymssql.py#L47

您可以看到,dbapi()数据库后端用来按需导入特定于供应商的数据库库的方法是一个类方法,因为它需要开始创建特定数据库连接的实例之前运行但是它不能运行是一个简单的函数或静态函数,因为他们希望它能够调用其他支持的方法,这些方法可能类似地需要在子类中而不是在其父类中更具体地编写。而且,如果您分派给一个函数或静态类,那么您将“忘记”并失去有关哪个类正在执行初始化的知识。


链接
断开

28

我最近想要一个非常轻量级的日志记录类,该类将根据可以通过编程设置的日志记录级别输出不同数量的输出。但是我不想每次想输出调试消息,错误或警告时都实例化该类。但是我还想封装该日志记录工具的功能,并在不声明任何全局变量的情况下使其可重用。

因此,我使用了类变量和@classmethod装饰器来实现这一点。

使用简单的Logging类,可以执行以下操作:

Logger._level = Logger.DEBUG

然后,在我的代码中,如果我想吐出一堆调试信息,我只需要编写代码

Logger.debug( "this is some annoying message I only want to see while debugging" )

错误可能会被解决

Logger.error( "Wow, something really awful happened." )

在“生产”环境中,我可以指定

Logger._level = Logger.ERROR

现在,将仅输出错误消息。调试消息将不会打印。

这是我的课:

class Logger :
    ''' Handles logging of debugging and error messages. '''

    DEBUG = 5
    INFO  = 4
    WARN  = 3
    ERROR = 2
    FATAL = 1
    _level = DEBUG

    def __init__( self ) :
        Logger._level = Logger.DEBUG

    @classmethod
    def isLevel( cls, level ) :
        return cls._level >= level

    @classmethod
    def debug( cls, message ) :
        if cls.isLevel( Logger.DEBUG ) :
            print "DEBUG:  " + message

    @classmethod
    def info( cls, message ) :
        if cls.isLevel( Logger.INFO ) :
            print "INFO :  " + message

    @classmethod
    def warn( cls, message ) :
        if cls.isLevel( Logger.WARN ) :
            print "WARN :  " + message

    @classmethod
    def error( cls, message ) :
        if cls.isLevel( Logger.ERROR ) :
            print "ERROR:  " + message

    @classmethod
    def fatal( cls, message ) :
        if cls.isLevel( Logger.FATAL ) :
            print "FATAL:  " + message

和一些测试它的代码:

def logAll() :
    Logger.debug( "This is a Debug message." )
    Logger.info ( "This is a Info  message." )
    Logger.warn ( "This is a Warn  message." )
    Logger.error( "This is a Error message." )
    Logger.fatal( "This is a Fatal message." )

if __name__ == '__main__' :

    print "Should see all DEBUG and higher"
    Logger._level = Logger.DEBUG
    logAll()

    print "Should see all ERROR and higher"
    Logger._level = Logger.ERROR
    logAll()

10
在我看来,这似乎并不是一个好的示例类方法所擅长的,因为可以通过仅使所有类方法成为模块级函数并完全消除该类,而以一种不太复杂的方式完成所有操作。
martineau 2010年

10
哦,更重要的是,Python提供了一个非常简单易用的日志记录类。与创建一个并非最佳解决方案相比,我更糟糕了,我重新发明了轮子。双手巴掌。但这至少提供了一个有效的例子。但是,从概念上讲,我不确定我是否同意放弃该类。有时,您希望封装代码以便于重用,而日志记录方法是重用的理想目标。
Marvo 2010年

3
为什么不使用静态方法?
bdforbes 2011年


10

当用户登录我的网站时,将从用户名和密码实例化User()对象。

如果我需要一个用户对象而不需要用户在那里登录(例如,管理员用户可能要删除另一个用户帐户,那么我需要实例化该用户并调用其delete方法):

我有获取用户对象的类方法。

class User():
    #lots of code
    #...
    # more code

    @classmethod
    def get_by_username(cls, username):
        return cls.query(cls.username == username).get()

    @classmethod
    def get_by_auth_id(cls, auth_id):
        return cls.query(cls.auth_id == auth_id).get()

9

我认为最明确的答案是AmanKow的答案。归结为您要如何组织代码。您可以将所有内容编写为模块级函数,并包装在模块的名称空间中,即

module.py (file 1)
---------
def f1() : pass
def f2() : pass
def f3() : pass


usage.py (file 2)
--------
from module import *
f1()
f2()
f3()
def f4():pass 
def f5():pass

usage1.py (file 3)
-------------------
from usage import f4,f5
f4()
f5()

上面的过程代码没有很好的组织,正如您看到的只有3个模块感到困惑之后,每种方法有什么作用?您可以为函数使用长的描述性名称(例如Java中的名称),但您的代码仍然很快变得难以管理。

面向对象的方式是将代码分解为可管理的块,即类和对象以及函数可以与对象实例或类相关联。

与模块级别的函数相比,使用类函数可以在代码中获得更高的划分级别。因此,您可以在一个类中对相关功能进行分组,以使它们对您分配给该类的任务更加特定。例如,您可以创建一个文件实用程序类:

class FileUtil ():
  def copy(source,dest):pass
  def move(source,dest):pass
  def copyDir(source,dest):pass
  def moveDir(source,dest):pass

//usage
FileUtil.copy("1.txt","2.txt")
FileUtil.moveDir("dir1","dir2")

这种方式更灵活,更可维护,您可以将功能分组在一起,并且每个功能的作用更加明显。另外,还可以防止名称冲突,例如,函数副本可能存在于您在代码中使用的另一个导入模块(例如,网络副本)中,因此,当您使用全名FileUtil.copy()时,您就可以解决此问题并同时复制两个函数可以并排使用。


1
为了像以前一样使用f1(),f2()等,是否不应该从模块import *和用法import *使用?
Jblasco 2015年

@Jblasco我更正了import语句,以消除混乱,是的,如果您导入模块,则必须在函数之前加上模块名称。即导入模块-> module.f1()等
firephil

我不同意这个答案。Python不是Java。显然,由于示例中故意选择了较差的名称,因此出现了代码难以管理的问题。相反,如果您将FileUtil类方法复制为file_util模块的函数,则在没有对象实际存在的情况下,它是可比较的并且不会滥用OOP(实际上,您可以说这是更可取的,因为您不会以from file_util import FileUtil其他冗长的词结尾)。可以通过import file_util代替来在过程代码中类似地避免名称冲突from file_util import ...
ForgottenUmbrella

7

老实说 我从未找到用于staticmethod或classmethod的方法。我还没有看到使用全局函数或实例方法无法完成的操作。

如果python更像Java那样使用私有成员和受保护成员,那将是不同的。在Java中,我需要一个静态方法来访问实例的私有成员以执行操作。在Python中,这几乎没有必要。

通常,当人们真正需要做的就是更好地使用python的模块级命名空间时,我会看到人们使用staticmethod和classmethod。


1
私人:_variable_name和受保护:__ variable_name
Bradley Kreider

1
一种不可缺少的用途是unittest的 setUpClass和tearDownClass。您正在使用单元测试,对不对?:)
dbn

7

它允许您编写可与任何兼容类一起使用的通用类方法。

例如:

@classmethod
def get_name(cls):
    print cls.name

class C:
    name = "tester"

C.get_name = get_name

#call it:
C.get_name()

如果您不使用@classmethod它,可以使用self关键字来实现,但是它需要一个Class的实例:

def get_name(self):
    print self.name

class C:
    name = "tester"

C.get_name = get_name

#call it:
C().get_name() #<-note the its an instance of class C

5

我曾经使用过PHP,最近又问自己,这种方法是怎么回事?Python手册非常技术性,而且措辞很短,因此对理解该功能没有帮助。我正在谷歌搜索,发现答案-> http://code.anjanesh.net/2007/12/python-classmethods.html

如果您不愿意单击它。我的解释更短一些。:)

在PHP中(也许不是所有人都知道PHP,但是这种语言非常简单,每个人都应该理解我在说什么),我们有如下静态变量:


class A
{

    static protected $inner_var = null;

    static public function echoInnerVar()
    {
        echo self::$inner_var."\n";
    }

    static public function setInnerVar($v)
    {
        self::$inner_var = $v;
    }

}

class B extends A
{
}

A::setInnerVar(10);
B::setInnerVar(20);

A::echoInnerVar();
B::echoInnerVar();

两种情况下的输出均为20。

但是在python中,我们可以添加@classmethod装饰器,因此有可能分别具有输出10和20。例:


class A(object):
    inner_var = 0

    @classmethod
    def setInnerVar(cls, value):
        cls.inner_var = value

    @classmethod
    def echoInnerVar(cls):
        print cls.inner_var


class B(A):
    pass


A.setInnerVar(10)
B.setInnerVar(20)

A.echoInnerVar()
B.echoInnerVar()

聪明吧?


Python示例的一个潜在问题是 B.setInnerVar(20)忽略,它将打印10两次(而不是在第二个未定义no的echoInnerBar()调用上给出错误inner_var。)
martineau 2010年

5

类方法提供“语义糖”(不知道此术语是否被广泛使用)或“语义便利”。

示例:您获得了一组表示对象的类。您可能需要类方法all()find()编写User.all()or User.find(firstname='Guido')。当然可以使用模块级别的功能来完成...


当然,您的示例假定该类正在跟踪其所有实例,并且在创建它们之后就可以访问它们-某些事情不会自动完成。
martineau 2010年

1
“语义糖”听起来很像“语法糖”。
XTL 2012年

3

刚从Ruby袭来的我是,所谓的方法和所谓的实例方法只是一个函数,其第一个参数具有语义含义,当该函数被称为一个对象(即obj.meth())。

通常,该对象必须是实例,但是@classmethod 方法装饰器会更改规则以传递类。您可以在实例上调用类方法(它只是一个函数)-第一个参数将是其类。

因为它只是一个函数,所以只能在任何给定范围(即class定义)中声明一次。因此,如果遵循的话,令Rubyist感到惊讶的是,您不能拥有同名的类方法和实例方法

考虑一下:

class Foo():
  def foo(x):
    print(x)

您可以呼叫foo执行个体

Foo().foo()
<__main__.Foo instance at 0x7f4dd3e3bc20>

但不在课堂上:

Foo.foo()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unbound method foo() must be called with Foo instance as first argument (got nothing instead)

现在添加@classmethod

class Foo():
  @classmethod
  def foo(x):
    print(x)

现在,调用实例将传递其类:

Foo().foo()
__main__.Foo

就像上课一样:

Foo.foo()
__main__.Foo

唯一的约定规定我们self在实例方法和cls类方法上使用第一个参数。我在这里都没有用它来说明它只是一个论点。在Ruby中,self关键字。

与Ruby对比:

class Foo
  def foo()
    puts "instance method #{self}"
  end
  def self.foo()
    puts "class method #{self}"
  end
end

Foo.foo()
class method Foo

Foo.new.foo()
instance method #<Foo:0x000000020fe018>

Python类方法只是一个装饰函数,您可以使用相同的技术来创建自己的装饰器。装饰方法包装实际方法(如果@classmethod它通过附加的类参数)。底层方法仍然存在,已隐藏但仍可访问


脚注:我是在类和实例方法之间的名称冲突引起了我的好奇心之后写的。我距离Python专家还很远,如果其中任何一个错误,我想发表评论。


“当函数作为对象的方法被调用时,它会以静默方式传递”-我认为这是不正确的。AFICT,Python中没有任何内容被“无声地传递”。IMO,从这个意义上讲,Python比Ruby更加明智(除外super())。AFICT,设置或读取属性时会发生“魔术”。obj.meth(arg)Python中的调用(与Ruby不同)仅表示(obj.meth)(arg)。没有任何东西可以在任何地方静默传递,obj.meth它只是一个可调用对象,它接受的参数比创建该参数的函数少。
Alexey '18

obj.meth依次意味着简单getattr(obj, "meth")
Alexey

为来自其他通用语言的用户量身定制一个答案很有意义。但是,这次对话中缺少的一件事是将Python描述符绑定在一起。函数是描述符,当函数是类的属性时,这就是第一个参数“自动”传递给实例的方式。这也是实现类方法的方式,并且在我链接的HOWTO中提供了纯python实现。它也是一个装饰器这一事实是一种辅助
juanpa.arrivillaga

2

这是一个有趣的话题。我认为python classmethod的操作方式像单例操作,而不是工厂操作(工厂返回一个产生的类实例)。之所以成为单例,是因为产生了一个公共对象(字典),但该类仅产生一次,但被所有实例共享。

为了说明这一点,这里是一个示例。请注意,所有实例均引用单个词典。据我了解,这不是工厂模式。这可能是python非常独特的。

class M():
 @classmethod
 def m(cls, arg):
     print "arg was",  getattr(cls, "arg" , None),
     cls.arg = arg
     print "arg is" , cls.arg

 M.m(1)   # prints arg was None arg is 1
 M.m(2)   # prints arg was 1 arg is 2
 m1 = M()
 m2 = M() 
 m1.m(3)  # prints arg was 2 arg is 3  
 m2.m(4)  # prints arg was 3 arg is 4 << this breaks the factory pattern theory.
 M.m(5)   # prints arg was 4 arg is 5

2

我几次问自己这个问题。即使这里的人们努力解释,恕我直言,我发现最好的答案(也是最简单的)是描述 Python文档中有关Class方法。

也有引用静态方法。如果有人已经知道实例方法(我假设),那么这个答案可能是将所有内容放在一起的最后一部分...

也可以在文档中找到有关此主题的更多详细说明: 标准类型层次结构(向下滚动至“ 实例方法”部分)


1

@classmethod从外部资源轻松实例化该类的对象很有用。考虑以下:

import settings

class SomeClass:
    @classmethod
    def from_settings(cls):
        return cls(settings=settings)

    def __init__(self, settings=None):
        if settings is not None:
            self.x = settings['x']
            self.y = settings['y']

然后在另一个文件中:

from some_package import SomeClass

inst = SomeClass.from_settings()

访问inst.x将提供与settings ['x']相同的值。


0

当然,一个类定义了一组实例。一类的方法适用于各个实例。类方法(和变量)用于挂起与所有实例集相关的其他信息的位置。

例如,如果您的班级定义了一组学生,则您可能需要班级变量或方法来定义诸如学生可以成为其成员的年级组之类的东西。

您还可以使用类方法来定义用于处理整个集合的工具。例如,Student.all_of_em()可能会返回所有已知的学生。显然,如果您的实例集具有比一组实例更多的结构,则可以提供类方法来了解该结构。Students.all_of_em(grade ='juniors')

诸如此类的技术往往导致将实例集的成员存储到以类变量为根的数据结构中。您需要注意避免破坏垃圾收集。

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.