Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
liujuanjuan1984 opened this issue Nov 11, 2019 · 2 comments
Open

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

liujuanjuan1984 opened this issue Nov 11, 2019 · 2 comments

Comments

@liujuanjuan1984
Copy link
Owner

liujuanjuan1984 commented Nov 11, 2019

What gets printed?

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

mylist = [1, 2, 3, 4]
addItem(mylist)
print(len(mylist))
@liujuanjuan1984
Copy link
Owner Author

liujuanjuan1984 commented Nov 12, 2019

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
Copy link
Owner Author

现在试试多引入一个变量:把变量 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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant