【Python学习手册(第四版)】学习笔记09.3-Python对象类型-分类、引用VS拷贝VS深拷贝、比较、相等、真假值等详解

个人总结难免疏漏,请多包涵。更多内容请查看原文。本文以及学习笔记系列仅用于个人学习、研究交流。

这部分稍杂,视需要选择目录读取。

主要讲的是对之前的所有对象类型作复习,以通俗易懂、由浅入深的方式进行介绍,所有对象类型共有的特性(例如,共享引用),引用、拷贝、深拷贝,以及比较、相等、真假值等,讨论对象类型领域内常见的错误(“陷阱”)。


目录

对象类型分类

对象灵活性

引用VS拷贝VS深拷贝

重点推荐阅读:

比较、相等性和真值

字典比较

Python中真和假的含义

None对象

bool类型

Python的类型层次

Type对象

Python中的其他类型

循环数据结构


对象类型分类

看过了前面的学习笔记,现在来对他们做一下总结,加以分类。再看一看它们所共有的一些属性。

下面是需要记住的一些要点:

  • ·对象根据分类来共享操作;例如,字符串、列表和元组都共享诸如合并、长度和索引等序列操作。
  • ·只有可变对象(列表、字典和集合)可以原处修改;我们不能原处修改数字、字符串或元组。
  • ·文件导出唯一的方法,因此可变性并不真的适用于它们——当处理文件的时候,它们的状态可能会修改,但是,这与Python的核心类型可变性限制不完全相同。
  • ·表9-3中的“数字”包含了所有数字类型:整数、浮点数、复数、小数和分数。
  • ·表9-3中的字符串包括str,bytes;bytearray字符串类型是可变的。
  • ·集合类似于一个无值的字典的键,但是,它们不能映射为值,并且没有顺序;因此,集合不是一个映射类型或者一个序列类型,frozenset是集合的一个不可变的版本。
  • ·除了类型分类操作,表9-3中所有类型都有可调用的方法,这些方法通常特定于它们的类型。

对象灵活性

一般来说:

  • 列表、字典和元组可以包含任何种类的对象。
  • 列表、字典和元组可以任意嵌套。
  • 列表和字典可以动态地扩大和缩小。

Python的复合对象类型支持任意结构,例如,字典的值可以是列表,这一列表可能包含了元组,而元组可能包含了字典,依此类推。只要能够满足创建待处理数据的模型的需要,嵌套多少层都是可以的。

来看一个嵌套的例子。

下面的交互式会话定义了一个嵌套复合序列对象的树,如图所示。

>>> l = ['abc', [(1,2),([3], 4)], 5]
>>> l[1]
[(1, 2), ([3], 4)]

要存取它的内容时,我们需要按要求串起多个索引操作。Python从左到右计算这些索引,每一步取出一个更深层嵌套对象的引用。下图描述了一般情况下,用于存取嵌套对象的语法。也许是过于复杂且莫名其妙的数据结构。

>>> l[1][1]
([3], 4)
>>> l[1][1][0]
[3]
>>> l[1][1][0][0]
3
>>>

引用VS拷贝VS深拷贝

赋值操作总是储存对象的引用,而不是这些对象的拷贝。在实际应用中,这往往就是你想要的。

不过,因为赋值操作会产生相同对象的多个引用,需要意识到在原处修改可变对象时可能会影响程序中其他地方对相同对象的其他引用。

如果不想这样做,就需要明确地告诉Python复制该对象。

当较大的对象参与时,就会变得更为严重。

例如,下面这个例子生成一个列表并赋值为X,另一个列表赋值为L,L嵌套对列表X的引用。这一例子中还生成了一个字典D,含有另一个对列表X的引用。

>>> x = [1,2,3]
>>> l = ['a',x,'b']
>>> d = {'x':x ,'y':2}

对我们先前生成的列表有三个引用:来自名字X、来自赋值为L的列表内部以及来自赋值为D的字典内部。

共享对象引用:因为变量X引用的列表也在被L和D引用的对象内引用,修改X的共享列表与L和D的看起来也有所不同。

由于列表是可变的,修改这三个引用中任意一个共享列表对象,也会改变另外两个引用的对象。

>>> x[1] = 'life'
>>> l
['a', [1, 'life', 3], 'b']
>>> d
{'x': [1, 'life', 3], 'y': 2}

