09 对象与类型:如何理解在 Python 中“一切皆对象”的概念?
Diego38 17 1

引言

Python 程序中处理的各种数据,不管以如何复杂的形式呈现,都是由不同的对象组成的,甚至对于我们所编写的 Python 代码本身而言,也是由对象来表示的,这便是 Python 中 “一切皆对象” 的概念。那么 “一切皆对象” 是如何体现在更加细节的语言层面的呢?在本小节,我们就来梳理一下和和这个概念相关的知识点。

如何具体的定义对象?

从数据模型的角度进行来看,对象是 Python 中对数据的抽象形式。在 Python 中,对象可以看作是具备以下特性的实体:

  • 任何对象都具有标识值;
  • 任何对象都具有类型;
  • 任何对象都具有值。

下面我们来一一看下这三点。

任何对象都具有标识值

标识值其实就是对象在内存中的地址,在 Python 中我们可以使用内置函数 id() 来查看某个对象的标识值,id() 函数的返回值为该对象的内存地址(需要特别注意的是,这里我们只针对 CPython 的实现。比如在 PyPy 的实现中,由于垃圾回收机制的不同,id() 返回的对象标识值并不是内存地址)。

那么我们是否可以说对象的标识值是唯一且不变的?答案是否定的,或者是不准确的。在对象的生命周期(对象的生命周期是指对象从创建到被回收的整个过程)中该标识值具有唯一性、不变性,而并非在整个程序生命周期内。但如果对象与对象的生命周期不重叠(比如一个对象被回收后,另外一个对象才被创建)的话,它们的标识值则具有相同的可能性。我们通过下面的代码便可以看到该现象:

>>> class A:
...     pass
... 
>>> a = A()
>>> b = A()
# a、b的生命周期重叠,具有不同的标识值
>>> print(id(a))
140294828940256
>>> print(id(b))
140294823188848
# 进行多次试验,可以看到下面第一个A()对象和最后一个A()对象生命周期未重叠,共享相同的标识值是可能的
>>> print(id(A()))
140294823189136
>>> print(id(A()))
140294823189232
# 省略多次试验过程
>>> print(id(A()))
140294823189136   

运算符 is 和 is not 可以用来在对象间进行标识值的比较。

>>> a = 1
>>> b = 2
>>> c = 1
>>> a is b
False
>>> a is not b
True
>>> a is c  # 为什么会返回 True?
True
>>> d = [1, 2]
>>> e = [1, 2]
>>> d is e
False

相信有一部分读者会对 a is c 返回 True 的结果存在疑问,按道理讲 a 和 c 虽然值相同,但应是两个不同的对象,大家是否还记得我们在内存分配与垃圾回收提到的小整数对象缓冲池?为了优化速度,Python 使用小整数对象池来避免小整数对象内存的频繁申请和回收,在小整数范围 [-5, 256] 内,相同的整数使用的是同一个对象。

类似的缓存优化手段也应用在了短字符串对象上,比如 a = 'hello', b = 'hello', a is b ,结果可想而知,a is b 返回 True ,即在内存中其实只有一个字符串 'hello' 。这样的优化手段并不会对我们所编写的代码产生影响,因为字符串本身是不可变对象,不会产生共享引用可变对象的副作用问题(即对于一个变量所引用对象的修改会影响到另外一个引用该对象的变量,比如 a = b = [1,2,3], a.append(4) ,b 也会受到这次修改的影响 )。

任何对象都具有类型

对象支持的操作,即对象所拥有的方法以及属性是由对象的类型决定的。我们可以使用内置函数 type(obj) 来查看对象的类型,通常与 obj.class 所返回的结果相同。对象的类型也是一种对象(类型对象),同样,类型对象也具有相应的类型。关于类型对象的详细内容会在下文中进行说明。通过下面的代码我们可以更加直观的看到对象的类型:

>>> my_name = "hello world"
>>> type(my_name)  # 对象的类型(类型对象)
<class 'str'>
>>> type(type(my_name))  # 对象的类型也是一种对象,即类型对象,类型对象也具有类型
<class 'type'> 
>>> my_name.__class__
<class 'str'>
>>> my_name.__class__.__class__
<class 'type'>

任何对象都具有值

对象的值即对象表示的数据。

除以上提到的这三点之外,一部分对象(类对象)还具有一个至多个基类,可以通过 bases 查看,其返回值为由类对象的基类所组成的元组。

