第八章 元类编程

8.1 property动态属性

from datetime import date, datetime
class User:
    def __init__(self, name, birthday):
        self.name = name
        self.birthday = birthday # 保存一个用户年龄到数据库,首选保存生日,不去保存年龄,因为年龄会变
        self._age = 0 

    @property
    def age(self):
        """
        如果不想知道birthday,只想知道age年龄
        """
        return datetime.now().year - self.birthday.year

    @age.setter # 对age属性进行设置
    def age(self, value):
        self._age = value
        
if __name__ == "__main__":
    user = User("bobby", date(year=1987, month=1, day=1))
    print(user.age)
    user.age = 30
    print(user._age)
33
30

@property装饰器会把函数age变成一个属性描述符,那么就可以使用user.age(属性的方式),而不是使用user.age()(函数调用的方式)。

@property只是做get,那还可以set,就是@age.setter。

8.2 __getattr__、__getattribute__魔法函数

魔法函数是python动态特性的最根本原因。__getattr__就是在查找不到属性的时候调用。

之前讲过python在类中属性查找的顺序,现在直接作为例子讲解

from datetime import date
class User:
    def __init__(self,name,birthday):
        self.name = name
        self.birthday = birthday
        
    def __getattr__(self, item):
        return("not find attr")  
    
if __name__ == "__main__":
    user = User("bobby",date(year=1987,month=1,day=1))
    print(user.age) # 如果没有__getattr__,会抛出异常AttributeError: 'User' object has 					# no attribute 'age'
not find attr

加上__getattr__后没有报错,返回了“not find attr”。user实例里查找不到age属性的时候,会进入__getattr__里,那么就可以在__getattr__里添加自己的逻辑。

比如,在属性里找不到name时,返回Name的值(相当于拼写纠错)

from datetime import date
class User:
    def __init__(self,name,birthday):
        self.Name = name
        self.birthday = birthday
        
    def __getattr__(self, item):
        return self.Name
    
if __name__ == "__main__":
    user = User("bobby",date(year=1987,month=1,day=1))
    print(user.name)
bobby

比如,在User里面维护一个dict info

from datetime import date
class User:
    def __init__(self, name, birthday,info={}):
        self.name = name
        self.birthday = birthday
        self.info = info

    def __getattr__(self, item):
        return self.info[item]

if __name__ == "__main__":
    user = User("bobby", date(year=1987, month=1, day=1),info={"company_name":"imooc"})
    print(user.company_name)
imooc

比如,可以只使用一个dict,虽然在属性中找不到name,但是进入__getattr__后,在自己写的逻辑中可以查找到name

class User:
    def __init__(self,info={}):
        self.info = info

    def __getattr__(self, item):
        return self.info[item]

if __name__ == "__main__":
    user = User(info={"company_name":"imooc", "name":"bobby"})
    print(user.name)
bobby

上述三个例子都是__getattr__的好处,再讲一下__getattribute__这个魔法函数。

class User:
    def __init__(self,info={}):
        self.info = info

    def __getattr__(self, item):
        return self.info[item]
    
    def __getattribute__(self, item):
        return "bobby"

if __name__ == "__main__":
    user = User(info={"company_name":"imooc", "name":"bobby"})
    print(user.name)

__getattribute__的优先级更高,首先会进入__getattribute__,再去找属性。

class User:
    def __init__(self,info={}):
        self.info = info

    def __getattr__(self, item):
        return self.info[item]

    def __getattribute__(self, item):
        return "bobby1"

if __name__ == "__main__":
    user = User(info={"company_name":"imooc", "name":"bobby"})
    print(user.test)
bobby1

就算访问一个不存在的属性test,也会返回bobby1。__getattribute__不建议重写,因为__getattr__调用的是__getattribute__,如果__getattribute__没写好,python程序就会崩溃。

8.3 属性描述符和属性查找过程

from datetime import date, datetime
class User:

    def __init__(self, name, email, birthday):
        self.name = name
        self.email = email
        self.birthday = birthday
        self._age = 0

    # def get_age(self):
    #     return datetime.now().year - self.birthday.year

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

    @age.setter
    def age(self, value):
        #检查是否是字符串类型
        self._age = value

if __name__ == "__main__":
    user = User("bobby", date(year=1987, month=1, day=1))
    user.age = 30

对上面的User,我们可以理解为数据库中的一个表结构,现在能看到的字段有name、birthday。我们希望控制name输入的都为字符串,name=1的时候是错的。如果还有一个字段email也需要为字符串,我们知道可以在@age.setter中检查字符串类型,那么可能对email也要写一个@email.setter,如果这样的字段过多,可能就需要写很多重复代码。那如何来控制这个过程和复用代码?

