Python的抽象类学习-1

2017, Nov 03    

最近在通过源代码的方式学习python的抽象类。有一些心得体会,希望能记录下来。学习的时候是基于python3.6,以及cpython master上的源代码来了解的。我对c语言,以及cpython的实现并不熟悉,所以c实现部分只是大概了解了一下。

实现自己的抽象类

python的abc模块中定义了抽象类的metaclass类ABCMeta,以及抽象方法装饰器abstractmethod, abstractclassmethod, abstractstaticmethod,抽象property装饰器abstractproperty等。我们可以基于这些工具来实现自己的抽象类,比如

from abc import ABCMeta
from abc import abstractmethod

class MyAbstractClass(metaclass=ABCMeta):
    
    @abstractmethod
    def my_method(self):
        raise NotImplementedError('my_method is not implemented')

不能实例化抽象类

首先尝试实例化MyAbstractClass看看:

inst = MyAbstractClass()
inst.my_method()

输出错误:
TypeError: Can't instantiate abstract class MyAbstractClass with abstract methods my_method

从错误报告可以看出,抽象类不允许实例化。我们知道实例化都是在__new__中进行了,查看ABCMeta无法找到这样的字符串。如果搜索cpython的实现,可以在“typeobject.c”中看到“object_new”方法会抛出这样的异常:

static PyObject *
object_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
{
    ...
    if (type->tp_flags & Py_TPFLAGS_IS_ABSTRACT) {
        ...
        PyErr_Format(PyExc_TypeError,
                     "Can't instantiate abstract class %s "
                     "with abstract methods %U",
                     type->tp_name,
                     joined);
    ...
    }
    return type->tp_alloc(type, 0);
}

从中,我们可以看到tp_flags如果有表示抽象的标志”Py_TPFLAGS_IS_ABSTRACT”,则会抛出这样的异常。那“tp_flags”是怎么设置上这个标志的呢,同样在typeobject.c中:

static int
type_set_abstractmethods(PyTypeObject *type, PyObject *value, void *context)
{
    /* __abstractmethods__ should only be set once on a type, in
       abc.ABCMeta.__new__, so this function doesn't do anything
       special to update subclasses.
    */
    ...
    if (res == 0) {
        PyType_Modified(type);
        if (abstract)
            type->tp_flags |= Py_TPFLAGS_IS_ABSTRACT;
        else
            type->tp_flags &= ~Py_TPFLAGS_IS_ABSTRACT;
    }
    return res;
}

从这里可以猜测,当一个类有通过抽象方法相关的装饰器装饰后的方法时,python在初始化这个类的时候,会设置这个类为抽象类,而不允许实例化。可以把“my_method”上的装饰器移除掉:

from abc import ABCMeta
from abc import abstractmethod

class MyAbstractClass(metaclass=ABCMeta):
    
    def my_method(self):
        raise NotImplementedError('my_method is not implemented')

inst = MyAbstractClass()
inst.my_method()

输出:
NotImplementedError: my_method is not implemented

可以看到,MyAbstractClass能够被初始化了。

标记抽象方法

刚才看到,C实现通过判断“abstractmethods ”,以此来设置类是否是抽象类。在此可以先看下装饰器做了什么事情:

def abstractmethod(funcobj):
    funcobj.__isabstractmethod__ = True
    return funcobj

装饰器对函数对象增加了属性“isabstractmethod”。在ABCMeta的__new__中:

    def __new__(mcls, name, bases, namespace):
        cls = super().__new__(mcls, name, bases, namespace)
        # Compute set of abstract method names
        abstracts = {name
                     for name, value in namespace.items()
                     if getattr(value, "__isabstractmethod__", False)}
        for base in bases:
            for name in getattr(base, "__abstractmethods__", set()):
                value = getattr(cls, name, None)
                if getattr(value, "__isabstractmethod__", False):
                    abstracts.add(name)
        cls.__abstractmethods__ = frozenset(abstracts)
        ...
        return cls

可以看到在创建实例时,会获取类以及其父类中有”isabstractmethod“,并且值为True的方法,保存在“abstractmethods”中。而python的C实现,正是通过判断“abstractmethods”,来决定是否是抽象类。

class MyAbstractClass(metaclass=ABCMeta):
    
    @abstractmethod
    def my_method(self):
        raise NotImplementedError('my_method is not implemented')

print(MyAbstractClass.__abstractmethods__)
print(MyAbstractClass.my_method.__isabstractmethod__)

输出:
frozenset({'my_method'})
True

ABCMetada的__new__中有一个小细节, “value = getattr(cls, name, None)”,获取继承的父类的方法时,并不是直接从父类的“__abstractmethods__”中得到,而是通过getattr从子类本身获取,这个是处理当子类实现了父类的抽象方法时,获取的是子类的实现。

继承抽象类

首先写一个子类,但是不实现父类的抽象方法

class MyImplement(MyAbstractClass):
    pass

print(MyImplement.__abstractmethods__)
print(MyImplement.my_method.__isabstractmethod__)
inst = MyImplement()

输出:

frozenset({'my_method'})
True
Traceback (most recent call last):
  File "e:\test1.py", line 56, in <module>
    inst = MyImplement()
TypeError: Can't instantiate abstract class MyImplement with abstract methods my_method

可见由于父类的抽象方法未被覆盖,所以子类依然被认为是抽象类。如果实现了抽象方法呢

class MyImplement(MyAbstractClass):
    
    def my_method(self):
        print('my_method')

print(MyImplement.__abstractmethods__)
# 由于my_method方法被覆盖,没有__isabstractmethod__属性,此行会报错
# print(MyImplement.my_method.__isabstractmethod__)
inst = MyImplement()
inst.my_method()

输出:
frozenset()
my_method

由于子类实现了抽象方法(覆盖)。方法本身没有“isabstractmethod”,因此“abstractmethods”为空,子类可以初始化。

结论

  • 一个类是否是一个抽象类,是由类的__abstractmethods__来决定的。python的C实现会通过这个属性,来决定是否为抽象类。
  • __abstractmethods__是一个set,包含类中所有的抽象方法(包括继承的)。
  • 通过检查类的方法(包括继承过来的方法),是否有“isabstractmethod”属性,且值为True,来判断方法是否是抽象方法。也即是说,如果类本身实现了父类的抽象方法,则此方法不再有“isabstractmethod”属性。抽象方法装饰器做的,就是给方法加上“isabstractmethod”属性,并设置为True.