python 元編程學習

什麼是元類

定義

python中的一切都是對象,類自己也是對象,元類則是建立類對象的類python

python對象的建立過程 及 元類常使用方法的簡介

加載模塊時,python解釋器 自動建立模塊中的類對象,全局函數對象 新建實例時,用戶 主動調用類對象來建立實例 元類經常使用方法: __prepare__, __new__, __init__, __call__shell

"""
  元類經常使用方法,以及類對象建立和實例對象建立的時間點
"""


class MyMeta(type):
    def __new__(mcs, name, bases, cls_dict):
        """ 元類建立類對象方法
        :param name: 類對象的類名
        :param bases: 類對象的父類
        :param cls_dict: 類對象的命名空間
        :return:
        """
        print('2. call MyMeta method: new', mcs)
        return super().__new__(mcs, name, bases, cls_dict)

    @classmethod
    def __prepare__(mcs, name, bases, **kwargs):
        """ 類建立前的準備工做,返回一個字典,爲空的命名空間對象
        :param name:
        :param bases:
        :param kwargs:
        :return:
        """
        print('1. call MyMeta method: prepare', mcs)
        return super().__prepare__(name, bases, **kwargs)

    def __init__(cls, name, bases, namespace, **kwargs):
        """ 類對象建立完成後的一些初始化工做
        :param name:
        :param bases:
        :param namespace:
        :param kwargs:
        """
        print('3. call MyMeta method: init', cls)
        super().__init__(name, bases, namespace)

    def __call__(cls, *args, **kwargs):
        """ 使用類對象建立實例時調用
        :param args:
        :param kwargs:
        :return:
        """
        print('1> call MyMeta method: call', cls)
        return super().__call__(*args, **kwargs)


class MyClass(metaclass=MyMeta):
    def __new__(cls, *args, **kwargs):
        print('2> call MyClass method: new', cls)
        return super().__new__(cls)

    def __init__(self, *args, **kwargs):
        print('3> call MyClass method: init', self)
        super().__init__()

    def __str__(self):
        return 'hello world'


def instance_get():
    print('call instance get method:')
    instance = MyClass()
    print(instance)


if __name__ == '__main__':
    print('load model completed!\n---------------------------------------------------------\n')
    instance_get()

複製代碼

上述代碼的輸出:數據庫

1. call MyMeta method: prepare <class '__main__.MyMeta'>
2. call MyMeta method: new <class '__main__.MyMeta'>
3. call MyMeta method: init <class '__main__.MyClass'>
load model completed!
---------------------------------------------------------

call instance get method:
1> call MyMeta method: call <class '__main__.MyClass'>
2> call MyClass method: new <class '__main__.MyClass'>
3> call MyClass method: init hello world
hello world
複製代碼

元類的使用例子

咱們能夠經過繼承元類 type,對其中的一些方法作處理,編寫咱們須要的定製化元類。元類在構建框架的時候用的較多,像django就使用了元類來構建orm框架(經過元類來感知字段的定義順序,從而順序的將字段映射到數據庫中。) 日常的開發中,可使用元類來建立單例、作對象緩存等,其它方式確定也能夠作這些工做,不過元類提供的方案比較便捷還比較優雅。django

1. 建立單例

經過上面的程序,咱們知道建立實例對象的入口方法是元類中的__call__方法,咱們能夠經過覆寫這個方法來實現單例模式編程

""" 使用元類實現單例模式 注意: 這種寫法非線程安全! """


class Singleton(type):
    def __init__(cls, *args, **kwargs):
        cls._instance = None  # 將實例定義爲本身的一個屬性
        super().__init__(*args, **kwargs)

    def __call__(cls, *args, **kwargs):
        print('call Singleton call method')
        if cls._instance is None:
            cls._instance = super().__call__(*args, **kwargs)
        return cls._instance


class SingletonCls(metaclass=Singleton):
    def __init__(self):
        print('call SingletonCls init method')


if __name__ == '__main__':
    s1 = SingletonCls()
    s2 = SingletonCls()
    print(s1 is s2)
