python-復盤-元類的再研究

什么是元類?

理解元類(metaclass)之前,我們先了解下Python中的OOP和類(Class)

面向對象全稱 Object Oriented Programming 簡稱OOP,這種編程思想被大家所熟知。它是把對象作為一個程序的基本單元,把數據和功能封裝在里面,能夠實現很好的復用性,靈活性和擴展性。OOP中有2個基本概念:類和對象:

  1. 類是描述如何創建一個對象的代碼段,用來描述具有相同的屬性和方法的對象的集合,它定義了該集合中每個對象所共有的屬性和方法
  2. 對象是類的實例(Instance)。

我們舉個例子:

In : class ObjectCreator(object):
...:     pass
...:
In : my_object = ObjectCreator()
In : my_object
Out: <__main__.ObjectCreator at 0x1082bbef0>   # 表示它是ObjectCreator 的實例的意思

而Python中的類并不是僅限于此:

In : print(ObjectCreator)
<class '__main__.ObjectCreator'>

ObjectCreator竟然可以被print,所以它的類也是對象!既然類是對象,你就能動態地創建它們,就像創建任何對象那樣。我在日常工作里面就會有這種動態創建類的需求,比如在mock數據的時候,現在有個函數func接收一個參數

In : def func(instance):
...:     print(instance.a, instance.b)
...:     print(instance.method_a(10))
...:

正常使用起來傳入的instance是符合需求的(有a、b屬性和method_a方法),但是當我想單獨調試func的時候,需要「造」一個,假如不用元類,應該是這樣寫:

In : def generate_cls(a, b):
...:     class Fake(object):
...:         def method_a(self, n):     #  Fake類的方法
...:             return n
...:     Fake.a = a
...:     Fake.b = b
...:     return Fake    # return 后,generate_cls函數就會變成Fake類的實例,自然就擁有了method_a方法
...:
In : ins = generate_cls(1, 2)()
In : ins.a, ins.b, ins.method_a(10)
Out: (1, 2, 10)

你會發現這不算是「動態創建」的:

  1. 類名(Fake)不方便改變
  2. 要創建的類需要的屬性和方法越多,就要對應的加碼,不靈活。



    怎么做呢:
In : def method_a(self, n):
...:     return n
...:     # 創建Fake類
In : ins = type('Fake', (), {'a': 1, 'b': 2, 'method_a': method_a})()
In : ins.a, ins.b, ins.method_a(10)
Out: (1, 2, 10)

到了這里,引出了type函數。本來它用來能讓你了解一個對象的類型:

In : type(1)
Out: int
In : type('1')
Out: str
In : type(ObjectCreator)
Out: type
In : type(ObjectCreator())
Out: __main__.ObjectCreator

另外,type如上所說還可以動態地創建類:type可以把對于類的描述作為參數,并返回一個類。

用來創建類的東東就是「元類」,放張圖吧:


MyClass = type('MyClass', (), {})

這種用法就是由于type實際上是一個元類,作為元類的type在Python中被用于在后臺創建所有的類。在Python語言上有個說法「Everything is an object」。包括整數、字符串、函數和類... 所有這些都是對象。所有這些都是由一個類創建的:

In : age = 35
In : age.__class__
Out: int
In : name = 'bob'
In : name.__class__
Out: str

現在,任何__class__中的__class__是什么?

In : age.__class__.__class__
Out: type
In : name.__class__.__class__
Out: type
...

如果你愿意,你可以把type稱為「類工廠」。type是Python中內建元類,當然,你也可以創建你自己的元類。



創建自己的元類

Python2創建類的時候,可以添加一個__metaclass__屬性:

class Foo(object):
    __metaclass__ = something...
    [...]

如果你這樣做,Python會使用元類來創建Foo這個類。Python會在類定義中尋找__metaclass__。如果找到它,Python會用它來創建對象類Foo。如果沒有找到它,Python將使用type來創建這個類。
在Python3中語法改變了一下:

class Simple1(object, metaclass=something...):
    [...]

本質上是一樣的。

    def __new__(cls, name, bases, attrs):
        def __init__(cls, func):
            cls.func = func
        def hello(cls):
            print 'hello world'
        t = type.__new__(cls, name, bases, attrs)
        t.__init__ = __init__
        t.hello = hello
        return t
class New_Hello(object):
    __metaclass__ = HelloMeta

New_Hello初始化需要添加一個參數,并包含一個叫做hello的方法:

In : h = New_Hello(lambda x: x)
In : h.func(10), h.hello()
hello world
Out: (10, None)

