23. 元類

hbutmeng發表於2024-08-20

1. 元類的概念

python中一切皆物件,八大基本資料型別是物件,類例項化得到的物件也是物件;類本身也是一種物件

type(python自帶的元類)---元類metaclass(自定義的元類)---類(class)---物件(obj)

元類,即高於用class定義的類的類,被稱為metaclass(元類),其中type就是Python自帶的元類。這意味著我們可以手動建立type的例項作為類的定義,或者透過定義派生自type的元類,對其進行修改,以實現更高階的功能和靈活性。

class Student():
    def __init__(self, name):
        self.name = name


# 1.例項化類得到物件
stu1 = Student(name='lavigne')
# 2.檢視物件的資料型別
print(type(stu1))  # <class '__main__.Student'>
# 3.檢視這個類的資料型別
print(type(Student))  # <class 'type'>
# 4.檢視產生字典、列表、字串的方法的資料型別
print(type(dict))  # <class 'type'>
print(type(list))  # <class 'type'>
print(type(str))  # <class 'type'>

透過檢視每一種資料的資料型別,會發現都共同擁有一個類,這個類就是type稱之為元類

2. 產生類的兩種方式

2.1 關鍵字建立

語法

class 類名(基類):  # 基類預設object 
    # 類體程式碼

class Student():
    def __init__(self, name):
        self.name = name

# 檢視類的元類
print(type(Student))  # <class 'type'>

2.2 透過type建立

語法

# 檢視列印時用的type的__init__方法
# print(type())


def __init__(cls, what, bases=None, dict=None):  # known special case of type.__init__
    """
    type(object) -> the object's type
    type(name, bases, dict, **kwds) -> a new type
    # (copied from class doc)
    """
    pass
# 類名 = type('類名',(父類1,父類2,...),{資料屬性名:資料屬性值,函式屬性名:函式屬性值,...})

示例

def read():
    ...

Student = type('Student', (object,), {'name': 'lavigne', 'read': read})  # 自動按位置傳入
print(type(Student))  # <class 'type'>
print(Student.__dict__)

# {'name': 'lavigne', 'age': 20, '__module__': '__main__', '__dict__': <attribute '__dict__' of 'Student' objects>,
'__weakref__': <attribute '__weakref__' of 'Student' objects>, '__doc__': None}
class Student: name = 'lavigne' read = read print(type(Student)) # <class 'type'> print(Student.__dict__) # {'__module__': '__main__', 'name': 'lavigne', 'age': 20, '__dict__': <attribute '__dict__' of 'Student' objects>,
'__weakref__': <attribute '__weakref__' of 'Student' objects>, '__doc__': None}

使用關鍵字class定義的類和使用列印型別的type定義的類屬性字典一致

3. 為什麼要使用元類

元類可以控制類的建立,可以定製類的具體資料

4. 元類的使用

4.1 問題引出

要求所有產生的類的名字都是大寫的

4.2 推導

物件的產生過程,是透過類裡面的_ _init_ _方法實現的

猜測:在元類裡面也有一個_ _init_ _方法

4.3 基本使用

# def __init__(cls, what, bases=None, dict=None):  # known special case of type.__init__
#     """
#     type(object) -> the object's type
#     type(name, bases, dict, **kwds) -> a new type
#     # (copied from class doc)
#     """
#     pass

# 1.重寫元類中的__init__方法,產生一個自定義元類
class NewType(type):
    def __init__(cls, name, bases, dct):
        print(f'自定義的元類__init__被呼叫,新類的名稱是:{name}')
        print(f'自定義的元類__init__被呼叫,新類的基類是:{bases}')
        print(f'自定義的元類__init__被呼叫,新類的屬性字典是:{dct}')
        super().__init__(name, bases, dct)  # 三個引數只是產生新類時實參與系統元類type的中間值,可以與type中的名字不一樣

# 2.元類的使用採用metaclass關鍵字宣告
class Student(metaclass=NewType):  # 3.建立一個新的類
    name = 'lavigne'

# 4.例項化類、產生一個物件
stu1 = Student()
# 自定義的元類__init__被呼叫,新類的名稱是:Student
# 自定義的元類__init__被呼叫,新類的基類是:()
# 自定義的元類__init__被呼叫,新類的屬性字典是:{'__module__': '__main__', '__qualname__': 'Student', 'name': 'lavigne'}
print(Student.__bases__)  # (<class 'object'>,)


# 5.在建立新的類時稍作修改,給類的引數裡面傳入object,觀察自定義的元類中bases引數的和類的bases引數變化 class Teacher(object, metaclass=NewType): name = 'avril' Teacher() # 自定義的元類__init__被呼叫,新類的名稱是:Teacher # 自定義的元類__init__被呼叫,新類的基類是:(<class 'object'>,) # 相比於方法一多了傳入的實參 # 自定義的元類__init__被呼叫,新類的屬性字典是:{'__module__': '__main__', '__qualname__': 'Teacher', 'name': 'avril'} print(Teacher.__bases__) # (<class 'object'>,) # 相比於方法一無變化

