深入理解 Python 中的类与元类

最近在使用 peewee 来做数据库 ORM,期间遇到一些疑问便查了下源码,结果发现好多以前没有好好理解并整理的知识。

参考视频:https://www.bilibili.com/video/BV1uA411V7dW?p=1

关于 Python 中类的基本用法,参考以前写的文章 《python 小技巧与坑 – python 的类》

总结:

对象(obj)是类(class)的一个实例,而类(class)本身其实也是一个对象,它是元类(metaclass)的实例。

在 Python 中,默认情况下,所有类的元类都是 type,所有类的基类都是 object。

规定术语:

我们先来规定下几个术语在本文中的特定意思,以免混淆。

类对象,用来特指类本身这个对象,而不是由类生成的对象实例。

类实例,用来特指由类生成的对象实例。

类属性(类变量),指在类定义体内部,__init__() 方法之外,定义的属性。类属性可以通过 Class.attr 调用也可以通过 instance.attr 调用。

实例属性(实例变量),指在类的 __init__() 方法中,或类定义体外部,通过 instance.attr 定义的属性。实例属性只能通过 instance.attr 调用。

一、特殊函数与特殊属性

1.1 如何区分对象

is 操作符用来判断两个对象是否为同一个。

id() 函数返回对象的唯一标识,在 CPython 实现中,返回的就是该对象的内存地址。可以使用 hex(id(obj)) 将其转换成16进制。

type() 函数返回对象的类型。

1.2 可重写(override)的类方法

1.2.1 __new__(cls[, …])

object.__new__(cls[, ...]) 方法用来创建 cls 类的对象实例。其中第一个参数 cls 代表当前类,其余参数应该与构造器表达式保持一致,或者使用可变参数囊括构造器表达式的参数(构造器表达式指 x = ClassName(args) 这种语法)。它的返回值应该是一个新的对象实例(cls 类的实例)。__new__() 实际上是一个静态方法,不过不需要加 @staticmethod 进行声明

当你使用构造器表达式 x = ClassName(args) 来创建对象的时候,Python 解释器会先调用 ClassName.__new__(cls) 创建一个对象实例,然后再对该对象调用 __init__(self[, ...]) 进行初始化。

如果 __new__() 方法没有返回一个对象实例,那么后续就不会调用 __init__(self[, ...]) 方法。

__new__(cls[, ...]) 的默认实现是调用父类的 __new__() 方法 spuer().__new__(cls, ),你可以在此之后对创建出来的对象实例进行修改。

1.2.2 __init__(self[, …])

__init__(self[, ...]) 的调用发生在 __new__(cls) 方法创建完对象实例之后,该对象实例返回给用户之前。self 参数代表该对象实例,其余参数应该与构造器表达式保持一致,或者使用可变参数囊括构造器表达式的参数。__init__() 不需要有返回值,构造器表达式返回的对象实例是由 __new__() 生成的。

在派生类的 __init__() 方法中,必须主动调用父类的 __init__() 方法, super().__init__([args...]),以确保对象实例中父类部分初始化成功。

1.2.3 __del__(self)

__del__() 方法会在对象实例即将销毁之前自动调用。注意 __del__() 方法实际上并不执行销毁对象、回收内存的操作,这些都是由 Python 垃圾回收机制完成的。所以即使你手动显式调用 x.__del__(),实际上只是运行了一次 __del__() 方法,并不会销毁对象,甚至连对象的引用计数都不会减少。

要想显式删除一个对象,需要使用 del 语句。del x 并不会直接调用 x.__del__(),而是将对象 x 的引用计数减1。当对象的引用计数为0的时候,Python 的 GC 机制会自动为其调用 __del__()。(很诡异,del 在 Python 中是一个 statement,而不是一个 function)

派生类的 __del__() 方法,必须主动调用父类的 __del__() 方法。

Python 并不保证在解释器退出时,会对所有还存活的对象调用 __del__() 方法。

1.2.4 __repr__(self)

Python 的内建函数 repr(x) 会自动调用对象的 x.__repr__() 方法。 __repr__() 必须返回一个“正式的”字符串(相比于 __str__() 而言)。如果可以的话,返回的字符串应该是一个有效的 Python 表达式,能够用来重建对象实例;如果做不到的话,应该返回类似<…some useful description…> 这样格式的字符串。

1.2.5 __str__(self)

