一、什么是元編程
元編程是一種編寫計算機程序的技術,這些程序可以將自己看作數據,因此你可以在運行時對它進行內省、生成和/或修改。
Python在語言層面對函數、類等基本類型提供了內省及實時創建和修改的能力;我們可以使用裝飾器向現有的函數、方法或類添加附加功能;同時我們也可以通過修改一些特殊的方法來變更類的行為;
二、使用的例子
- 面對一個復雜多變的json數據結構,雖然Python提供了處理JSon數據的API,但是返回的類型是dict,使用非常不方便不友好;接下來通過Python提供的元編程的能力,來實現一個類似對象層次結構的訪問方式;
import json
str = r'''{
"name":"mango",
"age": 30,
"address":{
"city":"beijing"
},
"schools":["xiaoxue","zhongxue"],
"sons":[
{
"name":"qing"
}
]
}'''
obj = json.loads(str)
print(type(obj))
print(obj.get('name'))
# <class 'dict'>
# mango
- 面向對象變成提倡封裝數字字段,通過訪問控制器來控制數據字段的校驗;接下來通過python提供的元編程能力進行實現;
三、通過__getattr__響應動態字段的獲取
__getattr__是一個實例方法,適用於訪問未定義的屬性的時候調用,即該屬性在實例中以及對應的類的基類以及祖先類中都不存在的時候調用;
獲取字段值的時候,我們先檢測對應的字段是否存在,如果不存在則拋出異常;如果字段存在,則檢測字段類型並決定是否對嵌套結構進行處理;
import json
from collections import abc
def loadJsonStr():
str = r'''{
"name":"mango",
"age": 30,
"address":{
"city":"beijing"
},
"schools":["xiaoxue","zhongxue"],
"sons":[
{
"name":"qing"
}
]
}'''
result = json.loads(str)
return result;
class JsonObject:
def __init__(self, jsondict):
self._data = dict(jsondict)
def __getattr__(self, name):
if name in self._data:
val = self._data.get(name)
if isinstance(val, abc.Mapping) or isinstance(val, abc.MutableSequence):
return self.initinner(val)
else:
return val
else:
raise AttributeError(f"{name} field does not exist")
def initinner(self, obj):
if isinstance(obj, abc.Mapping):
return self.__class__(obj)
elif isinstance(obj,abc.MutableSequence):
return [self.initinner(item) for item in obj]
else:
return obj
jobj = JsonObject(loadJsonStr())
print(jobj.name)
print(jobj.address)
print(jobj.address.city)
print(jobj.schools)
print(jobj.sons[0].name)
print(jobj.noField)
# mango
# <__main__.JsonObject object at 0x7ff7eac1cee0>
# beijing
# ['xiaoxue', 'zhongxue']
# qing
# AttributeError: noField field does not exist
五、使用__new__動態創建對象
我們通常把__init__稱為構造方法,但是其實用於構建實例的是__new__:這是一個必須返回一個實例的類方法。返回的實例會作為第一個參數(即self)傳給__init__方法。因為調用__init__方法時要傳入實例,而且禁止返回任何值,所以__init__方法其實是“初始化方法”。真正的構造方法是__new__。我們可以在構造函數中國完成對JSon字段值的解析處理;
import json
from collections import abc
def loadJsonStr():
str = r'''{
"name":"mango",
"age": 30,
"address":{
"city":"beijing"
},
"schools":["xiaoxue","zhongxue"],
"sons":[
{
"name":"qing"
}
]
}'''
result = json.loads(str)
return result;
class JsonObject:
def __new__(cls, args, **kwargs):
obj = args
if isinstance(obj, abc.Mapping):
return super().__new__(cls)
elif isinstance(obj,abc.MutableSequence):
return [cls(item) for item in obj]
else:
return obj
def __init__(self, jsondict):
self._data = dict(jsondict)
def __getattr__(self, name):
if name in self._data:
val = self._data.get(name)
if isinstance(val, abc.Mapping) or isinstance(val, abc.MutableSequence):
return self.__class__(val)
else:
return val
else:
raise AttributeError(f"{name} field does not exist")
jobj = JsonObject(loadJsonStr())
print(jobj.name)
print(jobj.address)
print(jobj.address.city)
print(jobj.schools)
print(jobj.sons[0].name)
print(jobj.noField)
# mango
# <__main__.JsonObject object at 0x7ff7eac1cee0>
# beijing
# ['xiaoxue', 'zhongxue']
# qing
# AttributeError: noField field does not exist
六、使用property裝飾器添加校驗邏輯
我們可以利用Python提供的property屬性,為數據字段添加校驗邏輯,從而可以避免調用方的變更;雖然property裝飾器定義在類上,但是編譯器會首先在類上查找,如果找不到才會從類的實例上查找;
import sys
class OrderItem:
def __init__(self, desc, count, price):
self.desc = desc
self.count = count
self.price = price
def subtotal(self):
return self.count * self.price
@property
def price(self):
print(f'{sys._getframe().f_code.co_name} getter')
return self._price
@price.setter
def price(self, val):
print(f'{sys._getframe().f_code.co_name} setter')
if val > 0:
self._price = val
else:
raise ValueError('price must be > 0')
@property
def count(self):
print(f'{sys._getframe().f_code.co_name} getter')
return self._count
@count.setter
def count(self, val):
print(f'{sys._getframe().f_code.co_name} setter')
if val > 0:
self._count = val
else:
raise ValueError('count must be > 0')
pbook = OrderItem('python books', 1, 50)
print(pbook.subtotal())
print(OrderItem.price)
print(OrderItem.price.setter)
print(OrderItem.price.getter)
print(vars(pbook))
jbook = OrderItem('java books', 0, 50)
print(jbook.subtotal())
# count setter
# price setter
# count getter
# price getter
# 50
# <property object at 0x7ffa8ddf8a90>
# <built-in method setter of property object at 0x7ffa8ddf8a90>
# <built-in method getter of property object at 0x7ffa8ddf8a90>
# {'desc': 'python books', '_count': 1, '_price': 50}
# count setter
# ValueError: count must be > 0
可以將創建字段的邏輯抽取出來作為公用的方法
import sys
def buildVolidateField(name):
_name = f'_{name}'
def getter(obj):
return obj.__dict__.get(_name)
def setter(obj, value):
if value > 0:
obj.__dict__[_name]= value
else:
raise ValueError(f'{name} must be > 0')
return property(getter, setter)
class OrderItem:
price = buildVolidateField('price')
count = buildVolidateField('count')
def __init__(self, desc, count, price):
self.desc = desc
self.count = count
self.price = price
def subtotal(self):
return self.count * self.price
pbook = OrderItem('python books', 1, 50)
print(pbook.subtotal())
print(OrderItem.price)
print(OrderItem.price.setter)
print(OrderItem.price.getter)
print(vars(pbook))
jbook = OrderItem('java books', 0, 50)
print(jbook.subtotal())
# 50
# <property object at 0x7fbc90cfdd60>
# <built-in method setter of property object at 0x7fbc90cfdd60>
# <built-in method getter of property object at 0x7fbc90cfdd60>
# {'desc': 'python books', '_count': 1, '_price': 50}
# ValueError: count must be > 0
七、使用描述符類實現字段校驗邏輯
描述符是實現了特定協議的類,這個協議包括__get__、__set__和__delete__方法。property類實現了完整的描述符協議。通常,可以只實現部分協議。其實,我們在真實的代碼中見到的大多數描述符只實現了__get__和__set__方法,還有很多只實現了其中的一個。
import sys
class VolidateDescr:
def __init__(self, name):
self.name = f'_{name}'
def __set__(self, instance, value):
if value > 0:
instance.__dict__[self.name] = value
else:
raise ValueError(f'{self.name} must be > 0')
def __get__(self, instance, default):
if instance is None:
return self;
else:
return instance.__dict__[self.name]
class OrderItem:
price = VolidateDescr('price')
count = VolidateDescr('count')
def __init__(self, desc, count, price):
self.desc = desc
self.count = count
self.price = price
def subtotal(self):
return self.count * self.price
pbook = OrderItem('python books', 1, 50)
print(pbook.subtotal())
print(OrderItem.price)
print(OrderItem.price.__set__)
print(OrderItem.price.__get__)
print(vars(pbook))
jbook = OrderItem('java books', 0, 50)
print(jbook.subtotal())
# 50
# <__main__.VolidateDescr object at 0x7f162d0ac9a0>
# <bound method VolidateDescr.__set__ of <__main__.VolidateDescr object at 0x7f162d0ac9a0>>
# <bound method VolidateDescr.__get__ of <__main__.VolidateDescr object at 0x7f162d0ac9a0>>
# {'desc': 'python books', '_count': 1, '_price': 50}
# ValueError: _count must be > 0
目前只是兩個數字字段添加了校驗,接下來為desc字符串字段添加非空校驗;兩種數據類型字段只有校驗的差異,我們將校驗邏輯跟字段的訪問控制進行抽離,分別實現兩個具體的校驗類;
import abc
class FieldDescr:
_countor = 0
def __init__(self):
self.name = f'_{self.__class__.__name__}_{self.__class__._countor}'
self.__class__._countor += 1
def __set__(self, instance, value):
setattr(instance, self.name, value)
def __get__(self, instance, owner):
if instance is None:
return self
else:
return getattr(instance, self.name)
class Validated(FieldDescr):
def __set__(self, instance, value):
value = self.validate(instance, value)
super().__set__(instance, value)
@abc.abstractmethod
def validate(self, instance, value):
'''this is abstract method'''
class GreatZeroIntField(Validated):
def validate(self, instance, value):
if value <= 0:
raise ValueError(f'{self.name} value must be > 0')
return value
class NoEmptyStrField(Validated):
def validate(self, instance, value):
value = value.strip()
if len(value) == 0:
raise ValueError('value cant not be empty or blank')
return value
class OrderItem:
descr = NoEmptyStrField()
price = GreatZeroIntField()
count = GreatZeroIntField()
def __init__(self, descr, price, count):
self.descr = descr
self.price = price
self.count = count
def subtotal(self):
return self.count * self.price
pbook = OrderItem('python books', 1, 50)
print(pbook.subtotal())
print(OrderItem.price)
print(OrderItem.price.__set__)
print(OrderItem.price.__get__)
print(vars(pbook))
jbook = OrderItem('java books', 0, 50)
print(jbook.subtotal())
# 50
# <__main__.GreatZeroIntField object at 0x7fa2eb37fd00>
# <bound method Validated.__set__ of <__main__.GreatZeroIntField object at 0x7fa2eb37fd00>>
# <bound method FieldDescr.__get__ of <__main__.GreatZeroIntField object at 0x7fa2eb37fd00>>
# {'_NoEmptyStrField_0': 'python books', '_GreatZeroIntField_0': 1, '_GreatZeroIntField_1': 50}
# ValueError: _GreatZeroIntField_0 value must be > 0
- 定制數據字段的名字
到現在我們已經封裝自動生成特性,自動生成的數據字段的名字並不能很好的跟類上對應的特性名稱對應上;接下來通過類裝飾器和元類來定制數據字段的名字;
類裝飾器在編譯器編譯完類之后執行,這個時候類上的特性已經生成完畢,我們可以遍歷類的__dict__,找到對應的特性並修改其name字段的值即可;
import abc
def renamePrivateField(cls):
for key,value in cls.__dict__.items():
if isinstance(value, Validated):
value.name = f'_{value.__class__.__name__}_{key}'
return cls
class FieldDescr:
_countor = 0
def __init__(self):
self.name = f'_{self.__class__.__name__}_{self.__class__._countor}'
self.__class__._countor += 1
def __set__(self, instance, value):
setattr(instance, self.name, value)
def __get__(self, instance, owner):
if instance is None:
return self
else:
return getattr(instance, self.name)
class Validated(FieldDescr):
def __set__(self, instance, value):
value = self.validate(instance, value)
super().__set__(instance, value)
@abc.abstractmethod
def validate(self, instance, value):
'''this is abstract method'''
class GreatZeroIntField(Validated):
def validate(self, instance, value):
if value <= 0:
raise ValueError(f'{self.name} value must be > 0')
return value
class NoEmptyStrField(Validated):
def validate(self, instance, value):
value = value.strip()
if len(value) == 0:
raise ValueError('value cant not be empty or blank')
return value
@renamePrivateField
class OrderItem:
descr = NoEmptyStrField()
price = GreatZeroIntField()
count = GreatZeroIntField()
def __init__(self, descr, price, count):
self.descr = descr
self.price = price
self.count = count
def subtotal(self):
return self.count * self.price
pbook = OrderItem('python books', 1, 50)
print(pbook.subtotal())
print(OrderItem.price)
print(OrderItem.price.name)
print(OrderItem.price.__set__)
print(OrderItem.price.__get__)
print(vars(pbook))
# 50
# <__main__.GreatZeroIntField object at 0x7f23e67bf2b0>
# _GreatZeroIntField_price
# <bound method Validated.__set__ of <__main__.GreatZeroIntField object at 0x7f23e67bf2b0>>
# <bound method FieldDescr.__get__ of <__main__.GreatZeroIntField object at 0x7f23e67bf2b0>>
# {'_NoEmptyStrField_descr': 'python books', '_GreatZeroIntField_price': 1, '_GreatZeroIntField_count': 50}
由於類裝飾器在類編譯完整之后直接執行,可能會出現被子類覆蓋的情況,元類可以很好的解決這個問題
import abc
class FieldDescr:
_countor = 0
def __init__(self):
self.name = f'_{self.__class__.__name__}_{self.__class__._countor}'
self.__class__._countor += 1
def __set__(self, instance, value):
setattr(instance, self.name, value)
def __get__(self, instance, owner):
if instance is None:
return self
else:
return getattr(instance, self.name)
class Validated(FieldDescr):
def __set__(self, instance, value):
value = self.validate(instance, value)
super().__set__(instance, value)
@abc.abstractmethod
def validate(self, instance, value):
'''this is abstract method'''
class GreatZeroIntField(Validated):
def validate(self, instance, value):
if value <= 0:
raise ValueError(f'{self.name} value must be > 0')
return value
class NoEmptyStrField(Validated):
def validate(self, instance, value):
value = value.strip()
if len(value) == 0:
raise ValueError('value cant not be empty or blank')
return value
class renamePrivateFieldMeta(type):
def __init__(cls, name, bases, attr_dict):
super().__init__(name, bases, attr_dict)
for key, value in cls.__dict__.items():
if isinstance(value, Validated):
value.name = f'_{value.__class__.__name__}_{key}'
class OrderEntity(metaclass=renamePrivateFieldMeta):
'''rename entity'''
class OrderItem(OrderEntity):
descr = NoEmptyStrField()
price = GreatZeroIntField()
count = GreatZeroIntField()
def __init__(self, descr, price, count):
self.descr = descr
self.price = price
self.count = count
def subtotal(self):
return self.count * self.price
pbook = OrderItem('python books', 1, 50)
print(pbook.subtotal())
print(OrderItem.price)
print(OrderItem.price.name)
print(OrderItem.price.__set__)
print(OrderItem.price.__get__)
print(vars(pbook))
# 50
# <__main__.GreatZeroIntField object at 0x7f393be8c070>
# _GreatZeroIntField_price
# <bound method Validated.__set__ of <__main__.GreatZeroIntField object at 0x7f393be8c070>>
# <bound method FieldDescr.__get__ of <__main__.GreatZeroIntField object at 0x7f393be8c070>>
# {'_NoEmptyStrField_descr': 'python books', '_GreatZeroIntField_price': 1, '_GreatZeroIntField_count': 50}