Python進(jìn)階7

元類編程

Python屬性函數(shù)

引言

  • Python中我們對于自己設(shè)置的類,盡量要使其屬性私有化,獲得更好的封裝性。
  • 如果要訪問和修改私有屬性,要為其設(shè)置setget方法。
  • Python中,可以使用特殊的裝飾器將setget方法屬性化,這樣就能夠使用更簡潔的語法去調(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)
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容