【Python源码剖析】对象模型概述
Python 是一门 面向对象 语言,实现了一个完整的面向对象体系,简洁而优雅。
与其他面向对象编程语言相比, Python 有自己独特的一面。 这让很多开发人员在学习 Python 时,多少有些无所适从。 那么,Python 对象模型都有哪些特色呢?
一切皆对象
首先,在 Python 世界, 基本类型也是对象 ,与通常意义的“对象”形成一个有机统一。 换句话讲, Python 不再区别对待基本类型和对象,所有基本类型内部均由对象实现。 一个整数是一个对象,一个字符串也是一个对象:
>>> a = 1
>>> b = 'abc'
其次, Python 中的 类型也是一种对象 ,称为 类型对象 。 整数类型是一个对象,字符串类型是一个对象,程序中通过 class 关键字定义的类也是一个对象。
举个例子,整数类型在 Python 内部是一个对象,称为 类型对象 :
>>> int
<class 'int'>
通过整数类型 实例化 可以得到一个整数对象,称为 实例对象 :
>>> int('1024')
1024
面向对象理论中的“ 类 ”和“ 对象 ”这两个基本概念,在 Python 内部都是通过对象实现的,这是 Python 最大的特点。

类型、对象体系
a 是一个整数对象( 实例对象 ),其类型是整数类型( 类型对象 ):
>>> a = 1
>>> type(a)
<class 'int'>
>>> isinstance(a, int)
True
那么整数类型的类型又是什么呢?
>>> type(int)
<class 'type'>
可以看到,整数类型的类型还是一种类型,即 类型的类型 。 只是这个类型比较特殊,它的实例对象还是类型对象。
Python 中还有一个特殊类型 object ,所有其他类型均继承于 object ,换句话讲 object 是所有类型的基类:
>>> issubclass(int, object)
True
综合以上关系,得到以下关系图:

内置类型已经搞清楚了,自定义类型及对象关系又如何呢?定义一个简单的类来实验:
class Dog(object):
def yelp(self):
print('woof')
创建一个 Dog 实例,毫无疑问,其类型是 Dog :
>>> dog = Dog()
>>> dog.yelp()
woof
>>> type(dog)
<class '__main__.Dog'>
Dog 类的类型自然也是 type ,其基类是 object (就算不显式继承也是如此):
>>> type(Dog)
<class 'type'>
>>> issubclass(Dog, object)
True

自定义子类及实例对象在图中又处于什么位置?定义一个猎犬类进行实验:
class Sleuth(Dog):
def hunt(self):
pass
可以看到, 猎犬对象( sleuth )是猎犬类( Sleuth )的实例, Sleuth 的类型同样是 type :
>>> sleuth = Sleuth()
>>> sleuth.hunt()
>>> type(sleuth)
<class '__main__.Sleuth'>
>>> type(Sleuth)
<class 'type'>
同时, Sleuth 类继承自 Dog 类,是 Dog 的子类,当然也是 object 的子类:
>>> issubclass(Sleuth, Dog)
True
>>> issubclass(Sleuth, object)
True

现在不可避免需要讨论 type 以及 object 这两个特殊的类型。
理论上, object 是所有类型的 基类 ,本质上是一种类型,因此其类型必然是 type 。 而 type 是所有类型的类型,本质上也是一种类型,因此其类型必须是它自己!
>>> type(object)
<class 'type'>
>>> type(object) is type
True
>>> type(type)
<class 'type'>
>>> type(type) is type
True
另外,由于 object 是所有类型的 基类 ,理论上也是 type 的基类( __base__ 属性):
>>> issubclass(type, object)
True
>>> type.__base__
<class 'object'>
但是 object 自身便不能有基类了。为什么呢? 对于存在继承关系的类,成员属性和成员方法查找需要回溯继承链,不断查找基类。 因此,继承链必须有一个终点,不然就死循环了。

这就完整了!
可以看到,所有类型的基类收敛于 object ,所有类型的类型都是 type ,包括它自己! 这就是 Python 类型、对象体系全图,设计简洁、优雅、严谨。
该图将成为后续阅读源码、探索 Python 对象模型的有力工具,像地图一样指明方向。 图中所有实体在 Python 内部均以对象形式存在,至于对象到底长啥样,相互关系如何描述,这些问题先按下不表,后续一起到源码中探寻答案。
变量只是名字
先看一个例子,定义一个变量 a ,并通过 id 内建函数取出其“地址”:
>>> a = 1
>>> id(a)
4302704784
定义另一个变量 b ,以 a 赋值,并取出 b 的“地址”:
>>> b = a
>>> id(b)
4302704784
惊奇地看到, a 和 b 这两个变量的地址居然是相同的!这不合常理呀!
对于大多数语言( C 语言为例),定义变量 a 即为其分配内存并存储变量值:

