跳转至

18 类型对象:虚拟机如何识别对象的类型?

你好,我是海纳。

到目前为止,我们已经实现了 Python 虚拟机中最重要的四个基本类型,也就是整数、字符串、列表和字典。为了实现这些基本类型,我们已经完成了很多类和对象的相关机制。其中包括打印、比较等常规的通用操作,也包括对数据元素的增删查改,以及排序、遍历等操作。

接下来的三节课,我们准备实现完备的对象系统,从而实现面向对象编程的大部分能力。前边的课程,每一段代码都能实现一个具体的功能,但从这一节课开始,内容会变得有点抽象。如果你学一遍不能理解的话,也不用灰心,可以静下心来多读几遍,想一想如果这个特性让你自己设计还有没有更好方法。慢慢的你就能理解了。

面向对象编程这一部分包括自定义类型、运行时判断对象类型、继承、函数和操作符重载等特性。这一节课我们先从类型说起。

实现类型对象

从刚开始学习编程的时候,我们就一直在和类型打交道。例如,我们说一个整型变量,其实就是指某个变量,它的类型是整型。

不同语言对于类型的处理非常不一样。有的语言是静态编译阶段决定的,运行时不能再修改,有的语言则支持运行时修改变量的类型。有的语言不需要程序员指定变量类型,还有的语言有复杂的类型推断系统帮助程序员简化开发。总之,每种语言对自己的类型系统都有独到的设计。那 Python 中的类型是如何设计的呢?

我们先用一个简单的例子来考察 Python 中的类型到底是什么。

print(list)                  # <type 'list'>

lst = list()
print(lst)                   # []
lst.append(1)
print(lst)                   # [1]
print(isinstance(lst, list)) # True

这段代码里的 list 代表的是列表的类型,它首先是一个对象,所以能够被打印(第 1 行)。但又和我们以前所接触的所有对象都不同,因为它还代表了一个类型。

我们像调用函数一样调用它,得到了一个列表的实例 lst。打印 lst,显示这是一个空列表,接下来可以对这个列表对象进行各种操作。这说明通过这种方式得到的列表对象和以前直接使用空列表进行赋值,是完全等价的。

list 的特别之处在于它看上去是一个类型,但它同时又是一个对象。在我们的虚拟机里,类型是使用 Klass 表示的,对象则都是继承自 HiObject。如果有一个东西既是类型,也是对象,一个最直接的做法是让 Klass 也继承自 HiObject,然后这个类型就可以被叫做 TypeObject。

如果这样做的话,我们前边辛辛苦苦建立起来的 Klass-Oop 二分结构就模糊掉了,你可以沿着这个思路再往下想一下,这个类型对象的 Klass 该怎么设计,然后很快你就会发现这个思路太烧脑,稍有不慎就会掉到思维的陷阱中去。

这里,我们选择一个不同的做法,既然 Klass 继承自 Object 不是一个好主意,在面向对象的程序设计中,当继承会带来混乱的时候,人们往往会选择使用组合。所以这里,我们就尝试使用组合来解决问题。

在 Klass 中引入一个 HiObject,让 Klass 和这个 HiObject 一一对应。当上下文中需要它是一个对象的时候,比如要打印这个对象,修改这个对象的属性等,就让 HiObject 出面。当需要它是一个类型的时候,比如创建这个类型的新对象,调用这个类型上定义的某个方法等,就让 Klass 出面。这样做的话,对象关系就简单清晰多了。

按照这个思路,我们可以定义一个名为 HiTypeObject 的类。

/* 
 * [object/hiObject.hpp]
 * meta-klass for the object system.
 */
class TypeKlass : public Klass {
private:
    TypeKlass() {}
    static TypeKlass* instance;

public:
    static TypeKlass* get_instance();

    virtual void print(HiObject* obj);
};

class HiTypeObject : public HiObject {
private:
    Klass*  _own_klass;

public:
    HiTypeObject();

    void    set_own_klass(Klass* k);
    Klass*  own_klass()             { return _own_klass; }
};

