python深入学习(7):元类编程

63 阅读5分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第14天,点击查看活动详情

元类编程

一,动态属性property

使用装饰器 @property 即可进行动态属性的创建,以及管理属性

场景一:

​ 如果之前我存入数据库的数据只有用户的生日,而没有年龄,但是我就是想通过属性调用的方式来获取用户生日

from datetime import datetime, date
class User:

    def __init__(self, name, birthday):
        self.name = name
        self.birthday = birthday

    @property
    def age(self):
        return datetime.now().year - self.birthday.year


if __name__ == '__main__':
    user = User('xyb', date(1999, 12, 1))
    print(user.age)

场景二:

​ 我在改变一个实例的属性的时候,我希望他只接受 str 类型的参数,否则在赋值的时候就报错

class User:

    def __init__(self, name):
        self.name = name

    @property
    def name(self):
        return self._name			# 这里的self._name是为了不陷入死循环

    @name.setter
    def name(self, value):
        if isinstance(value, str):
            self._name = value
        else:
            raise TypeError('Excepted a string')
            
    @name.deleter
    def name(self):
        raise AttributeError('Can\'t delete attribute')


if __name__ == '__main__':
    user = User('xyb')			# 这里__init_-方法创建实例的时候会自动调用我们创建的方法
    user.name = 'aaa'		
    print(user.name)
    del user.name				# 报错,元素被我们定义不能删除

二,__ getattr __ , __ getattribute __魔法函数

__getattr__ 在查找不到属性的时候调用,可以添加很多灵活的逻辑,对字典取值

class A:
    def __init__(self, info):
        self.info = info

    def __getattr__(self, item):
        """查找不到属性的时候调用"""
        return self.info[item]

if __name__ == '__main__':
    a = A({'name': 'xyb', 'age': 20})
    print(a.name)

__getattribute__ 只要访问任何属性,就会调用这个属性,我们这样就能掌控所有的属性入口

# 能不从写,尽量不写这个方法,写框架的时候回用到

三,属性描述符和属性查找过程

当我们想要对实例的属性进行一些值的判断操作,以便允许或抛出异常

前面介绍了 property 动态属性,可以让我们对属性的值进行操作,或者凭空生成实例的属性,但是我们如果在需要对实例的属性进行大量操作,动态属性就会不适用了,我们想要的是优化的代码,而不是大量重复的代码。这个时候,属性描述符闭包的作用就很大了

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vnKX8MNU-1592057846483)(assets/.png)]

  1. 使用属性描述符对实例属性进行操作
    class Integer:
        def __init__(self, name):
            self.name = name
    
        def __get__(self, instance, cls):
           	"""
            没有把这个类当成描述符来使用时,会触发if条件
            :param instance: 实例本身
            :param cls:
            :return:查询的属性值
            """
            if instance is None:
                return self
            else:
                return instance.__dict__[self.name]
    
        def __set__(self, instance, value):
            """
            在实例的属性被赋值的时候才会调用,即self.x = x这种
            :param instance: 实例本身
            :param value: 实例化的值
            :return: None
            """
            if not isinstance(value, int):
                raise TypeError('Except an int')
            instance.__dict__[self.name] = value
    
        def __delattr__(self, instance):
            """
            删除属性时触发
            :param instance: 实例本身
            :return: None
            """
            del instance.__dict__[self.name]
    
    
    class Point:
        x = Integer('x')
        y = Integer('y')
        def __init__(self, x, y):
            self.x = x
            self.y = y
            pass
    
    if __name__ == '__main__':
        p = Point(1, 's')
    
  2. 使用闭包进行对实例进行操作(启用起来和属性描述符没什么两样)
    def type_property(name, except_type):
        storage_name = '_' + name
    
        @property
        def prop(self):
            return getattr(self, storage_name)
    
        @prop.setter
        def prop(self, value):
            if not isinstance(value, except_type):
                TypeError('{} must be a {}'.format(name, except_type))
            setattr(self, storage_name, value)
    
        return prop
    
    
    class Person:
        name = type_property('name', str)
        age = type_property('age', int)
        def __init__(self, name, age):
            self.name = name
            self.age = age
    
    if __name__ == '__main__':
        from functools import partial
        String = partial(type_property, str)
        Integer = partial(type_property, int)
        p = Person('xyb', 20)
        print(p.__dict__)
        print(p.age)
    
  3. 查找过程
    class Integer:							# 数据描述符
        def __init__(self, name):
            self.name = name
    
        def __get__(self, instance, owner):
            pass
    
        def __set__(self, instance, value):
            pass
        
        
    class NonDataInteger:					# 数据描述符
        def __init__(self, name):
            self.name = name
    
        def __get__(self, instance, owner):
            pass
    

    数据描述符,数据描述符和属性查找的过程是不同的

    user = User(),那么user.age顺序如下:

    首先调用 __ getattribute __,而对于描述符 (get) 的调用,则是发生在 getattribute 内部的。

    1. 到数据描述符中调用__get__方法。
    2. 查找自身实例的 age 属性
    3. 查找父类和基类的 __dict__ age 属性
    4. 调用非数据描述符的__get__方法。
    5. 如果有__getattr__方法,调用其方法
    6. 都没有抛出异常

四,__ new __ 和 __ init __的区别

__new__ 是在对象生成之前调用,返回一个实例(创建实例交给父类做)

__init__ 是在对象生成之后调用,对实例进行初始化

如果__ new__方法不返回实例,那么不会调用__ init__