这个就涉及到属性描述符,比如有一个字段IntFied来统一做Int类型的验证。类变成属性描述符,只需要实现__get__、__set__和__del__魔法函数中的任意一个即可。属性描述符可以放到User里做检查。

import numbers
from datetime import date, datetime

class IntField:
    #数据描述符
    def __get__(self, instance, owner):
        return self.value # 返回实例属性,age=IntField(),就是把value赋值给age

    def __set__(self, instance, value):
        if not isinstance(value,numbers.Integral):
            raise ValueError("int value need")
        self.value = value # 不能使用instance.age,因为.age会调用set方法,这样会一直循环调用set
                          # 可以使用self.value,因为age是IntField的一个实例,age=IntField()
 
    def __delete__(self, instance):
        pass

class User:
    age = IntField() # 把IntField实例化传给age,age是User的类属性不是实例属性(是属性描述符的对象)

if __name__ == "__main__":
    user = User()
    user.age = 30 # 实际上调用的IntField的set方法
    print(user.age)
    user.age = "abc"
    print(user.age)
    ###
    user.age = -1
    print(user.age)
30
Traceback (most recent call last):
  File "E:/pythonProject/AdvancePython-master/chapter08/test.py", line 25, in <module>
    user.age = "abc"
  File "E:/pythonProject/AdvancePython-master/chapter08/test.py", line 11, in __set__
    raise ValueError("int value need")
ValueError: int value need
Process finished with exit code 1
##################################
Traceback (most recent call last):
  File "E:/pythonProject/AdvancePython-master/chapter08/test.py", line 27, in <module>
    user.age = -1
  File "E:/pythonProject/AdvancePython-master/chapter08/test.py", line 15, in __set__
    raise ValueError("positive value need")
ValueError: positive value need

属性描述符有两种:

1.一种是实现了__get__、__set__(数据描述符)

2.还有另外一种,只实现了__get__方法(非数据描述符)

class NoneDataIntField:
    #非数据属性描述符
    def __get__(self, instance, owner):
        return self.value

数据描述符和非数据描述符对属性查找的顺序是不一样的。

class User:
    age = IntField()
    # age = NonDataIntField()
    
if __name__ == "__main__":
    user = User()
    user.age = 30
    print(getattr(user,'age'))
'''
user.age
1.如果age是一个数据描述符,那么age出现在IntField的value中,不会出现在user实例中,user.dict没有age
2.如果age是一个非数据描述符,那么age出现在user.dict中
user.__dict__['age']='abc'
1.age出现在user.dict中,但是print(user.age)会出异常,因为user.age是去IntField的value中查找的

查找顺序是
1.如果age是一个数据描述符,那么user.age调用数据描述符的__get__方法,如果没有数据则到2
2.去user的__dict__中查找,有则返回obj.__dict__[‘age’],没有数据则到3
3.去基类User的__dict__中查找
	3.1 如果age是非数据描述符,调用其__get__方法,否则到3.2
	3.2 返回基类的__dict__[‘age’]
4.如果age不是描述符,则去查找基类User的__getattr__方法,查找不到则到5
5.抛出AttributeError

'''

8.4 __new__和__init__的区别

__new__魔法函数第一个参数是class(类),__new__允许我们在生成User对象之前加逻辑。和__init__的最根本区别是__new__方法里可以自定义类的生成过程,__init__方法第一个参数是实例对象(初始化实例对象),所以是在__new__生成类后再对对象进行操作。

class User:
    def __new__(cls, *args, **kwargs): # cls是User这个类
        print (" in new ")
    def __init__(self, name):
        self.name = name  # name属性在对象dict当中
        print (" in init")
        pass

#new 是用来控制对象的生成过程, 在对象生成之前
#init是用来完善对象的
#如果new方法不返回对象, 则不会调用init函数
if __name__ == "__main__":
    user = User(name="bobby")
 in new 
class User:
    def __new__(cls, *args, **kwargs): # cls是User这个类
        print (" in new ")
        return super().__new__(cls)
    def __init__(self, name):
        self.name = name  # name属性在对象dict当中
        print (" in init")
        pass

#new 是用来控制对象的生成过程, 在对象生成之前
#init是用来完善对象的
#如果new方法不返回对象, 则不会调用init函数
if __name__ == "__main__":
    user = User(name="bobby")
 in new 
 in init

8.5 自定义元类

#类也是对象,type创建类的类
def create_class(name):
    """
    根据传进来的name动态创建类
    :param name:
    :return:
    """
    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__":
    MyClass = create_class("user") # MyClass是一个类
    my_obj = MyClass()
    print(my_obj)