// [object/hiObject.cpp]
HiTypeObject::HiTypeObject() {
    set_klass(TypeKlass::get_instance());
}

// [object/klass.hpp]
class Klass {
private:
    Klass*        _super;
    HiTypeObject* _type_object;
    ...
public:
    ...
    void set_type_object(HiTypeObject* x) { _type_object = x; }
    HiTypeObject* type_object()           { return _type_object; }
    ...
};

如图所示,这个特殊的对象上有两个 Klass 引用,一个是继承自 HiObject 的 _klass 属性,代表这个对象本身的类型。另一个是 own_klass 指向与它绑定的 Klass,图中就是 ListKlass。所有的 HiTypeObject 对象的 _klass 都指向 TypeKlass。

图片

如果某个程序要打印一个 HiTypeObject 的对象的时候,就要把这个对象当成一个普通的 HiObject 来看待,所以我们和之前实现整型对象一样,在 TypeKlass 里实现打印的功能。

void TypeKlass::print(HiObject* obj) {
    Klass* own_klass = obj->as<HiTypeObject>()->own_klass();
    printf("<class ");
    own_klass->name()->print();
    printf(">");
}

这个 print 的方法比较简单,先打印一个字符串class,表示这是一个类型对象,然后把 own_klass 的 name 打出来就可以了。

接着,我们来让 HiTypeObject 与 Klass 相互绑定。

// [object/hiObject.cpp]
void HiTypeObject::set_own_klass(Klass* k) {
    _own_klass = k; 
    k->set_type_object(this);
}

// [object/hiList.cpp]
void ListKlass::initialize() {
    HiDict * klass_dict = new HiDict();
    klass_dict->put(new HiString("append"), 
        new FunctionObject(list_append));
    //...
    set_klass_dict(klass_dict);
    (new HiTypeObject())->set_own_klass(this);
    set_name(new HiString("list"));
}



// [object/hiInteger.cpp]
...

// [object/hiString.cpp]
void StringKlass::initialize() {
    HiDict* klass_dict = new HiDict();
    klass_dict->put(new HiString("upper"), new FunctionObject(string_upper));

    set_klass_dict(klass_dict);
    (new HiTypeObject())->set_own_klass(this);
    set_name(new HiString("str"));
}

// [object/hiDict.cpp]
void DictKlass::initialize() {
    HiDict* klass_dict = new HiDict();

    klass_dict->put(new HiString("setdefault"),
        new FunctionObject(dict_set_default));
    // ...
    set_klass_dict(klass_dict);
    (new HiTypeObject())->set_own_klass(this);
    set_name(new HiString("dict"));
}

这段代码里的 set_own_klass 方法实现了 klass 与 HiObject 的相互绑定(第 2 至 5 行)。然后在所有的内建类型中,都创建了一个新的 HiTypeObject 对象,与代表这个类型的 Klass 绑定。在整数类型、字符串类型、列表和字典类型中都要添加相同的操作,代码逻辑比较简单,这里我就不再详细解释了。

这里有一点代码的重构需要你注意一下。之前的实现中,所有的 Klass 初始化操作都安排在构造函数中。随着内建类型越来越多,相互依赖关系就变得非常复杂。例如,字典的初始化操作依赖字符串,字符串的初始化操作也依赖于字典。

TypeKlass 的初始化也存在同样的问题,它从 Klass 继承了一个指向 TypeObject 的引用,由于创建 TypeObject 对象的时候要使用 TypeKlass,如果在构造函数里做初始化工作,这种循环依赖就会造成无限递归。

为了解决相互依赖的问题,我们把单例类的构造与初始化进行分离,抽象出 initialize 方法以单独负责 Klass 的初始化。而构造函数全部变成空的,也就是说创建 Klass 实例阶段什么也不做。这样就能保证创建的时候一定是成功的。

根据这样的思路,TypeKlass 初始化代码就可以这样实现:

void TypeKlass::initialize() {
    HiTypeObject* tp_obj = new HiTypeObject();
    set_name(new HiString("type"));
    tp_obj->set_own_klass(this);
}

