元类是什么

在面向对象(OOP)编程中,我们可以用不同的类来描述不同的实体及操作,可以通过父类来设计一些“默认”操作,也可以用MixIn类来组合扩展一些额外操作,也可以用抽象类抽象方法来描述要实现的接口,面向接口编程。
大部分情况下我们并不需要用到元类。

元类是一种type(type的子类),是一种自定义类型,可以定制类的调用、对象创建、初始化、销毁等各种操作。

元类的使用场景

多数情况下元类用来对普通类来加以限制和规范。使用元类(自定义类型)可以在类的创建(__new__)、初始化()比如限制类必须包含特定属性和实现特定方法、限制类只能有一个实例对象。
典型使用场景如下:

  • 不允许类实例化
  • 单例模式:每个类只允许创建一个对象
  • 根据属性缓存对象:当某一属性相同时返回同一对象
  • 属性限制:类中必须包含某些属性或实现某些方法
  • ORM框架:用类及类属性来描述数据库表并转为数据库操作

元类使用示例

不允许类实例化

在某些情况下,假设我需要限制一些类不允许创建对象(只允许使用类名操作),可以使用元类加以限制,代码如下。

class NoInstances(type):  # 定义元类-继承type
    def __call__(self, *args, **kwargs):  # 控制类调用(实例化)过程
        """类调用"""
        raise TypeError("不允许实例化")


class User(metaclass=NoInstances):  # 声明使用元类(该类型)
     pass

user = User()  # ()即调用类的__call__操作,这里会抛出异常,因此无法直接实例化创建对象

单例模式

在某些情况下仅允许类创建一个实例对象,也可以使用元类进行限制,代码如下:

class Singleton(type):   # 单例类型-定制的元类
    def __init__(self, *args, **kwargs):
        self.__instance = None   # 添加一个私有属性,用于保存唯一的实例对象
        super().__init__(*args, **kwargs)

    def __call__(self, *args, **kwargs):  # 控制类调用
        if self.__instance is None:
            self.__instance = super().__call__(*args, **kwargs)  # 不存在则创建
            return self.__instance
        else:
            return self.__instance    # 否则返回已创建的


class User(metaclass=Singleton):
    def __init__(self):
        print('创建用户')

user1 = User()  # 创建对象
user2 = User()  # 创建对象
print(user1 is user2)  # 返回True,两者是同一对象

根据属性缓存对象

这个是单例模式的扩展,针对特定属性,完全相同的属性组合创建同一对象。

import weakref

class Cached(type):
   def __init__(self, *args, **kwargs):
       super().__init__(*args, **kwargs)
       self.__cache = weakref.WeakValueDictionary()  # 添加一个缓存字典

   def __call__(self, *args):
       if args in self.__cache:   # 通过 参数组合查询缓存字典中有没有对应的对象
           return self.__cache[args]   
       else:
           obj = super().__call__(*args)  # 创建对象
           self.__cache[args] = obj  # 根据参数组合(元祖类型)到缓存字典
           return obj


class User(metaclass=Cached):
   def __init__(self, name):
       print('创建用户({!r})'.format(name))
       self.name = name

a = User('张三')
b = User('李四')
c = User('张三')
print(a is b)  # False 名字不同,不是同一对象
print(a is c)  # True  名字相同,是同一对象

限制类必须包含特定属性

class TestCaseType(type):
    def __new__(cls, name, bases, attrs):
        print('name', name)
        print('bases', bases)
        print('attrs', attrs)
        if {'priority', 'timeout', 'owner', 'status', 'run_test'} - set(attrs.keys()):
            raise TypeError('测试用例类必须包含priority、status、owner、timeout属性并实现run_test方法')
        return super().__new__(cls, name, bases, attrs)


class TestA(metaclass=TestCaseType):
    # priority = 'P1'
    timeout = 10
    owner = 'kevin'
    status = 'ready'
    
    def run_test(self):
        pass


a = TestA()  # 这里注释了用例类的priority属性,实例化会报错

ORM框架