4.4 進階使用---由元類產生類名時加限制條件

要求:產生的類的名稱字母必須大寫

class NewType(type):
    def __init__(cls, name, bases, dct):
        print(f'自定義的元類__init__被呼叫,新類的名稱是:{name}')
        print(f'自定義的元類__init__被呼叫,新類的基類是:{bases}')
        print(f'自定義的元類__init__被呼叫,新類的屬性字典是:{dct}')
        if not name.isupper():
            raise TypeError(f'類名{name}字母必須大寫')
        super().__init__(name, bases, dct)


# 1.建立一個類
class student(metaclass=NewType):  # TypeError: 類名student首字母必須大寫
    ...


class Teacher(metaclass=NewType):  # TypeError: 類名Teacher字母必須大寫
    ...


class STUDENT(metaclass=NewType):  # 符合規範的會被正常呼叫
    ...
# 自定義的元類__init__被呼叫,新類的名稱是:STUDENT
# 自定義的元類__init__被呼叫,新類的基類是:()
# 自定義的元類__init__被呼叫,新類的屬性字典是:{'__module__': '__main__', '__qualname__': 'STUDENT'}

5. 元類的進階使用

5.1 引入

之前學過的_ _call_ _方法,在類內部定義一個_ _call_ _方法,物件加括號會自動執行產生該物件類裡面的_ _call_ _方法,並得到對應的返回值

類加括號呼叫:同理也會觸發元類中的_ _call_ _方法,從而獲得返回值,這個返回值正是例項化得到的物件

class NewType(type):
    def __init__(cls, name, bases, dct):
        print('自定義元類中的__init__被呼叫')
        super().__init__(name, bases, dct)

    def __call__(cls, *args, **kwargs):
        print('自定義元類中的__call__被呼叫')
        print(f'可變長位置引數:{args}, 可變長關鍵字引數:{kwargs}')
        obj = super().__call__(*args, **kwargs)
        print(obj)
        return obj  # 將產生的物件返回出去


class Student(metaclass=NewType):
    def __init__(self, name):
        print('產生物件的類中的__init__方法被呼叫')
        self.name = name

    def __call__(self, *args, **kwargs):
        print('產生物件的類中的__call__方法被呼叫')
        return '類中call方法的返回值'


stu1 = Student(name='lavigne')
# 自定義元類中的__init__被呼叫
# 自定義元類中的__call__被呼叫
# 可變長位置引數:(), 可變長關鍵字引數:{'name': 'lavigne'}
# 產生物件的類中的__init__方法被呼叫
# <__main__.Student object at 0x0000020A3D043D60>
print(stu1)
# <__main__.Student object at 0x0000020A3D043D60>
# 可以發現自定義元類的__call__的返回值obj和物件stu1一致

print(stu1())
# 產生物件的類中的__call__方法被呼叫
# 類中call方法的返回值

5.2 定製物件的產生過程

class NewType(type):
    def __init__(cls, name, bases, dct):
        print('自定義元類中的__init__被呼叫')
        super().__init__(name, bases, dct)

    def __call__(cls, *args, **kwargs):
        print('自定義元類中的__call__被呼叫')
        print(f'可變長位置引數:{args}, 可變長關鍵字引數:{kwargs}')
        if args:
            raise TypeError(f'產生物件只能透過關鍵字傳參,不能透過位置傳參')
        obj = super().__call__(*args, **kwargs)
        print(obj)
        return obj  # 將產生的物件返回出去


class Student(metaclass=NewType):
    def __init__(self, name):
        print('產生物件的類中的__init__方法被呼叫')
        self.name = name

    def __call__(self, *args, **kwargs):
        print('產生物件的類中的__call__方法被呼叫')
        return '類中call方法的返回值'


# # 正確的方法:
stu1 = Student(name='lavigne')
# 自定義元類中的__init__被呼叫
# 自定義元類中的__call__被呼叫
# 可變長位置引數:(), 可變長關鍵字引數:{'name': 'lavigne'}
# 產生物件的類中的__init__方法被呼叫
# <__main__.Student object at 0x000002127EBA3D60>
print(stu1)
# <__main__.Student object at 0x0000026D97243D60>
print(stu1())
# 產生物件的類中的__call__方法被呼叫
# 類中call方法的返回值


# 錯誤的方法:
stu2 = Student('avril')
# 自定義元類中的__init__被呼叫
# 自定義元類中的__call__被呼叫
# 可變長位置引數:('avril',), 可變長關鍵字引數:{}
# Traceback (most recent call last):
#   File "D:\project\pythonProject\4.py", line 41, in <module>
#     stu2 = Student('avril')
#   File "D:\project\pythonProject\4.py", line 10, in __call__
#     raise TypeError(f'產生物件只能透過關鍵字傳參,不能透過位置傳參')
# TypeError: 產生物件只能透過關鍵字傳參,不能透過位置傳參

