標籤:

『簡單的』Python 元類

Metaclasses are deeper magic than 99% of users should ever worry about. If you wonder whether you need them, you don』t (the people who actually need them know with certainty that they need them, and don』t need an explanation about why). - Tim Peters

平常都是寫業務邏輯,從來沒有使用過元類這種黑魔法(好吧,目前的編碼規範是不推薦在業務邏輯當中使用元類的,不好維護並且一般來說並無這個必要)。不過貌似只有造輪子的時候才會用到,就像上邊的引用里說的,當你不知道為什麼要使用元類時,你是沒必要使用它的,大部分時間python靈活的特性已經可以應付幾乎所有業務問題。最近重新看了下元類,突然有了一種霍然開朗的感覺,用幾個簡單的例子介紹一些元類(使用python3.5)。

什麼是元類?

元類是創建類的類。這麼說很繞口。 在python中,一切皆對象,類也不例外。 當我們用class關鍵字定義類的時候,python實際上會執行它然後生成一個對象。既然是對象,就可以進行賦值,拷貝,添加屬性,作為函數參數等。使用元類允許我們控制類的生成,比如修改類的屬性,檢查屬性的合法性等。

class MyClass: # python2中新式類要顯示繼承objectn passn

元類的創建方式

在Python中,有兩種創建類的方式,一種是平常我們使用的使用class關鍵字創建類:

class MyClass: # python2中新式類要顯示繼承objectn passn

還有一種方式是使用type函數創建,type的描述如下,平常我們一般使用type查看對象的類型,實際上type還有一個重要的功能就是創建類

Docstring:ntype(object_or_name, bases, dict)ntype(object) -> the objects typentype(name, bases, dict) -> a new typen

上邊MyClass的定義用type創建可以這麼寫: MyClass = type(Myclass, (), {})

對於有繼承關係和屬性的類來說,可以使用如下等價定義:

# 加上繼承nclass Base:n passnnclass Child(Base):n passn# 等價定義nChild = type(Child, (Base,), {}) # 注意Base後要加上逗號否則就不是tuple了nnn# 加上屬性nclass ChildWithAttr(Base):n bar = Truenn# 等價定義nChildWithAttr = type(ChildWithAttr, (Base,), {bar: True})nnn# 加上方法nclass ChildWithMethod(Base):n bar = Truenn def hello(self):n print(hello)nnndef hello(self):n print(hello)nn# 等價定義nChildWithMethod = type(ChildWithMethod, (Base,), {bar: True, hello: hello})n

看懂了上邊的等價定義對於理解元類的創建很重要。

創建一個元類

什麼時候需要創建元類呢?當我想控制類的創建,比如校驗或者修改類的屬性的時候,就可以使用元類。元類通過繼承type實現,在python2和python3中略有不同

class Meta(type):n passnn# python2nclass Base(object):n __metaclass__ = Metann# python3nclass Base(metaclass=Meta):n passnn# 如果寫兼容2和3的代碼可以使用six模塊nnfrom six import with_metaclassnnclass Meta(type):n passnnclass Base(metaclass=Meta):n passnnclass MyClass(with_metaclass(Meta, Base)):n passn

我們使用幾個很簡單的例子來演示元類的創建,第一個例子我們實現一個修改類的屬性名為小寫的元類:

class LowercaseMeta(type):n """ 修改類的屬性名稱為小寫的元類 """n def __new__(mcs, name, bases, attrs):n lower_attrs = {}n for k, v in attrs.items():n if not k.startswith(__): # 排除magic methodn lower_attrs[k.lower()] = vn else:n lower_attrs[k] = vn return type.__new__(mcs, name, bases, lower_attrs)nnnclass LowercaseClass(metaclass=LowercaseMeta):n BAR = Truenn def HELLO(self):n print(hello)nnprint(dir(LowercaseClass)) # 你會發現"BAR"和"HELLO"都變成了小寫nLowercaseClass().hello() # 用一個類的實例調用hello方法,神奇的地方就是這裡,我們修改了類定義時候的屬性名!!!n

第二個例子是給類添加一個add屬性,比如我經常手誤使用list.add而不是寫list.append方法:

class ListMeta(type):n """ 用元類實現給類添加屬性 """n def __new__(mcs, name, bases, attrs):n attrs[add] = lambda self, value: self.append(value)n return type.__new__(mcs, name, bases, attrs)nnclass MyList(list, metaclass=ListMeta):n passnnl = MyList()nl.add(1)nprint(l)nn# 但實際上給類動態添加屬性用類裝飾器反而更簡單ndef class_decorator(cls):n cls.add = lambda self, value: self.append(value)n return clsnn@class_decoratornclass MyList(list):n passnnnl = MyList()nl.append(1)nprint(l)n

元類的__new__和__init__

一般在python里__new__方法創建實例,__init__負責初始化一個實例。__new__方法返回創建的對象,而__init__方法禁止返回值(必須返回None)。有一個簡單的原則來判斷什麼使用使用__init__和__new__:

  • 如果需要修改類的屬性,使用元類的__new__方法
  • 如果只是做一些類屬性檢查的工作,使用元類的__init__方法

之前的示例都是使用__new__方式,我們來看個使用__init__方法的元類。假如我們有這樣一個需求,很多懶痴漢程序員不喜歡給類的方法寫docstring,怎麼辦呢?我們可以定義一個元類,強制讓所有人使用這個元類。如果哪個傢伙偷懶沒給方法寫docstring,咱就讓他連類的定義都不能通過。

class LazybonesError(BaseException):n """ 給懶蟲們的提示 """n passnnnclass MustHaveDocMeta(type):n def __init__(cls, name, bases, attrs):n for attr_name, attr_value in attrs.items():n if attr_name.startswith(__): # skip magic or private methodn continuen if not callable(attr_value): # skip non method attrn continuen if not getattr(attr_value, __doc__):n raise LazybonesError(n Hi Lazybones, please write doc for your "{}" method.format(attr_name)n )n type.__init__(cls, name, bases, attrs)nnnclass ClassByLazybones(metaclass=MustHaveDocMeta):n """ 這個類的定義是無法通過的,直接會報異常,讓你不給方法寫docstring """n def complicate(self):n passn

何時使用元類?

嗯,其實我沒啥經驗,還沒在業務代碼中使用過。使用元類可以攔截和修改類的創建,我們也使用使用別的技術來實現類屬性的修改,比如

  • monkey patching: 猴子補丁,實際上就是『運行時動態替換屬性』
  • class decorators: 類裝飾器,可以實現給類動態修改屬性。

有時候使用元類反而是最麻煩的技術。不過使用元類也有一下一些好處:

  • 意圖更加明確。當然你的metaclass名字要起好。
  • 面向對象。可以隱式繼承到子類。
  • 可以更好地組織代碼,更易讀。
  • 可以用__new__,__init__,__call__等方法更好地控制。

    我們最好選擇容易理解和維護的方式來實現。

元類的一些應用(單例,ORM, abc模塊等)

單例模式:元類經常用來實現單例模式

# 攔截(intercepting)class的創建nclass Singleton(type):n instance = Nonen def __call__(cls, *args, **kw):n# 通過重寫__call__攔截實例的創建,(實例通過調用括弧運算符創建的)n if not cls.instance:n cls.instance = super().__call__(*args, **kw)n return cls.instancennnclass ASingleton(metaclass=Singleton):n passnnclass BSingleton(metaclass=Singleton):n passnna = ASingleton()naa = ASingleton()nb = BSingleton()nbb = BSingleton()nassert a is aanassert b is bbn

ORM框架:

ORM是」Object Relational Mapping」的縮寫,叫做對象-關係映射,用來把關係數據的一行映射成一個對象,一個表對應成一個類,這樣就免去了直接使用SQL語句的麻煩,使用起來更加符合程序員的思維習慣。ORM框架里所有的類都是動態定義的,由使用類的用戶決定有哪些欄位,這個時候就只能用元類來實現了。感興趣的可以看看廖雪峰的python教程,裡邊有個簡單的orm實現。我在這裡重新鞏固一下。

