05月14, 2022

classmethod 和 staticmethod 是怎样实现的

在定义 Python 类时,成员方法有:

  • 普通方法,第一个参数为实例,通常命名为 self;
  • 类方法,第一个参数为类对象,通常命名为 cls;
  • 静态方法,没有特殊参数。
class MyClass:
  def hello(self):
    '''
    普通方法
    '''
    print("world")

  @classmethod
  def className(cls):
    '''
    类方法
    '''
    print(cls.__name__)

  @staticmethod
  def sum(a, b):
    '''
    静态方法
    '''
    return a + b

装饰器(Decorator)

定义类方法和静态方法需要两个装饰器:

  • classmethod
  • staticmethod

这是两个 Python 内置的方法,可以在 Python 中查看它们的类型:

>>> classmethod
<class 'classmethod'>
>>> staticmethod
<class 'staticmethod'>

它们是两个类。

我们之前的文章介绍过装饰器(Decorator):

  • 它是 1 个函数;
  • 参数也是 1 个函数;
  • 返回值还是 1 个函数;

下面是一个简单的例子:

def myDecorator(func):
  def wrapper(*args, **kargs):
    print("执行前")
    ret = func(*args, **kargs)
    print("执行后")
    return ret

  return wrapper

@myDecorator
def hello():
  print("world")


hello()

执行结果:

pi@raspberrypi:~ $ python3 test.py 
执行前
world
执行后

装饰器的意思是,以传入的函数为基础,在函数前后增加一些 “装饰” 代码。这有点像快递,打包机是装饰器,商品是输入的函数,快递盒就是 “装饰”,完整的包裹是输出的函数,打包机为商品增加了外观保护功能。

image.png

其中 @myDecorator 实际上是个语法糖,它会被 Python 解析为下面的等价形式:

def hello():
  print("world")
hello = myDecorator(hello)

刚才看到 classmethod, staticmethod 明明都是 class,这是怎么回事呢?

实际上只要是 callable 对象都可以,在 Python 中可以验证 classmethod, staticmethod 是否 callable:

>>> callable(classmethod)
True
>>> callable(staticmethod)
True

这两个装饰器被 Python 语法糖转换以后,相当于:

  def className(cls):
    print(cls.__name__)
  className = classmethod(className)

  def sum(a, b):
    return a + b
  sum = staticmethod(sum)

刚好是这两个类的实例化,参数分别是两个成员方法,最后的结果是 MyClass 拥有 className 和 sum 两个成员,但类型都不是 function:

>>> vars(MyClass)
mappingproxy({'__module__': '__main__', 'hello': <function MyClass.hello at 0x7f80592940>, 'className': <classmethod object at 0x7f80598fd0>, 'sum': <staticmethod object at 0x7f80598fa0>, '__dict__': <attribute '__dict__' of 'MyClass' objects>, '__weakref__': <attribute '__weakref__' of 'MyClass' objects>, '__doc__': None})

注意到它们分别变成了:

- 'className': <classmethod object at 0x7f80598fd0>
- 'sum': <staticmethod object at 0x7f80598fa0>

Descriptor(表示器)

Python 是一种动态语言,没有提供类似 interface 这样的机制,但 Python 规定了一系列协议(Protocol)。我觉得 Protocol 翻译为惯例或者约定更容易理解。

image.png

只要一个对象提供了指定的接口,那么它就满足这个 Protocol。Python 没有强制的接口检查,这种协议更接近调用约定。常见的比如 Iterator(迭代器)、Generator(生成器)、ContextManager(用于支持 With)等等,可以在 Python 文档中找到所有协议的介绍。

classmethod 和 staticmethod 类都实现了表示器(Descriptor)协议,实现了下述方法(全部或部分):

  • __get__(), 处理实例的查询;
  • __set__(), 处理实例的赋值;
  • __delete__(),处理实例的删除;

下面是 Python 文档中的例子:

class Ten:
    def __get__(self, obj, objtype=None):
        return 10
class A:
    x = 5                       # Regular class attribute
    y = Ten()                   # Descriptor instance

>>> a = A()                     # Make an instance of class A
>>> a.x                         # Normal attribute lookup
5
>>> a.y                         # Descriptor lookup
10

需要注意的是,表示器协议需要对象作为类或者实例的属性时才有效。

再回到 classmethodstaticmethod, 确认它们实现了 __get__ 方法:

>>> dir(MyClass.className)
['__call__', '__class__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__func__', '__ge__', '__get__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__self__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__']
>>> dir(MyClass.sum)
['__annotations__', '__call__', '__class__', '__closure__', '__code__', '__defaults__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__get__', '__getattribute__', '__globals__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__kwdefaults__', '__le__', '__lt__', '__module__', '__name__', '__ne__', '__new__', '__qualname__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__']

前面注意到,用 @classmethod@staticmethod 定义方法时,语法糖只是初始化了 classmethodstaticmethod 实例,对比前面提到的装饰器,“包装” 过程应当在访问属性时实现:

        # Decorator obj as an attibute, this method handlers the getattr call.
    def __get__(self, obj, obj_type = None):
        # warp the original function
        def wrapper(*args,**kargs):
            return self._func(obj_type, *args, **kargs)

        return wrapper

__get__ 方法接受 3 个参数(以 classmethod 装饰器说明):

  • self,属性自身,也就是 MyClass.className
  • obj,MyClass的实例,对于 classmethod,显然不需要实例;
  • obj_type,MyClass类。

显然,装饰函数值需要将 obj_type 插入到函数的参数列表第一个位置即可实现 `classmethod。

对于 staticmethod 则更简单,直接返回内部函数即可,直接忽略 objobj_type 两个参数,这也是静态函数和一般的独立函数一样的原因,因为静态函数就是一个保存在类中的独立函数。

总结

下面是我实现的类方法和静态方法装饰器,相信你已经了解它们是如何工作的。

# define our own classmethod decorator
class myClassMethod():
    # init a Decorator obj, see the Decorator protocol here: https://docs.python.org/3/glossary.html#term-descriptor
    def __init__(self,func):
        self._func = func

    # Decorator obj as an attibute, this method handlers the getattr call.
    def __get__(self, obj, obj_type = None):
        # warp the original function
        def wrapper(*args,**kargs):
            return self._func(obj_type, *args, **kargs)
        return wrapper

class myStaticMethod():
    def __init__(self, func):
        self._func = func
    def __get__(self, obj, obj_type = None):
        return self._func


# try it
class Test:
    @myClassMethod
    def hello(cls):
        print("world.")

    @myStaticMethod
    def sum(a, b):
        return a+b

t = Test() 

# works as classmethod and staticmethod does.

Test.hello()    # world
t.hello()       # world

print(Test.sum(1, 2)) # 3
print(t.sum(1, 2))    # 3

可以看到,它们工作良好。

今天捎带着介绍了一点我对 Protocol 的理解,重温 Decorator 的用法,认识了一个新的 Protocol:Descriptor。可以看到 Python 中灵活应用 Protocol,做很多有趣的东西。

本文链接:http://www.thinkinpython.com/post/how_classmethod_staticmethod_implements.html

-- EOF --