对于 Python 中的出现的所有数据,包括整型、字符串、列表、字典、函数、类等,我们都可以用以上三种特性来对它们进行衡量,即这些数据在 Python 都是以对象的形式呈现出来的。但在这个过程中我们仍然会产生一些疑问,比如:

  • 类是一种对象,那类的类型,类的类型的类型…,依次递进,类型关系最终的形式是什么?
  • 新式类中的基类 object ,它的类型又是什么?
  • 当编写 a=1 或 a=[1, 2, 3] 时并没有像平时编写 OOP 代码时显式实例化 a 的过程,为什么可以创建整型、列表对象等。

要解答这些疑问,我们需要更深入的理清其中的逻辑。

Python 对象机制中两种重要关系:实例化关系和继承关系

如下图所示,类 B 和类 A 之间为子类和基类的关系,即继承关系;类 B 和实例 b 为类型和实例的关系,即实例化关系。可能一部分读者会对“类型和实例的关系”这个表达存在疑问,其实在新式类中,类和类型已经合并,类即是类型,所以这里“类型和实例的关系” 与“类和实例的关系”表达的含义是一致的(对于新式类的变化详细内容会在后面的章节中展开)。

image

在判断实例化关系中,可以使用 isinstance(obj, classinfo) ,若 obj 为 classinfo 的实例或classinfo的直接、间接、虚拟子类的实例,则返回 True 。若 classinfo 是多个类组成的元组,当 obj 为其中任意一个的实例则返回 True 。

其中直接、间接子类的关系可以通过下图说明,B 类继承自 A 类,A 类继承自 object,则 B 类为 A 类的直接子类,B类为 object 的间接子类。

image

另外,虚拟子类是抽象基类中的内容,在这里我们只需要简单了解其基本概念即可(关于抽象基类的详细内容会在后面的章节中展开)。抽象基类( Abstract Base Class )提供了一种定义接口的方式,其定义子类共有的一些抽象方法,但并不需要具体实现。虚拟子类是一种抽象基类的使用方式,通过调用相关的注册方法将其他类“注册”到抽象基类,并不继承自基类。

在判断继承关系中,可以使用内置函数 issubclass(class, classinfo),和 isinstance(obj, classinfo) 类似,若 class 为 classinfo 的直接、间接、虚拟子类,则返回 True 。若 classinfo 是多个类组成的元组,class 为其中任意一个的子类,则返回 True(想要深入了解 isinstance() 和 issubclass() 的同学也可以查阅 PEP 3119 中相关内容,我们也会在抽象基类的章节中介绍 PEP 3119 )。

Python 对象机制中两个重要的对象:type 和 object

在 Python 的对象机制(或称对象系统)中,type 和 object 是实例化关系和继承关系中的非常重要的两个内置对象。type 在前文中作为内置函数用来查看对象的类型,但它另外一个重要角色则是实例化关系中最顶层的对象。object 则是在继承关系中最顶层的基类,在新式类中所有的类都直接或间接的继承自 object。

type 和 object 的关系:

>>> type
<class 'type'>
>>> object
<class 'object'>
>>> type(type)  # type的类型是其自身
<class 'type'>
>>> type(object)  # object的类型也是type
<class 'type'>
>>> type.__bases__  # type继承自object
(<class 'object'>,)
>>> object.__bases__  # object为继承关系的顶层对象
()

我们使用下图来更清晰的说明上述代码中两者之间的关系,实线表示实例化关系,虚线表示继承关系。可以看到 type 和 object 是一种相互依赖的关系:

image

  • type 是其自身的实例,即 type 的类型还是 type。这也回答了类型关系的最终形式这个问题;
  • type 是 object 的子类;
  • object 是 type 的实例,即基类 object 的类型是 type;
  • object 没有父类,是继承关系中的顶层对象。

    Python中的对象可以分为:类型对象和非类型对象

    首先,我们需要再次明确一个在前文中提到过的知识点,即在新式类中,类和类型的概念合并,类即是类型。我们通过 class 语句声明的自定义类和内置的 int、list、dict 等,都是类型(当然也是类)。

类型对象具有的特性:

  • 它们的类型是 type;
  • 它们可以被实例化,类型对象通过实例化创建的对象,这些对象的类型便是该类型对象;
  • 它们可以被继承,即可以通过继承关系拥有子类。