引用是其他语言中指针的更高级模拟。在不止一个地方储存相同的引用(变量、列表等)是可能的。你可以在程序范围内任何地方传递大型对象而不必在途中产生拷贝。然而,如果你的确需要拷贝,那么可以明确要求。

  • ·没有限制条件的分片表达式(L[:])能够复制序列。
  • ·字典copy方法(X.copy())能够复制字典。
  • ·有些内置函数(例如,list)能够生成拷贝(list(L))。
  • ·copy标准库模块能够生成完整拷贝。

举个例子,假如有一个列表和一个字典,你又不想凭借其他变量来修改它们的值。

>>> l = [1,2,3]
>>> d = {'a':1,'b':2}

可以简单地把拷贝赋值为其他变量,而不是相同对象的引用。

>>> a = l[:]
>>> b = d.copy()

>>> a[1] = 'hi'
>>> b['c'] = 3

>>> l,d
([1, 2, 3], {'a': 1, 'b': 2})

>>> a,b
([1, 'hi', 3], {'a': 1, 'b': 2, 'c': 3})

就原始的例子而言,可以通过对原始列表进行分片而不是简单的命名操作来避免引用的副作用。

>>> x = [1,2,3]

>>> l = ['a', x[:],'b']
>>> d = {'x':x[:] ,'y':2}

L和D现在会指向不同的列表而不再是X。结果就是,凭借X所做的修改只能够影响X而不会再影响L和D。类似地,修改L或D不会影响X。

重点推荐阅读:

拷贝需要注意的是:无条件值的分片以及字典copy方法只能做顶层复制。也就是--不能够复制嵌套的数据结构(如果有的话)。

本书中这里没有举例,所以不太容易理解。于是我翻看其他好多深拷贝、浅拷贝等的文章,都讲的很好,很细。但是很难理解。于是我自己举个例子吧

>>> l
['a', [1, 2, 3, (1, 2), [3, [2, 1], 2]], 'b']

>>> l1 = l[:]
>>> l1.append('ss')
>>> l1
['a', [1, 2, 3, (1, 2), [3, [2, 1], 2]], 'b', 'ss']

>>> l1[1].append('s')
>>> l1
['a', [1, 2, 3, (1, 2), [3, [2, 1], 2], 's'], 'b', 'ss']
>>> l
['a', [1, 2, 3, (1, 2), [3, [2, 1], 2], 's'], 'b']

>>> l[1].append('sd')
>>> l
['a', [1, 2, 3, (1, 2), [3, [2, 1], 2], 's', 'sd'], 'b']
>>> l1
['a', [1, 2, 3, (1, 2), [3, [2, 1], 2], 's', 'sd'], 'b', 'ss']

按上面的例子,l列表复制了一份成为l1,l中嵌套了一层列表。因为之前的例子并没有对嵌套里的内容做新增、修改、删除等,这里l1嵌套的列表里新增了一个一个字符串。l1 l也都新增了。

是不是l1的问题?换l新增,看到l l1嵌套的列表也都新增了。

再对比l1.append('ss'),l却没有,说明无条件值的分片以及字典copy方法只能做顶层复制。

那么修改、删除会不会也会改变非顶层呢? 会的

>>> l
['a', [99, 2, 3, (1, 2), [3, [2, 1], 2], 's', 'sd'], 'b']
>>> l1
['a', [99, 2, 3, (1, 2), [3, [2, 1], 2], 's', 'sd'], 'b', 'ss']


>>> l[1].pop()
'sd'
>>> l
['a', [99, 2, 3, (1, 2), [3, [2, 1], 2], 's'], 'b']
>>> l1
['a', [99, 2, 3, (1, 2), [3, [2, 1], 2], 's'], 'b', 'ss']

>>> del l[1][-1]
>>> l
['a', [99, 2, 3, (1, 2), [3, [2, 1], 2]], 'b']
>>> l1
['a', [99, 2, 3, (1, 2), [3, [2, 1], 2]], 'b', 'ss']
>>>

如果需要一个深层嵌套的数据结构的完整的、完全独立的拷贝要用copy.deepcopy()。

使用标准的copy模块——import copy语句,并编辑X=copy.deepcopy(Y)对任意嵌套对象Y做完整的复制。这一调用语句能够递归地遍历对象来复制它们所有的组成部分。

