第三章 列表简介

概要

本章作者提到了列表是什么,至于具体怎么操作列表则要到下一章细讲。列表是我们遇到的第一个可变类型。这是一个重要的概念,并且由此引申,我们可以重新认识三大定理第二条,变量名只是一个指针,这句话的深刻含义。

可变类型(mutable)与不可变类型(immutable)

列表是你学到的第一个可变类型,而上一章提到的数字(int,float)和字符串(str)则都属于不可变类型。Python 中的所有基础数据类型,都可以被分到这两类。具体见下表

名称

类型

int

不可变类型

float

不可变类型

bool

不可变类型

complex

不可变类型

tuple

不可变类型

frozenset

不可变类型

str

不可变类型

list

可变类型

set

可变类型

dict

可变类型

那么这两个类型是什么意思呢?要理解这个问题,我们要通过 id() 来查看内存位置。

理解不可变类型的指针特性

以 int 为例,int 是不可变类型,但这并不意味着当你用 a = 1 设定 a 的时候,a 就不可改变了。a 可以改变成任何别的数字,乃至别的类型,但当你这么做的时候,你会发现,a 指向的内存位置发生了变化。

# int 是不可变类型
>>> a = 1
>>> id(a)
4466205824

# 改变 a 的值时,a 的 id 也发生了变化
>>> a = 2
>>> id(a)
4466205856

这是因为,在 python 的内部,当一个不可变类型的对象被生成之后,他的值就不能再被改变了。当你重新改变 a 的值时,其实 python 做的事情是重新创建了一个新的对象,并把 a 指向它。id 的变化就证明了这一点。

理解可变类型的指针特性

而对于可变类型来说,你可以在那个对象的原位对其内容进行修改,而不会重新生成对象。如下所示

# list 是可变类型
>>> a = [1, 2, 3]
>>> id(a)
4469159088

# 以下操作都在原位修改 list,不会重新生成列表对象,a 的 id 也没有变化。
>>> a[0] = 10
>>> a.append(4)
>>> a
[10, 2, 3, 4]
>>> id(a)
4469159088

字符串的不可修改性

字符串属于不可变类型,这也是为什么当你尝试通过下标修改字符串时,它会出错

# str 不可通过下标赋值
>>> a = '1234'
>>> a[0] = '9'
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'str' object does not support item assignment

# list 就不会出错
>>> a = ['1', '2', '3', '4']
>>> a[0] = '9'
>>> a
['9', '2', '3', '4']

一个有趣的工具 Pythontutor

通过上面的实例,你应该可以理解可变类型与不可变类型在内存中的行为存在很大的差异。而对于可变类型(列表,以及后面要学到的字典,集合等)来说,事情还要更加复杂一些。

可变类型的许多操作是原位(inplace)进行的,就如同上面那样通过索引赋值,或者通过 append() 给列表增加元素。但是,也有的时候,我们希望重新拷贝一份数据再进行修改,或者很多函数操作其实在背后帮你做了拷贝。如果不清楚可变类型的行为,那就很容易弄混修改到底是发生在拷贝还是原位,产生潜在的错误。下面几个例子希望可以帮你对此有一个深入理解。

再看例子之前,介绍一个非常优秀的辅助工具,叫做Pythontutor。这个工具能够将一些简单的 python 代码产生的内存效果直接可视化。我将使用这个工具来解释下面的例子,你也应该把我的这几行例子放到Pythontut之中自己尝试一遍。它可以极好的帮助你理解 python 在内存中的行为!

第一个例子 - 深入理解列表修改

a = [1, 2, 3]
id(a)
a[0] = 0
a.append(4)
del a[1]
id(a)
# 两次 id(a) 的结果会变吗?为什么?

第二个例子 - 简单引用与复制

a = [1, 2, 3]
b = a
c = a.copy()
a[0] = 9
# b 和 c 现在是什么?

第三个例子 - 深入理解list.sort() 方法与 sorted() 函数的不同

a = [1, 3, 2, 4]
b = a
sorted_a = sorted(a)
unsorted_a = a.copy()
a.sort()
sorted_a.append(5)
# 上面的四个变量各是什么?

为什么要知道这些呢?

从上面的例子你会发现,列表的赋值并没有看上去这么简单。尤其是当两个变量都指向同一个列表时,通过一个变量改变列表,另一个变量也隐式的发生了变化!当你不清楚背后的道理时,这会是很多bug的来源。

但是这些事情也并没有这么的复杂,只要你能够理解python 三达定理第二条“Python 中的变量只是一个指针(pointer)”,加上 pythontutor 这个工具的帮助,你就会掌握有关可变类型的这一点点额外但有用的知识了。

关于原位修改还是拷贝修改的讨论还没有结束,因为数据分析中常用的类型,比如 numpy.Array 或者 pandas.Series 都涉及到同样的问题。及早知道这一点很有必要,之后我们还会重提来加深印象。

一点点剧透:numpy.ndarray 和 pandas 与列表的比较

numpy

numpy.ndarray 是 python 中储存一个 n 维矩阵最常用的数据类型。numpy.ndarray 是所有数据分析相关的包的基础,因为它是绝大多数情况下,数据在内存中存在的形式。pandas 这个包就是在 numpy 的基础上实现的。简单来说,使用 numpy 而不是列表存储数据,我们可以获得极大的速度提升和极为丰富的有关数学运算的函数或方法支持。

import numpy as np

>>> x = np.array([[1, 2, 3], [4, 5, 6]])

>>> type(x)
<type 'numpy.ndarray'>

>>> x
array([[1, 2, 3],
       [4, 5, 6]])

>>> x.dtype
dtype('int64')

pandas

pandas 这个包就是 python 中的 Excel。而 pandas 有两种最基本的数据类型,series 和 dataframe。简单来说 series 就是一维数据(表格里的一行或者一列),dataframe 就是二维数据(表格本身)

Series和列表一样,可以包含多种类型的数据(虽然大多数时候,我们只在 Series 中存放单一类型数据),但它与列表最大的不同是,series 有 index 作为每一个 item 的名称。这使得 series 既可以像列表一样通过数字索引获取数据,也可以像字典一样通过名称获得数据。同样的,dataframe 的行名和列名也都有 index 作为名称,这是 dataframe 的一个重要特征。当我们讨论完字典之后,我们会详细的介绍 pandas。

>>> import pandas as pd
# 第一个列表是 series 的实际值,第二个列表则是 index
>>> series = pd.Series([0, 1, 2, 3], index=['a', 'b', 'c', 'd'])
>>> series
a    0
b    1
c    2
d    3
dtype: int64
>>> series['a']
0
>>> series[0]
0

最后更新于