Django的models實現分析
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: raiseTypeError("Must be a string") if isinstance(int, age): self.age = age else: raise TypeError("Must be an int")
但是,如果更新年齡時就會遇到問題,無法重用校驗邏輯。
有沒有簡潔的方法呢?
2.2 使用property裝飾器
使用property也是一個方法,可以針對每個屬性來設置,但是如果一個類有多個屬性,代碼就會非常的多,並且產生大量的冗余,就像這樣。
class Student:View Codedef __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
代碼冗余太多,每個檢查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 = 4444View Code
執行結果:
raise ValueError("{} must larger min_value".format(self.storage_name)) ValueError: IntegerField property age must larger min_value
這樣,完全模仿了models的定義。
類的初始化和後續屬性賦值,都會自動調用__set__來設置並校驗。
5 原理解釋
5.1 屬性讀取順序
通過實例讀取屬性時,通常返回的是實例中定義的屬性。讀取順序如下:
- 實例屬性
- 類屬性
- 父類屬性
- __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實現分析