变量 b 内存空间与 a 独立,赋值时进行拷贝:

在 Python 中,一切皆对象,整数也是如此, 变量只是一个与对象关联的名字 :

而变量赋值,只是将当前对象与另一个名字进行关联,背后的对象是同一个:

因此,在 Python 内部,变量只是一个名字,保存指向实际对象的指针,进而与其绑定。 变量赋值只拷贝指针,并不拷贝指针背后的对象。
可变对象 与 不可变对象
定义一个整数变量:
>>> a = 1
>>> id(a)
4302704784
然后,对其自增 1 :
>>> a += 1
>>> a
2
>>> id(a)
4302704816
数值符合预期,但是对象变了!初学者一脸懵逼,这是什么鬼?
一切要从 可变对象 和 不可变对象 说起。 可变对象 在对象创建后,其值可以进行修改; 而 不可变对象 在对象创建后的整个生命周期,其值都不可修改。
在 Python 中,整数类型是不可变类型, 整数对象是不可变对象。 修改整数对象时, Python 将以新数值创建一个新对象,变量名与新对象进行绑定; 旧对象如无其他引用,将被释放。

每次修改整数对象都要创建新对象、回收旧对象,效率不是很低吗? 确实是。 后续章节将从源码角度来解答: Python 如何通过 小整数池 等手段进行优化。
可变对象是指创建后可以修改的对象,典型的例子是 列表 ( list ):
>>> l = [1, 2]
>>> l
[1, 2]
>>> id(l)
4385900424
往列表里头追加数据,发现列表对象还是原来那个,只不过多了一个元素了:
>>> l.append(3)
>>> l
[1, 2, 3]
>>> id(l)
4385900424
实际上,列表对象内部维护了一个 动态数组 ,存储元素对象的指针:

列表对象增减元素,需要修改该数组。例如,追加元素 3 :

定长对象 与 变长对象
Python 一个对象多大呢?相同类型对象大小是否相同呢? 想回答类似的问题,需要考察影响对象大小的因素。
标准库 sys 模块提供了一个查看对象大小的函数 getsizeof :
>>> import sys
>>> sys.getsizeof(1)
28
先观察整数对象:
>>> sys.getsizeof(1)
28
>>> sys.getsizeof(100000000000000000)
32
>>> sys.getsizeof(100000000000000000000000000000000000000000000)
44
可见整数对象的大小跟其数值有关,像这样 大小不固定 的对象称为 变长对象 。
我们知道,位数固定的整数能够表示的数值范围是有限的,可能导致 溢出 。 Python 为解决这个问题,采用类似 C++ 中 大整数类 的思路实现整数对象 —— 串联多个普通 32 位整数,以便支持更大的数值范围。 至于需要多少个 32 位整数,则视具体数值而定,数值不大的一个足矣,避免浪费。

这样一来,整数对象需要在头部额外存储一些信息,记录对象用了多少个 32 位整数。 这就是变长对象典型的结构,先有个大概印象即可,后续讲解整数对象源码时再展开。
接着观察字符串对象:
>>> sys.getsizeof('a')
50
>>> sys.getsizeof('abc')
52

字符串对象也是变长对象,这个行为非常好理解,毕竟字符串长度不尽相同嘛。 此外,注意到字符串对象大小比字符串本身大,因为对象同样需要维护一些额外的信息。 至于具体需要维护哪些信息,同样留到源码剖析环节中详细介绍。
那么,有啥对象是定长的呢?—— 浮点数对象 float :
>>> sys.getsizeof(1.)
24
>>> sys.getsizeof(1000000000000000000000000000000000.)
24
浮点数背后是由一个 double 实现,就算表示很大的数,浮点数对象的大小也不变。

为啥 64 位的 double 可以表示这么大的范围呢?答案是:牺牲了精度。
>>> int(1000000000000000000000000000000000.)
999999999999999945575230987042816
由于浮点数存储位数是固定的,它能表示的数值范围也是有限的,超出便会抛锚:
>>> 10. ** 1000
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
OverflowError: (34, 'Result too large')
更多章节
洞悉 Python 虚拟机运行机制,探索高效程序设计之道!
到底如何才能提升我的 Python 开发水平,向更高一级的岗位迈进? 如果你有这些问题或者疑惑,请订阅我们的专栏,阅读更多章节:
附录
更多 Python 技术文章请访问:小菜学Python,转至 原文 可获得最佳阅读体验。
订阅更新,获取更多学习资料,请关注 小菜学编程 :

