python:类方法的目的是什么?

我正在自学Python,而我最近的经验是Python不是Java,所以我花了一些时间将所有的类方法转换成函数。

我现在意识到,我不需要使用类方法来完成我在Java中使用static方法所做的工作,但是现在我不确定什么时候使用它们。关于Python类方法,我能找到的所有建议都是像我这样的新手应该避免使用的,并且在讨论它们时,标准文档是最不透明的。

有没有人有在Python中使用类方法的好例子,或者至少有人能告诉我什么时候可以合理地使用类方法?


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

如果您有一个类MyClass和一个在MyClass(工厂、依赖注入存根等)上操作的模块级函数,那么将它设置为classmethod。然后它将对子类可用。


工厂方法(可选构造函数)确实是类方法的一个经典例子。

基本上,只要您希望有一个方法自然地适合于类的名称空间,但又不与类的特定实例关联,那么类方法就非常适合。

例如,在优秀的unipath模块中:

当前目录

Path.cwd()返回当前目录;例如,Path("/tmp/my_temp_dir")。这是一个类方法。.chdir()使self成为当前目录。

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

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


这样考虑:普通方法对于隐藏分派的细节很有用:您可以输入myobj.foo(),而不用担心myobj对象的类或它的父类之一是否实现了foo()方法。类方法与此完全类似,但是使用的是类对象:它们允许您调用MyClass.foo(),而不必担心foo()是由MyClass专门实现的,因为它需要自己的专门化版本,还是让它的父类处理调用。

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

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

dbapi()方法可以看出,一个数据库后端使用导入特定于供应商的数据库库需要随需应变,是一个类方法,因为它需要运行在一个特定的实例开始创建数据库连接,但它绝不是一个简单的函数或静态函数,因为他们希望它能够调用其他支持方法可能同样需要写更具体子类的父类。如果您分派给函数或静态类,那么您将"忘记"并丢失关于哪个类正在进行初始化的知识。


我最近想要一个非常轻量级的日志类,它可以根据可编程设置的日志级别输出不同数量的输出。但我还想封装这个日志工具的功能,使其无需声明任何全局变量即可重用。

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

使用我的简单日志类,我可以做以下事情:

1
Logger._level = Logger.DEBUG

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

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

错误可以被排除

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

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

1
Logger._level = Logger.ERROR

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

这是我的类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
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

还有一些测试它的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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()


替代构造函数就是一个经典的例子。


我认为最明确的答案是AmanKow的一个。归结起来就是你想要如何组织你的代码。你可以把所有的东西都写成模块级的函数,这些函数被封装在模块的命名空间中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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中),但是您的代码很快就会变得不可管理。

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

与模块级函数相比,使用类函数可以在代码中获得另一个级别的除法。因此,可以在类中对相关函数进行分组,使它们更特定于分配给该类的任务。例如,你可以创建一个文件工具类:

1
2
3
4
5
6
7
8
9
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()时,可以删除问题,并且可以同时使用这两个复制函数。


当用户登录到我的网站时,user()对象由用户名和密码实例化。

如果我需要一个没有用户登录的用户对象(例如,管理员用户可能想删除另一个用户帐户,所以我需要实例化该用户并调用其删除方法):

我有获取user对象的类方法。

1
2
3
4
5
6
7
8
9
10
11
12
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()


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

例如:

1
2
3
4
5
6
7
8
9
10
11
@classmethod
def get_name(cls):
    print cls.name

class C:
    name ="tester"

C.get_name = get_name

#call it:
C.get_name()

如果你不使用@classmethod,你可以用self关键字,但它需要一个类的实例:

1
2
3
4
5
6
7
8
9
10
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


诚实?我从来没有发现静态方法或类方法的用法。我还没有见过不能使用全局函数或实例方法来完成的操作。

如果python像Java那样使用私有和受保护的成员,情况就会有所不同。在Java中,我需要一个静态方法来访问实例的私有成员。在Python中,这几乎没有必要。

通常,当人们真正需要做的只是更好地使用python的模块级名称空间时,我看到他们使用静态方法和类方法。


我以前使用PHP,最近我问自己,这个类方法是怎么回事?Python手册是非常技术性的,而且非常简短,所以它对理解该特性没有帮助。我在google上搜索了很久,终于找到了答案——> http://code.anjanesh.net/2007/12/pythonclassmethods.html。

如果你懒得点击它。我的解释更简短。:)

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class A
{

    static protected $inner_var = null;

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

    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。例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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()

聪明,不是吗?


类方法提供了一个"语义糖"(不知道这个术语是否被广泛使用)——或者"语义便利性"。

示例:您得到了一组表示对象的类。您可能希望使用类方法all()find()来编写User.all()User.find(firstname='Guido')。当然,这可以使用模块级函数来实现……


让我印象深刻的是,来自Ruby的一个所谓的类方法和一个所谓的实例方法只是一个将语义应用于其第一个参数的函数,当函数作为一个对象的方法(即obj.meth())被调用时,它将无声地传递。

通常该对象必须是一个实例,但是@classmethod方法修饰符修改规则来传递一个类。您可以在一个实例上调用一个类方法(它只是一个函数)——第一个argyment将是它的类。

因为它只是一个函数,它只能在任何给定的范围内声明一次(例如class定义)。因此,让Rubyist感到意外的是,您不能拥有同名的类方法和实例方法。

考虑一下:

1
2
3
class Foo():
  def foo(x):
    print(x)

您可以对一个实例调用foo

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

但不是在课堂上:

1
2
3
4
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:

1
2
3
4
class Foo():
  @classmethod
  def foo(x):
    print(x)

调用一个实例现在传递它的类:

1
2
Foo().foo()
__main__.Foo

就像调用一个类一样:

1
2
Foo.foo()
__main__.Foo

只有惯例规定我们对实例方法的第一个参数使用self,对类方法使用cls。我在这里没有使用任何一个来说明这只是一个论点。在Ruby中,self是一个关键字。

与Ruby:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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专家,如果这些都是错误的,我希望得到您的评论。


我好几次问自己同样的问题。尽管这里的人努力地解释它,我发现最好的答案(和最简单的)是Python文档中类方法的描述。

还引用了静态方法。如果有人已经知道实例方法(我假设是这样),那么这个答案可能是将它们组合在一起的最后一部分……

关于这个主题的进一步和深入的阐述也可在文件中找到:标准类型层次结构(向下滚动到实例方法部分)


这是一个有趣的话题。我的观点是,python classmethod的操作方式类似于单例,而不是工厂(工厂返回生成的类实例)。它是单例的原因是有一个公共对象,它只为类生成一次,但由所有实例共享。

这里有一个例子来说明这一点。注意,所有实例都有对单个字典的引用。这不是我所理解的工厂模式。这可能是python所特有的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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


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

例如,如果您的类定义了一组学生,您可能想要类变量或方法来定义学生可以属于的年级集之类的东西。

您还可以使用类方法来定义处理整个集合的工具。例如Student.all_of_em()可能返回所有已知的学生。显然,如果实例集的结构不止一组,则可以提供类方法来了解该结构。Students.all_of_em(等级="下属")

这样的技术倾向于将实例集的成员存储到植根于类变量的数据结构中。您需要小心避免破坏垃圾收集。