python之超程式設計

無風聽海發表於2021-11-29

一、什麼是超程式設計

超程式設計是一種編寫計算機程式的技術,這些程式可以將自己看作資料,因此你可以在執行時對它進行內省、生成和/或修改。

Python在語言層面對函式、類等基本型別提供了內省及實時建立和修改的能力;我們可以使用裝飾器向現有的函式、方法或類新增附加功能;同時我們也可以通過修改一些特殊的方法來變更類的行為;

二、使用的例子

  1. 面對一個複雜多變的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
  1. 物件導向變成提倡封裝數字欄位,通過訪問控制器來控制資料欄位的校驗;接下來通過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
  1. 定製資料欄位的名字

到現在我們已經封裝自動生成特性,自動生成的資料欄位的名字並不能很好的跟類上對應的特性名稱對應上;接下來通過類裝飾器和元類來定製資料欄位的名字;

類裝飾器在編譯器編譯完類之後執行,這個時候類上的特性已經生成完畢,我們可以遍歷類的__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}

相關文章