吃透Python-第 5 章 对象和类型
在 Python 中,一切皆为对象。为了更好地学习后面的内容,我们先来了解一下对象的本质。
对象和变量
类型和值
引用和绑定
标识值
id 函数
存储空间
赋值语句
del 语句
可变类型
不可变类型
身份运算符(is 运算符和 is not 运算符)
None 和 NoneType 类型
引用计数
浮点型(float 型)的特性和精度
算术转换
用于算术运算的内置函数
按位逻辑运算符(运算符 &、运算符 |、运算符 ^ 和运算符 ~)
位移运算符(运算符 << 和运算符 >>)
1 的补码和 2 的补码
5-1 对象
前面我们使用了各种类型的变量。Python 将一切皆作为对象来处理,所以我们不妨将 Python 中的一切视为对象,而非变量。
什么是对象
前面,本书曾将变量解释为“类似于存储值的箱子”。实际上,这是不正确的。在进入下一章的学习之前,我们有必要准确理解变量的概念。
首先来看以下对变量的基本说明。
要点 变量是对象的引用,它只是关联到对象的一个名字。
这种说法有点让人难以理解。我们通过交互式 shell 来进行确认(例 5-1)。
▶ 注意:打印输出的值受程序运行环境等因素的影响。下文亦然。
把 17 赋给变量 n 后,程序调用了两次 id 函数这一内置函数。id 函数用于返回对象的固有值,即标识值(identity)。
▶ 不同对象的标识值一定不同。
Python 的赋值操作并不是像图 5-1 a 那样对值进行复制。
如图 5-1 b 所示,首先有一个值 17 的 int 型对象。为了引用该对象,程序要使用名称 n 进行绑定(bind)。
图 5-1 对变量进行赋值
这就是整数字面量 17 的标识值和变量 n 的标识值相同的原因。
第 1 章讲过,给变量赋的值,其类型可以与存储在该变量中的值的类型不同。我们通过例 5-2 来确认变量的这种特性。
赋值为字符串后,n 的标识值发生了变化。
如图 5-2 所示,变量 n 的引用对象从 int 型的 5 变为了 str 型的 ‘ABC’。
图 5-2 赋值后引用了新的对象
当然,int 型对象 5 的类型和值都没有发生变化。
要点 在对变量进行赋值操作时,不会复制对象的值,只会引用对象。
下面我们来交换两个值(3-2 节)(例 5-3)。
变量 a 的标识值和变量 b 的标识值进行了交换。如图 5-3 所示,交换的是变量的引用对象而不是变量的值。
图 5-3 赋值后交换引用对象
现在,大家应该慢慢理解了 Python 中“没有变量,只有对象”的含义了吧。
各个对象会占一定的存储空间(storage),也就是内存(memory)。因此,标识值一般用存储空间上的地址表示。
要点 在 Python 中,一切皆为对象。对象占一定存储空间,并拥有标识值、类型和值等属性。
▶ 不同的对象拥有不同的标识值,对象能通过标识值来区分。
id 函数用于查看对象的标识值。第 1 章介绍的 type 函数可以查看对象的类型。
可变类型和不可变类型
前面的例子只进行了赋值操作并打印输出标识值。下面对变量进行运算,并确认变量值的变化情况。首先是整数(例 5-4)。
使用增量赋值运算符 += 对 n 的值进行递增后,n 的标识值发生了变化。
如图 5-4 所示,n 的引用对象从 int 型对象 12 变为 int 型对象 13。
图 5-4 更新整数值(更新引用对象)
数字和字符串是不可变类型,一旦赋值就不可以改变。
有人可能会反驳说变量 n 的值可以改变,但其实并非如此。正因为整数对象 12 的值不可以改变,所以我们才对 n(的引用对象)进行更新,以引用另一个整数对象 13。
▶ immutable 的意思是“不可变的”,下文提到的 mutable 的意思是“可变的”。
字符串也是如此。我们可以通过例 5-5 进行确认。
程序生成了新的字符串,在字符串拼接后,s 的标识值发生了变化(图 5-5)。
图 5-5 更新字符串(更新引用对象)
Python 有两种类型,分别是不可变类型和可变类型。其中,可变类型的值在给定后可以发生改变。
要点 Python 的类型根据值是否可变分为两类。
可变类型:列表、字典、集合等 ※值可以改变。
不可变类型:数字、字符串、元组等 ※值不可以改变。
另外,可变类型对象和不可变类型对象都不能更改类型本身。
身份运算符(is 运算符和 is not 运算符)
第 3 章讲解的比较运算符 == 和 !=,正如其名字表达的含义一样,是判断对象的值是否相等的运算符。因此,即使 x 和 y 是不同的对象,只要它们的值相等或者不相等,相应的运算符判断就能成立。
表 5-1 的身份运算符与比较运算符相似却不同。is 运算符和 is not 运算符用于判断对象本身是否相同,即对象本身是否具有同一性,不用于判断对象的值是否相等。
▶ 具体来说,身份运算符用于判断等号两边对象的标识值是否相等。像 a is b is c 这样连续使用该运算符的效果,与用 and 进行结合的效果相同。
表 5-1 身份运算符
代码清单 5-1 是判断两个变量同一性的示例程序。
程序读取变量 a 的值和变量 b 的值后判断它们是否相同。
图 5-6 显示了两个运行结果中变量和对象的情况。
图 5-6 变量的同一性(is 运算符和 is not 运算符)
在读取相同值的例子中,因为两个变量引用了相同的对象,所以 a is b 求值后为 True。
这里,程序使用了不可变的 int 型,而实际中程序一般会使用 is 运算符和 is not 运算符比较可变对象,或将可变对象与 None 进行比较。
▶ 存在值相同但标识值不同的情况(后面的章节会介绍)。
赋值语句
第 1 章介绍过,用于赋值的分隔符 = 不是运算符,而且赋值语句的内涵非常丰富。学到这里,相信大家对赋值语句已经有了一定的理解,现在我们来做一下总结。
在使用赋值语句时:
1等号右边的值没有复制到等号左边。
2等号左边的变量名绑定到等号右边的对象(使变量可以引用对象)。
3等号左边的变量名如果是第一次使用,则新生成一个变量。
▶ 3的内容和作用域相关,所以规则比较复杂,第 9 章我将对此进行详细介绍。
del 语句
与赋值语句相对的是 del 语句(del statement)。del 语句用于删除只作为名字的变量。我们通过例 5-6 来进行确认。
删除变量 x 后,如果输出 x 的值就会产生错误。
▶ 例 5-6 是有意设计的示例。在实际的程序中,我们可以使用 del 语句删除列表中的元素。
del 语句解除了名称与对象的绑定。如果该名称是对象的唯一引用,程序就能释放该对象的内存(第 117 页的专栏 5-1)。
▶ 使用 del 语句释放对象后,该对象的标识值可以被之后生成的其他对象使用。
None
NoneType 类型(NoneType type)的 None 可以识别任何对象。None 是一种特殊的值,用于区分“空值”和“不存在的值”。
▶ None 是拥有 NoneType 类型的值的唯一对象。另外,None 的标识值可以通过 id(None) 查看。None 的标识值与其他对象的标识值都不同。
如果对 None 求逻辑值会得到“假”。但 None 本身并不是逻辑值 False。
▶ 整数 0、浮点数 0.0、空字符串 ''、空列表 []、空元组 ()、空字典 {}、空集合 set() 等都被程序视为假(第 52 页),而且都与 None 不相等。
我们可以通过代码清单 5-2 ~代码清单 5-5 来确认上述内容。
另外,使用 str 函数转换 None 会得到字符串 ‘None’。
可使用 x or ‘’ 的情况有:在变量 x 引用了字符串的情况下,想得到该字符串本身;在变量 x 为 None 的情况下,想得到空字符串。现在我们通过例 5-7 来进行确认。
▶ 这里利用了 or 表达式的特性,即“x or y”求值后的值是 x 或 y(第 56 页、第 58 页)。
专栏 5-1 不再使用的对象会去哪里
本节提到过变量只用于引用对象,如果不可变类型的变量值更新,标识值也会更新,以引用其他对象。
那么,不再被引用(不再使用)的对象会去哪里呢?如果它们就这样闲置,会占用存储空间。
答案是,Python 使用了引用计数对此进行管理,让程序保存了各个对象被多少个变量所引用(如果某个对象被变量 a 和变量 b 引用,引用计数就为 2)。
如果对象的引用计数为 0,程序则认为不再需要该对象,并释放该对象占有的存储空间(该存储空间可以被再次使用)。注意,该操作并非立即进行。
在 C 语言中有多种存储空间的管理方式,包括程序从开始到结束始终保持存储空间的静态存储期(静态存储生命周期)、只在代码块中保持存储空间的自动存储期等。Python 对存储空间的管理方式与之完全不同。
5-2 类型和运算
我们已经知道,Python 中“只有对象”,变量只不过是名字而已。本节,我会讲解对象的类型和运算。
对象和内置类型
我们已经学习了许多类型。表 5-2 是 Python 主要内置类型(built-in type)的一览表(可变类型使用红字标记)。
表 5-2 主要的内置类型
▶ 序列型、集合型、映射型将分别在第 6 章、第 7 章和第 8 章进行讲解。
这张表展示的就是数据类型。函数(第 9 章)、模块(第 10 章)、类(第 11 章)等也是对象,都有各自的类型。因为在 Python 中一切皆为对象,所以类型本身也是一种对象。
逻辑型
第 3 章介绍了用于表示真假的逻辑型(bool 型)。现在我们通过例 5-8 确认一下 True 的值和 False 的值。
▶ Python 中有一个 True 对象和一个 False 对象。因此程序会反复使用这些已有的对象,而不是调用 bool(7) 或 bool(0) 来生成新的对象。
浮点型和实数的运算
第 1 章演示过在 Python 程序中 7 除以 3 得到的商存在误差(第 8 页)。
结果的小数部分本应该是 3 无限循环下去,但实际结果的第 16 位变成了 5。这是因为浮点型(float 型)表示的值在大小和精度方面存在限制。
我们通过假设来对此进行理解。
假设最大可以表示 12 位数字,精度为 6 位有效数字。
以 1234567890 为例。该数字有 10 位,在最大的数字范围 12 位以内。但是,由于该数字的精度不超过 6 位,所以对左数第 7 位进行四舍五入后得到 1234570000。图 5-7 是该数字在数学上的表示方法。
图 5-7 尾数和指数
图中的 1.23457 称为尾数,9 称为指数。尾数的位数相当于精度,指数的值相当于大小。
这里使用了十进制数,而实际上尾数部分和指数部分在 Python 内部都用二进制数表示。因此,在大小为 12 位、精度为 6 位的上述示例中,用十进制整数无法准确表示结果。
表示浮点数的 float 型,其属性与运行环境有关。因此,Python 提供了 sys.float_info 来查看 float 型的属性。具体请看例 5-9。
▶ 不同运行环境下输出的值不同。
max, min 可以表示的最大值/最小值
max_exp 可以表示的最大整数
dig 可以正确表示的最大十进制浮点数的位数
epsilon 1 和程序可以表示的最临近的 float 值之间的差(即机器精度)
这里就不说明其他值的含义了,请读者自行查阅各类文档。
因为浮点数的精度有限,所以在财务计算等对准确度要求较高的情况下,我们需要使用定义在 decimal 模块中的 Decimal 型(请自行查阅相关内容)。
另外,chap05/float_info.py 是与例 5-9 的输出内容相同的脚本程序。
算术转换
例 1-6 进行了整数和浮点数的运算。在对 7(或 7.0)和 3(或 3.0)进行加法运算时,只有 3 + 7 的结果是 int 型的整数,除此之外都是 float 型的浮点数。
这是因为算术运算遵循以下规则。
如果其中一个操作数为复数,则另一个操作数转换为复数。
如果操作数都不是复数且其中一个操作数是浮点数,则另一个操作数转换为浮点数。
如果操作数既不是复数,也不是浮点数,则双方的参数必须为整数,此时不需要转换。
算术运算使用的内置函数
Python 提供了许多内置函数来进行四则运算以外的算术运算。表 5-3 为这些内置函数的一览表。
代码清单 5-6 使用了该表格内的部分函数。
在蓝色底纹部分中,传递给 sum 函数的实参是用 () 包围的 (x, y, z),而不是 x, y, z。多个值用 () 包围后就变成了一个值(元组)。
▶ 元组的相关内容会在第 8 章进行讲解。
另外,float 函数、hex 函数、int 函数、oct 函数和 sum 函数均已介绍。
在给 pow 函数指定第 3 个参数 z 时,z 不能是负数。同时,x 和 y 必须是整数。
复数型
复数型(complex type)又称 complex 型,通过两个浮点数来表示值。实数部分和虚数部分不为 0 的复数用类似于 3.2 + 5.7j 的形式表示。5.7j 的部分称为虚数字面量。这些内容我们在第 3 章已经学习过了。
在 Python 中可以调用 z.real 和 z.imag 取出复数 z 的实数部分和虚数部分。下面是生成一个复数后取出其实数部分和虚数部分的示例。
▶ 本书作为入门书,对复数型的讲解到此为止。
处理位的运算符
在计算机内部,数值表现为位的 OFF/ON,而与之对应的是二进制数。大多数编程语言提供了处理位的方法。
Python 提供了专门用于处理整数位的运算符,我们来学习一下这些运算符。
按位逻辑运算符
首先来看对位进行逻辑运算的 4 种运算符。
& … 按位与运算符(bitwise and operator)
| … 按位或运算符(bitwise inclusive or operator)
^ … 按位异或运算符(bitwise exclusive or operator)
~ … 按位取反运算符(bitwise invert operator)
前 3 个运算符是二元运算符,只有运算符 ~ 是一元运算符。这些运算符的大致情况如表 5-4 所示。
表 5-4 按位逻辑运算符
▶ 操作数必须是整数。优先级从高到低依次为 ~、&、^ 和 |。另外,关于运算符 ~ 生成的值,我们会在第 125 页的专栏 5-2 中学习。
图 5-8 汇总了各个逻辑运算的真值表。
▶ a 、b 和 d 与第 3 章讲解的逻辑运算符类似(第 57 页表 3-2)。
图 5-8 位的逻辑运算
代码清单 5-7 对这些运算符进行了演示。程序读取两个整数,并打印输出各种逻辑运算的结果。
▶ 该程序在进行屏幕输出时使用了一些技巧。
变量 f 的值为用于格式化的字符串(6-3 节将对此进行详细讲解)。像运行示例那样,如果 w 等于 8,f 则变为 '{:08b}'。这是因为 '{{' 会变为 '{','}}' 会变为 '}','{}' 会变为 w(十进制数 w 变成了字符串)。
变量 m 的值为 2w -1。该值是一个有 w 位的二进制数,而且每一位都为 1。像运行示例那样,如果 w 等于 8,则 m 变成二进制数 0b11111111。
~ x 表示对 x 的位取反后的值。在 Python 的语言实现中,~ x 即 -(x + 1)(专栏 5-2)。
▶ 因为 ~ a 和 ~ b 变成负值,所以该程序输出了它们与 m 求逻辑与之后的值。
逻辑运算的应用
逻辑与、逻辑或和逻辑异或的运算符有以下用途。
逻辑与 :清除任意位(位变为 0)。
逻辑或 :设置任意位(位变为 1)。
逻辑异或:反转任意位。
代码清单 5-8 对此进行了演示。程序针对整数 a 的后 4 位执行清除、设置和反转操作并输出相应的结果。
▶ 仅针对后 4 位执行清除、设置和反转操作,其他位保持原样。
位移运算符
<< 运算符(<< operator)和 >> 运算符(>> operator)可将整数中所有的位向左或向右移动(错位),并生成相应的值。
这两个运算符统称为位移运算符(bitwise shift operator),如表 5-5 所示。
表 5-5 位移运算符
▶ 操作数必须是整数。
代码清单 5-9 从键盘读取整数后,对整数进行左移和右移,并输出相应结果。
▶ 在显示的字符串中,{:b} 表示以二进制数的格式输出字符串(6-3 节将对此进行讲解)。
现在我们运行该程序,来看一下两个运算符的作用。
使用运算符 << 进行左移
表达式 x<< n 表示 x 的所有的位左移 n 位。此时,右侧新产生的位填 0(图 5-9 )。左移后的结果为 x×2n。
▶ 因为二进制数每一位的权重是 2 的多次幂,所以如果左移 1 位,值就会变为原来的 2 倍。这和十进制数左移 1 位,值就会变为原来的 10 倍(例如,196 左移 1 位变成 1960)的原因相同。
使用运算符 >> 进行右移
x>>n 表示 x 的所有的位右移 n 位。右侧的 n 位被移出。右移后的结果为 x÷2n(图 5-9)。
▶ 二进制数右移 1 位,值就会变为原来的一半。这和十进制数右移 1 位,值就会变为原来的十分之一(例如,196 右移 1 位变成 19)的原因相同。
另外,如果不需要生成位移后的值,只对变量值本身进行位移,则可以使用增量赋值运算符 <<= 和 >>=。
图 5-9 对整数进行位移运算
整数左移 n 位和右移 n 位的结果分别与整数乘以 2n 和除以 2n 的结果相同。我们来通过代码清单 5-10 进行确认。
这里的运行示例输出的位移运算结果是负数。不论 x 是正数还是负数,程序都会按预想的情况进行位移运算。
专栏 5-2 补码和按位取反运算符
C 语言等编程语言一般只能表示16位或32位的有限位整数,并且符号也含在其中。
在这些编程语言中,负整数在内部的典型表示方式是 1 的补码(one’s complement)或 2 的补码(two’s complement)
图 5C-1 为求 1 的补码和 2 的补码的步骤(以整数 5 为例)。
图 5C-1 求出补码的步骤
1 的补码通过反转所有位得到。
2 的补码通过对 1 的补码加 1 得到。
在 Python 内部,负整数并不使用 1 的补码或 2 的补码表示(因为没有必要)。
按位取反运算符 ~ 其实没有反转位。~x 的运算结果通过模仿补码的表示方式计算 -(x + 1) 得到。
另外,连续使用两次运算符 ~ 会得到原来的值。假设 x 等于 5,~x 就等于 -6。对 -6 使用运算符 ~ 会得到 5,即 ~~x 等于 x。
总结
在 Python 中,变量、函数、类和模块皆为对象。
对象会占一定的存储空间(内存),并且拥有标识值(用于判断是否为同一个对象)、类型和值等属性。可以使用 id 函数获取标识值,使用 type 函数获取类型。
变量只是一个和对象绑定(引用对象)的名字。
is 运算符和 is not 运算符是身份运算符,用于判断对象是否为同一个(标识值是否相等)。
在 Python 中,根据值是否可以改变,类型可分为两类。
可变类型: 列表、字典、集合等 ※值可以改变。
不可变类型:数字、字符串、元组等 ※值不可以改变。
如果对不可变类型的变量(引用的对象)的值进行变更,则会生成新的对象,然后变量重新引用新的对象。
赋值语句复制的是对象的引用而不是值。另外,赋值的对象,即等号左边的变量名如果是首次使用,程序则生成新的变量并与等号右边的对象进行绑定。
与赋值语句相对应的是 del 语句,它用于删除作为名字的变量名。
None 与任何对象都不同,是 NoneType 类型的特殊值。
其他编程语言使用存储期(存储空间生命周期)对变量和对象进行管理。Python 与之不同,它使用引用计数,即引用对象的变量的个数,对变量和对象进行管理。
内置类型包括数值型(int 型、bool 型、float 型和 complex 型)、序列型(str 型、list 型和 tuple 型等)、集合型(set 型、frozenset 型)和映射型(dict 型)。
浮点型(float 型)可以表示的值在大小和精度方面存在限制。使用 sys.float_info 可以查看浮点型的属性。
在进行算术运算时,程序会根据操作数的类型进行算术转换。
复数型是用表示实数部分和虚数部分的两个浮点数来表示值的类型。例如 3.2 + 5.7j。其中,5.7j 称为虚数字面量。
因为在计算机内部,数值用位的 ON/OFF 来表示,所以 Python 可以轻易地表示二进制数。
Python 提供了求逻辑与的运算符 &、求逻辑或的运算符 |、求逻辑异或的运算符 ^ 和生成取反后的值的运算符 ~ 等按位逻辑运算符。
位移运算符 << 和 >> 将整数中的所有的位向左或向右移动后生成相应的值。