複製代碼

如下爲該腳本的輸出:緩存

call Singleton call method
call SingletonCls init method
call Singleton call method
True
複製代碼

2. 作對象緩存

對象建立代價比較打的時候,作對象緩存是個不錯的選擇,不少緩存是用專門的緩存池來完成,這裏也能夠直接用元類來作緩存。安全

import weakref


class CacheMeta(type):
    """ 使用元類作對象緩存處理 """

    def __init__(cls, name, bases, namespace, **kwargs):
        super().__init__(name, bases, namespace)
        cls._cache = weakref.WeakValueDictionary()

    def __call__(cls, *args):
        # 傳入的參數相同的,就給同一個對象,若是對應參數的對象還沒建立就先建立對象
        if args in cls._cache:
            return cls._cache[args]
        obj = super().__call__(*args)
        cls._cache[args] = obj
        return obj


class CacheClass(metaclass=CacheMeta):
    def __init__(self, name):
        print('Creating instance({!r})'.format(name))
        self.name = name


if __name__ == '__main__':
    o1 = CacheClass('hello')
    o2 = CacheClass('world')
    o3 = CacheClass('hello')
    print(o1 is o2)
    print(o1 is o3)
複製代碼

如下爲該腳本輸出bash

Creating instance('hello')
Creating instance('world')
False
True
複製代碼

3. 子類覆蓋方法的簽名檢查

使用元類能夠檢查子類的方法前面是否和父類的保持一直,若是要強迫繼承時簽名不能修改,這個是一個操做方式。平時使用IDE在進行編碼的時候,若是參數和父類不一致(參數名不一致不會提示),是有提示的,因此這點不用太過在乎。框架

from inspect import signature
import logging


class MatchSignaturesMeta(type):

    def __init__(cls, classname, bases, clsdict):
        super().__init__(classname, bases, clsdict)
        sup = super(cls, cls)

        for name, value in clsdict.items():
            if name.startswith('_') or not callable(value):
                continue
            sup_func = getattr(sup, name, None)
            if sup_func:
                sup_sig = signature(sup_func)
                val_sig = signature(value)
                if sup_sig != val_sig:
                    logging.warning(
                        'Signature mismatch in %s. %s != %s', 
                        value.__qualname__, 
                        sup_sig, 
                    val_sig
                )


class Root(metaclass=MatchSignaturesMeta):
    pass


class A(Root):
    def foo(self, x, y):
        pass

    def spam(self, x, *, z):
        pass


class B(A):
    def foo(self, a, b):
        pass

    def spam(self, x, z):
        pass


b = B()

複製代碼

在初始化的時候,日誌裏面會打印以下內容:函數

WARNING:root:Signature mismatch in B.foo. (self, x, y) != (self, a, b)
WARNING:root:Signature mismatch in B.spam. (self, x, *, z) != (self, x, z)
複製代碼

4. 動態建立新的類

建立類的時候,是將寫在類下面的屬性、方法和類對象(經過描述器協議)綁定起來,python提供了手動實現這個過程的方式,這種操做有點相似於反射。

import abc
import types


def __init__(self, name, shares, price):
    self.name = name
    self.shares = shares
    self.price = price


def cost(self):
    return self.shares * self.price


cls_dict = {
    '__init__': __init__,
    'cost': cost,
}

Stock = types.new_class(
    'Stock',  # 類名
    (),  # 父類
    {'metaclass': abc.ABCMeta},  # 元類及類定義參數
    lambda ns: ns.update(cls_dict)  # ns 爲 prepare 方法(見上)返回的字典對象
)
Stock.__module__ = __name__

s = Stock('abc', 200, 35)
print(s)
複製代碼

打印出來的結果代表,s就是類Stock的一個對象,這個和直接將Stock定義爲class是同樣的效果。

<__main__.Stock object at 0x7f153c19bb00>
複製代碼

參考: python cookbook 第九章; python高級編程 第三章

相關文章
相關標籤/搜索