元类 (exec和__call__)
目录:
- 元类介绍
- class关键字创建类的流程分析
- 自定义元类控制类StanfordTeacher的创建
- 自定义元类控制类StanfordTeacher的调用
- 再看属性查找
一 元类介绍
如果一切皆为对象,那么类StanfordTeacher本质也是一个对象,既然所有的对象都是调用类得到的,那么StanfordTeacher必然也是调用了一个类得到的,这个类称为元类
于是我们可以推导出===>产生StanfordTeacher的过程一定发生了:StanfordTeacher=元类(...)
type元类
- 类本身也是对象, 是通过实例化元类得到的对象, 元类是类的类
- python默认的元类是type, 也可以自定义元类来实现控制类的创建,控制类的调用
- 通过type元类来创建类
MyClass = type(class_name, class_bases, class_dict)
- class_name 类名
- class_bases 父类, 以元祖形式传入
- class_dict 类的属性, 以字典的形式传入 (类的名称空间)
# 用type元类来创建一个Chinese类 def __init__(self, name, gender, age): self.name = name self.gender = gender self.age = age class_name = 'Chinese' class_bases = (object,) class_dict = {'country': 'China', '__init__': __init__} Chinese = type(class_name, class_bases, class_dict) c1 = Chinese('bigb', 'male', 18) print(c1.name) # bigb print(c1.country) # China
二 class关键字创建类的流程分析
class关键字帮我们创建一个类应该细分为以下四个过程
创建类的两种方式
方式一:使用class关键字
class Chinese(object): country='China' def __init__(self,name,age): self.name=name self.age=age def talk(self): print('%s is talking' %self.name)
方式二:就是手动模拟class创建类的过程):将创建类的步骤拆分开,手动去创建
#准备工作: #创建类主要分为三部分 1 类名 2 类的父类 3 类的__dict__ #类名 class_name='Chinese' #类的父类 class_bases=(object,) #类体 class_body=""" country='China' def __init__(self,name,age): self.name=name self.age=age def talk(self): print('%s is talking' %self.name) """
步骤一(先处理类体 --> 名称空间):类定义的名字都会存放于类的名称空间中(一个局部的名称空间),我们可以实现定义一个空字典,然后用exec去执行类体的代码(exec产生名称空间的过程与真正的class过程相似,只是后者将__开头的属性变形),生成类的局部空间名称,即填充字典
class_dic={} exec(class_body,globals(),class_dic) print(class_dic) #{'country': 'China', 'talk': <function talk at 0x101a560c8>, '__init__': <function __init__ at 0x101a56668>}
步骤二:调用元类type(也可以自定义)来产生类Chinense
Foo=type(class_name,class_bases,class_dic) #实例化type得到对象Foo,即我们用class定义的类Foo print(Foo) print(type(Foo)) print(isinstance(Foo,type)) ''' <class '__main__.Chinese'> <class 'type'> True '''
四 自定义元类控制类StanfordTeacher的创建
一个类没有声明自己的元类,默认他的元类就是type,除了使用内置元类type,我们也可以通过继承type来自定义元类,然后使用metaclass关键字参数为一个类指定元类.
class Mymeta(type): #只有继承了type类才能称之为一个元类,否则就是一个普通的自定义类 pass # StanfordTeacher=Mymeta('StanfordTeacher',(object),{...}) class StanfordTeacher(object,metaclass=Mymeta): school='Stanford' def __init__(self,name,age): self.name=name self.age=age def say(self): print('%s says welcome to the Stanford to learn Python' %self.name)
自定义元类可以控制类的产生过程,类的产生过程其实就是元类的调用过程,即StanfordTeacher=Mymeta('StanfordTeacher',(object),{...}),调用Mymeta会先产生一个空对象StanfordTeacher,然后连同调用Mymeta括号内的参数一同传给Mymeta下的__init__方法,完成初始化,于是我们可以
class Mymeta(type): #只有继承了type类才能称之为一个元类,否则就是一个普通的自定义类 def __init__(self,class_name,class_bases,class_dic): # print(self) #<class '__main__.StanfordTeacher'> # print(class_bases) #(<class 'object'>,) # print(class_dic) #{'__module__': '__main__', '__qualname__': 'StanfordTeacher', 'school': 'Stanford', '__init__': <function StanfordTeacher.__init__ at 0x102b95ae8>, 'say': <function StanfordTeacher.say at 0x10621c6a8>} super(Mymeta, self).__init__(class_name, class_bases, class_dic) # 重用父类的功能 if class_name.islower(): raise TypeError('类名%s请修改为驼峰体' %class_name) if '__doc__' not in class_dic or len(class_dic['__doc__'].strip(' \n')) == 0: raise TypeError('类中必须有文档注释,并且文档注释不能为空') # StanfordTeacher=Mymeta('StanfordTeacher',(object),{...}) class StanfordTeacher(object,metaclass=Mymeta): """ 类StanfordTeacher的文档注释 """ school='Stanford' def __init__(self,name,age): self.name=name self.age=age def say(self): print('%s says welcome to the Stanford to learn Python' %self.name)
五 自定义元类控制类StanfordTeacher的调用
调用一个对象,就是触发对象所在类中的__call__方法的执行
类本身也是个对象, 因此在调用类实例化对象的时候, 就会触发元类当中的__call__
方法
class MyMeta(type): def __call__(self, *args, **kwargs): # 产生一个空对象 obj = self.__new__(self) # self是类对象 # 初始化空对象 self.__init__(obj, *args, **kwargs) # 返回初始化好的对象 return obj class Chinese(object, metaclass=MyMeta): # foo = MyMeta('foo', (object, ) {...}) country = 'China' def __init__(self, name, gender, age): self.name = name self.gender = gender self.age = age def kongfu(self): print('降龙十八掌!') # 这里调用了类对象Chinese, 因此会触发Chinese的类(元类)中的__call__方法 c1 = Chinese('bigb', 'male', 18) print(c1.name) ''' 1. __call__中,会调用chinese内的__new__生成了一个空对象 2. __call__中,会调用Chinese内的__init__初始化这个空对象
3. __call__返回了这个对象,并赋值给了c1
'''
现在 我们可以在此基础上通过修改 __call__
的逻辑从而控制类的调用过程
# 通过元类让Chinese类实例化出来的对象的属性变为私有属性
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)
print(self)
print(self.__new__ is object.__new__)
self.__init__(obj, *args, **kwargs)
# 将对象的属性变成私有属性(对象._类__属性名)
obj.__dict__={'_%s__%s' %(self.__name__,k):v for k,v in obj.__dict__.items()}
#obj.__dict__ = {f'_{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('SS',11,'MALE')
print(obj.__dict__)
五 再看属性查找
属性查找应该分成两层,一层是对象层(基于c3算法的MRO)的查找,另外一个层则是类层(即元类层)的查找
#查找顺序:
#1、先对象层:StanfordTeacher->Foo->Bar->object
#2、然后元类层:Mymeta->type
Mymeta下的__call__里的self.__new__在StanfordTeacher、Foo、Bar里都没有找到__new__的情况下,会去找object里的__new__,而object下默认就有一个__new__,所以即便是之前的类均未实现__new__,也一定会在object中找到一个,根本不会、也根本没必要再去找元类Mymeta->type中查找__new__
但我们还是推荐在__call__中使用self.__new__(self)去创造空对象,因为这种方式会检索三个类StanfordTeacher->Foo->Bar,而object.__new__则是直接跨过了他们三个
六 作业 记得要看要写
1、在元类中控制把自定义类的数据属性都变成大写
class Mymetaclass(type): def __new__(cls,name,bases,attrs): update_attrs={} for k,v in attrs.items(): if not callable(v) and not k.startswith('__'): update_attrs[k.upper()]=v else: update_attrs[k]=v return type.__new__(cls,name,bases,update_attrs) class Chinese(metaclass=Mymetaclass): country='China' tag='Legend of the Dragon' #龙的传人 def walk(self): print('%s is walking' %self.name) print(Chinese.__dict__) ''' {'__module__': '__main__', 'COUNTRY': 'China', 'TAG': 'Legend of the Dragon', 'walk': <function Chinese.walk at 0x0000000001E7B950>, '__dict__': <attribute '__dict__' of 'Chinese' objects>, '__weakref__': <attribute '__weakref__' of 'Chinese' objects>, '__doc__': None} '''
2、在元类中控制自定义的类无需__init__方法
1.元类帮其完成创建对象,以及初始化操作;
2.要求实例化时传参必须为关键字形式,否则抛出异常TypeError: must use keyword argument
3.key作为用户自定义类产生对象的属性,且所有属性变成大写
class Mymetaclass(type): # def __new__(cls,name,bases,attrs): # update_attrs={} # for k,v in attrs.items(): # if not callable(v) and not k.startswith('__'): # update_attrs[k.upper()]=v # else: # update_attrs[k]=v # return type.__new__(cls,name,bases,update_attrs) def __call__(self, *args, **kwargs): if args: raise TypeError('must use keyword argument for key function') obj = object.__new__(self) #创建对象,self为类Foo for k,v in kwargs.items(): obj.__dict__[k.upper()]=v return obj class Chinese(metaclass=Mymetaclass): country='China' tag='Legend of the Dragon' #龙的传人 def walk(self): print('%s is walking' %self.name) p=Chinese(name='lili',age=18,sex='male') print(p.__dict__)
3、在元类中控制自定义的类产生的对象相关的属性全部为隐藏属性
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('lili',18,'male') print(obj.__dict__)
4、基于元类实现单例模式
#步骤五:基于元类实现单例模式 # 单例:即单个实例,指的是同一个类实例化多次的结果指向同一个对象,用于节省内存空间 # 如果我们从配置文件中读取配置来进行实例化,在配置相同的情况下,就没必要重复产生对象浪费内存了 #settings.py文件内容如下 HOST='1.1.1.1' PORT=3306 #方式一:定义一个类方法实现单例模式 import settings class Mysql: __instance=None def __init__(self,host,port): self.host=host self.port=port @classmethod def singleton(cls): if not cls.__instance: cls.__instance=cls(settings.HOST,settings.PORT) return cls.__instance obj1=Mysql('1.1.1.2',3306) obj2=Mysql('1.1.1.3',3307) print(obj1 is obj2) #False obj3=Mysql.singleton() obj4=Mysql.singleton() print(obj3 is obj4) #True #方式二:定制元类实现单例模式 import settings class Mymeta(type): def __init__(self,name,bases,dic): #定义类Mysql时就触发 # 事先先从配置文件中取配置来造一个Mysql的实例出来 self.__instance = object.__new__(self) # 产生对象 self.__init__(self.__instance, settings.HOST, settings.PORT) # 初始化对象 # 上述两步可以合成下面一步 # self.__instance=super().__call__(*args,**kwargs) super().__init__(name,bases,dic) def __call__(self, *args, **kwargs): #Mysql(...)时触发 if args or kwargs: # args或kwargs内有值 obj=object.__new__(self) self.__init__(obj,*args,**kwargs) return obj return self.__instance class Mysql(metaclass=Mymeta): def __init__(self,host,port): self.host=host self.port=port obj1=Mysql() # 没有传值则默认从配置文件中读配置来实例化,所有的实例应该指向一个内存地址 obj2=Mysql() obj3=Mysql() print(obj1 is obj2 is obj3) obj4=Mysql('1.1.1.4',3307) #方式三:定义一个装饰器实现单例模式 import settings def singleton(cls): #cls=Mysql _instance=cls(settings.HOST,settings.PORT) def wrapper(*args,**kwargs): if args or kwargs: obj=cls(*args,**kwargs) return obj return _instance return wrapper @singleton # Mysql=singleton(Mysql) class Mysql: def __init__(self,host,port): self.host=host self.port=port obj1=Mysql() obj2=Mysql() obj3=Mysql() print(obj1 is obj2 is obj3) #True obj4=Mysql('1.1.1.3',3307) obj5=Mysql('1.1.1.4',3308) print(obj3 is obj4) #False
涉及到的知识点
exec方法
- exec()方法可以执行字符串形式的python代码块
- 使用方法
exec(object, global=None, local=None)
- object: 字符串类型的python代码块
- global: 代表全局名称空间, 必须是字典, 默认为None, 如传参则表明该代码块在全局名称空间中运行
- local: 代表局部名称空间, 可以是任何映射, 默认为None, 如传参则表明该代码块在局部空间中运行
code = ''' x = 0 sum = x + y + z print(sum) ''' y = 1 z = 2 global_dict = {'y': 2, 'z': 3} local_dict = {'y': 3, 'z': 4} exec(code) ''' y = 1 z = 2 x = 0 sum = x + y + z print(sum) ''' exec(code, global_dict) ''' 相当于 y = 2 z = 3 x = 0 sum = x + y + z print(sum) ''' exec(code, global_dict, local_dict) ''' 相当于 y = 2 z = 3 def exec_func(): y = 3 z = 4 x = 0 sum = x + y + z print(sum) exec_func() ''' ''' 3 5 7 '''
__call__
对象后面加括号,触发执行。
注:构造方法的执行是由创建对象触发的,即:对象 = 类名() ;而对于 __call__ 方法的执行是由对象后加括号触发的,即:对象() 或者 类()()
class Foo: def __call__(self, *args, **kwargs): print('实例执行 obj()') f1 = Foo() f1() # 调用Foo类下的__call__() Foo() # 触发__call__()方法,一切接对象
__new__和__init__
见:https://www.cnblogs.com/yunlong-study/p/14325491.html
代码参考
''' 通过自定义元类来实现: 1. 类名首字母必须大写 2. 类中必须有文档注释 ''' class MyMeta(type): def __init__(self, class_name, class_bases, class_dict): print(class_name) # chinese print(class_bases) # (<class 'object'>,) print(class_dict) # {'__module__': '__main__', '__qualname__': 'chinese', 'country': 'China', '__init__': <function chinese.__init__ at 0x0000000009FBFD90>, 'kongfu': <function chinese.kongfu at 0x0000000009FBFE18>} # 类名首字母必须大写 if not class_name.istitle(): raise TypeError('类的首字母必须大写!') # 类中必须有注释 if not class_dict.get('__doc__'): raise TypeError('类中必须有文档注释!') # 调用type中的__init__方法初始化对象 super().__init__(class_name, class_bases, class_dict) class chinese(object, metaclass=MyMeta): # foo = MyMeta('foo', (object, ) {...}) country = 'China' def __init__(self, name, gender, age): self.name = name self.gender = gender self.age = age def kongfu(self): print('降龙十八掌!') ''' raise TypeError('类的首字母必须大写!') TypeError: 类的首字母必须大写! ''' # 将类名大写, 再运行 class Chinese(object, metaclass=MyMeta): # foo = MyMeta('foo', (object, ) {...}) country = 'China' def __init__(self, name, gender, age): self.name = name self.gender = gender self.age = age def kongfu(self): print('降龙十八掌!') ''' raise TypeError('类中必须有文档注释!') TypeError: 类中必须有文档注释! '''
本文作者:云龙
本文链接:https://www.cnblogs.com/yunlong-study/p/14324656.html
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步