>>> import copy
>>> x = copy.deepcopy(l)
>>> l
['a', [1, 2, 3, (1, 2), [3, [2, 1], 2], 's', 'sd'], 'b']
>>> x
['a', [1, 2, 3, (1, 2), [3, [2, 1], 2], 's', 'sd'], 'b']
>>> x[1].append('aa')
>>> x
['a', [1, 2, 3, (1, 2), [3, [2, 1], 2], 's', 'sd', 'aa'], 'b']
>>> l
['a', [1, 2, 3, (1, 2), [3, [2, 1], 2], 's', 'sd'], 'b']

注意分片和copy方法以及deepcopy的使用注意。


比较、相等性和真值

一般来说,Python中不同的类型的比较方法如下:

  • ·数字通过相对大小进行比较。
  • ·字符串是按照字典顺序,一个字符接一个字符地对比进行比较("abc"<"ac")。
  • ·列表和元组从左到右对每部分的内容进行比较。
  • 字典的相对大小比较在Python 3.0后不支持
  • ·数字混合类型比较(例如,1<'spam')在Python 3.0后是错误的。

结构化对象的比较就好像是你把对象写成文字,并从左到右一次一个地比较所有部分。

所有的Python对象也可以支持比较操作——测试相等性、相对大小等。Python的比较总是检查复合对象的所有部分,直到可以得出结果为止。

当嵌套对象存在时,Python能够自动遍历数据结构,并从左到右递归地应用比较,要多深就走多深。过程中首次发现的差值将决定比较的结果。

例如,在比较列表对象时将自动比较它的所有内容。

>>> l1 = [1,('a',3)]
>>> l2 = [1,('a',3)]
>>> l1 == l2 ,l1 is l2
(True, False)

在这里L1和L2被赋值为列表,虽然相等,却是不同的对象。

如果l1 l2换成同样的短字符串,那么都是true。因为在Python内部暂时储存并重复使用短字符串作为最佳化,事实上内存里只有一个字符串'life'供S1和S2分享。

>>> s1 = 'life'
>>> s2 = 'life'
>>> s1 == s2, s1 is s2
(True, True)

那么更长的呢?

>>> s1 = 'a longer string'
>>> s2 = 'a longer string'
>>> s1 == s2, s1 is s2
(True, False)

当然,由于字符串是不可变的,对象缓存机制和程序代码无关——无论有多少变量与它们有关,字符串是无法在原处修改的。

“==”几乎是所有等值检验时都会用到的操作符;而is则保留了极为特殊的角色。

相对大小的比较也能够递归地应用于嵌套的数据结构。

>>> l1 = [1,('a',3)]
>>> l2 = [1,('a',2)]
>>> l1 == l2 , l1 < l2 , l1 > l2
(False, False, True)

嵌套的3大于2,这里的L1大于L2。

字典比较

在Python 3.0后,字典的大小比较删除了,因为当期望相等的时候,它们导致太多的负担。

替代方法,要么编写循环来根据键比较值,要么手动比较排序的键/值列表——items字典方法的和内置的sorted足够了:

>>> d1 = {'a':1,'b':2}
>>> d2 = {'a':1,'b':3}
>>> d1 == d2
False
>>> d1 < d2
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: '<' not supported between instances of 'dict' and 'dict'

>>> list(d1.items())
[('a', 1), ('b', 2)]
>>> sorted(d1.items())
[('a', 1), ('b', 2)]
>>> sorted(d1.items()) < sorted(d2.items())
True
>>> sorted(d1.items()) > sorted(d2.items())
False

Python中真和假的含义

在Python中,与大多数程序设计语言一样,整数0代表假,整数1代表真。不过,除此之外,Python也把任意的空数据结构视为假,把任何非空数据结构视为真。

真和假的概念是Python中每个对象的固有属性:每个对象不是真就是假,如下所示:

·数字如果非零,则为真。

·其他对象如果非空,则为真。

作为一个应用,判断对象自身是真或假,通常会看到Python程序员像if X:这样编写测试,其中,假设X是一个字符串,那么,等同于if X !='':。

None对象

Python还有一个特殊对象:None(表9-4中最后一项),总被认为是假。

这是Python中一种特殊数据类型的唯一值,一般都起到一个空的占位作用,与C语言中的NULL指针类似。

