python-復盤-元類的再研究

什么是元類?

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

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

  1. 類是描述如何創(chuàng)建一個對象的代碼段消恍,用來描述具有相同的屬性和方法的對象的集合,它定義了該集合中每個對象所共有的屬性和方法
  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狠怨,所以它的類也是對象!既然類是對象邑遏,你就能動態(tài)地創(chuàng)建它們佣赖,就像創(chuàng)建任何對象那樣。我在日常工作里面就會有這種動態(tài)創(chuàng)建類的需求记盒,比如在mock數(shù)據(jù)的時候憎蛤,現(xiàn)在有個函數(shù)func接收一個參數(shù)

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函數(shù)就會變成Fake類的實例,自然就擁有了method_a方法
...:
In : ins = generate_cls(1, 2)()
In : ins.a, ins.b, ins.method_a(10)
Out: (1, 2, 10)

你會發(fā)現(xiàn)這不算是「動態(tài)創(chuàng)建」的:

  1. 類名(Fake)不方便改變
  2. 要創(chuàng)建的類需要的屬性和方法越多冰肴,就要對應的加碼屈藐,不靈活榔组。



    怎么做呢:
In : def method_a(self, n):
...:     return n
...:     # 創(chuàng)建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函數(shù)联逻。本來它用來能讓你了解一個對象的類型:

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

另外搓扯,type如上所說還可以動態(tài)地創(chuàng)建類:type可以把對于類的描述作為參數(shù),并返回一個類包归。

用來創(chuàng)建類的東東就是「元類」锨推,放張圖吧:


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

這種用法就是由于type實際上是一個元類,作為元類的type在Python中被用于在后臺創(chuàng)建所有的類箫踩。在Python語言上有個說法「Everything is an object」爱态。包括整數(shù)、字符串境钟、函數(shù)和類... 所有這些都是對象锦担。所有這些都是由一個類創(chuàng)建的:

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

現(xiàn)在,任何__class__中的__class__是什么慨削?

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

如果你愿意洞渔,你可以把type稱為「類工廠」。type是Python中內建元類缚态,當然磁椒,你也可以創(chuàng)建你自己的元類。



創(chuàng)建自己的元類

Python2創(chuàng)建類的時候玫芦,可以添加一個__metaclass__屬性:

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

如果你這樣做浆熔,Python會使用元類來創(chuàng)建Foo這個類。Python會在類定義中尋找__metaclass__桥帆。如果找到它医增,Python會用它來創(chuàng)建對象類Foo。如果沒有找到它老虫,Python將使用type來創(chuàng)建這個類叶骨。
在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初始化需要添加一個參數(shù)祈匙,并包含一個叫做hello的方法:

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

PS: 這個例子只能運行于Python2忽刽。
在Python里__new__方法創(chuàng)建實例,init負責初始化一個實例夺欲。對于type也是一樣的效果跪帝,只不過針對的是「類」,在上面的HelloMeta中只使用了new創(chuàng)建類些阅,我們再感受一個使用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_)
...:

別往下看歉甚。思考下這樣創(chuàng)建出來的類有什么特殊的地方?

我揭曉一下(這次使用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}

有點明白么扑眉?其實就是在創(chuàng)建類的時候把類的屬性循環(huán)了一遍把不是__開頭的屬性最后存在了_new_dict上纸泄。

什么時候需要用元類?

日常的業(yè)務邏輯開發(fā)是不太需要使用到元類的腰素,因為元類是用來攔截和修改類的創(chuàng)建的聘裁,用到的場景很少。我能想到最典型的場景就是 ORM弓千。ORM就是「對象 關系 映射」的意思衡便,簡單的理解就是把關系數(shù)據(jù)庫的一張表映射成一個類,一行記錄映射為一個對象洋访。ORM框架中的Model只能動態(tài)定義镣陕,因為這個模式下這些關系只能是由使用者來定義,元類再配合描述符就可以實現(xiàn)ORM了(參董大)姻政。



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

type()

type()函數(shù)可以查看一個類型或變量的類型呆抑,Hello是一個class,它的類型就是type汁展,而h是一個實例鹊碍,它的類型就是class Hello。

我們說class的定義是運行時動態(tài)創(chuàng)建的食绿,而創(chuàng)建class的方法就是使用type()函數(shù)侈咕。

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

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

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

  1. class的名稱桥状;
  2. 繼承的父類集合帽揪,注意Python支持多重繼承,如果只有一個父類辅斟,別忘了tuple的單元素寫法转晰;
  3. class的方法名稱與函數(shù)綁定,這里我們把函數(shù)fn綁定到方法名hello上士飒。

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

正常情況下,我們都用class Xxx...來定義類芳撒,但是邓深,type()函數(shù)也允許我們動態(tài)創(chuàng)建出類來未桥,也就是說,動態(tài)語言本身支持運行期動態(tài)創(chuàng)建類芥备,這和靜態(tài)語言有非常大的不同冬耿,要在靜態(tài)語言運行期創(chuàng)建類,必須構造源代碼字符串再調用編譯器萌壳,或者借助一些工具生成字節(jié)碼實現(xiàn)亦镶,本質上都是動態(tài)編譯,會非常復雜

metaclass

除了使用type()動態(tài)創(chuàng)建類以外袱瓮,要控制類的創(chuàng)建行為缤骨,還可以使用metaclass。

metaclass尺借,直譯為元類绊起,簡單的解釋就是:

當我們定義了類以后,就可以根據(jù)這個類創(chuàng)建出實例褐望,所以:先定義類勒庄,然后創(chuàng)建實例。

但是如果我們想創(chuàng)建出類呢瘫里?那就必須根據(jù)metaclass創(chuàng)建出類实蔽,所以:先定義metaclass,然后創(chuàng)建類谨读。

連接起來就是:先定義metaclass局装,就可以創(chuàng)建類,最后創(chuàng)建實例劳殖。
我們先看一個簡單的例子铐尚,這個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來定制類爹脾,傳入關鍵字參數(shù)metaclass

class MyList(list, metaclass=ListMetaclass):
    pass

當我們傳入關鍵字參數(shù)metaclass時,魔術就生效了箕昭,它指示Python解釋器在創(chuàng)建MyList時灵妨,要通過ListMetaclass.__new__()來創(chuàng)建,在此落竹,我們可以修改類的定義泌霍,比如,加上新的方法述召,然后朱转,返回修改后的定義蟹地。

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

  1. 當前準備創(chuàng)建的類的對象;

  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'

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

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

ORM全稱“Object Relational Mapping”倾哺,即對象-關系映射轧邪,就是把關系數(shù)據(jù)庫的一行映射為一個對象,也就是一個類對應一個表羞海,這樣忌愚,寫代碼更簡單,不用直接操作SQL語句却邓。

要編寫一個ORM框架硕糊,所有的類都只能動態(tài)定義,因為只有使用者才能根據(jù)表的結構定義出對應的類來腊徙。

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

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

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

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

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

現(xiàn)在碟刺,我們就按上面的接口來實現(xiàn)該ORM。

首先來定義Field類薯酝,它負責保存數(shù)據(jù)庫表的字段名和字段類型:

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,如果沒有找到占哟,就繼續(xù)在父類Model中查找metaclass心墅,找到了,就使用Model中定義的metaclass的ModelMetaclass來創(chuàng)建User類榨乎,也就是說怎燥,metaclass可以隱式地繼承到子類,但子類自己卻感覺不到蜜暑。

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

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

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

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

在Model類中,就可以定義各種操作數(shù)據(jù)庫的方法袍患,比如save()坦康,delete(),find()诡延,update等等滞欠。

我們實現(xiàn)了save()方法,把一個實例保存到數(shù)據(jù)庫中肆良。因為有表名筛璧,屬性到字段的映射和屬性值的集合,就可以構造出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()方法已經(jīng)打印出了可執(zhí)行的SQL語句,以及參數(shù)列表巫糙,只需要真正連接到數(shù)據(jù)庫朗儒,執(zhí)行該SQL語句,就可以完成真正的功能。

小結

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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末恳不,一起剝皮案震驚了整個濱河市檩小,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌烟勋,老刑警劉巖规求,帶你破解...
    沈念sama閱讀 218,204評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異卵惦,居然都是意外死亡颓哮,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,091評論 3 395
  • 文/潘曉璐 我一進店門鸵荠,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人伤极,你說我怎么就攤上這事蛹找。” “怎么了哨坪?”我有些...
    開封第一講書人閱讀 164,548評論 0 354
  • 文/不壞的土叔 我叫張陵庸疾,是天一觀的道長。 經(jīng)常有香客問我当编,道長届慈,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,657評論 1 293
  • 正文 為了忘掉前任忿偷,我火速辦了婚禮金顿,結果婚禮上,老公的妹妹穿的比我還像新娘鲤桥。我一直安慰自己揍拆,他們只是感情好,可當我...
    茶點故事閱讀 67,689評論 6 392
  • 文/花漫 我一把揭開白布茶凳。 她就那樣靜靜地躺著嫂拴,像睡著了一般。 火紅的嫁衣襯著肌膚如雪贮喧。 梳的紋絲不亂的頭發(fā)上筒狠,一...
    開封第一講書人閱讀 51,554評論 1 305
  • 那天,我揣著相機與錄音箱沦,去河邊找鬼辩恼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的运挫。 我是一名探鬼主播状共,決...
    沈念sama閱讀 40,302評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼谁帕!你這毒婦竟也來了峡继?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 39,216評論 0 276
  • 序言:老撾萬榮一對情侶失蹤匈挖,失蹤者是張志新(化名)和其女友劉穎碾牌,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體儡循,經(jīng)...
    沈念sama閱讀 45,661評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡舶吗,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,851評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了择膝。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片誓琼。...
    茶點故事閱讀 39,977評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖肴捉,靈堂內的尸體忽然破棺而出腹侣,到底是詐尸還是另有隱情,我是刑警寧澤齿穗,帶...
    沈念sama閱讀 35,697評論 5 347
  • 正文 年R本政府宣布傲隶,位于F島的核電站,受9級特大地震影響窃页,放射性物質發(fā)生泄漏跺株。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,306評論 3 330
  • 文/蒙蒙 一脖卖、第九天 我趴在偏房一處隱蔽的房頂上張望乒省。 院中可真熱鬧,春花似錦畦木、人聲如沸作儿。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,898評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽攻锰。三九已至,卻和暖如春妓雾,著一層夾襖步出監(jiān)牢的瞬間娶吞,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,019評論 1 270
  • 我被黑心中介騙來泰國打工械姻, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留妒蛇,地道東北人。 一個月前我還...
    沈念sama閱讀 48,138評論 3 370
  • 正文 我出身青樓,卻偏偏與公主長得像绣夺,于是被迫代替她去往敵國和親吏奸。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,927評論 2 355

推薦閱讀更多精彩內容