6. 總結

如果要定製類的產生過程,那麼編寫自定義元類中的_ _init_ _方法

如果要定製物件的產生過程,那麼編寫自定義元類中的_ _call_ _方法

7. new方法與call方法

7.1 概念

_ _new_ _ 方法用於在自定義元類中產生空類,相當於框架

_ _ init_ _ 方法在自定義元類中用於產生類,在類中用於產生物件,相當於外觀和各種屬性

7.2 自定義元類中的_ _new_ _直接呼叫

並不是所有的地方都可以直接呼叫_ _new_ _,該方法過於底層

如果是在元類的_ _new_ _裡面,可以直接呼叫

class NewType(type):
    def __init__(cls, name, bases, dct):
        print('自定義元類中的__init__被呼叫')
        super().__init__(name, bases, dct)

    def __call__(cls, *args, **kwargs):
        print('自定義元類中的__call__被呼叫')
        print(f'可變長位置引數:{args}, 可變長關鍵字引數:{kwargs}')
        obj = super().__call__(*args, **kwargs)
        print(obj)
        return obj  # 將產生的物件返回出去

    def __new__(cls, *args, **kwargs):
        print('自定義元類中的__new__被呼叫')
        print(f'可變長位置引數:{args}, 可變長關鍵字引數:{kwargs}')
        obj2 = type.__new__(cls, *args, **kwargs)  # 產生的是一個空的類
        print(obj2)  
        return obj2


class Student(metaclass=NewType):
    def __init__(self, name):
        print('產生物件的類中的__init__方法被呼叫')
        self.name = name

    def __call__(self, *args, **kwargs):
        print('產生物件的類中的__call__方法被呼叫')
        return '類中call方法的返回值'
 

只要定義了類,即使沒向該類傳參產生物件,執行以上程式碼也會呼叫__new__方法:
# 自定義元類中的__new__被呼叫
# 可變長位置引數:('Student', (), {'__module__': '__main__', '__qualname__': 'Student', '__init__': <function Student.__init__ at 0x0000021A3BFE17E0>,
#               '__call__': <function Student.__call__ at 0x0000021A3BFE1870>}), 可變長關鍵字引數:{}
# <class '__main__.Student'>
# 自定義元類中的__init__被呼叫

stu1 = Student(name='lavigne')
# 自定義元類中的__new__被呼叫
# 可變長位置引數:('Student', (), {'__module__': '__main__', '__qualname__': 'Student', '__init__': <function Student.__init__ at 0x000001BB2D6417E0>, 
#              '__call__': <function Student.__call__ at 0x000001BB2D641870>}), 可變長關鍵字引數:{}
# <class '__main__.Student'>
# 自定義元類中的__init__被呼叫
----------------------------------------------------------------------
# 自定義元類中的__call__被呼叫
# 可變長位置引數:(), 可變長關鍵字引數:{'name': 'lavigne'}
# 產生物件的類中的__init__方法被呼叫
# <__main__.Student object at 0x000001BB2D633D00>

7.3 自定義元類中_ _call_ _間接呼叫

class NewType(type):
    def __init__(cls, name, bases, dct):
        print('自定義元類中的__init__被呼叫')
        super().__init__(name, bases, dct)

    def __new__(cls, *args, **kwargs):
        print('自定義元類中的__new__被呼叫')
        print(f'可變長位置引數:{args}, 可變長關鍵字引數:{kwargs}')
        obj2 = type.__new__(cls, *args, **kwargs)  # 產生的是一個空的類,是外界繼承該元類時的類
        print(obj2)
        return obj2

    def __call__(cls, *args, **kwargs):
        print('自定義元類中的__call__被呼叫')
        print(f'可變長位置引數:{args}, 可變長關鍵字引數:{kwargs}')
        obj = super().__call__(*args, **kwargs)  # 產生物件
        print(obj)
        return obj  # 將產生的物件返回出去


class Student(metaclass=NewType):
    def __init__(self, name):
        print('產生物件的類中的__init__方法被呼叫')
        self.name = name

    def __call__(self, *args, **kwargs):
        print('產生物件的類中的__call__方法被呼叫')
        return '類中call方法的返回值'


stu1 = Student(name='avril')
# 自定義元類中的__new__被呼叫
# 可變長位置引數:('Student', (), {'__module__': '__main__', '__qualname__': 'Student', '__init__': <function Student.__init__ at 0x00000249538117E0>, '__call__': <function Student.__call__ at 0x0000024953811870>}), 可變長關鍵字引數:{}
# <class '__main__.Student'>
# 自定義元類中的__init__被呼叫
# -------------------------------------------------------------------------
# 自定義元類中的__call__被呼叫
# 可變長位置引數:(), 可變長關鍵字引數:{'name': 'avril'}
# 產生物件的類中的__init__方法被呼叫
# <__main__.Student object at 0x0000024953803D00>

相關文章