元類編程
Python屬性函數(shù)
引言
-
Python中我們對于自己設(shè)置的類,盡量要使其屬性私有化,獲得更好的封裝性。 - 如果要訪問和修改私有屬性,要為其設(shè)置set和get方法。
-
Python中,可以使用特殊的裝飾器將set和get方法屬性化,這樣就能夠使用更簡潔的語法去調(diào)用這些方法。
使用案例
class Person:
def __init__(self, name, age):
self.__name = name
self.__age = age
# 這里以 age 為例
@property
def age(self):
return self.__age
@age.setter
def age(self, age):
if age < 0:
self.__age = age
elif age > 120:
self.__age = 120
else:
self.__age = age
me = Person("MetaTian", 20)
print(me.age) # 直接作為屬性進(jìn)行調(diào)用,get方法
me.age += 1 # get 和 set 方法同時(shí)使用
print(me.age) # get 方法
# result:
# 20
# 21
魔法函數(shù)getattr()和getattribute()
引言
- 這兩個(gè)函數(shù)是解釋器在查找對象屬性時(shí)要進(jìn)行調(diào)用的
- 如果沒找到代碼需要的屬性,則會調(diào)用
__getattr__() - 如果實(shí)現(xiàn)了
__getattribute__(),則不管請求什么屬性都會先調(diào)用這個(gè)魔法函數(shù)
使用案例
"""
如果沒有實(shí)現(xiàn) __getattr__(),調(diào)用未定義的屬性后,會報(bào)錯(cuò)
"""
class Person:
pass
me = Person()
print(me.age)
# result:
# AttributeError: 'Person' object has no attribute 'age'
class Person:
# attr 是代碼請求的屬性
def __getattr__(self, attr):
return "{0} dose not exist".format(attr)
me = Person()
print(me.age)
print(me.name)
# result:
# age dose not exist
# name dose not exist
"""
實(shí)現(xiàn)了__getattribute__()魔法函數(shù)
不論請求什么屬性,都返回同樣的值
"""
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
def __getattribute__(self, attr):
return "value"
me = Person("MetaTian", 19)
print(me.age, me.name, me.gender)
# result:
# value value value
屬性描述符
前言
- 描述符是對多個(gè)屬性運(yùn)用相同的邏輯來進(jìn)行存取的一種方式,它是實(shí)現(xiàn)了特定魔法函數(shù)的一個(gè)類。
- 只要實(shí)現(xiàn)了
__get__(),__set__(),__delete__()三個(gè)魔法函數(shù)中的任意一個(gè),這個(gè)類就是描述符。 -
property最大的缺點(diǎn)就是它修飾屬性的過程不能重復(fù)使用,如果要對多個(gè)屬性進(jìn)行非負(fù)檢查(>=0),那必須對每個(gè)屬性的set方法分別包裝。描述符就是可以重用的屬性。
使用案例
"""
定義了一個(gè) “非負(fù)的” 描述符
"""
class NonNegative:
def __init__(self, label):
self.label = label # 存儲描述符在對象級別的名稱
def __get__(self, instance, owner):
# 當(dāng)進(jìn)行屬性調(diào)用時(shí),obj.p
# instance = obj
# owner = type(obj)
return instance.__dict__.get(self.label)
def __set__(self, instance, value):
# 當(dāng)進(jìn)行屬性調(diào)用時(shí),obj.p = val
# instance = obj
# value = val
if not isinstance(value, int):
raise ValueError("Int value allowed")
if value < 0:
raise ValueError("Negative value not allowed: %s" % value)
instance.__dict__[self.label] = value # 把通過檢驗(yàn)的屬性值反向設(shè)置給調(diào)用對象
def __delete__(self, instance):
pass
"""
使用描述符的一個(gè)類
"""
class Person:
# 定義為類的屬性,才能觸發(fā)后面的屬性檢查
age = NonNegative("age") # label
def __init__(self, name, age):
self.name = name
self.age = age # 描述符會起作用,自動調(diào)用get和set魔法函數(shù)
me = Person("MetaTian", "21")
# result:
# ValueError: Int value allowed
him = Person("Rity", -18)
# result:
# ValueError: Negative value not allowed: -21
me = Person("MetaTian", 21)
him = Person("Rity", 21)
him.age += 1
me.age -= 1
print(him.age, me.age)
# result:
# 22 20
小結(jié)
描述符其實(shí)是目標(biāo)類的一個(gè)屬性,也就是說目標(biāo)類中有一個(gè)描述符實(shí)例。當(dāng)目標(biāo)類的實(shí)例準(zhǔn)備操作自身屬性時(shí),會首先將它交給類的個(gè)描述符實(shí)例進(jìn)行管理(__get__(), __set__(), __delete__()),然后由它把屬性設(shè)置到實(shí)例中(obj.__dict__[label])。
屬性調(diào)用順序
引言
-
Python的描述符有兩種類型:數(shù)據(jù)描述符和非數(shù)據(jù)描述符。 - 數(shù)據(jù)描述符:實(shí)現(xiàn)了
__get__()和__set__()魔法函數(shù)。 - 非數(shù)據(jù)描述符:只實(shí)現(xiàn)了
__get__()魔法函數(shù)。 - 使用不同的描述符,屬性查找的過程是不一樣的。
詳細(xì)
在使用me.age操作屬性的時(shí)候,解釋器對屬性age的查找順序是怎么樣的呢?
如果me是某個(gè)對象的實(shí)例,那么對于me.age或與其等價(jià)的getattr(me, "age")屬性操作方式,會首先調(diào)用__getattribute__(),如果調(diào)用過程中拋出了AttributeError,這時(shí)就會調(diào)用__getattr__()
如果age是一個(gè)屬性描述符,則相關(guān)屬性操作操作會委托給__get__()魔法函數(shù),這個(gè)過程發(fā)生在__getattribute__()內(nèi)部
此時(shí),age屬性的調(diào)用順序如下:
-
如果
age出現(xiàn)在Person或其基類的__dict__中,而且age是一個(gè)數(shù)據(jù)描述符,那么直接調(diào)用__get__()方法 -
如果
age出現(xiàn)在me的__dict__中,那么直接返回me.__dict__['age'] -
如果
age出現(xiàn)在Person或其基類的__dict__中:- 如果
age是非數(shù)據(jù)描述符,那么就調(diào)用__get__ - 否則返回
Person.__dict__['age']
- 如果
-
如果定義了
__getattr__(),則調(diào)用__getattr__() - 否則,拋出AttributeError
__new __和 __init __的區(qū)別
引言
-
__new__()允許我們在類的生成過程中加入自己的邏輯,是一個(gè)靜態(tài)方法。 -
__init__()可以在生成對象之后加入自己的邏輯,一般是初始化屬性。 -
__init__()的調(diào)用在__new__()之后。
使用案例
"""
new 用來控制對象的生成過程,在對象生成之前起作用
init 用來完善生成的對象,在對象生成之后調(diào)用
如果 new 中沒有返回生成的對象,則 init 方法不會調(diào)用
"""
def Person:
def __new__(cls, *args, **kwargs):
print("---in new---")
return super.__new__(cls)
def __init__(self, name):
print("---in init---")
self.name = name
me = Person("MetaTian")
# result:
# ---in new---
# ---in init---
自定義元類
引言
- 前面介紹過,
Python中一切皆對象,類也是一個(gè)對象,在Java中想要動態(tài)生成一個(gè)類,似乎并不容易,但是對于動態(tài)語言Python,這是比較容易實(shí)現(xiàn)和理解的。
def create_cls(name):
if name = "User": # 動態(tài)生成的 User 類
class User:
def __str__(self):
return "user"
return User
elif name = "Person": # 動態(tài)生成的 Person 類
class Person:
def __str__(self):
return "person"
return Person
ClsUser, ClsPson = create_cls("User"), create_cls("Person")
user, person = ClsUser(), ClsPson()
print(user, person)
# result:
# user person
- 使用
type動態(tài)地創(chuàng)建類會更加簡潔。 - 但是實(shí)際使用,往往很少直接使用
type,而是用自己定義的元類。
使用案例
"""
class type(name, bases, dict)
name: str, 要創(chuàng)建類的名稱
bases: tuple, 創(chuàng)建類要繼承的基類
dict: 創(chuàng)建類的屬性集合
"""
# 必須有 self 作為參數(shù)
def say(self):
print("I am a person")
Person = type("Person", (), {"name":"MetaTian", "say":say})
me = Person()
print(me.name)
me.say()
# result:
# MetaTian
# I am a person
Python中類的創(chuàng)建過程中,會首先尋找metaclass,通過metaclass去創(chuàng)建這個(gè)類,如果沒有找到metaclass,則向上找基類,使用它們的meataclass,如果都沒有,再直接調(diào)用type來創(chuàng)建這個(gè)類。
# 這是我們自己定義的一個(gè)元類
class MetaClass(type):
def __new__(cls, *args, **kwargs):
# 這里不加入其它邏輯,只構(gòu)建一個(gè)框架,委托給 type 進(jìn)行類的構(gòu)建
return super().__new__(cls, *args, **kwargs) # 這里需要傳遞參數(shù)
# 用我們自己定義的元類來控制 Person 類的構(gòu)建
class Person(metaclass=MetaClass):
def __init__(self, name):
self.name = name
def __str__(self):
return "I am {name}".format(name=self.name)
me = Person("MetaTian")
print(me)
# result:
# I am MetaTian
通過元類實(shí)現(xiàn)ORM
引言
使用案例
"""
需求
我們想構(gòu)建出一個(gè) Person 類,構(gòu)造其一個(gè)實(shí)例后
通過obj.save()方法,就可以將它存儲在數(shù)據(jù)庫中
"""
class Person:
# 這里的屬性對應(yīng)數(shù)據(jù)庫表中的每一項(xiàng)
# 在表中的哪一列,這一列數(shù)據(jù)的約束條件是什么
name = CharField(db_colunm=None, max_length=None)
# Person 類中,定義了另外一個(gè)類,用來存放其他的一些信息
class Meta:
db_table = "person" # 這里存放了對象對應(yīng)的數(shù)據(jù)庫表名,用來后面拼湊 sql 語句
me = Person("MetaTian")
me.save() # 在數(shù)據(jù)庫中插入一條記錄
首先,我們來構(gòu)建數(shù)據(jù)描述符,數(shù)據(jù)的存儲以及類型檢查都委托給它來完成,把簡單的調(diào)用方式暴露出來即可。這里不僅僅要處理需存儲的數(shù)據(jù),數(shù)據(jù)在數(shù)據(jù)庫中的一些屬性也要進(jìn)行處理,比如所在的column和存儲格式限制。
"""
分析
類中的屬性要采用描述符的方式,在存取數(shù)據(jù)的時(shí)候要進(jìn)行類型檢查
不僅要保存屬性值的信息,還要存儲屬性在數(shù)據(jù)庫表中的信息(max_length, db_column)
"""
class CharField:
def __init__(self, max_length=None, db_column=None):
self.db_column = dbcolumn
self.max_length = max_length
self._value = None # 描述符初始化的時(shí)候不給默認(rèn)值,一般由用戶在后面賦值
# 將這兩個(gè)屬性設(shè)置為必填項(xiàng)
if max_length is None:
raise ValueError("max_length info required")
if db_column is None:
raise ValueError("db_column info required")
# 返回值
def __get__(self, instance, owner):
return self._value
# 設(shè)置值
def __set__(self, instance, value):
if not isinstance(value, str):
raise ValueError("String value required")
if len(value) > self.max_length:
raise ValueError("valuen length invalid")
self._value = value # 通過檢查后,賦值保存
"""
現(xiàn)在我們有了如下結(jié)構(gòu):
還需要自己定義的一個(gè)元類來控制 Person 類的生成
"""
class CharField:
pass
class Person:
name = CharField(max_length=10, db_colunm="name") # name 屬性的存取檢查已經(jīng)完成
class Meta:
db_table = "person" # 這個(gè)類對象和 person 這個(gè)數(shù)據(jù)庫表對應(yīng)
自定義的元類,要修改__new__()方法,在類的創(chuàng)建過程中加入一些我們自己的邏輯。
class ModelMetaClass(type):
"""
這里對參數(shù)元組進(jìn)行了拆解,便于更好的觀察
name: 要構(gòu)建類對象的名稱
bases: 繼承的基類
attrs: 構(gòu)建類對象中的屬性,我們要從中提取出和業(yè)務(wù)邏輯有關(guān)的屬性進(jìn)行另外存儲
"""
def __new__(cls, name, bases, attrs, **kwargs):
for k, v in attrs.items():
print("{key}:{val}".format(key=k, val=v))
return super().__new__(cls, name, bases, attrs, **kwargs)
# result:
# __qualname__:Person
# __module__:__main__
# Meta:<class '__main__.Person.Meta'> # 這是我們在 Person 中定義的類,存放信息
# name:<__main__.CharField object at 0x00000226B18B4F28> # 這是我們需要的
class ModelMetaClass(type):
def __new__(cls, name, bases, attrs, **kwargs):
# 抽離出數(shù)據(jù)
fields = {}
for k, v in attrs.items():
if isinstance(v, CharField): # 數(shù)據(jù)域
fields[k] = v
for k in fields:
del attrs[k] # 清空原來的數(shù)據(jù)
# 抽離出表名稱
attrs_meta = attrs.get("Meta", None) # 關(guān)于字典 get 方法的使用,前面講過
db_table = name.lower() # 數(shù)據(jù)庫表名默認(rèn)是類的小寫形式
if attrs_meta: # 如果類存儲了表信息,用類中定義的
db_table = getattr(attrs_meta, "db_table")
del attrs["Meta"] # 表名稱信息已經(jīng)獲得,這里就不需要了
# 重組 attrs 參數(shù)
_meta = {"db_table":db_table}
attrs["_meta"] = _meta
attrs["fields"] = fields
for k, v in attrs.items():
print("{key}:{val}".format(key=k, val=v))
# 最后委托給 type ,完成類的創(chuàng)建
return super().__new__(cls, name, bases, attrs, **kwargs)
# result:
# __module__:__main__
# fields:{'name': <__main__.CharField object at 0x000001AFCD994F98>}
# __qualname__:Person
# _meta:{'db_table': 'person'}
"""
現(xiàn)在我們有了如下結(jié)構(gòu):
"""
# 控制類的生成
class ModelMetaClass(type):
pass
# 檢查屬性存取
class CharField:
pass
class Person(metaclass=ModelMetaClass):
name = CharField(max_length=10, db_colunm="name") # name 屬性的存取檢查已經(jīng)完成
class Meta:
db_table = "person" # 這個(gè)類對象和 person 這個(gè)數(shù)據(jù)庫表對應(yīng)
最后,還要在Person類中添加我們需要的邏輯,比如初始化操作、寫入數(shù)據(jù)庫操作、查詢操作等。因此要為每一個(gè)邏輯添加一個(gè)方法加入類中,但是會讓這個(gè)類顯得十分臃腫。我們構(gòu)建這個(gè)類的目的就是希望它能和數(shù)據(jù)庫的表結(jié)構(gòu)盡量一一對應(yīng),簡化我們的操作,同時(shí)不同類的操作需求是一樣的,因此,可以考慮將這些操作邏輯抽象成一個(gè)父類。
class BaseModel(metaclass=ModelMetaClass):
# 便于通用,這里要約定初始化的一種方式
def __init__(self, *args, **kwargs):
for k, v in kwargs.items():
setattr(self, k, v)
super.__init__()
def save(self):
fields, values = [], []
for k, v in self.fields.items():
fields.append(v.db_column)
values.append(str(getattr(self, k))) # 把所有的值都變成字符串,便于后面拼接
sql = "insert {table}({fields}) value({values})".format(table=self._meta["db_table"], fields=",".join(fields), values=",".join(values))
print(sql)
# todo 和數(shù)據(jù)庫連接有關(guān)的邏輯
class Person(BaseModel):
name = CharField(max_length=10, db_colunm="name") # name
class Meta:
db_table = "person" # 這個(gè)類對象和 person 這個(gè)數(shù)據(jù)庫表對應(yīng)
me = Person(name="MetaTian")
me.save()
完整的代碼
class CharField:
def __init__(self, max_length=None, db_column=None):
self.db_column = db_column
self.max_length = max_length
self._value = None
if max_length is None:
raise ValueError("max_length info required")
if db_column is None:
raise ValueError("db_column info required")
def __get__(self, instance, owner):
return self._value
def __set__(self, instance, value):
if not isinstance(value, str):
raise ValueError("String value required")
if len(value) > self.max_length:
raise ValueError("valuen length invalid")
self._value = value
class ModelMetaClass(type):
def __new__(cls, name, bases, attrs, **kwargs):
# BaseModel和其子類都要通過這個(gè)元類來進(jìn)行創(chuàng)建
# 子類才有相關(guān)的 Meta 信息,進(jìn)行信息重組,這里進(jìn)行過濾
if name == "BaseModel":
return super().__new__(cls, name, bases, attrs, **kwargs)
# 抽離出數(shù)據(jù)
fields = {}
for k, v in attrs.items():
if isinstance(v, CharField):
fields[k] = v
for k in fields:
del attrs[k]
# 抽離出表名稱
attrs_meta = attrs.get("Meta", None) # 關(guān)于字典 get 方法的使用,前面講過
db_table = name.lower() # 數(shù)據(jù)庫表名默認(rèn)是類的小寫形式
if attrs_meta: # 如果類存儲了表信息,用類中定義的
db_table = getattr(attrs_meta, "db_table")
# 重組 attrs 參數(shù)
_meta = {"db_table":db_table}
attrs["_meta"] = _meta
attrs["fields"] = fields
del attrs["Meta"] # 表名稱信息已經(jīng)獲得,這里就不需要了
# 最后委托給 type ,完成類的創(chuàng)建
return super().__new__(cls, name, bases, attrs, **kwargs)
class BaseModel(metaclass=ModelMetaClass):
# 便于通用,這里要約定初始化的一種方式
def __init__(self, *args, **kwargs):
for k, v in kwargs.items():
setattr(self, k, v)
super().__init__()
def save(self):
fields, values = [], []
for k, v in self.fields.items():
fields.append(v.db_column)
values.append(str(getattr(self, k, None))) # 把所有的值都變成字符串,便于后面拼接
sql = "insert {table}({fields}) value({values})".format(table=self._meta["db_table"], fields=",".join(fields), values=",".join(values))
# todo 和數(shù)據(jù)庫有關(guān)的邏輯
print(sql)
class Person(BaseModel):
name = CharField(max_length=10, db_column="name") # name 屬性的存取檢查已經(jīng)完成
class Meta:
db_table = "person" # 這個(gè)類對象和 person 這個(gè)數(shù)據(jù)庫表對應(yīng)
me = Person(name="MetaTian")
me.save()
# result:
# insert person(name) value(MetaTian)