1. 程式人生 > >Django的models實現分析

Django的models實現分析

創建 優雅 __init__ python面向對 test object類 meta lin wid

1 引子

1.1 神奇的Django中的models

我們先來看一段在Django項目中常用的代碼:

設置數據庫models代碼:

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

這裏有幾個神奇的地方,涉及到了python中最神秘的幾個特性。

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

  • 字段名稱name\age自動轉換為了數據庫中的字段名稱
  • 自動校驗數據類型,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》

元類部分

Django的models實現分析