user

通过一个函数和一个字符串可以动态获取一个类,这个在python中是很简单的。但是这个动态创建类还是比较复杂的,需要我们自己去函数里定义class语句,只是把定义class的语句放到了函数当中,而且不灵活。那如何动态创建类呢,不需要自己去写class这种语法,而使用到type。

type用得很多的是用来获取某个对象的类型

#类也是对象,type创建类的类
def create_class(name):
    """
    根据传进来的name动态创建类
    :param name:
    :return:
    """
    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__":
    MyClass = create_class("user")
    my_obj = MyClass()
    print(type(my_obj))
<class '__main__.create_class.<locals>.User'>

type其实可以用来创建类,点进去看

image-20200917215937503

可以看到有三种参数构造的方法,不同构造方法返回的结果是不一样的。

创建类用到的是第一种,传递(name,bases(基类),dict(属性))。

# type动态创建类
User = type("User", (), {})
# "User"类名称,
# ()一个tuple(传递类继承的基类),什么都不继承也必须写
# {}是属性,没有属性时为{}

if __name__ == "__main__":
    User = type("User", (), {})
    my_obj = User()
    print(my_obj)
<__main__.User object at 0x0000016A713E2550>

可以看到,通过type和通过class创建出来的都是类。再进一步扩展

# type动态创建类
User = type("User", (), {})
# "User"类名称,
# ()一个tuple(传递类继承的基类),什么都不继承也必须写
# {}是属性,没有属性时为{}

if __name__ == "__main__":
    User = type("User", (), {"name":"user"}) 
    # 这个用法类似于
    # class:
    # 	name="user"
    my_obj = User()
    print(my_obj.name)
user

创建类的时候不光有属性,还有内部方法,那如何使用type创建方法呢?

# type动态创建类
User = type("User", (), {})
# "User"类名称,
# ()一个tuple(传递类继承的基类),什么都不继承也必须写
# {}是属性,没有属性时为{}
def say(self): # 必须有self
    """
    和class中定义方法是一样的,也需要传递self
    """
    return "i am user"
    # return self.name

if __name__ == "__main__":
    User = type("User", (), {"name":"user","say":say}) # 传入函数时,必须是函数名称,不能是say()
    my_obj = User()
    print(my_obj.say())
i am user

所以通过type函数,可以动态创建类,添加属性和方法。

如果User要继承一个基类

# type动态创建类
User = type("User", (), {})
# "User"类名称,
# ()一个tuple(传递类继承的基类),什么都不继承也必须写
# {}是属性,没有属性时为{}
def say(self): # 必须有self
    """
    和class中定义方法是一样的,也需要传递self
    """
    return "i am user"
    # return self.name

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

if __name__ == "__main__":
    User = type("User", (BaseClass,), {"name":"user","say":say}) 
    # 传入函数时,必须是函数名称,不能是say()
    # 在()中继承类,必须加,
    my_obj = User()
    print(my_obj.answer())
i am baseclass

那么什么是元类?元类就是创建类的类,type实际上是一个类。

image-20200921121132637

对象是由class创建的,而class是由元类创建的。但实际上,很少用type创建类,一般是用元类的写法来创建类。

在Python类的实例化过程中,如果不定义metaclass=MetaClass,会调用type这个类创建User的类对象。这个对象是类,且在全局中是唯一的。如果定义了metaclass,则在类的实例化过程中,首先会寻找metaclass,如果找到则调用该metaclass去创建User类,

class MetaClass(type): # 继承了type,因此是一个元类
    pass

class BaseClass(metaclass=MetaClass):
    def answer(self):
        return "i am baseclass"

class User(BaseClass): # MetaClass控制User实例化的过程
    pass

如果User继承BaseClass,BaseClass中定义了metaclass,那么创建User类时,首先会去寻找User类是否定义了metaclass,如果找不到则到BaseClass中寻找metaclass,如果再找不到就去模块中找metaclass,最后才会使用type创建User类。这个是python中控制类对象生成的过程。

class MetaClass(type): # 继承了type,因此是一个元类
    def __new__(cls, *args, **kwargs):
        pass # 在此处打断点debug

class User(metaclass=MetaClass): # MetaClass控制User实例化的过程
    pass

if __name__ == "__main__":
    my_obj = User()
    print(my_obj.answer())

image-20200921123424901

创建User实例的时候,先要创建User类,首先进入MetaClass(元类),传递args(类似type(object_or_name, bases, dict))。那么在MetaClass中可以控制生成User实例的过程,