PS: 這個例子只能運行于Python2。
在Python里__new__方法創建實例,init負責初始化一個實例。對于type也是一樣的效果,只不過針對的是「類」,在上面的HelloMeta中只使用了new創建類,我們再感受一個使用init`的元類:

In : class HelloMeta2(type):
...:     def __init__(cls, name, bases, attrs):
...:         super(HelloMeta2, cls).__init__(name, bases, attrs)
...:         attrs_ = {}
...:         for k, v in attrs.items():
...:             if not k.startswith('__'):
...:                 attrs_[k] = v
...:         setattr(cls, '_new_dict', attrs_)
...:

別往下看。思考下這樣創建出來的類有什么特殊的地方?

我揭曉一下(這次使用Python 3語法):

In : class New_Hello2(metaclass=HelloMeta2):
...:     a = 1
...:     b = True
In : New_Hello2._new_dict
Out: {'a': 1, 'b': True}
In : h2 = New_Hello2()
In : h2._new_dict
Out: {'a': 1, 'b': True}

有點明白么?其實就是在創建類的時候把類的屬性循環了一遍把不是__開頭的屬性最后存在了_new_dict上。

什么時候需要用元類?

日常的業務邏輯開發是不太需要使用到元類的,因為元類是用來攔截和修改類的創建的,用到的場景很少。我能想到最典型的場景就是 ORM。ORM就是「對象 關系 映射」的意思,簡單的理解就是把關系數據庫的一張表映射成一個類,一行記錄映射為一個對象。ORM框架中的Model只能動態定義,因為這個模式下這些關系只能是由使用者來定義,元類再配合描述符就可以實現ORM了(參董大)。



廖雪峰元類講解 二種方法-type/metaclass

type()

type()函數可以查看一個類型或變量的類型,Hello是一個class,它的類型就是type,而h是一個實例,它的類型就是class Hello。

我們說class的定義是運行時動態創建的,而創建class的方法就是使用type()函數。

type()函數既可以返回一個對象的類型,又可以創建出新的類型,比如,我們可以通過type()函數創建出Hello類,而無需通過class Hello(object)...的定義:

>>> def fn(self, name='world'): # 先定義函數
...     print('Hello, %s.' % name)    # 輸入 fn(1) 結果也是Hello,world
...
>>> Hello = type('Hello', (object,), dict(hello=fn))  # 創建Hello class,type的套路
>>> h = Hello()              # h是一個實例,它有一個hello的函數
>>> h.hello()
Hello, world.
>>> print(type(Hello))
<class 'type'>
>>> print(type(h))
<class '__main__.Hello'>

要創建一個class對象,type()函數依次傳入3個參數:

  1. class的名稱;
  2. 繼承的父類集合,注意Python支持多重繼承,如果只有一個父類,別忘了tuple的單元素寫法;
  3. class的方法名稱與函數綁定,這里我們把函數fn綁定到方法名hello上。

通過type()函數創建的類和直接寫class是完全一樣的,因為Python解釋器遇到class定義時,僅僅是掃描一下class定義的語法,然后調用type()函數創建出class。

正常情況下,我們都用class Xxx...來定義類,但是,type()函數也允許我們動態創建出類來,也就是說,動態語言本身支持運行期動態創建類,這和靜態語言有非常大的不同,要在靜態語言運行期創建類,必須構造源代碼字符串再調用編譯器,或者借助一些工具生成字節碼實現,本質上都是動態編譯,會非常復雜

metaclass

除了使用type()動態創建類以外,要控制類的創建行為,還可以使用metaclass。

metaclass,直譯為元類,簡單的解釋就是:

當我們定義了類以后,就可以根據這個類創建出實例,所以:先定義類,然后創建實例。

但是如果我們想創建出類呢?那就必須根據metaclass創建出類,所以:先定義metaclass,然后創建類。

連接起來就是:先定義metaclass,就可以創建類,最后創建實例。
我們先看一個簡單的例子,這個metaclass可以給我們自定義的MyList增加一個add方法:

定義ListMetaclass,按照默認習慣,metaclass的類名總是以Metaclass結尾,以便清楚地表示這是一個metaclass:

# metaclass是類的模板,所以必須從`type`類型派生:
class ListMetaclass(type):
    def __new__(cls, name, bases, attrs):
        attrs['add'] = lambda self, value: self.append(value)
        return type.__new__(cls, name, bases, attrs)

有了ListMetaclass,我們在定義類的時候還要指示使用ListMetaclass來定制類,傳入關鍵字參數metaclass

class MyList(list, metaclass=ListMetaclass):
    pass

當我們傳入關鍵字參數metaclass時,魔術就生效了,它指示Python解釋器在創建MyList時,要通過ListMetaclass.__new__()來創建,在此,我們可以修改類的定義,比如,加上新的方法,然后,返回修改后的定義。

__new__()方法接收到的參數依次是:

  1. 當前準備創建的類的對象;

  2. 類的名字;

  3. 類繼承的父類集合;

  4. 類的方法集合。
    測試一下MyList是否可以調用add()方法:

>>> L = MyList()
>>> L.add(1)
>> L
[1]

而普通的list沒有add()方法:

>>> L2 = list()
>>> L2.add(1)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'list' object has no attribute 'add'

動態修改有什么意義?直接在MyList定義中寫上add()方法不是更簡單嗎?正常情況下,確實應該直接寫,通過metaclass修改純屬變態。

但是,總會遇到需要通過metaclass修改類定義的。ORM就是一個典型的例子。

ORM全稱“Object Relational Mapping”,即對象-關系映射,就是把關系數據庫的一行映射為一個對象,也就是一個類對應一個表,這樣,寫代碼更簡單,不用直接操作SQL語句。

要編寫一個ORM框架,所有的類都只能動態定義,因為只有使用者才能根據表的結構定義出對應的類來。

讓我們來嘗試編寫一個ORM框架。

編寫底層模塊的第一步,就是先把調用接口寫出來。比如,使用者如果使用這個ORM框架,想定義一個User類來操作對應的數據庫表User,我們期待他寫出這樣的代碼:

class User(Model):
    # 定義類的屬性到列的映射:
    id = IntegerField('id')
    name = StringField('username')
    email = StringField('email')
    password = StringField('password')

# 創建一個實例:
u = User(id=12345, name='Michael', email='test@orm.org', password='my-pwd')
# 保存到數據庫:
u.save()

其中,父類Model和屬性類型StringField、IntegerField是由ORM框架提供的,剩下的魔術方法比如save()全部由metaclass自動完成。雖然metaclass的編寫會比較復雜,但ORM的使用者用起來卻異常簡單。

現在,我們就按上面的接口來實現該ORM。

首先來定義Field類,它負責保存數據庫表的字段名和字段類型:

class Field(object):

    def __init__(self, name, column_type):
        self.name = name
        self.column_type = column_type

    def __str__(self):
        return '<%s:%s>' % (self.__class__.__name__, self.name)

在Field的基礎上,進一步定義各種類型的Field,比如StringField,IntegerField等等:

class StringField(Field):

    def __init__(self, name):
        super(StringField, self).__init__(name, 'varchar(100)')

class IntegerField(Field):

    def __init__(self, name):
        super(IntegerField, self).__init__(name, 'bigint')

下一步,就是編寫最復雜的ModelMetaclass了:

class ModelMetaclass(type):

    def __new__(cls, name, bases, attrs):
        if name=='Model':
            return type.__new__(cls, name, bases, attrs)
        print('Found model: %s' % name)
        mappings = dict()
        for k, v in attrs.items():
            if isinstance(v, Field):
                print('Found mapping: %s ==> %s' % (k, v))
                mappings[k] = v
        for k in mappings.keys():
            attrs.pop(k)
        attrs['__mappings__'] = mappings # 保存屬性和列的映射關系
        attrs['__table__'] = name # 假設表名和類名一致
        return type.__new__(cls, name, bases, attrs)

以及基類Model:

class Model(dict, metaclass=ModelMetaclass):

    def __init__(self, **kw):
        super(Model, self).__init__(**kw)

    def __getattr__(self, key):
        try:
            return self[key]
        except KeyError:
            raise AttributeError(r"'Model' object has no attribute '%s'" % key)

    def __setattr__(self, key, value):
        self[key] = value

    def save(self):
        fields = []
        params = []
        args = []
        for k, v in self.__mappings__.items():
            fields.append(v.name)
            params.append('?')
            args.append(getattr(self, k, None))
        sql = 'insert into %s (%s) values (%s)' % (self.__table__, ','.join(fields), ','.join(params))
        print('SQL: %s' % sql)
        print('ARGS: %s' % str(args))

當用戶定義一個class User(Model)時,Python解釋器首先在當前類User的定義中查找metaclass,如果沒有找到,就繼續在父類Model中查找metaclass,找到了,就使用Model中定義的metaclass的ModelMetaclass來創建User類,也就是說,metaclass可以隱式地繼承到子類,但子類自己卻感覺不到。

在ModelMetaclass中,一共做了幾件事情:

  1. 排除掉對Model類的修改;

  2. 在當前類(比如User)中查找定義的類的所有屬性,如果找到一個Field屬性,就把它保存到一個mappings的dict中,同時從類屬性中刪除該Field屬性,否則,容易造成運行時錯誤(實例的屬性會遮蓋類的同名屬性 );

3.把表名保存到table中,這里簡化為表名默認為類名。

在Model類中,就可以定義各種操作數據庫的方法,比如save(),delete(),find(),update等等。

我們實現了save()方法,把一個實例保存到數據庫中。因為有表名,屬性到字段的映射和屬性值的集合,就可以構造出INSERT語句。

編寫代碼試試:

u = User(id=12345, name='Michael', email='test@orm.org', password='my-pwd')
u.save()

輸出如下:

Found model: User
Found mapping: email ==> <StringField:email>
Found mapping: password ==> <StringField:password>
Found mapping: id ==> <IntegerField:uid>
Found mapping: name ==> <StringField:username>
SQL: insert into User (password,email,username,id) values (?,?,?,?)
ARGS: ['my-pwd', 'test@orm.org', 'Michael', 12345]

可以看到,save()方法已經打印出了可執行的SQL語句,以及參數列表,只需要真正連接到數據庫,執行該SQL語句,就可以完成真正的功能。

小結

metaclass是Python中非常具有魔術性的對象,它可以改變類創建時的行為。這種強大的功能使用起來務必小心。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,002評論 6 542
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,400評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,136評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,714評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,452評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,818評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,812評論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,997評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,552評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,292評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,510評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,035評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,721評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,121評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,429評論 1 294
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,235評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,480評論 2 379

推薦閱讀更多精彩內容