Python 类与元类的深度挖掘 I【经验】


Posted in Python onMay 06, 2016

上一篇介绍了 Python 枚举类型的标准库,除了考虑到其实用性,还有一个重要的原因是其实现过程是一个非常好的学习、理解 Python 类与元类的例子。因此接下来两篇就以此为例,深入挖掘 Python 中类与元类背后的机制。

翻开任何一本 Python 教程,你一定可以在某个位置看到下面这两句话:

Python 中一切皆为对象(Everything in Python is an object);

Python 是一种面向对象编程(Object Oriented Programming, OOP)的语言。

虽然在上面两句话的语境中,对象(Object)的含义可能稍有不同,但可以肯定的是对象在 Python 中具有非常重要的意义,也是我们接下来将要讨论的所有内容的基础。那么,对象到底是什么?

对象(Object)

对象是 Python 中对数据的一种抽象,Python 程序中所有数据都是通过对象或对象之间的关系来表示的。[ref: Data Model]

港台将 Object 翻译为“物件”,可以将其看作是一个盛有数据的盒子,只不过除了纯粹的数据之外还有其它有用的属性信息,在 Python 中,所有的对象都具有id、type、value三个属性:

+---------------+
|  |
| Python Object |
|  |
+------+--------+
| ID | |
+---------------+
| Type | |
+---------------+
| Value| |
+---------------+

其中 id 代表内存地址,可以通过内置函数 id() 查看,而 type 表示对象的类别,不同的类别意味着该对象拥有的属性和方法等,可以通过 type() 方法查看:

 def who(obj):

print(id(obj), type(obj))


who(1)


who(None)


who(who)


4515088368 


4514812344 


4542646064

对象作为 Python 中的基本单位,可以被创建、命名或删除。Python 中一般不需要手动删除对象,其垃圾回收机制会自动处理不再使用的对象,当然如果需要,也可以使用 del 语句删除某个变量;所谓命名则是指给对象贴上一个名字标签,方便使用,也就是声明或赋值变量;接下来我们重点来看如何创建一个对象。对于一些 Python 内置类型的对象,通常可以使用特定的语法生成,例如数字直接使用阿拉伯数字字面量,字符串使用引号 '',列表使用 [],字典使用 {} ,函数使用 def 语法等,这些对象的类型都是 Python 内置的,那我们能不能创建其它类型的对象呢?

类与实例

既然说 Python 是面向对象编程语言,也就允许用户自己创建对象,通常使用 class 语句,与其它对象不同的是,class 定义的对象(称之为类)可以用于产生新的对象(称之为实例):

class A:

pass


a = A()


who(A)


who(a)


140477703944616 


4542635424

上面的例子中 A 是我们创建的一个新的类,而通过调用 A() 可以获得一个 A 类型的实例对象,我们将其赋值为 a,也就是说我们成功创建了一个与所有内置对象类型不同的对象 a,它的类型为 __main__.A!至此我们可以将 Python 中一切的对象分为两种:

可以用来生成新对象的类,包括内置的 int、str 以及自己定义的 A 等;

由类生成的实例对象,包括内置类型的数字、字符串以及自己定义的类型为 __main__.A 的 a。

单纯从概念上理解这两种对象没有任何问题,但是这里要讨论的是在实践中不得不考虑的一些细节性问题:

需要一些方便的机制来实现面向对象编程中的继承、重载等特性;

需要一些固定的流程让我们可以在生成实例化对象的过程中执行一些特定的操作;

这两个问题主要关于类的一些特殊的操作,也就是这一篇后面的主要内容。如果再回顾一下开头提到的两句话,你可能会想到,既然类本身也是对象,那它们又是怎样生成的?这就是后一篇将主要讨论的问题:用于生成类对象的类,即元类(Metaclass)。

super, mro()

0x00 Python 之禅中提到的最后一条,命名空间(namespace)是个绝妙的理念,类或对象在 Python 中就承担了一部分命名空间的作用。比如说某些特定的方法或属性只有特定类型的对象才有,不同类型对象的属性和方法尽管名字可能相同,但由于隶属不同的命名空间,其值可能完全不同。在实现类的继承与重载等特性时同样需要考虑命名空间的问题,以枚举类型的实现为例,我们需要保证枚举对象的属性名称不能有重复,因此我们需要继承内置的 dict 类:

 

 class _EnumDict(dict):

def __init__(self):


dict.__init__(self)


self._member_names = []


def keys(self):


keys = dict.keys(self)


return list(filter(lambda k: k.isupper(), keys))


ed = _EnumDict()


ed['RED'] = 1


ed['red'] = 2


print(ed, ed.keys())


{'RED': 1, 'red': 2} ['RED']

在上面的例子中 _EnumDict 重载同时调用了父类 dict 的一些方法,上面的写法在语法上是没有错误的,但是如果我们要改变 _EnumDict 的父类,不再是继承自 dict,则必须手动修改所有方法中 dict.method(self) 的调用形式,这样就不是一个好的实践方案了。为了解决这一问题,Python 提供了一个内置函数 super():

print(super.__doc__)

super() -> same as super(__class__, )


super(type) -> unbound super object


super(type, obj) -> bound super object; requires isinstance(obj, type)


super(type, type2) -> bound super object; requires issubclass(type2, type)


Typical use to call a cooperative superclass method:


class C(B):


def meth(self, arg):


super().meth(arg)


This works for class methods too:


class C(B):


@classmethod


def cmeth(cls, arg):


super().cmeth(arg)

我最初只是把 super() 当做指向父类对象的指针,但实际上它可以提供更多功能:给定一个对象及其子类(这里对象要求至少是类对象,而子类可以是实例对象),从该对象父类的命名空间开始搜索对应的方法。

以下面的代码为例:

 class A(object):

def method(self):


who(self)


print("A.method")


class B(A):


def method(self):


who(self)


print("B.method")


class C(B):


def method(self):


who(self)


print("C.method")


class D(C):


def __init__(self):


super().method()


super(__class__, self).method()


super(C, self).method() # calling C's parent's method


super(B, self).method() # calling B's parent's method


super(B, C()).method() # calling B's parent's method with instance of C


d = D()


print("\nInstance of D:")


who(d)


4542787992 


C.method


4542787992 


C.method


4542787992 


B.method


4542787992 


A.method


4542788048 


A.method


Instance of D:


4542787992

当然我们也可以在外部使用 super() 方法,只是不能再用缺省参数的形式,因为在外部的命名空间中不再存在 __class__ self:

 super(D, d).method() # calling D's parent's method with instance d

4542787992 


C.method

  上面的例子可以用下图来描述:

+----------+
| A |
+----------+
| method() <---------------+ super(B,self)
+----------+  |
    |
+----------+  +----------+
| B |  | D |
+----------+ super(C,self) +----------+
| method() <---------------+ method() |
+----------+  +----------+
    |
+----------+  |
| C |  |
+----------+  | super(D,self)
| method() <---------------+
+----------+

 可以认为 super() 方法通过向父类方向回溯给我们找到了变量搜寻的起点,但是这个回溯的顺序是如何确定的呢?上面的例子中继承关系是 object->A->B->C->D 的顺序,如果是比较复杂的继承关系呢?

 

 class A(object):

pass


class B(A):


def method(self):


print("B's method")


class C(A):


def method(self):


print("C's method")


class D(B, C):


def __init__(self):


super().method()


class E(C, B):


def __init__(self):


super().method()


d = D()


e = E()


B's method


C's method

Python 中提供了一个类方法 mro() 可以指定搜寻的顺序,mro 是Method Resolution Order 的缩写,它是类方法而不是实例方法,可以通过重载 mro() 方法改变继承中的方法解析顺序,但这需要在元类中完成,在这里只看一下其结果:

 D.mro()

[__main__.D, __main__.B, __main__.C, __main__.A, object]


E.mro()


[__main__.E, __main__.C, __main__.B, __main__.A, object]


super() 方法就是沿着 mro() 给出的顺序向上寻找起点的:


super(D, d).method()


super(E, e).method()


B's method


C's method


super(C, e).method()


super(B, d).method()


B's method


C's method
Python 相关文章推荐
Python中的startswith和endswith函数使用实例
Aug 25 Python
python基于Tkinter库实现简单文本编辑器实例
May 05 Python
利用python打印出菱形、三角形以及矩形的方法实例
Aug 08 Python
Python实现求两个csv文件交集的方法
Sep 06 Python
flask中主动抛出异常及统一异常处理代码示例
Jan 18 Python
python构建深度神经网络(DNN)
Mar 10 Python
Python中一行和多行import模块问题
Apr 01 Python
PyQt5每天必学之日历控件QCalendarWidget
Apr 19 Python
在PyCharm下打包*.py程序成.exe的方法
Nov 29 Python
Python实现计算字符串中出现次数最多的字符示例
Jan 21 Python
详解Python中打乱列表顺序random.shuffle()的使用方法
Nov 11 Python
Python爬虫基于lxml解决数据编码乱码问题
Jul 31 Python
Python 迭代器工具包【推荐】
May 06 #Python
Python中内建函数的简单用法说明
May 05 #Python
Python使用Paramiko模块编写脚本进行远程服务器操作
May 05 #Python
Python环境下搭建属于自己的pip源的教程
May 05 #Python
使用Python判断质数(素数)的简单方法讲解
May 05 #Python
Python编程中归并排序算法的实现步骤详解
May 04 #Python
Python手机号码归属地查询代码
May 04 #Python
You might like
php 截取字符串并以零补齐str_pad() 函数
2011/05/07 PHP
浅谈php安全性需要注意的几点事项
2014/07/17 PHP
PHP配置把错误日志以邮件方式发送方法(Windows系统)
2015/06/23 PHP
CI框架实现框架前后端分离的方法详解
2016/12/30 PHP
php安装扩展mysqli的实现步骤及报错解决办法
2017/09/23 PHP
php精度计算的问题解析
2019/06/21 PHP
php实现 master-worker 守护多进程模式的实例代码
2019/07/20 PHP
php传值和传引用的区别点总结
2019/11/19 PHP
JavaScript 判断浏览器类型及版本
2009/02/21 Javascript
由JavaScript技术实现的web小游戏(不含网游)
2010/06/12 Javascript
javaScript对文字按照拼音排序实现代码
2013/12/27 Javascript
关于img的href和src取变量及赋值的方法
2014/04/28 Javascript
深入探寻javascript定时器
2015/01/02 Javascript
Javascript通过overflow控制列表闭合与展开的方法
2015/05/15 Javascript
jQuery往返城市和日期查询实例讲解
2015/10/09 Javascript
移动开发之自适应手机屏幕宽度
2016/11/23 Javascript
遍历json获得数据的几种方法小结
2017/01/21 Javascript
vue中本地静态图片路径写法
2018/03/06 Javascript
基于Vue 2.0 监听文本框内容变化及ref的使用说明介绍
2018/08/24 Javascript
vue组件暴露和.js文件暴露接口操作
2020/08/11 Javascript
python实现发送邮件功能
2017/07/22 Python
Python使用回溯法子集树模板解决爬楼梯问题示例
2017/09/08 Python
numpy 计算两个数组重复程度的方法
2018/11/07 Python
对Python模块导入时全局变量__all__的作用详解
2019/01/11 Python
详解Django定时任务模块设计与实践
2019/07/24 Python
python opencv将图片转为灰度图的方法示例
2019/07/31 Python
Python各种扩展名区别点整理
2020/02/27 Python
Django 用户登陆访问限制实例 @login_required
2020/05/13 Python
10 套华丽的CSS3 按钮小结
2012/10/03 HTML / CSS
巴西最大的体育用品商城:Netshoes巴西
2016/11/29 全球购物
购买英国原创艺术:Art Gallery
2018/08/25 全球购物
Chinti & Parker官网:奢华羊绒女装和创新针织设计
2021/01/01 全球购物
总经理岗位职责描述
2014/02/08 职场文书
公司文体活动总结
2015/05/07 职场文书
试了下Golang实现try catch的方法
2021/07/01 Golang
Redis高可用集群redis-cluster详解
2022/03/20 Redis