把构造和初始化分成两步执行以后,在 initialize 执行的时候,所有的 Klass 实例都已经构造完成了,这个时候就可以随意地使用各种数据类型,而不用担心循环依赖的问题。重构以后的 genesis 函数也变得非常清晰。

void Universe::genesis() {
    HiTrue       = new HiString("True");
    HiFalse      = new HiString("False");
    HiNone       = new HiString("None");

    TypeKlass::get_instance()->initialize();
    DictKlass::get_instance()->initialize();
    StringKlass::get_instance()->initialize();
    ListKlass::get_instance()->initialize();
}

做完了这个工作以后,我们就会看到如下图所示的结构,TypeKlass 会与它自己的 TypeObject 形成一个循环引用,除此之外,它和其他的 Klass 并没有什么本质的不同。

图片

最后一个步骤,由于 "int" "list" 等符号都是内建的,就像这部分开始的时候那个例子,我们可以直接在代码中使用这些符号,而这些符号所绑定的其实就是 TypeObject 对象。我们来把这些符号放到内建表里。

Interpreter::Interpreter() {
    _builtins = new HiDict();
    ...
    _builtins->put(new HiString("int"),      IntegerKlass::get_instance()->type_object());
    _builtins->put(new HiString("str"),      StringKlass::get_instance()->type_object());
    _builtins->put(new HiString("list"),     ListKlass::get_instance()->type_object());
    _builtins->put(new HiString("dict"),     DictKlass::get_instance()->type_object());
}

到这里为止,我们的虚拟机就可以正确地执行 print(list) 这条语句了。

有了类型对象, 接下来就可以实现 isinstance 方法,以及通过调用类型来创建对象这两个重要功能了。但是在实现这两个功能之前,我们必须再完善一个功能,那就是实现 object 这个公共基类。

实现公共基类 object

在 Python 中,所有的类都是object的子类,无论整数、字符串、列表还是其他的用户自定义的类,无一例外。这里我们就来实现object这个公共基类。

和 list、int 这些符号一样,object 也代表了一种类型。这个类型就是人们常说的普通对象类型。实际上,在虚拟机还处在最早的阶段时,就已经有了 HiObject 类了。虚拟机执行的计算、运行时栈、全局变量表、局部变量表等等,所有的机制都是建立在 HiObject 的基础上。

但是在这节课之前,我们从来没有考虑过,如果使用 new HiObject() 语句创建一个单独的对象,那它的 Klass 应该是什么呢?所以这里就需要为 HiObject 定义新的 Klass,我们就叫它ObjectKlass。

// [object/hiObject.hpp]
class ObjectKlass : public Klass {
private:
    ObjectKlass();
    static ObjectKlass* instance;

public:
    static ObjectKlass* get_instance();
};

// [object/hiObject.cpp]
ObjectKlass* ObjectKlass::instance = NULL;

ObjectKlass::ObjectKlass() {
    set_super(NULL);
}

ObjectKlass* ObjectKlass::get_instance() {
    ...
}

这个定义平平无奇,但它其实引发了整个对象体系的大地震。就像这部分刚开始提到的,在Python 中,所有的类都是object的子类,我们必须正确地表达这种继承关系。

其实在 Klass 的结构里,我们早早就留下了与继承相关的一个属性,就是 super。

图片

如图所示,通过设置 super 属性,完整的继承体系就建立起来了。我们这里只以 IntegerKlass 的初始化代码来说明,其他的三种类型就不一一展示了,你可以自己修改。

void IntegerKlass::initialize() {
    set_name(new HiString("int"));
    (new HiTypeObject())->set_own_klass(this);
    set_super(ObjectKlass::get_instance());
}

在继承体系下,一个对象既是它的直接类型的实例,又是它的直接类型的父类的实例。比如整数 3,它的直接类型是 IntegerKlass,所以它是 int 类型的实例,IntegerKlass 的父类是 ObjectKlass,所以它同时又是 object 类型的实例。