Python 的内建函数 str(x), print(x) 会自动调用 x.__str__() 方法。该方法必须返回一个“非正式的”字符串,用来代表该对象实例。与 __repr__() 不同的是,__str__() 并不期望返回一个可重建对象的 Python 表达式字符串,可以更随意进行定制。

默认的__str__(self) 方法其实调用的就是 self.__repr__()

1.2.6 __call__(self[, …])

当对象 x 被以函数形式 x() 使用的时候,就会自动调用 x.__call__() 方法(x.__call__() 等价于 Class_of_x.__call(x) )。

1.2.7 示例代码

class User(object):

    def __new__(cls, *args, **kwargs):
        # __new__ 方法用于创建 cls 类的对象
        print('\n===== 调用 User.__new__() 方法')
        print('cls: ', cls)
        print('type(cls): ', type(cls))
        print('args: ', args)
        print('kwargs: ', kwargs)

        obj = super().__new__(cls)
        print('id(obj): ', hex(id(obj)))
        return obj

    def __init__(self, name):
        # 对 self 这个对象进行初始化
        print('\n===== 调用 User.__init__() 方法')
        print('id(self): ', hex(id(self)))
        
        super(User, self).__init__()
        self.name = name
        pass

    def __del__(self):
        # __del__() 方法并不能销毁对象,它只是在对象即将被 GC 销毁之前会被系统自动调用
        print('\n===== 调用 User.__del__() 方法')
        pass
    
    def __call__(self, *args, **kwds):
        # 当你在对象后面加上括号,像函数一样调用的时候,形如:u1(),就会自动调用该方法。
        print('\n===== 调用 User.__call__() 方法')
        pass

    def __str__(self) -> str:
        # 当在外部 print(user) 或者 str(user) 的时候,
        print('\n===== 调用 User.__str__() 方法')
        # return super().__repr__()
        return '这是 __str__() 的输出: ' + self.__repr__()

    def __repr__(self) -> str:
        print('\n===== 调用 User.__repr__() 方法')
        return super().__repr__()

# 创建类的对象实例,实际上执行了两个步骤:
# 1. 调用 __new__() 方法 创建对象
# 2. 调用 __init__() 方法 初始化对象
u1 = User('funway')

print(u1)

u1()

1.2.8 __get__, __set__, __delete__

如果一个类 Class_A 定义了这三个方法,只有当这个类的实例 obj_a 是作为另一个类的类变量(Class_B.a = Class_A())的时候才有可能调用这三个方法

这时候,赋值语句 obj_b.a = xxx,调用的就是 a.__set__(),取值语句 obj_b.aClass_B.a 调用的就是  a.__get__()

我们将定义了这三个方法的类叫做 “描述符”(Descriptors)。

class CharField(object):
    def __init__(self, text):
        super(CharField, self).__init__()
        self.__text = text
    
    def __get__(self, instance, owner):
        print('MyDict.__get__: %s, %s, %s' % (self, instance, owner))
        if instance is None:
            return self
        else:
            return self.__text
        pass

    def __set__(self, instance, value):
        print('MyDict.__set__: %s, %s, %s' % (self, instance, value))
        self.__text = value
        pass

class User(object):
    name = CharField('funway')
    def __init__(self):
        super().__init__()
        # self.name = 'john'
        pass
    pass

u1 = User()
print('test 1 =======================')
print(User.name)  # 调用 CharField.__get__(name, ...)
print(u1.name)    # 调用 CharField.__get__(name, ...)
print('test 2 =======================')
u1.name = 'zoe'   # 调用 CharField.__set__(name, ...)
print(u1.name)    # 调用 CharField.__set__(name, ...)
print('test 3 =======================')
User.name = 'nobody'  # 直接覆盖 User.name 类属性
print(User.name, type(User.name))
print(u1.name)

peewee 库的 FieldAccessor 类就是一种描述符类,它使得用户可以通过 Model.field 或 model.field 来访问这个类属性底层的真实字段。

1.2.9 __get**__, __set**__

__getitem__(self, key)

用来实现 obj[key] 语法,当用户使用 obj[key] 时候会自动调用该方法。

__setitem__(self, key, value)

用来实现 obj[key] 语法,当用户使用 obj[key] = value 时候会自动调用该方法。

__getattribute__(self, attr)

只要用户使用 obj.attr 语法,就会无条件调用该方法。默认的 __getattribute__ 只负责获取已有属性。

尽量不要重写该方法,否则很容易陷入无限循环中然后抛出 RecursionError 异常。

__getattr__(self, attr)

