zoukankan      html  css  js  c++  java
  • Django的models实现分析

    1      引子

    1.1     神奇的Django中的models

    我们先来看一段在Django项目中常用的代码:

    设置数据库models代码:

    class Students(models.Model):
        name = models.CharField()
        age = models.IntegerField()

    这里有几个神奇的地方,涉及到了python中最神秘的几个特性。

    先看下有哪些神奇的地方:

    • 字段名称nameage自动转换为了数据库中的字段名称
    • 自动校验数据类型,models.IntegerField(),会校验设置的数据类型

    这里用的是python的两个语法特性:

    • 描述符协议
    • 元类

    我们来一步一步解开神秘面纱。

    2      数据校验

    2.1     数据校验难点

    Python虽然是强类型的脚本语言,但是在定义变量时却无法指定变量的类型。

    例如,我们在Student类中定义一个age字段,合法值一般为包含0的正整数,但是在python中无正整数的类型,只能自己来校验。

    class Student:
        def __init__(self, name, age):
            if isinstance(name,str):
                self.name = name
            else:
                raise TypeError("Must be a string")
    
            if isinstance(int, age):
                self.age = age
            else:
                raise TypeError("Must be an int")

    但是,如果更新年龄时就会遇到问题,无法重用校验逻辑。

    有没有简洁的方法呢?

    2.2     使用property装饰器

    使用property也是一个方法,可以针对每个属性来设置,但是如果一个类有多个属性,代码就会非常的多,并且产生大量的冗余,就像这样。

    class Student:
        def __init__(self, name, age, class_no, address, phone):
            self._name = None
            self._age = None
            self.__class_no = None
            self._address = None
            self._phone = None
    
            self.name = name
            self.age = age
            self.class_no = class_no
            self.address = address
            self.phone = phone
    
        @property
        def name(self):
            return self._name
    
        @name.setter
        def name(self, value):
            if not isinstance(value, str):
                raise ValueError("Must be string")
            self._name = value
    
        @property
        def age(self):
            return self._age
    
        @age.setter
        def age(self, value):
            if isinstance(value, int) and value > 0:
                self._age = value
            else:
                raise ValueError("age value error")
        
        @property
        def address(self):
            return self._address
        
        @address.setter
        def address(self, value):
            if not isinstance(value, str):
                raise ValueError("Must be string")
            self._address = value
    View Code

    代码冗余太多,每个检查str的都要复制一遍代码。

    3      Python描述符

    描述符提供了优雅、简洁、健壮和可重用的解决方案。简而言之,一个描述符就是一个对象,该对象代表了一个属性的值。

    这就意味着如果一个Student对象有一个属性“name”,那么描述符就是另一个能够用来代表属性“name”持有值的对象。

    描述符协议中“定义了__get__”、“__set__”或”__delete__” 这些特殊方法,描述符是实现其中一个或多个方法的对象。

    3.1     版本一

     1 class NameProperty:
     2     def __init__(self, name=""):
     3         self.name = name
     4 
     5     def __get__(self, instance, owner):
     6         if instance is None:
     7             return self
     8         return instance.__dict__.get(self.name)
     9 
    10     def __set__(self, instance, value):
    11         if not isinstance(value, str):
    12             raise TypeError("name must be string")
    13         instance.__dict__[self.name] = value
    14         
    15 
    16 class Student:
    17     name = NameProperty('name')
    18     age = None
    19     heghth = None
    20     weight = None
    21 
    22     def __init__(self, name):
    23         self.name = name
    24 
    25     def __str__(self):
    26         return self.name
    27 
    28     @property
    29     def age(self):
    30         return self.age
    31 
    32     @age.setter
    33     def age(self, value):
    34         if not isinstance(value, int):
    35             raise ValueError("must be int")
    36         self.age = value
    37 
    38 s = Student("Stitch")
    39 print(s)
    40 s.name = 'name'
    41 print(s.name)
    View Code

    这个版本存在一个问题,就是name = NameProperty("sss"),必须设置一个名称,才可以使用。这个与我们使用django的models时不太一样,在使用models时,不写参数也可以的。

    3.2     版本二

    不用输入变量名称。

    class NameProperty:
        index = 0
    
        def __init__(self):
            self.name = str(self.__class__.index)  # 使用类的变量
            self.__class__.index += 1
    
        def __get__(self, instance, owner):
            return getattr(instance, self.name)
    
        def __set__(self, instance, value):
            if not isinstance(value, str):
                raise TypeError("name must be string")
            instance.__dict__[self.name] = value
    
    
    class Student:
        name = NameProperty()
        age = None
    
        def __str__(self):
            return self.name
    
    s = Student()
    s.name = "www"
    print(s)
    
    s2 = Student()
    s2.name = "http"
    print(s2)
    print(s.name)
    View Code

    这个版本还存在一个问题,如果一个类型有多个字段使用了NameProperty时,错误提示时,无法表示出此变量的名称,只能表示出一个index值。用户看到这个时,无法判断是那个变量出了问题。

    4      使用元类

    元类是python的中一个难点,在大部分场景下都不会用到。但是在编写框架方面却是必不可缺少的利器。

    4.1     版本三

    使用元类来控制类的行为:

    class NameProperty:
        index = 0
    
        def __init__(self):
            self.storage_name = str(self.__class__.index)  # 使用类的变量
            self.__class__.index += 1
    
        def __get__(self, instance, owner):
            return getattr(instance, self.storage_name)
    
        def __set__(self, instance, value):
            if not isinstance(value, str):
                raise TypeError("%s must be string" % self.storage_name)
            instance.__dict__[self.storage_name] = value
    
    
    class EntityMeta(type):
        def __init__(cls, name, bases, attr_dict):
            super().__init__(name, bases, attr_dict)
            for key, attr in attr_dict.items():
                if isinstance(attr, NameProperty):
                    type_name = type(attr).__name__
                    attr.storage_name = '{} property {}'.format(type_name, key)
    
    
    class Student(metaclass=EntityMeta):
        name = NameProperty()
        age = None
        nicky_name = NameProperty()
    
        def __str__(self):
            return self.name
    
    s = Student()
    s.name = "www"
    print(s)
    
    s2 = Student()
    s2.name = "test"
    s2.nicky_name = 4444
    print(s2)
    print(s2.nicky_name)
    View Code

    执行输出为:

    raise TypeError("%s must be string" % self.storage_name)
    
    TypeError: NameProperty property nicky_name must be st

    语法解释:

    版本三相比版本二,最大的变化在于Student类继承了自定义元类EntityMeta。

    如果对于python面向对象编程有了解的话,python的所有类都继承自type,type是所有类的元类。。

    在这里,我们自定义的元类EntityMeta,具备一个功能就是判断类属性是否为NameProperty类型,如果为这个类型,则这个类型的实例属性storage_name值赋值为类名和属性名

    4.2     版本四—模仿django的models

    模仿Django的models实现:

    import abc
    
    class NameProperty:
        index = 0
    
        def __init__(self):
            self.storage_name = str(self.__class__.index)  # 使用类的变量
            self.__class__.index += 1
    
        def __get__(self, instance, owner):
            return getattr(instance, self.storage_name)
    
        def __set__(self, instance, value):
            # instance.__dict__[self.storage_name] = value
            setattr(instance, self.storage_name, value)
    
    
    class Validated(abc.ABC, NameProperty):
        def __set__(self, instance, value):
            value = self.validate(instance, value)
            super().__set__(instance, value)
    
        @abc.abstractclassmethod
        def validate(self, instance, value):
            """return validated value or raise ValueError"""
    
    
    class ChartField(Validated):
        def validate(self, instance, value):
            if not isinstance(value, str):
                raise TypeError("{} must be str".format(self.storage_name))
            return value
    
    
    class IntegerField(Validated):
        def __init__(self, min_value=None):
            self.min_value = min_value
    
        def validate(self, instance, value):
            if not isinstance(value, int):
                raise TypeError("{} must be int".format(self.storage_name))
            if self.min_value and value < self.min_value:
                raise ValueError("{} must larger min_value".format(self.storage_name))
            return value
    
    
    class EntityMeta(type):
        def __init__(cls, name, bases, attr_dict):
            super().__init__(name, bases, attr_dict)
            for key, attr in attr_dict.items():
                if isinstance(attr, Validated):
                    type_name = type(attr).__name__
                    attr.storage_name = "{} property {}".format(type_name, key)
    
    
    class Entity(metaclass=EntityMeta):
        pass
    
    
    class Student(Entity):
        name = ChartField()
        age = IntegerField(min_value=0)
        nicky_name = ChartField()
    
        def __init__(self, name, age, nicky_name):
            self.name = name
            self.age = age
            self.nicky_name = nicky_name
    
        def __str__(self):
            return self.name
    
    s2 = Student("test", 12, "toddy")
    s2.age = -1
    print(s2.nicky_name)
    s2.nicky_name = 4444
    View Code

     

    执行结果:

    raise ValueError("{} must larger min_value".format(self.storage_name))
    
    ValueError: IntegerField property age must larger min_value 

     

    这样,完全模仿了models的定义。

    类的初始化和后续属性赋值,都会自动调用__set__来设置并校验。

    5      原理解释

    5.1     属性读取顺序

    通过实例读取属性时,通常返回的是实例中定义的属性。读取顺序如下:          

    1. 实例属性
    2. 类属性
    3. 父类属性
    4. __getattr__()方法

    先记住这个顺序,后面理解描述需要。属性描述符都是定义在类中的,而不是在对象中。

    5.2     描述符

    某个类,只要是内部定义了方法 __get__, __set__, __delete__ 中的一个或多个(set,delete必须有一个),就可以称为描述符。

    方法的原型为:

      ① __get__(self, instance, owner)

      ② __set__(self, instance, value)

      ③ __del__(self, instance)

    描述符只绑定到类上,在实例上不生效。

    描述的调用实质为:type(objectA).__dict__[“key”].__get__(None, objectB),objectB为描述符,objectA为定义类。

    5.3     元类

    元类,就是创建类的类。一般类都继承自object类,默认会创建一些方法。

    元类决定了类出初始化后有哪些特征和行为。如果我们想自定义一个类,具备某种特殊的行为,则需要自定义元类。

    • 类也是对象,所有的类都是type的实例
    • 元类(Meta Classes)是类的类
    • __metaclass__ = Meta 是 Meta(name, bases, dict) 的语法糖
    • 可以通过重载元类的 __new__ 方法,修改定义的行为

    6  其他案例

    Django的django-rest-framework框架的serializer 也是用的这个语法实现的。

    7      参考资料

    编号

    标题

    链接

    1

    元类

    https://stackoverflow.com/questions/100003/what-is-a-metaclass-in-python

    2

    描述符

    http://python.jobbole.com/81899/

    3

    《流畅的python》

    元类部分

  • 相关阅读:
    mysql报Fatal error encountered during command execution的解决办法
    C语言之算法初步(汉诺塔--递归算法)
    C语言中变量的作用域和生命周期
    C语言数据在内存分配
    ~~~
    数据结构笔记
    SQL笔记
    Java零碎知识点
    如何让eclipse在程序修改后,点击运行可以自动保存。
    [转载] java中静态代码块的用法 static用法详解
  • 原文地址:https://www.cnblogs.com/StitchSun/p/7723983.html
Copyright © 2011-2022 走看看