在继承体系建立好以后,Python 中一切皆是 object 就有了最直观的理解:Python 中的继承关系是通过 Klass 的 super 指针串联起来的,所有类型的 Klass 沿着它的 super 指针向上查找,最终都会停留在 ObjectKlass里。

理解了这个逻辑,实现 isinstance 函数就成了一个很简单的任务了。isinstance 函数的作用是检查一个对象是否是某一种类型的实例。所以,我们先检查这个对象的直接类型,然后顺着继承链向上查找就可以了。

// [runtime/functionObject.cpp]
HiObject* isinstance(HiList* args) {
    HiObject* x = args->get(0);
    HiTypeObject* y = args->get(1)->as<HiTypeObject>();

    Klass* k = x->klass();
    while (k != nullptr) {
        if (k == y->own_klass())
            return Universe::HiTrue;

        k = k->super();
    }

    return Universe::HiFalse;
}

// [runtime/interpreter.cpp]
Interpreter::Interpreter() {
    _builtins = new HiDict();
    ...
    _builtins->put(new HiString("len"),      new FunctionObject(len));
    _builtins->put(new HiString("isinstance"),new FunctionObject(isinstance));
}

我们把前边出现的所有图综合起来,就可以得到下面这张图了。这张图展现了对象系统的全景。

图片

这里我还要特别讲一下 type 函数的实现。这个函数的作用是返回一个对象的类型。在我们的虚拟机中,表示对象的类型是使用 TypeObject,所以type 函数的实现,其实就是找到一个对象的 klass,然后取和这个 klass 相绑定的 TypeObject 对象。

// [runtime/functionObject.cpp]
HiObject* type_of(HiList* args) {
    HiObject* arg0 = args->get(0);
    return arg0->klass()->type_object();
}

// [runtime/interpreter.cpp]
Interpreter::Interpreter() {
    _builtins = new HiDict();
    ...
    _builtins->put(new HiString("type"),     new FunctionObject(type_of));
    _builtins->put(new HiString("isinstance"),new FunctionObject(isinstance));
}

使用 type 函数,结合类型系统的全景图,就可以理解以下几行语句的执行结果为什么是这样了,代码中的注释部分就是这条语句的执行结果,你可以看一下。

t = type(1)
print(t)         # <class 'int'>
print(type(t))   # <class 'type'>

i = 0
while i < 5:
    t = type(t)
    print(t)     # <class 'type'>
    i = i + 1

当 t 为 HiTypeObject 时,它的 klass 就是 TypeKlass。所以对它调用 type 函数,结果就是 TypeKlass 所对应的 TypeObject。再取这个 TypeObject 的 type ,就会得到其自身。

类型的判断功能基本已经完成了,接下来,我们重点关注通过类型创建对象的功能。

通过类型创建对象

将类型作为函数调用来创建对象是类型系统中最重要的一个功能。我们先来看一个例子和它所对应的字节码。

a = int()
#  0 LOAD_NAME                0 (int)
#  3 CALL_FUNCTION            0
#  6 STORE_NAME               1 (a)

print(a)
b = str("hello")
print(b)
c = list()
print(c)
d = dict()
print(d)

在代码里,反编译出的字节码清楚地显示,把类型对象 int 作为函数,执行CALL_FUNCTION。

到目前为止,我们所实现的 CALL_FUNCTION 只能对 MethodObject、FunctionObject 进行调用,所以我们必须增加对 TypeObject 的支持。

TypeObject 被调用的时候,我们可以通过它的 own_klass 来创建对象。如果 own_klass 是 IntegerKlass,就创建整数对象;如果 own_klass 是 ListKlass,就创建列表对象。

我们分两步来实现这个功能,首先是在 CALL_FUNCTION 里增加对 TypeObject 的支持。

void Interpreter::build_frame(HiObject* callable, ObjList args) {
    if (callable->klass() == NativeFunctionKlass::get_instance()) {
        ...
    }
    else if (MethodObject::is_method(callable)) {
        ...
    }
    else if (callable->klass() == FunctionKlass::get_instance()) {
        ...
    }
    else if (callable->klass() == TypeKlass::get_instance()) {
        PUSH(callable->as<HiTypeObject>()->own_klass()->allocate_instance(args));
    }
}