非类型对象相对更好理解一些,它们是更加具体化的对象,比如整型 1、列表 [1, 2, 3] 等,它们不能拥有子类,更不能被实例化。我们可以使用 isinstance(obj, type) 是否返回为 True,来检验 obj 是否为类型对象(通过后面课程中元类的学习,也会加深我们对于 type 以及 isinstance(obj, type) 的理解)。

>>> list  # 内置类型对象
<class 'list'>
>>> type(list)
<class 'type'>
>>> list.__bases__
(<class 'object'>,)
>>> a = 1
>>> type(a)
<class 'int'>
>>> class B:  # 自定义类型对象
...     pass
... 
>>> b = B()  # 实例对象(非类型对象)
>>> type(b)
<class '__main__.B'>
>>> type(B)
<class 'type'>
>>> B.__bases__
(<class 'object'>,)

结合内置类型对象、自定义类型对象、继承关系、实例化关系,“一切皆对象” 的核心概念便可以使用下图进行说明:

image

值得一提的是,当编写 a=1 或 a=[1, 2, 3] 这样的内置类型对象时,并没有像平时编写 OOP 代码那样显式实例化 a 的过程,这是由于 Python 语法在内置类型创建新对象时发挥着作用,数值文本创建 int 实例,方括号创建 list 实例等。

Python 对象知识扩展

在上面的描述中,我们梳理了 Python 中与 “一切皆对象” 这个概念相关的知识点及其脉络,接下来我们结合上面提到的 Python 中对象相关的知识点进行更进一步的扩展。

对象的等价性与同一性:==is

在 Python 中,所有对象都支持等价性和同一性比较。等价性比较指的是使用 == 运算符测试两个对象,Python 会递归的比较 == 左右两侧的对象,这意味着如果存在嵌套对象的话,Python 会对嵌套对象的数据结构进行遍历,由左至右逐个逐层的比较相应的元素的值,直至得出比较结果。

而同一性比较指的是使用 is 表达式测试两个对象,Python 会比较两个对象是否为同一个对象,即我们上面所说的在对象间进行标识值 id() 的比较。

>>> a = [1, ['a', 3, [{1:2}]]]
>>> b = [1, ['a', 3, [{1:2}]]]
>>> a == b
True
>>> a is b
False

None 对象

None 在 Python 中是很常见的对象,注意,它是一个占用内存的对象,并不是 undefined 之类。本质上来说,None 是单例对象(单例对象是指该对象的类必须保证只有一个实例存在,关于单例模式的的详细内容会在后面的章节中展开),在 Python 中未显式指明返回值的函数都将返回 None。

PEP8中提到,“Comparisons to singletons like None should always be done with is or is not, never the equality operators”,即与类似 None 这样的单例对象比较时,应始终使用 is 或者 is not,而不是等式运算符,这是 PEP8 为开发者提供的 “最佳实践”。

我们从两个角度出发进行一定的分析:

首先,我们已经了解了 is 表达式和 == 运算符的区别。在这个前提下先看一段代码:

>>> is_none = None
>>> is_none is None
True
>>> is_none == None
True

上面的这段代码,我们可以看出这两种比较的方式都可以得到预期的结果。继续看下面的代码:

>>> class OverloadEqual:
...     def __eq__(self, sth):
...         """运算符重载"""
...         return True
...     
>>> not_none = OverloadEqual()
>>> not_none is None
False
>>> not_none == None  # 调用 not_none.__eq__(self, sth) 方法
True

我们在 OverloadEqual 这个自定义类中,对运算符 == 调用的特殊方法 eq 进行了重载(关于运算符重载的详细内容会在后面的章节中展开),可以看出 not_none == None 的返回值依赖于 OverloadEqual 类中对 eq 的具体实现,这样的运算符重载使得输出结果未必符合预期结果。

其次,我们对两种方式的执行效率进行简单比较:

>>> import timeit
>>> timeit.timeit("None is None", number=1000000000)
29.025038077001227
>>> timeit.timeit("None == None", number=1000000000)
44.52065715400022

可以看到前者在执行效率上明显高于后者(输出结果绝对值的大小依赖硬件环境),这是因为 is 只比较对象标识值,而 == 则需要完成更多工作。