ORM(Object-Relational Mapping)是一种将对象和关系数据库之间的映射的技术,它可以让我们使用面向对象的方式来操作数据库。
Django中的ORM模型、以及SQLAlchemy都是基于元类实现的,将数据库操作映射为 类声明和对象操作,下面是一个基于元类的,简单的ORM框架的实现。

class ModelMeta(type):  # 元类
    def __new__(cls, name, bases, attrs):
        if name == 'Model':
            return super().__new__(cls, name, bases, attrs)

        table_name = attrs.get('__table__', name.lower())  # 如果类中包含table_name属性,则以该属性作为表明
        mappings = {}
        fields = []
        primary_key = None

        for k, v in attrs.items():
            if isinstance(v, Field):
                mappings[k] = v
                if v.primary_key:
                    if primary_key:
                        raise RuntimeError('Duplicate primary key for field: {}'.format(k))   # 只允许一个Field声明为主键
                    primary_key = k
                else:
                    fields.append(k)

        if not primary_key:
            raise RuntimeError('Primary key not found for table: {}'.format(table_name))   # 不允许没有主键

        for k in mappings.keys():
            attrs.pop(k)

        attrs['__table__'] = table_name
        attrs['__mappings__'] = mappings
        attrs['__fields__'] = fields
        attrs['__primary_key__'] = primary_key

        return super().__new__(cls, name, bases, attrs)

class Model(metaclass=ModelMeta):  # 数据模型-对应一张数据库表
    def __init__(self, **kwargs):
        for k, v in kwargs.items():
            setattr(self, k, v)

    def save(self):  # 对象保存方法-对应数据库表插入数据
        fields = []
        params = []
        args = []

        for k, v in self.__mappings__.items():
            if v.primary_key:
                continue
            fields.append(v.name)
            params.append('?')
            args.append(getattr(self, k, None))

        sql = 'INSERT INTO {} ({}) VALUES ({})'.format(self.__table__, ','.join(fields), ','.join(params))
        print('SQL:', sql)
        print('ARGS:', args)

class Field:  # 数据库字段
    def __init__(self, name, column_type, primary_key=False):
        self.name = name
        self.column_type = column_type
        self.primary_key = primary_key

    def __str__(self):
        return '<{}:{}>'.format(self.__class__.__name__, self.name)

class StringField(Field):  # 字符串类型字典-对应varchar
    def __init__(self, name, primary_key=False):
        super().__init__(name, 'varchar(100)', primary_key)

class IntegerField(Field):   # 整型字典-对应bigint
    def __init__(self, name, primary_key=False):
        super().__init__(name, 'bigint', primary_key)

在这个示例中,我们定义了一个ModelMeta元类,它用于创建继承自Model的类。在这个元类中,我们首先获取类中定义的所有字段,并将它们存储在__mappings__字典中。同时,我们还获取了主键字段,并将其存储在__primary_key__属性中。

Model类中,我们定义了一个__init__方法,它用于初始化对象的属性。我们还定义了一个save方法,它用于将对象保存到数据库中。在这个方法中,我们首先获取所有非主键字段,并将它们存储在fields列表中。然后,我们使用params列表来存储占位符,使用args列表来存储参数。最后,我们使用这些信息来构建SQL语句,并将其打印出来。

Field类中,我们定义了一个__str__方法,它用于打印字段的名称和类型。我们还定义了StringFieldIntegerField两个子类,它们分别表示字符串类型和整数类型的字段。

使用这个ORM框架,我们可以定义一个继承自Model的类,并在其中定义字段。例如:

class User(Model):
    id = IntegerField('id', primary_key=True)
    name = StringField('username')
    email = StringField('email')
    password = StringField('password')

然后,我们就可以创建一个User对象,并将其保存到数据库中:

user = User(id=1, name='Alice', email='alice@example.com', password='123456')
user.save()

这个示例只是一个简单的ORM框架,实际的ORM框架还需要支持更多的功能,比如查询、更新、删除等操作。但是,通过这个示例,您可以了解到如何使用元类来创建ORM框架。

参考:python3-cookbook 使用元类控制实例的创建