liujuanjuan1984 / ucanuupnobb

you can you up, no bb. 自学 python 编程过程中的挑战、笔记及我的践友们。
17 stars 9 forks source link

做两遍,错两遍,列表的可变特性 #79

Open liujuanjuan1984 opened 4 years ago

liujuanjuan1984 commented 4 years ago

What gets printed?

def addItem(listParam):
    listParam += [1]

mylist = [1, 2, 3, 4]
addItem(mylist)
print(len(mylist))
liujuanjuan1984 commented 4 years ago

1、刷题出错,即是成长线索

每天在 xue.cn 上至少刷题 10 道,刷得次数多了,开始遇到之前做过的题目。很快我就发现,自己竟然在同一道题上连续错两次,这引起了我的警觉——这是重要线索,它告诉我:相关知识点,我掌握的太烂。怎么办呢?给自己充裕的时间,通过梳理的方式,彻底搞懂它。

该知识点是:python 数据容器的可变与不可变特性。题目如下:

What gets printed?

def addItem(listParam):
    listParam += [1]

mylist = [1, 2, 3, 4]
addItem(mylist)
print(len(mylist))

2、数据容器有哪些,如何归类它们?

《自学是门手艺》 书中 “数据容器” 这一节开篇即总结:

在 Python 中,有个数据容器(Container)的概念。

其中包括字符串、由 range() 函数生成的等差数列、列表(List)、元组(Tuple)、集合(Set)、字典(Dictionary)。

这些容器,各有各的用处。其中又分为可变容器(Mutable)和不可变容器(Immutable)。可变的有列表、集合、字典;不可变的有字符串、range() 生成的等差数列、元组。集合,又分为 Set 和 Frozen Set;其中,Set 是可变的,Frozen Set 是不可变的。

字符串、由 range() 函数生成的等差数列、列表、元组是有序类型(Sequence Type),而集合与字典是无序的。

另外,集合没有重合元素。

image

我这个人是排斥死记硬背的,可有时还必须得暂时死记硬背一下。然后赶紧通过刷题或实战巩固记忆。——但凡死记硬背之后在刷题或实战中反复出错,那就是理解没到位,得继续深究。

以上述题目而言,我清楚知道:1) 列表是可变容器 2) 变量的作用域。但我没能彻底搞懂,为什么列表可突破变量的作用域这个常规限定。

3、前人有哪些参考资料,如何使用?

我搜索了一些资料,想要理解 python 数据容器是否可变的背后原理。下面这几篇给到我许多帮助:

《python中变量的存储与拷贝》 《python 里的可变对象与不可变对象具体怎么理解》 《Python中变量存储的方式》

以上有些内容,我理解起来是吃力的。我意识到,我需要按照自己的认知节奏来梳理相关知识,我还需要写一些验证代码加深影响。——仅仅阅读别人的文章或笔记,其实很难帮助自己真正掌握那个知识点,还是得自己整理归纳,梳理通顺,这个过程降低认知负担,加深知识记忆,而自己写验证代码能检查是否记住,也进一步巩固知识点记忆。

这点认识,对自学编程的人来说,大概率是老生常谈。但事情的关键不是你知道,事情的关键是自己能做到。对不对?

btw,这次没能直接在官网搜一手资料。

4、可变,体现在哪些地方?

通常,我们会对数据进行增删改查四大操作。但可变,并不是指代变量所存储的数据是否能发生改变。毕竟,除了区块链技术以较高的成本与性能代价能实现数据的不可删改,互联网世界的任何数据都具备删改的能力。

那么,当我们谈论 python 的数据容器是否可变时,我们到底在谈论什么?

是否为可变在于内存单元的值是否可以被改变。 如果是内存单元的值不可改变的,在对对象本身操作的时候,必须在内存的另外地方再申请一块内存单元(因为老的内存单元不可变),老的内存单元就丢弃了(如果还有其他ref,则ref数字减1,类似unix下的hard-link)。 如果是可变的,对对象操作的时候,不需要再在其他地方申请内存,只需要在此对象后面连续申请(+/-)即可,也就是它的地址会保持不变,但区域会变长或者变短。 摘自《python 里的可变对象与不可变对象具体怎么理解》

这段文字解释的相当清楚,数据容器是否可变,并不是指代数据是否可变,而是指代数据容器的内存单元的值是否可以被改变。

那,什么是内存单元呢?

在高级语言中,变量是对内存及其地址的抽象。 对于 python 而言,一切变量都是对象,变量的存储,采用了引用语义的方式,存储的只是一个变量的值所在的内存地址,而不是这个变量的值本身。 对于复杂的数据结构来说,里面的存储的也只是每个元素的地址而已。 摘自 《Python中变量存储的方式》

似乎可以把“内存单元的值”等同于“内存地址”。

对于新手需要知道的是, id(var) 可用于查询变量的内存地址,type(var) 可用于查询变量的类型。立即写一些代码,了解如何查看内存地址,以及对变量数据作出局部改变时,内存地址是否变化。

image

liujuanjuan1984 commented 4 years ago

现在试试多引入一个变量:把变量 a 赋值给变量 b,内存地址会一样吗?如果再修改变量 b,数据和内存地址依然还一样吗?

首先,是不可变容器字符串。

image

从代码中可发现:把字符串 a 的值赋值给字符串 b 时,a 和 b 的数据与内存地址一样。当对字符串 b 进行修改并重新赋值给 b 时,字符串 a 的数据和内存地址保持不变,而字符串 b 的数据和内存地址发生改变。

需要注意的是,b.replace()无法对字符串b实现数据的修改,仅能通过重新赋值给b,才能实现对变量b的数据完成修改。——验证代码也很简单。

image

然后,是可变容器,列表。

image

从代码中可发现:把列表a的值赋值给列表b时,a和b的数据与内存地址一样。当对列表b进行局部修改时,列表a和b 内存地址保持不变,和之前一样,而数据同时发生变化。

无论是字符串还是列表,我们在构建新变量 b 时,都是直接通过把变量 a 赋值给 b 即b = a 语句完成的。如果是采用copy() 或切片的方式,以上现象会发生变化吗?

先看看字符串。字符串没有copy()的方法,但有切片的方法。另外需要反复强调的是,字符串不能通过对局部赋值来修改其数据,只能通过重新赋值给原变量的方式,修改原变量的数据。

image

再看看列表。

通过切片方式构造变量 b 时,其内存地址就已经和变量 a 不同。对变量 b 进行局部数据修改时,变量 a 的数据完全不受影响。——无论是全切片,还是局部切片都如此。

image

通过copy() 方法,其效果与切片方式相同。

image

而这次反复难倒我的题目,涉及到函数传参。我们分别再试试看字符串和列表的表现。

先看字符串。a 作为全局变量,并没有因为被传参到函数 f 并经过处理而发生改变,数据不变,内容地址也不变。但函数内改变了字符串的数据并重新赋值给原变量时,函数体内的改参数其内存地址与数据都发生了改变。——这是符合作用域常规的现象。

image

再看看列表。a 作为全局变量,内存地址一直未发生变化,仅数据因为被传参到函数中发生了变化。

image

不难推导,如果传入函数的并不是变量 a 而是变量 a 的全切片,切片完成时已有新的内存地址指代新的变量,而变量a则不受影响。

image