美文网首页程序员
Python- ORM原理的基础

Python- ORM原理的基础

作者: 董小贱 | 来源:发表于2020-01-08 16:18 被阅读0次

    了解了一下ORM, 对其实现感到很有兴趣,花时间总结了一下. 这篇文章里边没有关于ORM的分析,只是对其实现的基础做一个总结.

    0. 从@property装饰器说起

    0.1 在绑定属性时, 如果我们直接把属性暴露出去,会导致意想不到的错误.比如:把体重设置为负值.
    先来看一段代码, 输出BMI指数的:
    
    class BMI:
        """计算BMI指数"""
        
        def __init__(self, name, height, weight):
            self.name = name
            self.height = height / 100
            self.weight = weight
        
        def bmi(self):
            """体重指数BMI=体重/身高的平方(国际单位kg/㎡)"""
            bmi_value = self.weight / self.height ** 2
            print(f"{self.name}的BMI指数是{bmi_value:.2f}")
    
    
    bmi = BMI("董小贱", 174, 87)
    bmi.bmi()
    >> 董小贱的BMI指数是28.74 
    

    很显然, 在输入的height和weight的值是不能小于等于0的数.但是现在的情况下,并没有做这类的限制,并不能满足现实中的要求,那么现在就可以用@property来实现需求.

    class BMI:
        """计算BMI指数"""
        
        def __init__(self, name, height, weight):
            self.name = name
            self.height = height / 100
            self.weight = weight
        
        def bmi(self):
            """体重指数BMI=体重/身高的平方(国际单位kg/㎡)"""
            bmi_value = self.weight / self.height ** 2
            print(f"{self.name}的BMI指数是{bmi_value:.2f}")
        
        @property
        def height(self):
            return self.__height
        
        @height.setter
        def height(self, value):
            if value <= 0:
                raise ValueError('Height must not be less than or equal to 0')
            else:
                self.__height = value
                
        @property
        def weight(self):
            return self.__weight
        
        @weight.setter
        def weight(self, value):
            if value <= 0:
                raise ValueError('weight must not be less than or equal to 0')
            else:
                self.__weight = value
    
    
    bmi = BMI("董小贱", 174, 87)
    bmi.bmi()
    
    

    注:为什么把值赋到self.__heightself.__weight中, 是因为如果还是把值给到self.height和self.weight中的话, 会造成死循环而导致异常的抛出

    另外需要注意的是: 特性都是类属性, 但是特性管理的其实都是实例属性的存取.

    0.2 关于特性的一些问题点
    0.2.1: 实例属性会覆盖类属性
    In [2]: class Test():
       ...:     name = "dongxiaojian"
       ...:     
    
    In [3]: test = Test() 
    
    In [4]: test.name # 获取实例对象中不存在的属性name, 此时获取到的是类属性name
    Out[4]: 'dongxiaojian'
    
    In [5]: test.name = "董小贱" # 为实例对象的name属性赋值
    
    In [6]: test.name #获取实对象的name属性, 现在获取到的不再是类属性
    Out[6]: '董小贱'
    
    In [7]: Test.name # 现在类属性通过类来访问
    Out[7]: 'dongxiaojian'
    
    0.2.2 实例属性不会覆盖类特性
    In [8]: class Test():
       ...:     @property
       ...:     def name(self):
       ...:         return "dongxiaojian"
       ...:     
    
    In [9]: Test.name # 通过类访问name特性, 获取的是特性对象本身,不会运行特性的读值方法。
    Out[9]: <property at 0x1082d5f48>
    
    In [10]: test = Test()
    
    In [11]: test.name # 通过实例访问返回的是值
    Out[11]: 'dongxiaojian'
    
    In [12]: test.name = "董小贱" # 通过实例赋值报错,导致赋值失败
    ---------------------------------------------------------------------------
    AttributeError                            Traceback (most recent call last)
    <ipython-input-12-babb940ddd04> in <module>()
    ----> 1 test.name = "董小贱"
    
    AttributeError: can't set attribute
    
    In [13]: vars(test) # 实例属性还是为空
    Out[13]: {}
    
    In [14]: test.__dict__['name'] = "董小贱" # 可以把值存入到__dict__中,不会报错
    
    In [15]: test.name # 存入__dict__中的值不会被实例访问到, 特性没有被实例属性所遮盖.
    Out[15]: 'dongxiaojian'
    
    In [16]: Test.name = "dong" # 通过类将特性覆盖掉
    
    In [17]: test.name # 现在可以通过实例访问到保存在__dict__中的属性值
    Out[17]: '董小贱'
    
    In [18]: Test.name # 现在的类属性的值
    Out[18]: 'dong'
    
    0.2.3 新的类特性会覆盖实例属性(以下代码在0.2.2的基础上)
    In [19]: Test.name # 接着0.2.2的, 类属性
    Out[19]: 'dong'
    
    In [20]: test.name  # 接着0.2.2的, 实例属性
    Out[20]: '董小贱'
    
    In [21]: Test.name = property(lambda self: "这是创建的新特性") # 使用特性覆盖类属性
    
    In [22]: test.name # 实例属性已经被覆盖
    Out[22]: '这是创建的新特性'
    
    In [23]: Test.name # 类属性返回的对象
    Out[23]: <property at 0x1082f56d8>
    
    In [24]: del Test.name # 删掉类属性
    
    In [25]: test.name # 实例属性恢复
    Out[25]: '董小贱'
    
    In [26]: Test.name # 无法访问到类属性
    ---------------------------------------------------------------------------
    AttributeError                            Traceback (most recent call last)
    <ipython-input-26-ee2a064643f5> in <module>()
    ----> 1 Test.name
    
    AttributeError: type object 'Test' has no attribute 'name'
    
    0.3 通过property函数来实现一个特性工厂函数
    0.3.1 可以发现, 0.1中的代码实现了具体的需求, 但是可以发现代码是很冗余, 那怎么实现呢? 请看以下:
    class BMI:
        """计算BMI指数"""
    
        def __init__(self, name, height, weight):
            self.name = name
            self.height = height / 100
            self.weight = weight
        
        def bmi(self):
            """体重指数BMI=体重/身高的平方(国际单位kg/㎡)"""
            bmi_value = self.weight / self.height ** 2
            print(f"{self.name}的BMI指数是{bmi_value:.2f}")
        
        def get_height(self):
            return self.__height
        
        def set_hight(self, value):
            if value <= 0:
                raise ValueError('value must not be less than or equal to 0')
            else:
                self.__height = value
        
        def get_weight(self):
            return self.__weight
        
        def set_weight(self, value):
            if value <= 0:
                raise ValueError('value must not be less than or equal to 0')
            else:
                self.__weight = value
        
        weight = property(get_weight,set_weight )
        height = property(get_height, set_hight)
        
    bmi = BMI("董小贱", 174, 87)
    bmi.bmi()
    
    0.3.2 可以看得出, 开始很冗余, 那么根据上边的代码来实现工厂函数:
    def func(storage_name):
        """为属性提供限制"""
        def get_value(instance):
            return instance.__dict__[storage_name]
      
        def set_value(instance, value):
            if isinstance(value, int): value = float(value)
            if not isinstance(value, float): raise ValueError("Value must be float")
            if value < 0:
                raise ValueError("value must not be less than or equal to 0")
            else:
                instance.__dict__[storage_name] = value
        
        return property(get_value, set_value)
    
    class BMI:
        """计算BMI指数"""
        height = func('height')
        weight = func('weight')
        
        def __init__(self, name, height, weight):
            self.name = name
            self.height = height / 100
            self.weight = weight
        
        def bmi(self):
            """体重指数BMI=体重/身高的平方(国际单位kg/㎡)"""
            bmi_value = self.weight / self.height ** 2
            print(f"{self.name}的BMI指数是{bmi_value:.2f}")
    
    
    bmi = BMI("董小贱", 174, 87)
    bmi.bmi()
    
    

    其中的func函数,就是一种装饰器的写法, instance其对应的应该是BMI对象的实例, 这里如果写成self会很怪,所以用的instance替代. 类属性heightweight其实是property对象, 所以这里会覆盖实例中的同名属性.这里的代码看起来很奇怪, 可以理解为把0.3.1中类中的代码提取出来,然后封装成函数了.

    0.4 通过特性删除属性

    上边讲的大都是设置值以及获取值, 通过特性还能删除属性.下边一个简单的例子, 通过装饰器实现:

    In [19]: class Test():
        ...:     
        ...:     def __init__(self,value):
        ...:         self.height = value
        ...:     
        ...:     @property
        ...:     def height(self):
        ...:         return self._height
        
        ...:     @height.setter
        ...:     def height(self, value):
        ...:         self._height = value
        ...:     
        ...:     @height.deleter
        ...:     def height(self):
        ...:         self._height = 0
        ...:         print (self._height)
        ...:   
    In [20]: test = Test(150)
    
    In [21]: test.height
    Out[21]: 150
    
    In [22]: del test.height
    0
    
    In [23]: test.height
    Out[23]: 0
    
    In [24]: test.height = 160
    
    In [25]: test.height
    Out[25]: 160
    
    In [26]: del test.height
    0
    
    

    property()函数也有对应的参数, 下边是property的帮助文档, 其中fdel对应的接受的参数是删除属性值得函数:

    In [27]: help(property)
    
    Help on class property in module builtins:
    
    class property(object)
     |  property(fget=None, fset=None, fdel=None, doc=None) -> property attribute
     |  
     |  fget is a function to be used for getting an attribute value, and likewise
     |  fset is a function for setting, and fdel a function for del'ing, an
     |  attribute. 
     |  
    

    特性的内容差不多就这么多的东西, 下边看下个内容, 描述符

    1. 描述符相关

    1.1 描述符是对多个属性运用相同存取逻辑的一种方式. 描述符是实现了特定协议的类, 这个协议包括__get____set____delete__方法. property类实现了完整的描述符协议.

    现在将0.3.2的函数改写成描述符类:

    class Quantity:
        """为属性提供限制"""
        
        def __init__(self, storage_name):
            self.storage_name = storage_name
        
        def __set__(self, instance, value):  # 这里改为__set__()方法.
            if isinstance(value, int): value = float(value)
            if not isinstance(value, float): raise ValueError("Value must be float")
            if value < 0:
                raise ValueError("value must not be less than or equal to 0")
            else:
                instance.__dict__[self.storage_name] = value
    
    class BMI:
        """计算BMI指数"""
        height = Quantity('height')
        weight = Quantity('weight')
        
        def __init__(self, name, height, weight):
            self.name = name
            self.height = height / 100
            self.weight = weight
        
        def bmi(self):
            """体重指数BMI=体重/身高的平方(国际单位kg/㎡)"""
            bmi_value = self.weight / self.height ** 2
            print(f"{self.name}的BMI指数是{bmi_value:.2f}")
    
    
    bmi = BMI("董小贱", 174, 87)
    bmi.bmi()
    

    通过描述符类写跟0.3.2中的函数实现的一样的效果, 这其中的几个定义:

    • 描述符类: 实现描述符协议的类, 即:Quantity类.
    • 托管类: 把描述符实例声明为类属性的类, 即BMI类.
    • 描述符实例: 描述符类的各个实例, 声明为托管类的类属性.即:height和weight.
    • 托管实例: 托管类的实例. 即bmi.
    • 储存属性: 托管实例中存储自身托管属性的属性.实例中的height和weight属性就是存储属性.
    • 托管属性: 托管类中有描述符实例处理的公开属性.值存储在储存属性中. 也就是说, 描述符实例和存储属性为托管属性建立了基础.

    值得注意的是: 编写__set__方法时, 要记住self和instance参数的意思: self是描述符实例, instance是托管实例.管理实例属性的描述符应该把值存储在托管实例中. 因此, Python 才为描述符中的那个方法提供了 instance 参数。如果将各个托管属性的值直接存在描述符实例中,就是讲上边的例子中的instance.__dict__[self.storage_name] = value写成self.__dict__[self.storage_name] = value, 这种写法是有问题的, 这其中的self是描述符实例, 即使托管类中的类属性, 实际运行中, 可能有多个托管实例,而托管类的类属性即描述符实例只有两个: BMI.height和BMI.weight, 多个托管实例共享两个描述符实例所对应的值显然是有问题的.

    以上的代码看起起来还是不够简洁, 我们并不想在Quantity()实例中写成固定参数, 现在修改如下:

    import uuid
    
    
    class Quantity:
        """为属性提供限制"""
        
        def __init__(self):
            self.storage_name = str(uuid.uuid4())
        
        def __set__(self, instance, value):
            if isinstance(value, int): value = float(value)
            if not isinstance(value, float): raise ValueError("Value must be float")
            if value < 0:
                raise ValueError("value must not be less than or equal to 0")
            else:
                instance.__dict__[self.storage_name] = value
                
        def __get__(self, instance, owner): # 这里必须指定__get__, 因为storage_name和托管属性的名称不相同.
            return getattr(instance, self.storage_name)
    
    class BMI:
        """计算BMI指数"""
        height = Quantity()
        weight = Quantity()
        
        def __init__(self, name, height, weight):
            self.name = name
            self.height = height / 100
            self.weight = weight
        
        def bmi(self):
            """体重指数BMI=体重/身高的平方(国际单位kg/㎡)"""
            bmi_value = self.weight / self.height ** 2
            print(f"{self.name}的BMI指数是{bmi_value:.2f}")
    
    
    bmi = BMI("董小贱", 174, 87)
    bmi.bmi()
    
    

    值得注意的是: __get__方法有三个参数: self, instance和owner. 其中instance指的是托管实例即bmi, 通过描述符获取实例属性时用的到, owner指的是托管类即BMI的引用, 通过描述符获取类属性时用的到.以上的例子中, 如果通过BMI.height时会报错:AttributeError: 'NoneType' object has no attribute 'eceb1f2d-5e4a-462f-8177-ce9b5ec836a7'

    那么怎么想property那样返回描述符的对象呢? 只需要在__get__加个判断就好, 改动如下:

    def __get__(self, instance, owner): 
      if instance:
            return getattr(instance, self.storage_name)
      else:
        return self
    

    其实以上还有个问题, 就是报错的信息都是uuid的信息, 并不是对应的托管实例属性的属性信息, 调试起来的话相当不方便, 解决这个问题的方法先按下不表, 咱们接着看关于描述符的一些信息.

    1.2 描述符类型(覆盖性描述符和非覆盖型描述符)

    python中存取属性的方式是不对等的: 通过实例读取属性时, 通常返回的是实例中定义的属性, 如果实例中没有指定的属性, name会获取类属性; 但是为实例属性赋值时,通常会在实例中创建属性, 不会影响到类. 这种不对等的方式也影响到了描述符的行为. 根据描述符是否实现了__set__方法(是否会覆盖实例属性的值), 分为覆盖型描述符非覆盖型描述符

    1.2.1 覆盖型描述符

    实现 __set__方法的描述符属于覆盖型描述符,描述符是类属性, 实现了__set__方法的话, 会覆盖对实例属性的赋值操作.

    1. 如果同时实现了__set____get__方法, 也称强制描述符(影响了实例属性的读写, 实例属性的读写都要通过描述符处理). 例子如上边的1.1
    2. 如果只实现了__set__,没有实现__get__的覆盖型描述符,通过实例读取描述符会返回描述符对象本身, 因为没有处理读操作的 __get__ 方法。如果直接通过实例的__dict__ 属性创建同名实例属性, 以后再设置那个属性时, 仍会由 __set__ 方法插手接管, 但是读取那个属性的话,就会直接从实例中返回新赋予的值, 而不会返回描述符对象。也 就是说, 实例属性会遮盖描述符, 不过只有读操作是如此.(这里有点绕, 可以简单的理解为通过__dict__修改的实例属性, 会覆盖通过__set__修改的值, 正常修改的话, 还是会通过__set__方法设置.)(影响实例属性的写操作, 不影响其读操作.)
    import uuid
    
    class Quantity:
        """为属性提供限制"""
        
        def __init__(self):
            self.storage_name = str(uuid.uuid4())
        
        def __set__(self, instance, value):
            if isinstance(value, int): value = float(value)
            if not isinstance(value, float): raise ValueError("Value must be float")
            if value < 0:
                raise ValueError("value must not be less than or equal to 0")
            else:
                instance.__dict__[self.storage_name] = value
        
    class BMI:
        """计算BMI指数"""
        weight = Quantity()
        
        def __init__(self):
        
            self.weight = 3
     ######################以下是执行结果########################
    In [31]: bmi = BMI()
    
    In [32]: BMI.weight
    Out[32]: <__main__.Quantity at 0x10f41f320>
    
    In [33]: bmi.weight
    Out[33]: <__main__.Quantity at 0x10f41f320>
    
    In [34]: bmi.weight = 6
    
    In [35]: BMI.weight
    Out[35]: <__main__.Quantity at 0x10f41f320>
    
    In [36]: bmi.weight
    Out[36]: <__main__.Quantity at 0x10f41f320>
    
    In [37]: bmi.__dict__['weight'] = 9
    
    In [38]: BMI.weight
    Out[38]: <__main__.Quantity at 0x10f41f320>
    
    In [39]: bmi.weight
    Out[39]: 9
        
    

    从此可以看出:特性也是强制描述符, 如果没有提供设置值函数, 获取特性的值时会抛出AttributeError异常.

    1.2.2 非覆盖性描述符

    只实现了__get__方法的描述符属于非覆盖性描述符, 如果设置了同名的实例属性, 实例属性会覆盖描述符(影响描述符的读写操作), 只是描述符无法处理那个实例属性.

    In [45]: import uuid
        ...: 
        ...: 
        ...: class Quantity:
        ...:     """为属性提供限制"""
        ...:     
        ...:     def __init__(self):
        ...:         self.storage_name = str(uuid.uuid4())
        ...:     
        ...:             
        ...:     def __get__(self, instance, owner):
        ...:         return self
        ...: 
        ...: 
        ...: class BMI:
        ...:     """计算BMI指数"""
        ...:     weight = Quantity()
        ...:     
        ...:     def __init__(self,):
        ...:         self.weight = 3
        ...:     
        ...: 
    
    In [46]:  ## 以下是执行结果
    
    In [46]: bmi = BMI()
    
    In [47]: bmi.weight
    Out[47]: 3
    
    In [48]: BMI.weight
    Out[48]: <__main__.Quantity at 0x10f4086d8>
    
    In [49]: bmi.weight = 9
    
    In [50]: bmi.weight
    Out[50]: 9
    
    In [51]: BMI.weight
    Out[51]: <__main__.Quantity at 0x10f4086d8>
    
    1.3 在类中覆盖描述符

    读类属性的操作可以由依附在托管类上定义 有 __get__ 方法的描述符处理,但是写类属性的操作不会由依附在托管类上定义有 __get__ 方法的描述符处理。所以, 这就造成了不管描述符是不是覆盖型, 为类属性赋值都能覆盖描述符.

    好, 现在的描述符已经差不多了, 现在解决之前遗留的那个问题: 使用描述符, 报错的信息不是实例属性的名称,如何使其成为实例属性的名称.

    2. 上述问题的解决

    2.1 类装饰器

    看下边的代码

    import uuid
    
    def class_decorator(cls):
        for key, attr in cls.__dict__.items():
            if isinstance(attr, Quantity):
                type_name = type(attr).__name__
                attr.storage_name = f"{type_name}_{key}" # 注意,这里不能直接用key的值,否则,获取对应值的时候会到导致死循环
              
        return cls
    
    
    class Quantity:
        """为属性提供限制"""
        
        def __init__(self):
            self.storage_name = str(uuid.uuid4())
        
        def __set__(self, instance, value):
            if isinstance(value, int): value = float(value)
            if not isinstance(value, float): raise ValueError("Value must be float")
            if value < 0:
                raise ValueError("value must not be less than or equal to 0")
            else:
                instance.__dict__[self.storage_name] = value
        
        def __get__(self, instance, owner):
            return getattr(instance, self.storage_name) # 如果上边直接用key的值,会导致这里造成死循环, 导致错误抛出
    
    
    @class_decorator
    class BMI:
        """计算BMI指数"""
        height = Quantity()
        weight = Quantity()
        
        def __init__(self, name, height, weight):
            self.name = name
            self.height = height / 100
            self.weight = weight
        
        def bmi(self):
            """体重指数BMI=体重/身高的平方(国际单位kg/㎡)"""
            bmi_value = self.weight / self.height ** 2
            print(f"{self.name}的BMI指数是{bmi_value:.2f}")
    
    
    bmi = BMI("董小贱", 174, 87)
    bmi.bmi()
    

    通过类装饰器, 将本来产生的uuid替换掉, 可以实现报错的情况下显示可追溯的报错信息。 但是新的问题也随之出来了: 装饰器不能继承,只队直接依附的类有效。被装饰的类的子类可能继承也可能不继承装饰器所作的改动。
    那么就需要用到元编程了.

    2.2 元编程基础

    元类是制造类的工厂, 是用于构建类的类.

    python 中一切皆对象, 那么, 类也是对象. 一般的类都是都是继承自object, 默认的情况下, python中的类是type类的实例. 那么他们之间的关系是:objecttype 的实例,而 typeobject 的子类.(先有鸡还是先有蛋??). 所有的类都是type的实例,元类就是type的子类.因此可以作为类工厂(其实例就是类). 普通的类是通过__init__方法来初始化实例.同样的, 元类可以通过实现 __init__ 方法定制实例(即类)。元类的 __init__ 方法可以做到类装饰器能做的任何事情.

    2.3 用元编程实现
    import uuid
    
    class Quantity:
        """为属性提供限制"""
    
        def __init__(self):
            self.storage_name = str(uuid.uuid4())
        
        def __set__(self, instance, value):
            if isinstance(value, int): value = float(value)
            if not isinstance(value, float): raise ValueError("Value must be float")
            if value < 0:
                raise ValueError("value must not be less than or equal to 0")
            else:
                instance.__dict__[self.storage_name] = value
        
        def __get__(self, instance, owner):
            return getattr(instance, self.storage_name)  
    
    class Meta(type): # 继承自type制作作元类
      
        def __init__(cls, name, bases, attr_dict): # 一般情况下, self写作cls, 因为元类产生的实例是类.
            super().__init__(name, bases, attr_dict)
            for key, attr in attr_dict.items():
                if isinstance(attr, Quantity):
                    type_name = type(attr).__name__
                    attr.storage_name = f"{type_name}_{key}"
    
    class BMI(metaclass=Meta): # 指定元类是Meta
        """计算BMI指数"""
        height = Quantity()
        weight = Quantity()
        
        def __init__(self, name, height, weight):
            self.name = name
            self.height = height / 100
            self.weight = weight
        
        def bmi(self):
            """体重指数BMI=体重/身高的平方(国际单位kg/㎡)"""
            bmi_value = self.weight / self.height ** 2
            print(f"{self.name}的BMI指数是{bmi_value:.2f}")
    
    bmi = BMI("董小贱", 174, 87)
    bmi.bmi()
    
    

    参考资料: <<流畅的python>>

    相关文章

      网友评论

        本文标题:Python- ORM原理的基础

        本文链接:https://www.haomeiwen.com/subject/yqesactx.html