热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

python3高级知识元类(metaclass)深度剖析

一、简介在面向对象的程序设计中类和对象是其重要角色,我们知道对象是由类实例化而来,那么类又是怎么生成的呢?答案是通过元类。本篇文章将介绍元类相关知识,并剖析元类生成类的过程,以及元


一、简介


在面向对象的程序设计中类和对象是其重要角色,我们知道对象是由类实例化而来,那么类又是怎么生成的呢?答案是通过元类。本篇文章将介绍元类相关知识,并剖析元类生成类的过程,以及元类的使用等内容,希望能帮助到正在学习python的同仁。


一、一切皆对象


在python中有这样一句话“一切皆对象”,没错你所知道的dict、class、int、func等等都是对象,让我们来看以下一段代码来进行说明:



#!/usr/bin/env python3
# -*- coding:utf-8 -*-
# Author:wd
class Foo(object):
pass
def func():
print('func')
print(Foo.__class__)
print(func.__class__)
print(int.__class__)
print(func.__class__.__class__)
结果:





说明:__class__方法用于查看当前对象由哪个类生成的,正如结果所见其中Foo和int这些类(对象)都是由type创建,而函数则是由function类创建,而function类则也是由type创建,究其根本所有的这些类对象都是由type穿件。这里的type就是python内置的元类,接下来谈谈type。


二、关于type


上面我们谈到了所有的类(对象)都是由type生成,那么不妨我们看看type定义,以下是python3.6中内置type定义部分摘抄:



class type(object):
"""
type(object_or_name, bases, dict)
type(object) -> the object's type
type(name, bases, dict) -> a new type
"""
def mro(self): # real signature unknown; restored from __doc__
"""
mro() -> list
return a type's method resolution order
"""
return []

从描述信息中我们可以看到,type(object)->返回对象type类型,也就是我们常常使用该方法判断一个对象的类型,而type(name, bases, dict) -> 返回一个新的类(对象)。


让我们详细描述下这个语法: 




type(类名,该类所继承的父类元祖,该类对应的属性字典(k,v))

利用该语法我们来穿件一个类(对象)Foo:



Foo=type('Foo',(object,),{'Name':'wd'})
print(Foo)
print(Foo.Name)
结果:

wd

当然也可以实例化这个类(对象):



Foo=type('Foo',(object,),{'Name':'wd'})
obj=Foo()
print(obj.Name)
print(obj)
print(obj.__class__)
结果:
wd
<__main__.Foo object at 0x104482438>


这样创建方式等价于:



class Foo(object):
Name='wd'

其实上面的过程也就是我们使用class定义类生成的过程,而type就是python中的元类。


三、元类


什么是元类


经过以上的介绍,说白了元类就是创建类的类,有点拗口,姑且把这里称为可以创建类对象的类。列如type就是元类的一种,其他的元类都是通过继承type或使用type生成的。通过元类我们可以控制一个类创建的过程,以及包括自己定制一些功能。例如,下面动态的为类添加方法:



def get_name(self):
print(self.name)
class MyType(type):
def __new__(cls, cls_name, bases, dict_attr):
dict_attr['get_name'] = get_name #将get_name 作为属性添加到类属性中
return super(MyType, cls).__new__(cls, cls_name, bases, dict_attr)
class Foo(metaclass=MyType):
def __init__(self, name):
self.name = name
obj = Foo('wd')
obj.get_name()#调用该方法
结果:
wd

以上示例说明:


1.MyType是继承了type,也就是说继承了其所有的功能与特性,所以它也具有创建类的功能,所以它也是元类;


2.类Foo中使用了metaclass关键字,表明该类由MyType进行创建。


3.创建Foo类时候会先执行MyType的__new__方法(后续会这些方法进行更详细的说明),并接受三个参数,cls_name, bases, dict_attr,在改方法中我们在类属性字典中添加了get_name属性,并将它与函数绑定,这样生成的类中就有了该方法。 


使用元类



了解类元类的作用,我们知道其主要目的就是为了当创建类时能够根据需求改变类,在以上的列子中我们介绍了使用方法,其中就像 stackoverflow
中关于对元类的使用建议一样,绝大多数的应用程序都非必需使用元类,并且使用它可能会对你的代码带来一定的复杂性,但是就元类的使用而言其实很简单,其场景在于:


1.对创建的类进行校验(拦截);


2.修改类;


3.为该类定制功能;


使用元类是时候经典类和新式类时候有些不同,新式类通过参数metaclass,经典类通过__metaclass__属性:



class Foo(metaclass=MyType): #新式类
pass
class Bar: # 经典类
__metaclass__ = MyType
pass

在解释元类的时候有提到过,元类可以是type,也可以是继承type的类,当然还可以是函数,只要它是可调用的。但是有个必要的前提是该函数使用的是具有type功能的函数,否则生成的对象可能就不是你想要的(在后续的原理在进行讲解)。以下示例将给出使用函数作为元类来创建类:



def class_creater(cls_name, bases, dict_attr):
return type(cls_name, bases, dict_attr)
class Foo(metaclass=class_creater):
def __init__(self,name):
self.name=name
obj=Foo('wd')
print(obj.name) #wd

原理


当我们使用class定义类时候,它会执行以下步骤:



  1. 获取类名,以示例中class Foo为例,类名是Foo。

  2. 获取父类,默认object,以元祖的形式,如(object,Foo)

  3. 获取类的属性字典(也叫名称空间)

  4. 将这三个参数传递给元类(也就是metaclass参数指定的类),如果没有metaclass参数则使用type生成类。


在这几个步骤中,前三个步骤没有什么可说的,但是对于元类生成类的这一过程接下来我们将详细介绍。 


元类创建类的过程


其实如果你对面向对象非常熟悉的话,其过程也是非常容易理解的,在介绍类生成的过程之前,我们需要对三个方法做充分的理解:__init__、__new__、__call__。



  1. __init__ :通常用于初始化一个新实例,控制这个初始化的过程,比如添加一些属性, 做一些额外的操作,发生在类实例被创建完以后。它是实例级别的方法。触发方式为:类()

  2. __new__ :通常用于控制生成一个类实例的过程,依照Python官方文档的说法,__new__方法主要是当你继承一些不可变的时(比如int, str, tuple), 提供给你一个自定义这些类的实例化过程的途径。它是类级别的方法。

  3. __call__ :当类中有__call__方法存在时候,该类实列化的对象就是可调用的,触发方式为:对象()。


并且一个类在实例化的过程中执行顺序是先执行__new__在执行__init__(这是重点),以下用一个示例来说明:




class Foo(object):
def __init__(self, name):
print('this is __init__')
self.name = name
def __new__(cls, *args, **kwargs):
print('this is __new__')
return object.__new__(cls)
def __call__(self, *args, **kwargs):
print("this is __call__")
obj=Foo('wd') # 实例化
obj() # 触发__call__
结果:
this is __new__
this is __init__
this is __call__

有了这个知识,再来看看使用元类生成类,以下代码定义来一个元类继承来type,我们重写__new__和__init__方法(其实什么也没干),为了说明类的生成过程:



class MyType(type):
def __init__(self, cls_name, bases, cls_attr):
print("Mytype __init__", cls_name, bases)
def __new__(cls, cls_name, bases, cls_attr):
print("Mytype __new__", cls_name, bases)
return super(MyType, cls).__new__(cls, cls_name, bases, cls_attr)
class Foo(metaclass=MyType):
def __init__(self, name):
print('this is __init__')
self.name = name
def __new__(cls, *args, **kwargs):
print('this is __new__')
return object.__new__(cls)
print("line -------")
obj = Foo('wd') # 实例化
结果:
Mytype __new__ Foo ()
Mytype __init__ Foo ()
line -------
this is __new__
this is __init__

解释说明:



  • 首先metaclass接受一个可调用的对象,而在这里该对象是一个类,也就是说会执行MyType(),并把cls_name,bases,cls_attr传递给MyType,这不就是MyType的示例化过程吗,所以你在结果中可以看到,分割线是在"Mytype __new__”和“Mytype __init__”之后输出,接下来在看MyType。



  • MyType元类的实例化过程和普通类一样,先执行自己__new__方法,在执行自己的__init__方法,在这里请注意__new__方法是控制MyType类生成的过程,而__init__则是实例化过程,用于生成类Foo。这样一来是不是对类的生成过程有了非常深刻的认识。 


这还不够清楚,在以上的示例中Foo即是类,也是对象,它是由元类实例化的对象,那它执行Foo(‘wd’)相当于是执行:对象(),即执行的是元类的__call__方法,那么在以上示例中我们在元类中加入__call__方法,看看在执行Foo(‘wd’)会不会调用__call__:




class MyType(type):
def __init__(self, cls_name, bases, cls_attr):
print("Mytype __init__", cls_name, bases)
def __new__(cls, cls_name, bases, cls_attr):
print("Mytype __new__", cls_name, bases)
return super(MyType, cls).__new__(cls, cls_name, bases, cls_attr)
def __call__(self, *args, **kwargs):
print('Mytype __call__')
class Foo(metaclass=MyType):
def __init__(self, name):
print('this is __init__')
self.name = name
def __new__(cls, *args, **kwargs):
print('this is __new__')
return object.__new__(cls)
def __call__(self, *args, **kwargs):
print("this is __call__")
print("before -------")
obj = Foo('wd') # 实例化
print("after -------")
print(obj)
结果:
Mytype __new__ Foo ()
Mytype __init__ Foo ()
before -------
Mytype __call__
after -------
None

你会发现,当Foo实例化时候执行了元类的__call__,你从python的一切皆对象的方式来看,一切都是顺理成章的,因为这里的Foo其实是元类的对象,对象+()执行元类的__call__方法。请注意,在Foo进行实例化时候返回的对象是None,这是因为__call__方法返回的就是None,所以在没有必要的前提下最好不要随意重写元类的__call__方法,这会影响到类的实例化。__call__方法在元类中作用是控制类生成时的调用过程。


通过__call__方法我们能得出结果就是__call__方法返回什么,我们最后得到的实例就是什么。还是刚才栗子,我们让Foo实例化以后变成一个字符串:



class MyType(type):
def __init__(self, cls_name, bases, cls_attr):
print("Mytype __init__", cls_name, bases)
def __new__(cls, cls_name, bases, cls_attr):
return super(MyType, cls).__new__(cls, cls_name, bases, cls_attr)
def __call__(self, *args, **kwargs):
return 'this is wd'
class Foo(metaclass=MyType):
def __init__(self, name):
print('this is __init__')
self.name = name
def __new__(cls, *args, **kwargs):
print('this is __new__')
return object.__new__(cls)
def __call__(self, *args, **kwargs):
print("this is __call__")
obj = Foo('wd') # 实例化
print(type(obj),obj)
结果:
Mytype __init__ Foo ()
this is wd

既然__call__方法返回什么,我们实例化生成的对象就是什么,那么在正常的流程是返回的是Foo的对象,而Foo的对象是由Foo的__new__和Foo的__init__生成的,所以在__call__方法的内部又有先后调用了Foo类的__new__方法和__init__方法,如果我们重写元类的__call__方法,则应该调用对象的__new__和__init__,如下:



class MyType(type):
def __init__(self, cls_name, bases, cls_attr):
print("Mytype __init__", cls_name, bases)
def __new__(cls, cls_name, bases, cls_attr):
return super(MyType, cls).__new__(cls, cls_name, bases, cls_attr)
def __call__(self, *args, **kwargs):
print("Mytype __call__", )
obj = self.__new__(self)
print(self, obj)
self.__init__(obj, *args, **kwargs)
return obj
class Foo(metaclass=MyType):
def __init__(self, name):
self.name = name
def __new__(cls, *args, **kwargs):
return object.__new__(cls)
obj = Foo('wd') # 实例化
print(obj.name)
结果:
Mytype __init__ Foo ()
Mytype __call__
<__main__.Foo object at 0x1100c9dd8>
wd

同样,当函数作为元类时候,metaclass关键字会调用其对应的函数生成类,如果这个函数返回的不是类,而是其他的对象,那么使用该函数定义的类就得到的就是该对象,这也就是为什么我说使用函数作为元类时候,需要有type功能,一个简单的示例:



def func(cls_name, bases, dict_attr):
return 'this is wd'
class Foo(metaclass=func):
def __init__(self, name):
self.name = name
def __new__(cls, *args, **kwargs):
return object.__new__(cls)
print(Foo, "|", type(Foo)) # 结果:this is wd |
obj=Foo('wd') #报错

结语


现在说python一切皆对象可以说非常到位了,因为它们要不是类的对象,要不就是元类的对象,除了type。再者元类本身其实是复杂的,只是我们在对这元类生成类的这一过程做了深度的分析,所以在我们编写的程序中可能极少会用到元类,除非有特殊的需求,比如动态的生成类、修改类的一些东西等,当然你想让你的代码看来“复杂”也可以尝试使用。但是在有些情况下(如在文章中提到的几个场景中)使用元类能更巧妙的解决很多问题,不仅如此你会发现元类在很多开源框架中也有使用,例如django、flask,你也可以借鉴其中的场景对自己的程序进行优化改进。







推荐阅读
  • 浅谈Python3中打开文件的方式(With open)
    浅谈Python3中打开文件的方式(With open)-目录0.背景知识1.常规方式:读取文件-----open()2.推荐方式:读取文件-----WithOpen1).读取方式 ... [详细]
  • python3 logging
    python3logginghttps:docs.python.org3.5librarylogging.html,先3.5是因为我当前的python版本是3.5之所 ... [详细]
  • Python3怎么获取文件属性
    这篇文章给大家分享的是有关Python3怎么获取文件属性的内容。小编觉得挺实用的,因此分享给大家做个参考,一起跟随小编过来看看吧。os.stat(path ... [详细]
  • Python Flask学习之安装SQL,python3,Pycharm(网上下载安装即可)
    1,下载时更改pypi源。可以额外安装虚拟化环境:pipinstall-ihttp:pypi.douban.comsimple--trusted-hos ... [详细]
  • 提升Python编程效率的十点建议
    本文介绍了提升Python编程效率的十点建议,包括不使用分号、选择合适的代码编辑器、遵循Python代码规范等。这些建议可以帮助开发者节省时间,提高编程效率。同时,还提供了相关参考链接供读者深入学习。 ... [详细]
  • 本文介绍了lua语言中闭包的特性及其在模式匹配、日期处理、编译和模块化等方面的应用。lua中的闭包是严格遵循词法定界的第一类值,函数可以作为变量自由传递,也可以作为参数传递给其他函数。这些特性使得lua语言具有极大的灵活性,为程序开发带来了便利。 ... [详细]
  • 开发笔记:加密&json&StringIO模块&BytesIO模块
    篇首语:本文由编程笔记#小编为大家整理,主要介绍了加密&json&StringIO模块&BytesIO模块相关的知识,希望对你有一定的参考价值。一、加密加密 ... [详细]
  • Python正则表达式学习记录及常用方法
    本文记录了学习Python正则表达式的过程,介绍了re模块的常用方法re.search,并解释了rawstring的作用。正则表达式是一种方便检查字符串匹配模式的工具,通过本文的学习可以掌握Python中使用正则表达式的基本方法。 ... [详细]
  • 闭包一直是Java社区中争论不断的话题,很多语言都支持闭包这个语言特性,闭包定义了一个依赖于外部环境的自由变量的函数,这个函数能够访问外部环境的变量。本文以JavaScript的一个闭包为例,介绍了闭包的定义和特性。 ... [详细]
  • 浏览器中的异常检测算法及其在深度学习中的应用
    本文介绍了在浏览器中进行异常检测的算法,包括统计学方法和机器学习方法,并探讨了异常检测在深度学习中的应用。异常检测在金融领域的信用卡欺诈、企业安全领域的非法入侵、IT运维中的设备维护时间点预测等方面具有广泛的应用。通过使用TensorFlow.js进行异常检测,可以实现对单变量和多变量异常的检测。统计学方法通过估计数据的分布概率来计算数据点的异常概率,而机器学习方法则通过训练数据来建立异常检测模型。 ... [详细]
  • 基于dlib的人脸68特征点提取(眨眼张嘴检测)python版本
    文章目录引言开发环境和库流程设计张嘴和闭眼的检测引言(1)利用Dlib官方训练好的模型“shape_predictor_68_face_landmarks.dat”进行68个点标定 ... [详细]
  • Python爬虫中使用正则表达式的方法和注意事项
    本文介绍了在Python爬虫中使用正则表达式的方法和注意事项。首先解释了爬虫的四个主要步骤,并强调了正则表达式在数据处理中的重要性。然后详细介绍了正则表达式的概念和用法,包括检索、替换和过滤文本的功能。同时提到了re模块是Python内置的用于处理正则表达式的模块,并给出了使用正则表达式时需要注意的特殊字符转义和原始字符串的用法。通过本文的学习,读者可以掌握在Python爬虫中使用正则表达式的技巧和方法。 ... [详细]
  • Learning to Paint with Model-based Deep Reinforcement Learning
    本文介绍了一种基于模型的深度强化学习方法,通过结合神经渲染器,教机器像人类画家一样进行绘画。该方法能够生成笔画的坐标点、半径、透明度、颜色值等,以生成类似于给定目标图像的绘画。文章还讨论了该方法面临的挑战,包括绘制纹理丰富的图像等。通过对比实验的结果,作者证明了基于模型的深度强化学习方法相对于基于模型的DDPG和模型无关的DDPG方法的优势。该研究对于深度强化学习在绘画领域的应用具有重要意义。 ... [详细]
  • 本文整理了315道Python基础题目及答案,帮助读者检验学习成果。文章介绍了学习Python的途径、Python与其他编程语言的对比、解释型和编译型编程语言的简述、Python解释器的种类和特点、位和字节的关系、以及至少5个PEP8规范。对于想要检验自己学习成果的读者,这些题目将是一个不错的选择。请注意,答案在视频中,本文不提供答案。 ... [详细]
  • python教程分享Python获取时光网电影数据的实例代码
    一、前言有时候觉得电影真是人类有史以来最伟大的发明,我喜欢看电影,看电影可以让我们增长见闻,学习知识。从某种角度上而言,电影凭借自身独有的魅力大大延长了人类的”寿命&r ... [详细]
author-avatar
GodlikeZ寰
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有