【Python源码剖析】对象模型概述的更多相关文章
- Python源码剖析|百度网盘免费下载|Python新手入门|Python新手学习资料
百度网盘免费下载:Python源码剖析|新手免费领取下载 提取码:g78z 目录 · · · · · · 第0章 Python源码剖析——编译Python0.1 Python总体架构0.2 Pyth ...
- 老李推荐:第1章2节《MonkeyRunner源码剖析》概述:边界
老李推荐:第1章2节<MonkeyRunner源码剖析>概述:边界 边界 怎么样才算分析清楚一个事物的原理是什么呢?就以前面提到的<LINUX内核源代码情景分析>为例子,分 ...
- 老李推荐: 第1章1节《MonkeyRunner源码剖析》概述:前言
老李推荐: 第1章1节<MonkeyRunner源码剖析>概述:前言 前言 相信大家做过安卓移动平台UI自动化开发的必然会用过,至少听过MonkeyRunner这个名字.MonkeyR ...
- 老李推荐:第1章3节《MonkeyRunner源码剖析》概述:架构
老李推荐:第1章3节<MonkeyRunner源码剖析>概述:架构 原理架构 MonkeyRunner使用起来非常的简单,只需要导入以下几个类基本上就能满足测试脚本编写的需求,比如: ...
- Python 源码剖析(一)【python对象】
处于研究python内存释放问题,在阅读部分python源码,顺便记录下所得.(基于<python源码剖析>(v2.4.1)与 python源码(v2.7.6)) 先列下总结: ...
- python源码剖析学习记录-01
学习<Python源码剖析-深度探索动态语言核心技术>教程 Python总体架构,运行流程 File Group: 1.Core Modules 内部模块,例如:imp ...
- Python 源码剖析 目录
Python 源码剖析 作者: 陈儒 阅读者:春生 版本:python2.5 版本 本博客园的博客记录我会适当改成Python3版本 阅读 Python 源码剖析 对读者知识储备 1.C语言基础知识, ...
- Python源码剖析——02虚拟机
<Python源码剖析>笔记 第七章:编译结果 1.大概过程 运行一个Python程序会经历以下几个步骤: 由解释器对源文件(.py)进行编译,得到字节码(.pyc文件) 然后由虚拟机按照 ...
- Python源码剖析——01内建对象
<Python源码剖析>笔记 第一章:对象初识 对象是Python中的核心概念,面向对象中的"类"和"对象"在Python中的概念都为对象,具体分为 ...
随机推荐
- java学习笔记之原型模式及深浅拷贝
一.原型模式的基本介绍 在聊原型模式之前,我们来思考一个小问题,传统的方式我们是如何克隆对象呢? 那我们以多利羊(Sheep)为例,来说明上述这个问题,具体代码见下面: 多利羊(Sheep) publ ...
- linux下安装gmp遇到 configure:error:no usable m4 in$path or /user/5bin解决方案
安装过程中遇到如下报错: 上面的报错是因为你没有安装m4,安装m4就可以了:以下两种命令人选其一: #yum install m4 或 #apt-get install m4 ps:如果遇到权限问题就 ...
- ql的python学习之路-day14
前言:本节主要学习时间模块time.datetime python中的几种时间表示:1)时间戳 2)格式化的字符串时间 3)struct_time元组格式的时间 time.datetime模块源码: ...
- Sentinel源码解析一(流程总览)
引言 Sentinel作为ali开源的一款轻量级流控框架,主要以流量为切入点,从流量控制.熔断降级.系统负载保护等多个维度来帮助用户保护服务的稳定性.相比于Hystrix,Sentinel的设计更加简 ...
- 「雕爷学编程」Arduino动手做(24)——水位传感器模块
37款传感器与模块的提法,在网络上广泛流传,其实Arduino能够兼容的传感器模块肯定是不止37种的.鉴于本人手头积累了一些传感器和模块,依照实践出真知(一定要动手做)的理念,以学习和交流为目的,这里 ...
- 3.9 Go Slice切片
3.9 Go Slice切片 Go语言切片(Slice) 切片是可动态变化的序列,是对数组的引用,引用类型,遵循引用传递的机制 slice类型写作[ ]T,T是slice元素类型,var s1 []i ...
- 3.5 Go布尔型
1.Go布尔型 一个布尔类型的值只有两种:true 和 false. if 和 for 语句的条件部分都是布尔类型的值,并且==和<等比较操作也会产生布尔型的值. package main im ...
- java web基础
WEB基础 C/S:即服务器-客服机(Client-Server)结构.C/S结构通常采用两层结构,服务器负责数据的管理,客户机负责完成与用户的交互任务.客户通过局域网与服务器相连,接受用户的请求,并 ...
- django 中CBV和FBV 路由写法的区别
使用视图函数时, FBV: django完成URL解析之后,会直接把request对象以及URL解析器捕获的参数(比如re_path中正则表达捕获的位置参数或关键字参数)丢给视图函数 CBV: 这些参 ...
- 26-13 order by排序
表中数据是集合,集合是没有顺序的.order by返回的数据是有顺序的,故此我们把order by以后返回的数据集合叫“游标”. --------------------------通过order b ...