文章目录

  • Python的数据模型
  • 对象的标识
  • 对象的类型
  • 对象的值
  • is 关键字
  • 判断一个变量是否指向函数
  • types模块中定义的类型



本文是作者对Python官方文档的理解和试验结果,不保证技术准确性。

Python的数据模型

Python数据模型指的是数据如何在Python中存储,标识和使用的方式。对程序员而言,最需要关心的是理解数据标识和正确地使用数据。

在Python中,几乎所有的东西都是“对象”。Python对象是对数据的抽象。数据除了我们常见的数值,字典,列表以外,代码,类定义,函数定义,甚至类型本身都是对象 (这个句子的意义现在还不明确,随后会逐渐明晰)。

在Python中,每个对象都有1)一个唯一的标识,2)对象的类型,和3)对象的值。

对象的标识

一旦对象被创建出来,这个唯一的标识就不会发生改变。在CPython实现中,可以使用id()函数返回某个对象的标识,通常认为返回值是对象在内存中的地址。需要指出的是,在实际编程中,我们可以用一个变量名代表对象,但是严格意义上,这个变量名并不是该对象的标识。变量名只是对这个对象的引用。

# 返回对象的标识。
# 这个标识是一个整数,确保在对象的生命期内唯一并且保持不变。
id(object)

# 注意:某个对象被销毁后,在创建另一个新对象时,可能会重用这个标识。
# 所以不可以用标识来确认对象是否依然存在。

下面看几个具体的例子。

# 没错,3是一个对象,具有唯一的标识。
>>> id(3)
9306720

# int类型居然也是一个对象
>>> id(int)
8926144

# 嗯,True也是一个对象
>>> id(True)
8874656

# None也是一个对象
>>> id(None)
8943264

# NotImplemented 也是对象
>>> id(NotImplemented)
8942768

# 内置函数 open 也是一个对象
>>> id(open)
139638727172976

# 模块是一个对象
>>> import sys
>>> id(sys)
139638727138672

# 函数也是一个对象
>>> def foo():
...     pass
...
>>> id(foo)
139638725635968


# 好吧,类定义本身也是一个对象
>>> class foo:
...     pass
...
>>> id(foo)
36497584

从上面的例子中可以看出,Python允许定义同名的类与函数。实际上,一旦下面的代码定义了同名的对象,上面定义的对象就无法再被引用,可能会进入垃圾回收阶段。

对象的类型

对象的类型决定了对象可能的取值范围以及可以在对象上执行的操作。通常情况下,对象一旦被创建,对象的类型也随即确定,无法改变。

Python官网指出,在某些特殊受控的情况下,对象的类型也可以改变。但是并不被认为是一个好主意 — 因为一旦处理不好,会导致Python出现非常诡异的行为。

Python 提供了 type() 函数(其实是一个类),用来返回一个对象的类型。type() 函数的返回值是一个对象,这个对象的类型是type。通常type()的返回值与object.__class__的值相同。

从下面Python官网对type()函数的描述,可以看出type实际上是一个类定义。不过type类型的对象除了保存传入的对象的类型之外,并没有其他任何特别的操作。

# 本文关心这个函数/类
class type(object)

# 暂时不关心这个函数。
class type(name, bases, dict)

下面看几个例子。

# 3是整型类型
# 当执行type(3)时,其实是生成了了一个type类是实例。
>>> type(3)
<class 'int'>

# True是布尔类型
>>> type(True)
<class 'bool'>

# sys是模块类型
>>> type(sys)
<class 'module'>

# open是一个内置函数或方法
>>> type(open)
<class 'builtin_function_or_method'>

# 定义函数func
>>> def func():
...     pass
...
# func是函数类型
>>> type(func)
<class 'function'>

# 定义类,类名是foo
>>> class foo:
...     pass
...
# foo的类型是 type (感觉返回class更好)。
# 这个返回值表示 “类定义”本身的 “类型”
>>> type(foo)
<class 'type'>

# 你以为type是一个内置函数,其实不是它是一个类定义。
>>> type(type)
<class 'type'>

# 生成一个类的实例
>>> myfoo = foo()
# myfoo 是 __main__.foo类的实例
>>> type(myfoo)
<class '__main__.foo'>

# 以后再详细介绍istance函数。
# 现在可以通过这个函数确定某个对象是否是某个class的实例
>>> isinstance(myfoo, foo)
True

对象的值

对象的值是当前对象保存的数据。有些对象的值可以改变,被称为 可变对象(immutable object),有些在创建后不可改变,被称为 不可变对象 (mutable object)。可变对象的常见例子包括字典和列表;不可变对象的常见例子包括数值,字符床和元组类型。

当我们定义一个变量a并将其赋值为3时。实际操作是:创建了一个整数类型的对象,其值为3,将变量a指向这个对象。在这里整数对象3是不可变对象,不能再将这个对象的值改变为其他值。如果将a赋值为其他值(例如5),会创建新的数值对象,然后将a指向新对象。

