程序员

Python- ORM原理的基础

2020-01-08  本文已影响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中的函数实现的一样的效果, 这其中的几个定义:

值得注意的是: 编写__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>>

上一篇下一篇

猜你喜欢

热点阅读