orm有兩個重要的類,一個是Model表示資料庫中的表,一個是Field表示資料庫中的欄位。通常通過以下方式使用(py3.5):

class User(Model):n id = IntegerField(id)n name = StringField(name)nu = User(id=1, name=laowang)nu.save()n

接下來定義Field類,Model的元類和基類:

class Field:n """ 負責保存資料庫表的欄位名和欄位類型 """n def __init__(self, name, column_type):n self.name = namen self.column_type = column_typenn def __str__(self):n return <%s:%s> % (self.__class__.__name__, self.name)nnnclass IntegerField(Field):n def __init__(self, name):n super().__init__(name, bigint)nnnclass StringField(Field):n def __init__(self, name):n super().__init__(name, varchar(100))nnn# 編寫ModelMetaclass元類nclass ModelMetaclass(type):n def __new__(mcs, name, bases, attrs):n if name == Model:n return type.__new__(mcs, name, bases, attrs)n print(Found model: %s % name)nn mappings = {} # 保存fieldn for attr_name, attr_value in attrs.items():n if isinstance(attr_value, Field):n print(Found maping: %s ==> %s % (attr_name, attr_value))n mappings[attr_name] = attr_valuenn for k in mappings.keys():n attrs.pop(k) # 去除field屬性nn# 把所有的Field移到__mappings__里,防止實例的屬性覆蓋類的同名屬性n attrs[__mappings__] = mappingsn attrs[__tablename__] = name.lower() # 使用類名小寫作為表名n return type.__new__(mcs, name, bases, attrs)nnn# 編寫基類Modelnclass Model(dict, metaclass=ModelMetaclass):nn def __init__(self, **kwargs):n super().__init__(**kwargs)nn def __getattr__(self, key): # 為了實現可以用"."訪問屬性n try:n return self[key]n except KeyError:n raise AttributeError("Model object has no attribute %s" % key)nn def __setattr__(self, k, v):n self[k] = vnn def save(self):n fields = []n params = []n args = []nn for field_name, field in self.__mappings__.items():n fields.append(field.name)n params.append(?)n args.append(getattr(self, field_name, None))nn# 拼成sql語句n sql = inset into %s (%s) values (%s) % (n self.__tablename__, ,.join(fields), ,.join(params)n )n print(SQL: %s % sql)n print(ARGS: %s % str(args))nnn# python3.5nclass User(Model):n id = IntegerField(id)n name = StringField(name)nnu = User(id=1, name=laowang)nu.save()nn""" 輸出如下nFound model: UsernFound maping: id ==> <IntegerField:id>nFound maping: name ==> <StringField:name>nSQL: inset into user (id,name) values (?,?)nARGS: [1, laowang]n"""n

abc模塊:抽象基類支持

抽象基類就是包含一個或者多個抽象方法的類,它本身不實現抽象方法,強制子類去實現,同時抽象基類自己不能被實例化,沒有實現抽象方法的子類也無法實例化。python內置的abc(abstract base class)來實現抽象基類。

# 為了實現這兩個特性,我們可以這麼寫nclass Base:n def foo(self):n raise NotImplementedError()nn def bar(self):n raise NotImplementedError()nnclass Concrete(Base):n def foo(self):n return foo() callednn# Oh no, we forgot to override bar()...n# def bar(self):n# return "bar() called"n

但是這麼寫依然可以實例化Base,python2.6以後引入了abc模塊幫助我們實現這個功能。

from abc import ABCMeta, abstractmethodnnclass Base(metaclass=ABCMeta):n @abstractmethodn def foo(self):n passnn @abstractmethodn def bar(self):n passnnclass Concrete(Base):n def foo(self):n passn# We forget to declare bar() again...n

使用這種方式如果沒有在子類里實現bar方法你是沒有辦法實例化子類的。合理使用抽象基類定義明確的介面。另外應該優先使用collections定義的抽象基類,比如要實現一個容器我們可以繼承 collections.Container

Ref:

stackoverflow.com/quest


推薦閱讀:

初學python,pycharm和Spyder哪個好?
Python中,if與elif有何區別?
黑客你好,請使用Python編寫一個滲透測試探測器

TAG:Python |