在实际编码中我们需要注意两点,第一,区分 if x 和 if x is not None ,这在工作中是一个较容易忽视的点。当你想要测试某对象是否被赋值为非 None 的对象时,一定要使用后者,因为当 x 为空字符串等其他非 None 但逻辑值检测为 False 的对象时,它将无法通过前者的检查。第二,使用 if x is not None 而不是 if not x is None ,虽然两个语句在功能上相同,但前者的可读性更高。

type(obj) 和 obj.class 的区别是什么?

type() 函数会返回传入对象的类型,该返回值为 type 对象(即该返回值的类型为 type ),obj.class 则返回类实例所属的类。type(obj) 和 obj.class 的主要区别在于新式( new-style )类和经典( classic )类的表现上(关于经典类和新式类的详细对比会在后面的章节中展开)。在新式类中,通常两者的返回结果相同;在经典类中,前者返回始终为 instance,而后者则会返回用户自定义的类对象。

# 由于 Python3 中均为新式类,所以我们在 Python2.7 下进行这一小部分的代码展示
>>> class Classic:  # 经典类
...     pass
... 
>>> class NewStyle(object):  # 新式类
...     pass
... 
>>> classic = Classic()
>>> type(classic)  # 两种方式在经典类上的表现
<type 'instance'> 
>>> classic.__class__
<class __main__.Classic at 0x7f44e7f4d328>
>>> new_style = NewStyle()  # 两种方式在新式类上的表现
>>> type(new_style)
<class '__main__.NewStyle'>
>>> new_style.__class__
<class '__main__.NewStyle'>
>>> a = 'hello world'  # 内置类型为新式类
>>> type(a)
<type 'str'>
>>> a.__class__
<type 'str'>

我们在上面说到 “在新式类中,通常两者的返回结果相同“,那么存在两者不同的情况吗?我们来看下面的代码:

>>> class A:
...     pass
... 
>>> class B:
...     __class__ = A
...     
>>> b = B()
>>> type(b)  # 在新式类下,两种方式的返回结果存在不相同的情况
<class '__main__.B'>
>>> b.__class__
<class '__main__.A'>

上面的代码展示了在重载类B的 class 后,两种方式的返回结果产生了差异,有些代码这样做的目的是隐藏对象的类型。在新式类中,大部分开发者持有 type(obj) 为查看对象类型的最佳实践的观点,当然,也有一部分开发者持有相反意见。那么,我们能否改变对象的类型呢?在上述代码的基础上,我们很容易改变其形式来实现:

>>> class A:
...     pass
... 
>>> class B:
...     pass
... 
>>> b = B()
>>> b.__class__ = A
>>> type(b)  # b的类型随之改变
<class '__main__.A'>
>>> b.__class__
<class '__main__.A'>

对于上述代码所展示的场景,我们更多的是加深对 type(obj) 和 class 的认识,并不建议大家在实际工作中过多使用这样的方式,这会导致代码的可维护性降低。

type(obj)、obj.class、isinstance(obj, classinfo) 在检查对象类型时的优劣 在理解了 `isinstance(obj, classinfo) 的工作方式后,我们通过下表对三种检查对象类型的方式进行对比:

... 在调用时是否需要除 obj 以外的类型参数 是否考虑继承或虚拟子类等关系 适合的场景
type(obj) 不需要 在新式类下,当 obj 的类型或类型范围未知时可使用,比如代码调试时
obj.class 不需要 在经典类下,当 obj 的类型或类型范围未知时可使用,比如代码调试时
isinstance(obj, classinfo) 需要 根据已知类检查 obj 类型时的最佳选择,比如函数的入参检查,变量的类型检查等等
最后,还需要强调的是,Python 作为动态语言更多提倡的是 ”鸭子类型“( duck typing ),应尽量避免显式类型检查,按照预期的类型对 obj 进行操作,处理由此产生的异常即可。但当你不得不对某个对象的类型进行检查时,请选择最优的方法。

总结

本小节我们对 Python 中一切皆对象的概念进行了讨论和分析,不管是整型、列表、字典,亦或是函数、类等,都是对象,都具有对象的三种特性,即标识值、类型、值。同时,Python 中的对象可以分为类型对象和非类型对象,其中类型对象可以被继承,被实例化。除此之外,type 和 object 分别为类型关系和继承关系中的顶层对象,它们之间互相依赖,object 的类型是 type,type 继承自 object 。

预览图
评论区

索引目录