从下面的示例中可以看出,两次赋值后,变量a指向的两个对象的标识是不同的,说明a指向不同的对象。而a与b的标识是相同的,说明a与b都指向同一个对象。

>>> a = 3
>>> id(a)
9306720

>>> b = 3
>>> id(b)
9306720

>>> a = 5
>>> id(a)
9306784

Python中的不可变对象的含义有些微妙。 以元组为例,元组本身是不可变对象。但是如果元组中的元素(item) 指向了其他可变对象,例如一个字典。尽管不能通过再赋值将元组中的某个元素指向其他的对象,但是如果元素本身指向的是可变对象,在语法上,可以通过元组索引格式找到并改变这个可变对象。看下面的例子会非常清楚。

# 声明两个列表
>>> l1=['a', 'b']
>>> l2=[1, 2]

# 将列表加入元组
>>> t = (l1, l2)
>>> print(t)
(['a', 'b'], [1, 2])

# 试图将元组中的第一个元素指向新的对象
# 引发类型错误:tuple对象不支持元素赋值
>>> t[0] = 'Hello'
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'tuple' object does not support item assignment

# 第一个元素是列表,可以改变这个列表
>>> t[0][0] = 'A'
>>> print(t)
(['A', 'b'], [1, 2])

# 元组中的第一个元素 与 d1 都指向同一个列表对象。
>>> id(d1)
139638726904880

>>> id(t[0])
139638726904880

>>> print(d1)
['A', 'b']

需要特别指出的是,在生成元组时,元组中的元素只是保存了到列表的引用,可以看出id(d1)和id(t[0])的返回值指向同一个对象。当通过元组索引方式改变第一个列表中的元素后,也即等价于改变了d1列表。

通常我们可以使用print()函数打印一个对象的值。下面是print函数的声明

print(*objects, sep=' ', end='\n', file=sys.stdout, flush=False)

print函数在内部会调用str()函数(其实是一个类定义),这样可以将每个对象的字符串表示打印到指定的文件中,默认是标准输出。
需要注意的是,,str()返回的字符串未必一定是对象的值,尽管通常都是这样。

class str(object='')
class str(object=b'', encoding='utf-8', errors='strict')

当调用str(object)时,实际上会执行 object.str()函数(返回对象的字符串表示)。如果这个对象没有定义__str__()函数,则str()函数会尝试使用repr(object)的返回值。

TODO: str() 与 repr()的区别。

is 关键字

is 是Python的关键字,用于构成判断表达式,用于判断两个变量是否指向同一个对象,返回True或者False。注意,并不是比较两个对象是否相等。 可以肯定的说,如果两个变量指向同一个对象,这两个变量一定相等;反之不然 (两个变量指向不同的对象,这两个对象可能相等,也可能不相等)。

is 表达式执行如下的逻辑操作:
a is b ⇐⇒ id(a) == id(b)

>>> a = True
>>> a is True
True
>>> id(a) == id(True)
True



# 尽管列表l1与l2都是空列表,也相等。
# 但是l1与l2并不是同一个列表。
>>> l1 = []
>>> l2 = []
>>> id(l1)
140122781394016
>>> id(l2)
140122781393136
>>> l1 is l2
False

需要注意的是, 由于垃圾回收机制,id 可能会出现重复使用的情况,所以不能使用事先保存的标识号来判断两个对象是否是同一个对象。下面的例子中可以看到id重用的情况,说明Python的垃圾回收机制在快速的回收不再使用的对象。

>>> a = "Hello"
>>> id(a)
140122920036976
>>> a = "Hell"
>>> id(a)
140122792516912
>>> a = "Hel"
>>> id(a)
140122792516464

>>> a = "He"
>>> id(a)
140122792516912 # id重用
>>> a = "ll"
>>> id(a)
140122792516464 # id重用

判断一个变量是否指向函数

>>> def foo():
...     pass
>>>
>>> import types

# 错误的判断方式
# foo实际上是函数定义本身
>>> foo is types.FunctionType
False

# 正确的判断方式
>>> type(foo) is types.FunctionType
True

>>> type(foo)
<class 'function'>

>>> types.FunctionType
<class 'function'>

types模块中定义的类型

https://docs.python.org/3/library/types.html#standard-interpreter-types

>>> pprint.pprint(types.__all__)
['FunctionType',
 'LambdaType',
 'CodeType',
 'MappingProxyType',
 'SimpleNamespace',
 'GeneratorType',
 'CoroutineType',
 'AsyncGeneratorType',
 'MethodType',
 'BuiltinFunctionType',
 'BuiltinMethodType',
 'WrapperDescriptorType',
 'MethodWrapperType',
 'MethodDescriptorType',
 'ClassMethodDescriptorType',
 'ModuleType',
 'TracebackType',
 'FrameType',
 'GetSetDescriptorType',
 'MemberDescriptorType',
 'new_class',
 'resolve_bases',
 'prepare_class',
 'DynamicClassAttribute',
 'coroutine']