Python中@classmethod和@staticmethod的区别

Python中@classmethod和@staticmethod的区别

接上一篇介绍Python中@staticmethod和@classmethod的用法的文章。虽然@classmethod@staticmethod非常相似,但两个修饰符的使用情况仍略有不同

从它们的使用上来看:

  • @classmethod必须引用一个类对象作为第一个参数,即第一个参数需要是表示自身类的cls参数。同时@classmethod因持有cls参数,所以可以调用类的属性,类的方法,实例化对象等,避免硬编码。
  • @staticmethod则可以完全没有参数,但在@staticmethod中要调用到这个类的一些属性方法,只能直接类名.属性名或类名.方法名()。

示例

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

@classmethod
def from_string(cls, date_as_string):
day, month, year = map(int, date_as_string.split("-"))
date1 = cls(day, month, year)
return date1

@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


date2 = Date.from_string("27-10-2018")
is_date = Date.is_date_valid("27-10-2018")

解释

让我们假设这样一个类的例子,用来处理日期信息(这将是我们的样板):

1
2
3
4
5
class Date(object):
def __init__(self, day=0, month=0, year=0):
self.day = day
self.month = month
self.year = year

显然,这个类可以用来存储关于某些日期的信息(没有时区信息;假设所有日期都以UTC表示)。

这个类中有__init__,它是Python类实例的初始化方法,它接收参数作为类实例方法,具有第一个非可选参数self(作为对新创建实例的引用)。

Class Method

我们有一些任务,通过使用@classmethod可以很好地完成它们。

假设我们想要创建许多Date类实例,其日期信息来自外部输入(编码格式为’dd-mm-year’的字符串),并假设我们必须在项目源代码的不同位置执行此操作。

所以我们这里必须做到:

  1. 解析输入的字符串以接收日、月、年作为三个整数变量或由这些变量组成的三元组。
  2. 通过将上面求到的值传递给初始化调用来创建Date类实例。

代码看起来会是这样:

1
2
day, month, year = map(int, string_date.split('-'))
date1 = Date(day, month, year)

如果使用@classmethod修饰符写在类中,将会是:

1
2
3
4
5
6
7
    @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('27-10-2018')

让我们更仔细地看看上面的代码实现,并回想一下我们做了什么?

  • 我们在一个地方实现了日期字符串解析函数,现在它可以重用。

  • 将日期字符串解析函数封装在类中并且工作正常(当然你可以在其他地方实现日期字符串解析作为单个函数,但这个解决方案更适合OOP范例)。

  • cls是一个保存类本身的对象,而不是类的实例。这很酷😎,因为如果我们继承Date类,所有子类也会定义from_string()

Static Method

@staticmethod确实与@classmethod很相似,但@staticmethod不需要任何强制性参数(如类方法或实例方法)。

让我们看看下一个任务(下一个用例):

假设我们有一个日期字符串,我们想要以某种方式进行验证它是否符合要求的格式。此任务也需要封装在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('27-10-2018')

运行上述代码得到is_date是个boolen型变量,而非is_date_valid函数返回的day,month,year三个整型数据。

因此,我们可以从@staticmethod的使用中看到,我们无法访问类的内容——它基本上只是一个函数,在语法上称为方法,无法访问对象及其内部(字段和其他类方法)。而使用@classmethod却可以做到。

补充

上面的文章已经很全面地总结了@classmethod@staticmethod的区别。在这里我想强调当你创建构造函数时,你应该选择@classmethod而不是@staticmethod的另一个原因。在上面的例子中,使用@classmethod from_string()作为Factory,接收不符合__init__要求的参数创建Date类实例。使用@staticmethod可以完成同样的操作,如下面代码所示:

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

运行结果显示new_yearmillenium_new_year都是Date类实例。

但是,如果仔细观察就会发现,millenium_new_year是以硬编码的方式创建的Date类实例。这意味着即使一个类继承Date类,该子类仍将创建普通的Date对象即父类对象,而不具有该子类本身的任何属性。请参阅以下示例代码:

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

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

datetime1.display() # returns "10-10-1990 - 00:00:00PM"
datetime2.display() # returns "10-10-2000" because it's not a DateTime object but a Date object. Check the implementation of the millenium method on the Date class

DateTime类继承Date类,因此具有Date类的millenium()方法。datetime2通过调用DateTime继承来的millenium()方法来创建DateTime类实例。然而代码却显示datetime2并不是DateTime类实例(isinstance(datetime2, DateTime) # False)。怎么回事?这是因为使用了@staticmethod修饰符

在大多数情况下,这是你不希望出现的。如果你想要的是一个”完整“的类实例,并且是通过调用它的父类方法所创建的话,那么@classmethod就是你所需要的。

Date.millenium()重写为(这是上述代码中唯一改变的部分):

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

确保该类的创建不是通过硬编码。cls可以是任何子类,生成的对象将正确地成为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你不希望出现的情况就会消失。使用了@staticmethod修饰符定义构造函数就是问题出现的关键。

文章的内容有点多,可能需要花一些时间进行理解,最后提供一个小示例帮助大家加深记忆一下@classmethod@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
25
26
class A(object):
bar = 1
def foo(self):
print 'foo'

@staticmethod
def static_foo():
print 'static_foo'
print A.bar

@classmethod
def class_foo(cls):
print 'class_foo'
print cls.bar
cls().foo()

A.static_foo()
A.class_foo()


output:
static_foo
1
class_foo
1
foo

引用文章:

  1. 飘逸的python - @staticmethod和@classmethod的作用与区别 - mattkang - CSDN博客
  2. python - Meaning of @classmethod and @staticmethod for beginner? - Stack Overflow