class User:
    def __new__(cls, *args, **kwargs):
        print('in new')
        ob = super().__new__(cls)
        print(ob)
        return ob

    def __init__(self, name):
        print('in init')
        print(self)
        self.name = name
        

if __name__ == '__main__':
    user = User('xyb')
    print(user.name)
    
...

in new
<__main__.User object at 0x000001FB70E5B1D0>
in init
<__main__.User object at 0x000001FB70E5B1D0>
xyb

五,元类编程

类本身也是对象,type创建的类的类

引子:

​ 通过一个函数来动态的创建类

def create_class(name):
    if name == 'user':
        class User:
            def __str__(self):
                return 'user'
        return User
    elif name == 'company':
        class Company:
            def __str__(self):
                return 'company'
        return Company


if __name__ == '__main__':
    user = create_class('user')()
    print(user)

通过 type 动态创建类

class BaseClass:
    def answer(self):
        return 'i am baseclass'

def get_name(self):
    return self.name


if __name__ == '__main__':
    User = type('User', (BaseClass, ), {'name': 'user', 'get_name': get_name})
    my_obj = User()
    print(my_obj.get_name())
    print(my_obj.answer())
创建元类控制类的生成

什么是元类?

元类就是创建类的类。对象 <-- class(对象) <-- type

type 在全局是唯一的,也是一个元类,所有的类的创建,都是由 type 去创建的类对象

  • 但是,python 在创建类之前,会首先寻找 MetaClass 这个属性,让这个元类帮他创建类对象

  • MateClass 的优先度大于继承的类的元类

__new__ 方法剥离出来,单独写一个元类来处理 __new__ 方法,,这样我们就能很好的控制整个创建实例的过程,比方说没有实例抽象基类的方法,我们就可以在没创建类的时候在元类里面抛异常

class MetaClass(type):
    # 继承了 type 就是一个元类,用来控制实例的生成
    def __new__(cls, *args, **kwargs):
        return super().__new__(cls, *args, **kwargs)

class User(metaclass=MetaClass):
    def __init__(self, name):
        self.name = name

    def __str__(self):
        return 'user'

if __name__ == '__main__':
    user =User('xyb')
    print(user)

六,通过元类实现orm

orm 就是通过 python 一个类来映射到数据库数据库的一张表,对类操作,能将数据写入表中

class Field:
    pass

class IntField(Field):
    def __init__(self, name, db_column, min_value=None, max_value=None):
        self.name = name
        self.db_column = db_column
        self.min_value = min_value
        self.max_value = max_value
        if min_value:
            if not isinstance(min_value, int):
                raise TypeError('min_value excepted an int')
            elif min_value < 0:
                raise ValueError('min_value must be positive int')
        if max_value:
            if not isinstance(min_value, int):
                raise TypeError('min_value excepted an int')
            elif max_value < 0:
                raise ValueError('max_value must be positive int')
        if max_value and min_value:
            if max_value < min_value:
                raise ValueError('max_value must be bigger than min_value')

    def __get__(self, instance, owner):
        if instance is None:
            return self
        else:
            return instance.__dict__[self.name]

    def __set__(self, instance, value):
        if not isinstance(value, int):
            raise TypeError('Except an str')
        if value < self.min_value or value > self.max_value:
            raise ValueError('Value must between min_value and max_value')
        instance.__dict__[self.name] = value


class CharField(Field):
    def __init__(self, name, db_column, max_len):
        self.name = name
        self.column = db_column
        self.max_len = max_len
        if not isinstance(max_len, int):
            raise TypeError('max_len must be an int')
        elif max_len < 0:
            raise ValueError('max_len must be bigger than 0')

    def __get__(self, instance, owner):
        if instance is None:
            return self
        else:
            return instance.__dict__[self.name]

    def __set__(self, instance, value):
        if not isinstance(value, str):
            raise TypeError('Except an str')
        if len(value) > self.max_len:
            raise ValueError('value must be smaller than max_len')
        instance.__dict__[self.name] = value


class ModelMetaClass(type):
    def __new__(cls, name, base, attrs, **kwargs):
        if name == 'BaseModel':
            return super().__new__(cls, name, base, attrs, **kwargs)
        fields = {}
        for key, value in attrs.items():
            if isinstance(value, Field):
                fields[key] = value
        attrs_meta = attrs.get('Meta', None)
        _meta = {}
        db_table = name.lower()
        if attrs_meta is not None:
            table = getattr(attrs_meta, 'db_table', None)
            if table is not None:
                db_table = table
        _meta['db_table'] = db_table
        attrs['_meta'] = _meta
        attrs['fields'] = fields
        del attrs['Meta']
        return super().__new__(cls, name, base, attrs, **kwargs)

class BaseModel(metaclass=ModelMetaClass):
    def __init__(self, *args, **kwargs):
        for key, value in kwargs.items():
            setattr(self, key, value)
        return super().__init__()
    def save(self):
        fields = []
        values = []
        for key, value in self.fields.items():
            db_column = value.db_column
            if db_column is None:
                db_column = key.lower()
            fields.append(db_column)
            value = getattr(self, key)
            values.append(str(value))
        sql = "insert {db_table}({fields}) value({values})".format(db_table=self._meta['db_table'], fields=','.join(fields), values=','.join(values))

class User(BaseModel):

    name = CharField('name', db_column="w", max_len=10)
    age = IntField('age', db_column="a", min_value=0, max_value=100)

    class Meta:
        db_table = 'user'
        
        
if __name__ == '__main__':
    user = User()
    user.name = 'xyb'
    user.age = 28