类方法的目的是什么?

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

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

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

151955 次浏览

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

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

如果你有一个MyClass类,和一个操作MyClass的模块级函数(工厂,依赖注入存根等),让它成为classmethod。然后它将可用于子类。

诚实?我从未发现staticmethod或classmethod的用途。我还没有看到一个操作不能使用全局函数或实例方法来完成。

如果python像Java那样使用private和protected成员,情况将有所不同。在Java中,我需要一个静态方法来访问实例的私有成员来做一些事情。在Python中,很少需要这样做。

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

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

基本上,类方法适用于任何您希望有一个方法自然地适合类的名称空间,但不与类的特定实例相关联的时候。

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

当前目录

    <李> Path.cwd()
    • 返回实际的当前目录;例如,Path("/tmp/my_temp_dir")。这是一个类方法。
    • 李< / ul > < / > <李> .chdir()
      • 使self为当前目录。
      • 李< / ul > < / >

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

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

我以前用过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()

聪明,不是吗?

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

所以我使用类变量和@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()

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

例如:你有一组表示对象的类。你可能想让类方法all()find()来编写User.all()User.find(firstname='Guido')。当然,这可以使用模块级函数来实现……

可以这样想:普通方法对于隐藏分派的细节很有用:你可以输入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()方法,该数据库后端使用导入特定于供应商的数据库库需要随需应变,是一个类方法,因为它需要运行之前开始创建一个特定的数据库连接的实例,但它绝不是一个简单的函数或静态函数,因为他们希望它能够调用其他支持方法可能同样需要写更具体子类的父类。如果你分派给一个函数或静态类,那么你就会“忘记”并失去关于哪个类在进行初始化的知识。

我认为最明确的答案是__abc01。归根结底,这取决于你想如何组织你的代码。你可以把所有东西都写成模块级的函数,这些函数被包装在模块的命名空间中

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()时,您可以消除这个问题,并且两个复制函数可以并排使用。

当用户登录我的网站时,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()

这是一个有趣的话题。我对它的理解是,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

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

例如:

@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

我也问过自己几次同样的问题。尽管这里的家伙努力解释它,恕我直言,我找到的最好的答案(也是最简单的)答案是Python文档中Class方法的描述

还有对静态方法的引用。如果有人已经知道实例方法(我假设是这样),这个答案可能是把它们放在一起的最后一块……

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

当然,类定义了一组实例。类的方法作用于单个实例。类方法(和变量)用于将与实例集相关的其他信息挂起。

例如,如果你的类定义了一组学生,你可能想要类变量或方法来定义学生可以成为成员的年级集。

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

这样的技术往往会导致将实例集的成员存储到植根于类变量的数据结构中。这时,您需要注意避免使垃圾收集受挫。

刚刚从Ruby中让我震惊的是,所谓的方法和所谓的实例方法只是一个应用于其第一个参数的语义函数,当函数作为对象(即obj.meth())的方法被调用时,该参数被无声地传递。

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

因为它只是一个函数,它只能在任何给定的作用域中声明一次(即class定义)。因此,对于ruby开发者来说,类方法和实例方法不能同名. If是一个意外。

考虑一下:

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作为第一个参数,在类方法中使用__ABC1。我在这里用这两个都不是来说明它只是一个论点。在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专家,如果有任何错误,我希望得到评论。

@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']相同的值。

类和对象概念在组织事物时非常有用。的确,方法可以完成的所有操作也可以使用静态函数完成。

想象一个场景,建立一个学生数据库系统来维护学生的详细信息。 你需要了解学生、老师和员工的详细信息。您需要构建计算费用、工资、分数等的函数。费用和分数只适用于学生,工资只适用于员工和教师。因此,如果你为每种类型的人创建单独的类,代码将被组织起来

如果你不是一个“训练有素的程序员”,这应该有助于:

我想我已经理解了上面和网上其他地方的技术解释,但我总是有一个问题:“不错,但我为什么需要它?”什么是实际的用例?现在生活给了我一个很好的例子来阐明一切:

我使用它来控制由多线程模块实例化的类的实例之间共享的全局共享变量。在人性化的语言中,我正在运行多个代理,为深度学习并行创建示例。(想象多个玩家同时玩ATARI游戏,每个人都将他们的游戏结果保存到一个公共存储库(SHARED VARIABLE))

我用以下代码实例化玩家/代理(在主/执行代码中):

a3c_workers = [A3C_Worker(self.master_model, self.optimizer, i, self.env_name, self.model_dir) for i in range(multiprocessing.cpu_count())]
    它创建了许多播放器,因为我的comp上有处理器核心 A3C_Worker——定义代理的类 A3c_workers -是该类实例的列表(即每个实例是一个玩家/代理)

现在我想知道所有玩家/代理玩了多少游戏,因此在A3C_Worker定义中,我定义了所有实例共享的变量:

class A3C_Worker(threading.Thread):
global_shared_total_episodes_across_all_workers = 0

现在,当工人们完成他们的游戏时,每完成一场比赛,他们都会增加1个数字

在我的示例生成结束时,我关闭了实例,但共享变量已经分配了所玩游戏的总数。所以当我重新运行它时,我最初的总集数是之前的总和。但是我需要这个计数来代表每次单独运行的值

为了解决这个问题,我指定:

class A3C_Worker(threading.Thread):
@classmethod
def reset(cls):
A3C_Worker.global_shared_total_episodes_across_all_workers = 0

在执行代码中调用:

A3C_Worker.reset()

注意,它是对CLASS整体的调用,而不是它单独的任何实例。因此,从现在开始,对于我发起的每个新代理,它将把我的计数器设置为0。

使用通常的方法定义def play(self):,将需要我们为每个实例单独重置该计数器,这将需要更多的计算,并且难以跟踪。