-- TOC --
这是两个Python内置的用于类方法的Python装饰器。@classmethod定义操作class而不是instance的方法,@staticmethod只是定义一个在class这个namespace内的方法。
顾名思义,在很多时候就能够理解百分子七八十。
默认情况下,类方法的第一个参数通常叫做self,它对应对象。而@classmethod这个装饰器,改变的就是第一个参数,被此装饰器修饰的类方法,在调用的时候:
第1个参数由Python解释器自动提供,本文介绍的这两个装饰器的作用,就是改变了定义在class中的接口的第1个参数。
Python的类和对象,除了定义常规的与对象绑定的成员变量和函数,还可以定义与类自身绑定的类变量,而使用@classmethod修饰的函数接口,我觉得就可以理解为与类绑定的方法。这类方法的主要作用,是用来装饰一种被称为工厂函数Factory Function的东西,通过这类方法做一些检查或入参的转换,然后创建并返回一个对象。
下面是一点测试代码,注意被@classmethod修饰的接口:
class xyz():
def __init__(self, price):
self.price = price
@classmethod
def create(cls, price):
try:
price = float(price)
except Exception:
return
return cls(price)
x1 = xyz(1.234)
print(x1.price)
x2 = xyz.create('2.3456')
print(x2.price)
if x3 := xyz.create('abcde'):
print(x3.price)
else:
print('x3 is None')
class xyz的classmethod create,用来尝试将price转换成一个float类型,然后创建对象并返回。如果转换float失败,返回None。这段代码运行效果如下:
1.234
2.3456
x3 is None
@classmethod其实背后涉及一个代码设计问题,即我们为什么要使用classmethod来创建对象?
直接创建对象不好吗?
直接创建当然OK,以上面代码为例,x1就属于直接创建。但如果这个class是要给别人使用的,你最好多做一些判断,此时就需要在init中做float转换,如果转换失败呢?__init__
这个接口,只能返回None,不管执行成功还是失败。就算只有自己用这个class,也存在一个封装上的小细节,对price做float转换,应该是xyz这个class的自己内部的事情,放在class内进行体现了良好的封装性。因此,我们难以避免要在init中尝试转换float,失败会怎样?
在init中尝试float转换,当然可能失败,但此时return不能用,只能raise。
外围代码此时就必须加上try...except...结构。
而且,代码效率上会受到损失,因为init涉及的隐藏流畅比较复杂:
__init__(self,...)
接口,注意这个接口的第1个入参是self,说明此时对象已经在内存中了;如果在init中raise(只能用raise),除了必须的try...except...结构外,Python解释器还需要收回这个对象的内存。如果class中定义了__del__(self)
接口,在收回内存之前,这个接口也会被调用。
看到了吧,如果使用@classmethod,在申请对象内存之前就完成必要的检查判断,以上这一系列的麻烦事儿就都可以避免。
另一种实现classmethod的方法,是直接使用__new__
接口,示例如下:
class xyz():
def __new__(cls, price):
try:
price = float(price)
except Exception:
return
return object.__new__(cls)
def __init__(self, price):
self.price = price
x1 = xyz(1.234)
print(x1.price)
x2 = xyz('2.3456')
print(x2.price)
if x3 := xyz('abcde'):
print(x3.price)
else:
print('x3 is None')
输出与之前完全一样。
在创建对象时,Python解释器首先调用这个new,在这里面做price的float转换,如果失败就返回None(返回给解释器,解释器判断为None,不再调用init,将None再返回给用户代码),如果成功,调用object.__new__(cls)
,这一行代码并没有将price作为入参,只是申请对象的内存,并返回对象,然后Python解释器再自动去调用init。
其实,此时的init接口显得稍微有些多余了,上面的代码,还是精简成这样:
class xyz():
def __new__(cls, price):
try:
price = float(price)
except Exception:
return
obj = object.__new__(cls)
obj.price = price
return obj
x1 = xyz(1.234)
print(x1.price)
x2 = xyz('2.3456')
print(x2.price)
if x3 := xyz('abcde'):
print(x3.price)
else:
print('x3 is None')
如果使用@classmethod
,也精简成这样:
class xyz():
@classmethod
def create(cls, price):
try:
price = float(price)
except Exception:
return
obj = cls()
obj.price = price
return obj
x1 = xyz.create(1.234)
print(x1.price)
x2 = xyz.create('2.3456')
print(x2.price)
if x3 := xyz.create('abcde'):
print(x3.price)
else:
print('x3 is None')
x4 = xyz()
# how to set price for x4?
使用@classmethod,或使用__new__
,以及是否需要定义__init__
,我觉得需要综合考虑,还要看你个人的代码风格和习惯。
@classmethod方法脱离了具体对象,@staticmethod就更彻底了,连class都脱离了:
我觉得可以这样理解,@staticmethod接口之所以有需要被定义在某个class里面,因为它可能是专门为此class以及对应的instance服务的。也可能需要class来划分一个namespace出来,单独封装一系列特别的接口。不管如何理解,我觉得主要还是满足了封装的需要。
再来一个case,说明和体会,Python解释器是如何做函数接口调用的:
class xyz():
name = 'xyz'
def __init__(self, price):
self.price = price
@classmethod
def get_name(cls):
return cls.name
@staticmethod
def price_tag():
return '$'
x = xyz(1.234)
print(x.price)
print(x.get_name())
print(x.price_tag())
print(xyz.get_name())
print(xyz.price_tag())
classmethod和staticmethod,都可以通过instance调用,也可以通过class调用。
Python装饰器相当于给函数套了一层壳,虽然被套的函数会被调用执行,但在执行前后,可能会多出来一些步骤。class的成员函数,默认第1个参数为instance,@classmethod将第1个参数设定为class,@staticmethod将第1个参数去掉了。同时,这两个装饰器,允许了通过class来调用的方式。
本文链接:https://cs.pynote.net/sf/python/202203151/
-- EOF --
-- MORE --