对于列表来说,你是无法为偏移赋值的,除非这个偏移是已经存在的。

要预先分配一个100项的列表,这样你可以在100个偏移的任何一个加上None对象:

>>> l = [None] * 100
>>> l
[None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None]

这不会限制列表的大小(它随后仍然可以增长或缩短),但是直接预先设置一个初始大小,会考虑到将来的索引赋值。

当然,可以以同样的方式用0来初始化一个列表,但最佳的实践是如果还不知道列表的内容的话使用None。

None不是意味着“未定义”。None是某些内容,而不是没有内容(尽管起名字是没有内容)——它是一个真正的对象,并且有一块内存,由Python给定一个内置的名称。

bool类型

Python的布尔类型bool不过是扩展了Python中真、假的概念。True和False这些只是整数1和0的定制版本而已。

由于这个新的类型的运行方式,这的确只是先前所说的真、假概念的较小扩展而已,这样的设计就是为了让真值更为明确。

  • ·当明确地用在真值测试时,True和False这些文字就变成了1和0,但它们使得程序员的意图更明确。
  • ·交互模式下运行的布尔测试的结果打印成True和False的字样,而不是1和0,以使得程序的结果更明确。

像if这样的逻辑语句中,没必要只用布尔类型。所有对象本质上依然是真或假,即使使用其他类型,提到的所有布尔概念依然是可用的。

Python还提供了一个内置函数bool,它可以用来测试一个对象的布尔值:

>>> bool(3)
True
>>> bool('a')
True
>>> bool({})
False
>>> bool(())
False

实际上,很少真正注意到逻辑测试所产生的布尔类型,因为if语句和其他的选择工具自动地使用布尔结果。

Python的类型层次

图9-3总结了Python允许的所有内置对象类型以及它们之间的关系。我们已经讨论了它们当中最主要的,图9-3中多数其他对象种类都相当于程序单位(例如,函数和模块),或者说明了解释器的内容(例如,堆栈和编译码)。

按类别组织的Python的主要内置对象类型。Python中所有一切都是某种类型的对象,即便是某个对象的类型!任何对象的类型都是类型为"type"的对象。

Python系统中的任何东西都是对象类型,而且可以由Python程序来处理。例如,可以传递一个类给函数,赋值为一个变量,放入一个列表或是字典中等。

Type对象

即使是类型本身在Python中也是对象类型。

内置函数type(X)能够返回对象X的类型对象。类型对象可以用在Python中的if语句来进行手动类型比较。

Python 3.0中的类型标准库模块同样提供其他不能作为内置类型使用类型的名称。而且用isinstance函数进行类型测试也是有可能的。例如,下列所有类型测试都为真:

>>> type([1]) == type([])
True
>>> type([1]) == list
True
>>> isinstance([1],list)
True

>>> import types
>>> def f():pass
...
>>> type(f) == types.FunctionType
True

因为目前Python的类型也可以再分为子类,一般都建议使用isinstance技术。


Python中的其他类型

在后面将要遇到的函数、模块和类,典型Python的安装还有几十种其他可用的对象类型,允许作为C语言的扩展程序或是Python的类:正则表达式对象、DBM文件、GUI组件、网络套接字等。

这些附带工具和我们至今所见到的内置类型之间的主要区别在于,内置类型有针对它们的对象的特殊语言生成语法(例如,4用于整数,[1,2]用于列表,open函数用于文件)。

在内置模块中输入的其他工具必须先导入才可以使用。例如,为了生成一个正则表达式对象,你需要输入re并调用re.compile()。参考Python的库手册。

循环数据结构

如果遇到一个复合对象包含指向自身的引用,就称之为循环对象。

无论何时Python在对象中检测到循环,都会打印成[...],而不会陷入无限循环。

>>> l = ['life']
>>> l.append(l)
>>> l
['life', [...]]

除了要了解方括号括起来的三个点代表对象中带有循环之外,有一种情况也应该知道,因为它会造成误区——如果不小心,循环结构可能导致程序代码陷入无法预期的循环当中。

例如,有些程序中保留了已经被访问过的列表或者字典,并通过检测来确定他们是否在循环当中。

除非你真的需要,否则不要使用循环引用。虽然有很多创建循环的不错的理由,但除非你知道代码会如何处理,否则你可能不想让对象在实际中频繁地引用自身。

  • 37
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

兴焉

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值