一直对python中@classmethod和@staticmethod的用法和区别不是很理解,一顿google之后发现还是stackoverflow真的是程序员的好朋友.在这个问题Meaning of @classmethod and @staticmethod for beginner?中,几个大神的回答还是很好的.

@Rostyslav Dzinko这样说

虽然classmethod和staticmethod十分相似,但他们之间确实存在一些细微的差别:classmethod必须有一个类对象作为方法的第一个参数,然而staticmethod却可以不需要参数. 下面👇,举个例子🌰:

样板代码

假设我们需要一个类来处理和日期相关的信息:

1
2
3
4
5
6
class Date(object):

    def __init__(self, day=0, month=0, year=0):
        self.day = day
        self.month = month
        self.year = year

显然,这个类能够存储一些日期相关的信息,通过__init__方法,我们传入day,month和year能够实例化一个Date对象,其中__init__方法的第一个参数self就代表我们新建的Date实例.

Class Method

通过@classmethod我们可以比较优雅的完成一些任务.如果仅仅通过__init__方法来完成Date类的实例化,就必须这样实现:x = Date(day,month,year).如果现在想要将一个日期的字符串形式(‘dd-mm-yy’)转为Date对象,我们需要完成这两个步骤:

  1. 将字符串转为day,month,year三个整型对象或者一个包含三个值的元组;
  2. 通过__init__方法完成Date对象的实例化. 上边的两步实现过程就像这样:
1
2
day, month, year = map(int, string_date.split('-'))
date1 = Date(day, month, year)

在其他编程语言中,以c++为例,它可以重构自己的构造函数来接受某个日期的字符串形式最终返回一个Date实例.但是python没有这样的特性,于是classmethod就在这里派上了用场:

1
2
3
4
5
6
@classmethod
    def from_string(cls, date_as_string):
        day, month, year = map(int, date_as_string.split('-'))
        date1 = cls(day, month, year)
        return date1
date2 = Date.from_string('11-09-2012')

上边利用classmethod来完成将字符串转为Date实例主要有这些优势:

  1. 将字符串转化的过程放在类中,并且能够重用;
  2. 封装的较好,符合面向对象思想;
  3. classmethod中的cls代表Date,它不是类的一个实例,就是类对象本身,如果我们派生了其他的子类,它们也都能继承from_string方法.

Static Method

说完了classmethod,接着唠一唠staticmethod.它其实和classmethod十分相似,但是它不需要像类方法或者普通的实例方法一样需要必须的参数(cls或者self). 再举个例子🌰: 通过classmethod我们完成了将一个字符串转为Date实例的过程,现在给我们一个字符串,在使用Date.from_string(‘str’)生成实例之前,判断这个str是否满足要求. 很显然,这个方法和类Date有密切的联系,但仅仅判断一个字符串是否满足转换的要求,并不需要实例化一个Date对象,这时候staticmethod就可以派上用场:

1
2
3
4
5
6
7
@staticmethod
    def is_date_valid(date_as_string):
        day, month, year = map(int, date_as_string.split('-'))
        return day <= 31 and month <= 12 and year <= 3999

# usage:
is_date = Date.is_date_valid('11-09-2012')

也就是说,staticmethod可以像一个普通的方法被调用,它与这个类有明确的相关性,但是不需要访问这个类内部的属性或者方法.

@Yaw Boakye补充说

Rostyslav Dzinko说的非常好,不过他(Yaw Boakye)从另外一个方面对classmethod和staticmethod的区别做了补充: 在上边的例子中,使用@classmethod from_string作为一个生成Date实例的工厂,然而通过@staticmethod也可以完成类似的操作:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Date:
	def __init__(self, month, day, year):
    	self.month = month
    	self.day   = day
    	self.year  = year


  	def display(self):
    	return "{0}-{1}-{2}".format(self.month, self.day, self.year)


  	@staticmethod
  	def millenium(month, day):
    	return Date(month, day, 2000)

new_year = Date(1, 1, 2013)               # Creates a new Date object
millenium_new_year = Date.millenium(1, 1) # also creates a Date object. 

# Proof:
new_year.display()           # "1-1-2013"
millenium_new_year.display() # "1-1-2000"

isinstance(new_year, Date) # True
isinstance(millenium_new_year, Date) # True

通过@staticmethod millenium我们现在也可以生成类似于工厂函数@classmethod from_string的功能,生成Date实例.但是这样的实现却有点硬编码的嫌疑,因为@staticmethod millenium的最终返回结果是通过return Date(month, day, 2000)这句代码实现的,也就是说它明确了返回对象就是就是一个Date的实例. 下面我们派生一个Date的子类DateTime:

1
2
3
4
5
6
7
8
9
class DateTime(Date):
  def display(self):
      return "{0}-{1}-{2} - 00:00:00PM".format(self.month, self.day, self.year)

datetime1 = DateTime(10, 10, 1990)
datetime2 = DateTime.millenium(10, 10)

datetime1.display() # returns "10-10-1990 - 00:00:00PM"
datetime2.display() # returns "10-10-2000"

我们可以看到datetime1.display()的返回值"10-10-1990 - 00:00:00PM"datetime2.display()的返回值是returns "10-10-2000",这是怎么回事?原因在于datetime1和datetime2实例创建的方法是不同的:

  1. datetime1通过DateTime的__init__方法生成 isinstance(datetime1, DateTime) # True
  2. datetime2通过DateTime.millenium方法生成 isinstance(datetime2, DateTime) # False

这里就可以看到@staticmethod和@classmethod的一些不同,将millenium使用@classmethod重写:

1
2
3
@classmethod
def millenium(cls, month, day):
    return cls(month, day, 2000)

这里的cls替代了之前的Date,它可以是任何一个派生出来的子类,millenium返回的实例当然也是对应的cls的实例.

1
2
3
4
5
6
7
8
9
datetime1 = DateTime(10, 10, 1990)
datetime2 = DateTime.millenium(10, 10)

isinstance(datetime1, DateTime) # True
isinstance(datetime2, DateTime) # True


datetime1.display() # "10-10-1990 - 00:00:00PM"
datetime2.display() # "10-10-2000 - 00:00:00PM"

小小的总结一下

通过这两个很精彩的解释,现在对@classmethod和@staticmethod有了进一步的理解:

  1. @classmethod,由于其强制要求有cls参数存在,可以更多的用于当作一个类实例工厂🏭,或者可以作为一个可以用于派生类中的构造函数;
  2. @staticmethod,如果一个方法不需要使用类内部的属性和方法,但确实和类有明确的相关性,它就可以使用@staticmethod来修饰.