一、引言
-
元類屬於python面向對象編程的深層魔法,99%的人都不得要領,一些自以為搞明白元類的人其實也只是自圓其說、點到為止,從對元類的控制上來看就破綻百出、邏輯混亂,今天我就來帶大家來深度了解python元類的來龍去脈。
-
筆者深入淺出的背后是對技術一日復一日的執念,希望可以大家可以尊重原創,為大家能因此文而解開對元類所有的疑惑而感到開心!!!
二、什么是元類
- 在python中一切皆對象,那么我們用class關鍵字定義的類本身也是一個對象,負責產生該對象的類稱之為元類,即元類可以簡稱為類的類
class Foo: # Foo=元類()
pass
三、為什么用元類
- 元類是負責產生類的,所以我們學習元類或者自定義元類的目的:是為了控制類的產生過程,還可以控制對象的產生過程
四、內置函數exec(儲備)
cmd = """
x=1
print('exec函數運行了')
def func(self):
pass
"""
class_dic = {}
# 執行cmd中的代碼,然后把產生的名字丟入class_dic字典中
exec(cmd, {}, class_dic)
exec函數運行了
print(class_dic)
{'x': 1, 'func': <function func at 0x10a0bc048>}
五、class創建類
-
如果說類也是對象,那么用class關鍵字的去創建類的過程也是一個實例化的過程,該實例化的目的是為了得到一個類,調用的是元類
-
用class關鍵字創建一個類,用的默認的元類type,因此以前說不要用type作為類別判斷
class People: # People=type(...)
country = 'China'
def __init__(self, name, age):
self.name = name
self.age = age
def eat(self):
print('%s is eating' % self.name)
print(type(People))
<class 'type'>
5.1 type實現
-
創建類的3個要素:類名,基類,類的名稱空間
-
People = type(類名,基類,類的名稱空間)
class_name = 'People' # 類名
class_bases = (object, ) # 基類
# 類的名稱空間
class_dic = {}
class_body = """
country='China'
def __init__(self,name,age):
self.name=name
self.age=age
def eat(self):
print('%s is eating' %self.name)
"""
exec(
class_body,
{},
class_dic,
)
print(class_name)
People
print(class_bases)
(<class 'object'>,)
print(class_dic) # 類的名稱空間
{'country': 'China', '__init__': <function __init__ at 0x10a0bc048>, 'eat': <function eat at 0x10a0bcd08>}
- People = type(類名,基類,類的名稱空間)
People1 = type(class_name, class_bases, class_dic)
print(People1)
<class '__main__.People'>
obj1 = People1(1, 2)
obj1.eat()
1 is eating
- class創建的類的調用
print(People)
<class '__main__.People'>
obj = People1(1, 2)
obj.eat()
1 is eating
六、自定義元類控制類的創建
- 使用自定義的元類
class Mymeta(type): # 只有繼承了type類才能稱之為一個元類,否則就是一個普通的自定義類
def __init__(self, class_name, class_bases, class_dic):
print('self:', self) # 現在是People
print('class_name:', class_name)
print('class_bases:', class_bases)
print('class_dic:', class_dic)
super(Mymeta, self).__init__(class_name, class_bases,
class_dic) # 重用父類type的功能
-
分析用class自定義類的運行原理(而非元類的的運行原理):
-
拿到一個字符串格式的類名class_name='People'
-
拿到一個類的基類們class_bases=(obejct,)
-
執行類體代碼,拿到一個類的名稱空間class_dic={...}
-
調用People=type(class_name,class_bases,class_dic)
-
class People(object, metaclass=Mymeta): # People=Mymeta(類名,基類們,類的名稱空間)
country = 'China'
def __init__(self, name, age):
self.name = name
self.age = age
def eat(self):
print('%s is eating' % self.name)
self: <class '__main__.People'>
class_name: People
class_bases: (<class 'object'>,)
class_dic: {'__module__': '__main__', '__qualname__': 'People', 'country': 'China', '__init__': <function People.__init__ at 0x10a0bcbf8>, 'eat': <function People.eat at 0x10a0bc2f0>}
6.1 應用
-
自定義元類控制類的產生過程,類的產生過程其實就是元類的調用過程
-
我們可以控制類必須有文檔,可以使用如下的方式實現
class Mymeta(type): # 只有繼承了type類才能稱之為一個元類,否則就是一個普通的自定義類
def __init__(self, class_name, class_bases, class_dic):
if class_dic.get('__doc__') is None or len(
class_dic.get('__doc__').strip()) == 0:
raise TypeError('類中必須有文檔注釋,並且文檔注釋不能為空')
if not class_name.istitle():
raise TypeError('類名首字母必須大寫')
super(Mymeta, self).__init__(class_name, class_bases,
class_dic) # 重用父類的功能
try:
class People(object, metaclass=Mymeta
): #People = Mymeta('People',(object,),{....})
# """這是People類"""
country = 'China'
def __init__(self, name, age):
self.name = name
self.age = age
def eat(self):
print('%s is eating' % self.name)
except Exception as e:
print(e)
類中必須有文檔注釋,並且文檔注釋不能為空
七、__call__(儲備)
- 要想讓obj這個對象變成一個可調用的對象,需要在該對象的類中定義一個方法、、__call__方法,該方法會在調用對象時自動觸發
class Foo:
def __call__(self, *args, **kwargs):
print(args)
print(kwargs)
print('__call__實現了,實例化對象可以加括號調用了')
obj = Foo()
obj('nick', age=18)
('nick',)
{'age': 18}
__call__實現了,實例化對象可以加括號調用了
八、__new__(儲備)
我們之前說類實例化第一個調用的是__init__,但__init__其實不是實例化一個類的時候第一個被調用 的方法。當使用 Persion(name, age) 這樣的表達式來實例化一個類時,最先被調用的方法 其實是 __new__ 方法。
__new__方法接受的參數雖然也是和__init__一樣,但__init__是在類實例創建之后調用,而 __new__方法正是創建這個類實例的方法。
注意:new() 函數只能用於從object繼承的新式類。
class A:
pass
class B(A):
def __new__(cls):
print("__new__方法被執行")
return cls.__new__(cls)
def __init__(self):
print("__init__方法被執行")
b = B()
九、自定義元類控制類的實例化
class Mymeta(type):
def __call__(self, *args, **kwargs):
print(self) # self是People
print(args) # args = ('nick',)
print(kwargs) # kwargs = {'age':18}
# return 123
# 1. 先造出一個People的空對象,申請內存空間
# __new__方法接受的參數雖然也是和__init__一樣,但__init__是在類實例創建之后調用,而 __new__方法正是創建這個類實例的方法。
obj = self.__new__(self) # 雖然和下面同樣是People,但是People沒有,找到的__new__是父類的
# 2. 為該對空對象初始化獨有的屬性
self.__init__(obj, *args, **kwargs)
# 3. 返回一個初始化好的對象
return obj
- People = Mymeta(),People()則會觸發__call__
class People(object, metaclass=Mymeta):
country = 'China'
def __init__(self, name, age):
self.name = name
self.age = age
def eat(self):
print('%s is eating' % self.name)
# 在調用Mymeta的__call__的時候,首先會找自己(如下函數)的,自己的沒有才會找父類的
# def __new__(cls, *args, **kwargs):
# # print(cls) # cls是People
# # cls.__new__(cls) # 錯誤,無限死循環,自己找自己的,會無限遞歸
# obj = super(People, cls).__new__(cls) # 使用父類的,則是去父類中找__new__
# return obj
-
類的調用,即類實例化就是元類的調用過程,可以通過元類Mymeta的__call__方法控制
-
分析:調用Pepole的目的
-
先造出一個People的空對象
-
為該對空對象初始化獨有的屬性
-
返回一個初始化好的對象
-
obj = People('nick', age=18)
<class '__main__.People'>
('nick',)
{'age': 18}
print(obj.__dict__)
{'name': 'nick', 'age': 18}
一十、自定義元類后類的繼承順序
結合python繼承的實現原理+元類重新看屬性的查找應該是什么樣子呢???
在學習完元類后,其實我們用class自定義的類也全都是對象(包括object類本身也是元類type的 一個實例,可以用type(object)查看),我們學習過繼承的實現原理,如果把類當成對象去看,將下述繼承應該說成是:對象OldboyTeacher繼承對象Foo,對象Foo繼承對象Bar,對象Bar繼承對象object
class Mymeta(type): # 只有繼承了type類才能稱之為一個元類,否則就是一個普通的自定義類
n = 444
def __call__(self, *args,
**kwargs): #self=<class '__main__.OldboyTeacher'>
obj = self.__new__(self)
self.__init__(obj, *args, **kwargs)
return obj
class Bar(object):
n = 333
class Foo(Bar):
n = 222
class OldboyTeacher(Foo, metaclass=Mymeta):
n = 111
school = 'oldboy'
def __init__(self, name, age):
self.name = name
self.age = age
def say(self):
print('%s says welcome to the oldboy to learn Python' % self.name)
print(
OldboyTeacher.n
) # 自下而上依次注釋各個類中的n=xxx,然后重新運行程序,發現n的查找順序為OldboyTeacher->Foo->Bar->object->Mymeta->type
111
print(OldboyTeacher.n)
111
-
查找順序:
-
先對象層:OldoyTeacher->Foo->Bar->object
-
然后元類層:Mymeta->type
-
依據上述總結,我們來分析下元類Mymeta中__call__里的self.__new__的查找
class Mymeta(type):
n = 444
def __call__(self, *args,
**kwargs): #self=<class '__main__.OldboyTeacher'>
obj = self.__new__(self)
print(self.__new__ is object.__new__) #True
class Bar(object):
n = 333
# def __new__(cls, *args, **kwargs):
# print('Bar.__new__')
class Foo(Bar):
n = 222
# def __new__(cls, *args, **kwargs):
# print('Foo.__new__')
class OldboyTeacher(Foo, metaclass=Mymeta):
n = 111
school = 'oldboy'
def __init__(self, name, age):
self.name = name
self.age = age
def say(self):
print('%s says welcome to the oldboy to learn Python' % self.name)
# def __new__(cls, *args, **kwargs):
# print('OldboyTeacher.__new__')
OldboyTeacher('nick',
18) # 觸發OldboyTeacher的類中的__call__方法的執行,進而執行self.__new__開始查找
總結,Mymeta下的__call__里的self.__new__在OldboyTeacher、Foo、Bar里都沒有找到__new__的情況下,會去找object里的__new__,而object下默認就有一個__new__,所以即便是之前的類均未實現__new__,也一定會在object中找到一個,根本不會、也根本沒必要再去找元類Mymeta->type中查找__new__
十一、練習
需求:使用元類修改屬性為隱藏屬性
class Mymeta(type):
def __init__(self, class_name, class_bases, class_dic):
# 加上邏輯,控制類Foo的創建
super(Mymeta, self).__init__(class_name, class_bases, class_dic)
def __call__(self, *args, **kwargs):
# 加上邏輯,控制Foo的調用過程,即Foo對象的產生過程
obj = self.__new__(self)
self.__init__(obj, *args, **kwargs)
# 修改屬性為隱藏屬性
obj.__dict__ = {
'_%s__%s' % (self.__name__, k): v
for k, v in obj.__dict__.items()
}
return obj
class Foo(object, metaclass=Mymeta): # Foo = Mymeta(...)
def __init__(self, name, age, sex):
self.name = name
self.age = age
self.sex = sex
obj = Foo('nick', 18, 'male')
print(obj.__dict__)
{'_Foo__name': 'egon', '_Foo__age': 18, '_Foo__sex': 'male'}