第二步就是实现 allocate_instance 方法。我们在所有的类型 Klass 里都增加这个方法,包括整型、字符串、列表和字典。

// [object/klass.hpp]
class Klass {
private:
    ...
public:
    ...
    virtual HiObject* allocate_instance(ArrayList<HiObject*>* args) { return 0; }
    ...
};

// [object/hiInteger.cpp]
HiObject* IntegerKlass::allocate_instance(ArrayList<HiObject*>* args) {
    if (!args || args->length() == 0)
        return new HiInteger(0);
    else
        return NULL;
}

// allocate_instance for list
HiObject* ListKlass::allocate_instance(HiList* args) {
    if (!args || args->length() == 0)
        return new HiList();
    else
        return nullptr;
}

// allocate_instance for string
HiObject* StringKlass::allocate_instance(HiList* args) {
    if (!args || args->length() == 0) {
        return new HiString("");
    }
    else
        return args->get(0)->as<HiString>();
}

// allocate_instance for dict
HiObject* DictKlass::allocate_instance(HiList* args) {
    if (!args || args->length() == 0)
        return new HiDict();
    else
        return nullptr;
}

这段代码的逻辑比较简单,每个类型中的 allocate_instance 方法的实现都很相似。实际上,通过 list 创建列表的时候是可以接受参数的,它的参数是一个可迭代访问的对象。等后面我们实现了完备的迭代器以后,再将它补齐,这里就先使用 nullptr 代表尚未完全支持初始化对象的功能。

编译运行,这部分的测试用例就可以顺利执行了。到这里,对象系统的重构工作就基本完成了。

总结

这节课我们重构了整个虚拟机的内建对象系统,包括整数、字符串、列表和字典。

第一部分我们实现了 TypeObject。每一个 Klass 都有一个对应的 TypeObject。Python 中一切皆是对象,就连类型也是对象。所以,当程序的上下文要求把类型当作对象来传递或者打印的时候,虚拟机内部就会转换成 TypeObject,如果用于判断一个对象的类型时,就会使用 Klass 来完成相应的功能。

第二部分实现了公共基类 object,所有的 Python 对象都是 object 的派生类。在构建 object 的同时,我们也完善了类的继承机制。

最后一部分,我们实现了通过类型创建对象的能力。创建对象使用的语法和函数调用的语法是相同的,它们最后生成的字节码也是相同的,都是 CALL_FUNCTION,所以我们就在函数调用的执行部分添加了类型判断。如果被调用者是一个类型对象时,就代表这个时候应该创建一个新的对象。

通过以上三个部分,我们基本实现了内建对象的类型系统。下节课,我们在这节课的基础上,继续探索自定义类型如何实现。

思考题

你能列举出多少构建列表的方式?比如列表字面量,列表推导式等等。欢迎你把你的答案分享到评论区,也欢迎你把这节课的内容分享给其他朋友,我们下节课再见!

精选留言(3)
  • 骨汤鸡蛋面 👍(2) 💬(1)

    是不是可以认为 1. 从c++视角出发实现一个“编程语言(解释器)”的业务,该如何抽象?一般面向对象,所有对象都有一个公共父类(比如叫object),因为在上层使用看来,即便一个python对象没有数据和方法,但是c++解释执行的时候,对应的c++对象要有一些解释执行时会用到的数据、方法。 2. 如果编程语言只支持int/str/list/dict等基本类型,则使用Kclass/HiObject抽象就够用了,代码里声明一个int,解释器就对应创建一个HiInteger 就行了。只支持基本类型肯定不够,得允许用户扩展,但也不可能让用户直接定义Kclass/HiObject,于是放开给用户定义TypeKlass,再支持根据TypeKlass 生成HiTypeObject(将类型作为函数调用来创建对象),HiTypeObject再作为 Klass 的成员。

    2024-06-15

  • 冯某 👍(0) 💬(0)

    记录一下

    2024-12-08

  • ifelse 👍(0) 💬(0)

    学习打卡

    2024-11-02