obj.attr 调用 __getattribute__ 在已有属性字典中找不到 attr 时候,会调用该方法。

__setattr__(self, attr, value)

只要用户使用 obj.attr = value 语法,就会无条件调用该方法。

尽量不要重写该方法,否则很容易陷入无限循环中然后抛出 RecursionError 异常。

1.3 特殊属性

instance.__class__

对象实例 instance 所属的类

class.__bases__

包含该类 class 所有父类的元组

definition.__name__ 

definition 的名字,definition 可以是类 class, 函数 function, 方法 method, 描述符 descriptor, 生成器 generator instance。

definition.__qualname__

qualified name 指的是比普通 __name__ 更详细的名字,包含层级信息。

object.__dict__

对象 object 的属性字典,包含该对象的所有属性信息。注意!这里的 object 可以是一个对象实例 instance,也可以是一个类 class!

对于 instance.__dict__,只包含该对象实例自己的对象属性(在 __init__() 方法中定义以及在类体外部定义的),不包含类属性。

对于 class.__dict__,则只包含该类的类属性。

二、元类 metaclass

2.1 类也是对象,是元类的对象

我们常说的对象,是指由类创建的对象实例。在 Python 中,类本身也是一个对象(下文中,我们将「类对象」这个术语用来特指类这个对象,用「类实例」特指由类创建出来的对象实例)。我们将用来创建类对象的类叫做元类(metaclass)。默认情况下,Python 中的所有类都是 type 类的实例。(同时,Python 中的所有类都是 object 类的派生。这确实绕 =。=#)

当使用 class ClassNmae(): 语句定义一个类的时候,其实 python 解释器是通过元类 type 生成了一个类对象,并赋值给这个 ClassName 变量。

# 1、用 class 语句定义一个类 Dog
class Dog(object):
    name = 'nuanuan'
    def bark(self):
        print('woof!')
        pass

# 2、用 type 类的构造器表达式定义一个类,类名也是 Dog,赋值给变量 Puppy
# type(类名, (父类元组), {定义类成员的字典})
Puppy = type('Dog', (object, ), {'name': '暖暖', 'bark': lambda self: print('汪!')})

d = Dog()
# d = Puppy()
print(d.name)
d.bark()
    
print('d.__class__: ', d.__class__)           # 对象 d 所属的类
print('Dog.__class__: ', Dog.__class__)       # 对象 Dog 所属的类
print('Dog.__bases__: ', Dog.__bases__)       # 类 Dog 的所有父类
print('Puppy.__class__: ', Puppy.__class__)   # 对象 Puppy 所属的类
print('Puppy.__bases__: ', Puppy.__bases__)   # 类 Puppy 的所有父类
print('type.__class__: ', type.__class__)     # 对象 type 所属的类(也是 type)
print('type.__bases__: ', type.__bases__)     # 类 type 的所有父类(也是 object)
print('object.__class__: ', object.__class__) # 对象 object 所属的类(还是 type)
print('object.__bases__: ', object.__bases__) # 类 object 的所有父类(object 不再有父类)

元类 type 的构造器表达式形式如此:type(class_name, bases_tuple, attrs_dict)。其中 class_name 表示类名,bases_tuple 是包含所有父类的元组,attrs_dict 是一个包含类变量和类方法的字典。现在我们已经确认 type() 可以用来动态创建一个类,但实际上它不是很方便。因为在 attrs_dict 参数中,我们很难使用 lambda 表达式写一个复杂函数然后赋值给一个 attr。当然你也可以将某个 attr 关联到一个外部函数,但我觉得这样并不太好。

于是,Python 提供了可自定义的元类,只要一个类派生自 type 类,那么它就是一个元类。

2.2 自定义元类

① 定义元类时,元类必须派生自 type 类。 class MyType(type):

② 在常规类定义中使用元类时,需要使用 metaclass 关键字。 class ClassName(object, metaclass=MyType):

# 1、定义一个元类 MyType
class MyType(type):
    pass

# 2、定义一个常规类,继承自 object(可省略不写),元类是 MyType
# class User(object, metaclass=MyType):
class User(metaclass=MyType):
    pass

u = User()
print(User.__class__)
print(User.__bases__)

2.2.1 反向思考,元类如何生成常规类

在上面的代码中,我们定义了 User 类的元类是 MyType。也就是说,User 类是 MyType 类的对象实例!

那么我们回顾下上文 1.2 中所说的 __new__()__init__() 方法。类要生成对象,需要先调用 __new__() 生成对象实例,然后调用 __init__() 对这个实例进行初始化,最后才返回。

而用户调用 u = User() 这个语句的时候,如果把 User 看作一个对象,那么 obj() 调用的其实是 obj.__class__.__call__()!在这里就是 MyType.__call__() 方法。

现在我们扩展一下元类 MyType,让它打印出“工作流程”,但具体的生成操作还由父类 type 进行。

# 1、定义一个元类 MyType
# 暂时只打印工作流程,实际工作仍由父类 type 完成
class MyType(type):
    def __new__(cls, *args, **kwargs):
        print('MyType.__new__  创建对象(常规类)')
        
        new_cls = super().__new__(cls, *args, **kwargs)
        return new_cls
    
    def __init__(self, *args, **kwargs):
        print('MyType.__init__  初始化对象(常规类)')
        super().__init__(*args, **kwargs)
    
    def __call__(self, *args, **kwargs):
        print('MyType.__call__  用户调用了「对象()」这个表达式,亦即常规类的构造器表达式')
        return super().__call__(*args, **kwargs)
        # return None        # 你可以试试看返回 None 😏
    pass

# 2、定义一个常规类,继承自 object(可省略不写),元类是 MyType
class User(metaclass=MyType):
    name = 'funway'
    
    def greet(self):
        print('hello, my name is %s' % self.name)
        pass
    pass

print('User 类定义完成')
u = User()   # 将会执行 User.__class__.__call__() 方法,也就是 MyType.__call__() 方法!
u.greet()

print('User.__class__: ', User.__class__)
print('User.__bases__: ', User.__bases__)

另外,由于 MyType.__call__() 要执行的操作就是 User(),所以我们可以直接在其中自己生成 User 的对象实例,初始化后并返回。所以上面代码中的 MyType.__call__() 等价于如下:

def __call__(self, *args, **kwargs):
    print('MyType.__call__  用户调用了「对象()」这个表达式,亦即常规类的构造器表达式')
    
    # 这时候的 self,是元类 MyType 的对象实例,亦即常规类 User
    new_obj = self.__new__(self, *args, **kwargs)
    self.__init__(new_obj, *args, **kwargs)
    return new_obj

2.3 子类继承父类的元类

如果一个类,它的父类中定义了 metaclass,那么这个类也会继承该 metaclass,并由该 metaclass 进行创建。

如果一个类派生自多个父类,且每个父类各自有自己的 metaclass。这时候很可能会发生元类冲突 “ TypeError: metaclass conflict ”。需要用户自己想办法去解决,最简单的办法就是在子类定义中显式指定 metaclass。

三、使用元类实现单例模式

3.1 __new__() 实现

其实不使用元类,只用 __new__() 方法来实现更简单。。

# ######
# Note: this is just a simple example and is not thread safe !!!
# ######
class MyPrinter(object):
    # 类成员变量,通过类名调用 ClassName.class_virable
    _instance = None
    def __new__(cls):
        # 判断 _instance 类变量是否为空,空的话则新建一个实例并返回
        if cls._instance == None:
            cls._instance = object.__new__(cls)
            return cls._instance
        # 非空的话,则直接返回该实例
        else:
            return cls._instance
        pass

    def __init__(self):
        print('init')
        # 这时就尽量不要在 __init__ 中做什么修改单例对象的操作了,因为它也每次都会被调用
        pass

p1=MyPrinter()
print(p1)
p2=MyPrinter()
print(p2)

输出如下图所示:

注意:上述示例代码中的单例模式并非线程安全的!要想实现 thread-safe 的单例模式,请参考《Python 创建线程安全的单例》 一文。(同样,👇下面的 metaclass 实现方式也不是线程安全的)

3.2 metaclass 实现

class SingletonMeta(type):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        self._instance = None  # 等价于定义了 MyPrinter._instance 类变量
        pass

    def __call__(self, *args, **kwargs):
        # 如果不存在该单例对象
        if not self._instance:
            # 新建对象
            self._instance = self.__new__(self, *args, **kwargs)
            # 初始化对象
            self.__init__(self._instance, *args, **kwargs)
            pass
        
        return self._instance
    pass

class MyPrinter(object, metaclass=SingletonMeta):
    pass

p1 = MyPrinter()
p2 = MyPrinter()
print(p1)
print(p2)
print(p1 is p2)

四、peewee 源码分析(Model 类)

元类赋予了我们可以动态创建类的能力,这样说很难以理解,我们用一个现实场景来解释一下。元类最常用的地方就是数据库的 ORM(对象关系映射)框架,我们可以很容易地定义表示数据库表的 Model 类,然后由元类为我们执行通用的数据库绑定、字段绑定等操作。

peewee 就是这么一个 ORM 库,我也是因为最近用到了 peewee,才萌生了写这篇文章的念头。

4.1 基本用法

import uuid, os

from peewee import *

db = SqliteDatabase(os.path.dirname(__file__) + '/my_app.db')

class User(Model):
    uid = CharField(unique=True, default=lambda: uuid.uuid4().hex)
    class Meta:
        database = db

db.connect()
db.create_tables([User,])
u1 = User()
u1.save()
print('u1: %s, id(u1)=%s' % (u1, hex(id(u1))))

我们从这一段简单的代码来看一下 peewee 库是怎么实现 ORM 的。

❶ User 继承自 Model 类。

❷ Model 类在 peewee 中的定义是 class Model(with_metaclass(ModelBase, Node)),也就是说 Model 类继承自 with_metaclass(ModelBase, Node)

with_metaclass() 是一个函数,其函数定义是:

MODEL_BASE = '_metaclass_helper_'

def with_metaclass(meta, base=object):
    return meta(MODEL_BASE, (base,), {})

那么with_metaclass(ModelBase, Node) 等价于 ModelBase('_metaclass_helper_', (Node, ), {})。让我回忆下上文的 type(class_name, (bases_tuple), {attrs_dict}),它其实就是以 ModelBase 为元类,以 Node 为基类,创建了一个名为 ‘_metaclass_helper_’ 的临时类。(注意,该函数返回一个类对象。多次调用该函数,返回的类对象类名是一样的,但实际地址是不一样的!所以可以说每次返回的是不同的类,只是类名一样 =。=#)

为什么要多此一举搞一个临时类出来呢?好像说是为了兼容 Python2 的语法。

OK,知道了 with_metaclass() 的本质,那么 Model 类的定义可以简化为 class Model(Node, metaclass=ModelBase),它以 Node 类为基类,以 ModelBase 为元类。同样,User 类就是以 Model 为基类,以 ModelBase 为元类。

❹ 所以,当用户定义 User 类的时候,实际上调用的是元类 ModelBase 的 ModelBase.__new__()ModelBase.__init__()  方法来创建并初始化这个类对象。

然后当用户使用 u1= User() 创建 User 类实例的时候,调用的是元类 ModelBase 的 ModelBase.__call__() 方法。

❺ 我们将 peewee 中的代码简化,其中最终要的几个类为:ModelBaseMetadataFiledAccessorModel

import uuid, os

MODEL_BASE = '_metaclass_helper_'

def with_metaclass(meta, base=object):
    return meta(MODEL_BASE, (base,), {})

# ModelBase 是控制生成 Model 类的元类
class ModelBase(type):
    def __new__(cls, name, bases, attrs, **kwargs):
        print('\n@ ModelBase.__new__', cls, name)
        print('  bases: ', bases)
        print('  attrs: ', attrs)

        if name == MODEL_BASE or bases[0].__name__ == MODEL_BASE:
            print('  对于临时类 _metaclass_helper_ 或者它的第一级子类 Model,直接返回新建的类')
            return super(ModelBase, cls).__new__(cls, name, bases, attrs,
                                                 **kwargs)
        
        print('  对于 Model 的子类,执行自定义操作')
        print('    1、获取内部类 Meta 的定义(Meta 主要用来定义数据库名、表名、主键等信息)')
        meta_options = {}
        meta = attrs.pop('Meta', None)
        if meta:
            for k, v in meta.__dict__.items():
                if not k.startswith('_'):
                    meta_options[k] = v
        print('    meta: ', meta)
        print('    meta_options: ', meta_options)
        
        print('    2、新建类对象', name)
        cls = super(ModelBase, cls).__new__(cls, name, bases, attrs, **kwargs)
        
        # 这两个是自定义的类属性
        print('      2.1 添加 User.__data__ 类属性,但我没发现这个类属性有啥用,因为最终实际数据是存放在 user.__dat__ 这个实例属性中的!')
        cls.__data__ = cls.__rel__ = None
        
        print('      2.2 添加 User._meta 属性,并给它加入该类的元信息(数据库名、表名、主键等)')
        cls._meta = None
        # cls._meta = Metadata(cls, **meta_options)  # 代表元属性的对象

        print('      2.3 获取类的字段信息 fields')
        fields = []
        for key, value in cls.__dict__.items():
            # 会先通过 isinstance(value, Field) 判断是不是字段类型,是的才添加进 fields
            fields.append((key, value))
        
        print('      2.4 将字段信息 fields 通过 add_field() 添加到 cls._meta 中')
        print('          add_field() 还有一个很重要的作用!!')
        print('          就是调用 Field.bind() 方法,将 User.uid 这个原本定义为 CharField 类型的属性,替换为 FieldAccessor 类!')
        # for name, field in fields:
            # cls._meta.add_field(name, field)
        
        print('      2.5 执行其他操作,比如验证类对象、设置外键等')
        # ...

        print('    3、 返回类对象 %s' % name)
        return cls
    pass

# Metadata 类是存放 Model 的元数据的类,包括 数据库名,表名,主键,字段信息
# 【很重要】的一点是,在 Metadata.add_field() 方法中,
# 它会将 User.uid 类属性,转移到自己的 self.fieds, self.columns 实例属性中
# 并且将原来类型为 CharField 类型的 User.uid 类属性,通过 Field.bind() 方法转换成 FieldAccessor 类型。
# FiledAccessor 类定义了 __get__, __set__ 方法,允许通过 u1.uid 直接访问该字段的真实值。
class Metadata(object):
    pass

# Node 类是用来作为查询语句的中间节点,暂且不讨论
class Node(object):
    pass

# Model 是该 ORM 中所有自定义 model_class 的基类
# 它的一大作用就是将元数据中的默认字段值放到 self.__data__ 实例属性中
class Model(with_metaclass(ModelBase, Node)):
    def __init__(self, *args, **kwargs):
        print('@ Model.__init__', args ,kwargs)

        print('  1. 设置 __data__ 属性,为 _meta 元数据中记录的字段默认值')
        # self.__data__ = self._meta.get_default_dict()
        
        print('  2. 设置 _dirty 属性,代表被修改过的 “脏字段”')
        # self._dirty = set(self.__data__)

        print('  3. 设置其他对象属性')
        for k in kwargs:
            setattr(self, k, kwargs[k])
    pass

# 自定义的 model_class 类
class User(Model):
    uid = uuid.uuid4().hex
    class Meta: 
        database = 'xx.db'
    pass

print('User 类定义完成')

u1 = User()

总结一下就是:

① 用户自定义的模型类 User 继承自 Model 类,Model 类的元类是 ModelBase

② 所以 User 类也是 ModelBase 元类的对象实例,由 ModelBase.__new__() 负责创建。

③ 在创建 User 类的时候,ModelBase 会为其生成一个 User._meta 类属性,它是 Metadata 类型的对象实例。然后将 User 的数据名、表名、主键、字段信息等元数据保存到 User._meta 中。

④ 在 User._meta.add_field() 方法中,还会将用户定义的 User.uid 这些代表字段信息的类属性,转换成 FiledAccessor 类型。这样,用户就可以使用 user.uid 来访问真实的字段值(FiledAccessor.__get__(), FiledAccessor.__set__())。而此时访问 User.uid 返回的则是 FiledAccessor.field

⑤ 在实例化 User 的时候,由 Model.__init__() 负责初始化 self.__data__ 这个实例属性,这里保存着 User 模型真实的字段值。

4.2 为什么 Model 实例打印出来会是 None?

peewee 中 Model 实例打印出来的时候显示为 None,其实是因为这段代码:

class Model(with_metaclass(ModelBase, Node)):
    # ...
    def __str__(self):
        return str(self._pk) if self._meta.primary_key is not False else 'n/a'

Model 类重写了 __str__(self) 方法,打印的是主键字段 self.pk。如果未设置主键,默认情况下就是 None,所以打印的是 str(None)

只有像如下代码,在 Model 类中特别设置 _meta.primary_key = False 的,才会返回出 ‘n/a’。

class NoPrimaryKey(Model):
    # ...
    class Meta:
        primary_key = False

要想改变这个情况,变成原先预期的打印出对象类型与内存地址,我们可以在子类中重写 Model 的 __str__() 方法:

def __str__(self):
    # return self.__repr__()  # 这里不能使用 self.__repr__()。是因为在 peewee.py 中,ModelBase 基类在 __repr__() 方法中调用了 __str__()方法。
                              # 这时如果不重写 __repr__(),却又在 __str__() 中调用 __repr__(),就会导致调用死循环。
    return object.__repr__(self)

 

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top