class MetaClass(type): # 继承了type,因此是一个元类
    def __new__(cls, *args, **kwargs):
        return super().__new__(cls,*args, **kwargs) # metaclass中定义调用父类需要传入参数

class User(metaclass=MetaClass): # MetaClass控制User实例化的过程
    def __init__(self,name):
        self.name = name
    def __str__(self):
        return "user"

if __name__ == "__main__":
    my_obj = User(name='wry')
    print(my_obj)
user

把创建User类的过程,委托给元类,比如ABC模块中定义某些抽象基类的时候,调用了metaclass ABCMeta。

image-20200921124902299

image-20200921125101182

ABCMeta中重写了new方法,可以做很多检查,比如要实例化的类是否实现了某些魔法函数。

元类也有很多应用常见,比如实例化类时做检查(类是否实现某些方法),或者做一些事先的工作。

8.6 元类实现简单的orm

首先明白我们的需求是什么。

# 需求
class User:
    """
    类映射到数据库中的一张表,对类进行操作,把数据写到数据库中,可以脱离sql语句
    """
    name = CharField(db_column="",max_length=10) # db_column是数据库中的列名,max_length是数据库列最大长度
    age = IntField(db_column="",min_value=0,max_value=100)

    class Meta:
        """
        内部一个类,和上面的列名区别开,定义一些其他东西
        """
        db_table = "user" # 具体到哪一张表

if __name__ == "__main__":
    user = User()
    user.name = "bobby" # 定义name列
    user.age = 28 # 定义age列
    user.save() # 保存到数据库中

用属性描述符描述name,实现属性赋值报错功能。

import numbers

class IntField:
    # 数据描述符
    def __init__(self,db_column,min_value=None,max_value=None):
        self._value = None # 属性描述符保存的值是存在value里的
        self.min_value = min_value
        self.max_value = max_value
        self.db_column = db_column
        if min_value is not None: # 用户一旦传进来min_value,就需要判断是否是整型
            if not isinstance(min_value,numbers.Integral):
                raise ValueError("min_value must be int")
            elif min_value < 0:
                raise ValueError("min_value must be positive int")
        if max_value is not None: # 用户一旦传进来max_value,就需要判断是否是整型
            if not isinstance(max_value,numbers.Integral):
                raise ValueError("max_value must be int")
            elif max_value < 0:
                raise ValueError("max_value must be positive int")
        if max_value and min_value is not None:
            if min_value > max_value:
                raise ValueError("min_value must be smaller than max_value")

    def __get__(self, instance, owner):
        return self._value

    def __set__(self, instance, value):
        if not isinstance(value, numbers.Integral):
            raise ValueError("int value need")
        if value < self.min_value or value > self.max_value:
            raise ValueError("value must between min_value and max_value")
        self._value = value

class CharField:
    def __init__(self, db_column, max_length=None):
        self._value = None
        self.db_column = db_column
        if max_length is None:
            raise ValueError("you must spcify max_lenth for charfiled")
        self.max_length = max_length

    def __get__(self, instance, owner):
        return self._value

    def __set__(self, instance, value):
        if not isinstance(value, str):
            raise ValueError("string value need")
        if len(value) > self.max_length:
            raise ValueError("value len excess len of max_length")
        self._value = value

class ModelMetaClass(type):
    def __new__(cls, *args, **kwargs):
        # 可以在这里取属性值
        pass  # 在此行打断点debug

# 需求
class User(metaclass=ModelMetaClass):
    """
    类映射到数据库中的一张表,对类进行操作,把数据写到数据库中,可以脱离sql语句
    """
    name = CharField(db_column="",max_length=10) # db_column是数据库中的列名,max_length是数据库列最大长度
    age = IntField(db_column="",min_value=0,max_value=100)

    class Meta:
        """
        内部一个类,和上面的列名区别开,定义一些其他东西
        """
        db_table = "user" # 具体到哪一张表

if __name__ == "__main__":
    user = User()
    user.name = "bobby"
    user.age = 28
    user.save()

image-20200921151435602

可以看到进入了ModelMetaClass的new方法,可以看到三个属性(类名称,tuple,属性值dict)。

如果把属性都取出来

class ModelMetaClass(type):
    def __new__(cls, name,bases,attrs, **kwargs):
        for key, value in attrs.items():
            pass # 在此行打断点debug

image-20200921152833572

通过attrs,把所有与表相关的fields(相当于表中的列)提取出来,但是需要判断是否是int还是char

posted @ 2020-09-21 16:25  yueqiudian  阅读(150)  评论(0编辑  收藏  举报