@classmethod和@staticmethod

-- TOC --

这是两个Python内置的用于类方法的Python装饰器。@classmethod定义操作class而不是instance的方法,@staticmethod只是定义一个在class这个namespace内的方法。

@classmethod

顾名思义,在很多时候就能够理解百分子七八十。

默认情况下,类方法的第一个参数通常叫做self,它对应对象。而@classmethod这个装饰器,改变的就是第一个参数,被此装饰器修饰的类方法,在调用的时候:

  1. 不通过对象(instance)调用,而是通过类(class)调用;
  2. 第1个参数不是对象,而是类,通常名为cls。

第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涉及的隐藏流畅比较复杂:

  1. 首先,Python解释器会为对象申请一块内存,做一些必要的内部初始化;
  2. 然后,解释器调用__init__(self,...)接口,注意这个接口的第1个入参是self,说明此时对象已经在内存中了;
  3. 最后,解释器返回这个对象。

如果在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__,我觉得需要综合考虑,还要看你个人的代码风格和习惯。

@staticmethod

@classmethod方法脱离了具体对象,@staticmethod就更彻底了,连class都脱离了:

  1. 用类方法来调用被@staticmethod修饰的接口;
  2. 没有默认的第1个cls参数,不会有对象或类传递进去!

我觉得可以这样理解,@staticmethod接口之所以有需要被定义在某个class里面,因为它可能是专门为此class以及对应的instance服务的。也可能需要class来划分一个namespace出来,单独封装一系列特别的接口。不管如何理解,我觉得主要还是满足了封装的需要。

CASE STUDY

再来一个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 --