# 数据结构
数据结构用来将**数据**通过遵循某种**结构**组织在一起。换句话说,它们是用来存储一系列相关的数据。
在Python中有四种内建数据结构--**列表、元组、字典和集合**,我们将看到它们如何使用,它们是怎样使我们的写代码的日子变得更轻松的。
## 列表
`列表`是一种数据结构,它是数据的有序集合。例如,你可以在列表中存储一个**序列**。我们举个栗子,想像一下你的购物清单,那里有你想要购买的商品的一个清单。在python中,我们使用逗号分隔列表中的数据。
数据的列表应包含在方括号内,以便Python明白你在指定一个列表。一旦您创建了一个列表,你可以添加、删除或是搜索列表中的数据。因为我们可以添加和删除数据,我们说一个列表是一个**可变**的数据类型,即这种类型可以更改。
## 对象和类的快速介绍
尽管直到现在,我一直推迟讨论对象和类,现在却需要解释一下,这样你才可以更好地理解列表。我们将在[后面的章节](oop.md)中详细探讨这一课题。
列表是使用对象和类的一个例子。当我们使用一个变量`i`,为它分配一个值,例如把整数`5`赋值给它,你可以认为它是创建了一个类为`int`(即类型)的对象(即实例)`i`。你还可以通过阅读`help(int)`更好地理解这一点。
类也有**方法**,也就是我们定义的只在类中生效的函数。只有当你有那个类的对象时,你才可以使用这些函数。例如,Python为`list`(列表)类提供了一个`append`方法,它允许你在列表的末尾添加一个数据。例如,`mylist.append('an item')`将给列表`mylist`添加一个字符串。注意,我们使用点运算符访问对象的方法。
类也有**字段**,也就是我们定义的只在类中生效的变量。只有当你有那个类的对象时,你才可以使用那些变量。字段是通过点运算符访问的。例如,`mylist.field`。
例子 (保存为ds_using_list.py):
```python
# 这是我的购物清单
shoplist = ['苹果', '芒果', '胡萝卜', '香蕉']
print('我要买', len(shoplist), '个物品。')
print('清单是:', end=' ')
for item in shoplist:
print(item, end=' ')
print('\n我还要买大米。')
shoplist.append('大米')
print('现在我的清单是', shoplist)
print('现在我将要为我的清单排序')
shoplist.sort()
print('排序后的购物清单是', shoplist)
print('我要买的第一个物品是', shoplist[0])
olditem = shoplist[0]
del shoplist[0]
print('我已经买了', olditem)
print('现在我的清单是', shoplist)
```
输出:
```shell
C:\> python ds_using_list.py
我要买 4 个物品。
清单是: 苹果 芒果 胡萝卜 香蕉
我还要买大米。
现在我的清单是 ['苹果', '芒果', '胡萝卜', '香蕉', '大米']
现在我将要为我的清单排序
排序后的购物清单是 ['大米', '胡萝卜', '芒果', '苹果', '香蕉']
我要买的第一个物品是 大米
我已经买了 大米
现在我的清单是 ['胡萝卜', '芒果', '苹果', '香蕉']
```
**它是如何工作的:**
变量`shoplist`是去超市采购的一个购物清单。在`shoplist`中,我们只存储了要购买商品名字的字符串,你可以向清单中添加**任何对象**,包括数字甚至是其它的清单。
我们也使用了循环`for..in`遍历清单中的所有数据。到现在为止,你必须认识到列表也是一个序列。关于序列的特性将在后面的章节中讨论。
注意,在`print`函数中使用`end`参数,表明输出以一个空格结束而不是通常的换行。
接下来,和前面讨论过的一样,我们使用列表对象的`append`方法向列表中添加一个数据。然后,我们通过`print`语句打印出列表的内容,以检查这个数据确实添加到了列表中。
然后,我们使用列表对象的`sort`方法为列表排序。这个方法作用到列表本身,并不返回一个修改过的列表。理解这一点很很重要,因为它不同于对字符串的操作。这也是为什么说列表是**可变**的,而字符串是**不可变**的原因。
然后,我们在超市购买了一个物品,我们想把它从购物清单中移除,通过使用`del`语句来实现。这里,我们提到我们想要移除清单中的哪个物品,`del`语句为我们将它从清单中移除。我们指明想从清单移除第一项,因此,我们使用`del shoplist[0]`(记住,Python从0开始计数)。
如果你想知道列表对象定义的所有方法,详见`help(list)`。
## 元组
元组是用来容纳多个对象。它类似于列表,但是没有列表提供的功能丰富。元组的一个主要特征是他们**不可变**,像字符串,即您不能修改元组。
元组是通过在一对可选的圆括号中,对象之间用逗号分隔来定义的。
元组通常用在这种情况下:当一个语句或一个用户定义的函数所使用的数据集合不会改变的时候。
例子 (保存为ds_using_tuple.py):
```python
# 我推荐使用括号表示元组的开始和结束,尽管括号是可选的。
# 毕竟显式声明比隐式声明更加直观
zoo = ('蟒蛇', '大象', '企鹅') # 记住圆括号是可选的
print('动物园中动物有数量有', len(zoo))
new_zoo = '猴子', '骆驼', zoo # 括号是可选的,最好加上
print('在新动物园中笼子的数量是', len(new_zoo))
print('在新动物园所有的动物是', new_zoo)
print('从老动物园中带来的动物是', new_zoo[2])
print('从老动物园带来最后的动物是', new_zoo[2][2])
print('在新动物园中动物的数量有', len(new_zoo)-1+len(new_zoo[2]))
```
输出:
```shell
C:\> python ds_using_tuple.py
动物园中动物有数量有 3
在新动物园中笼子的数量是 3
在新动物园所有的动物是 ('猴子', '骆驼', ('蟒蛇', '大象', '企鹅'))
从老动物园中带来的动物是 ('蟒蛇', '大象', '企鹅')
从老动物园带来最后的动物是 企鹅
在新动物园中动物的数量有 5
```
**它是如何工作的:**
变量`zoo`指向一个物品的元组。我们看到`len`函数可以用来获取元组的长度。这说明一个元组同样也是一个序列。
因为老动物园zoo将要关闭,我们现在将这些动物迁移到一个新的动物园`new_zoo`。因此,(新动物园)new_zoo的元组包含一些自有的动物以及从老动物园zoo带来的动物。请注意,在一个元组中的元组不会失去其特性。
就像列表一样,我们可以通过在一对方括号中指定数据的位置,访问元组中的数据。这被称为**索引运算符**。我们通过指定`new_zoo[2]`访问新动物园`new_zoo`中的第三项,通过指定`new_zoo[2][2]`访问新动物园`new_zoo`的第三项的第三项。一旦理解这种写法,这是非常简单的。
> **有0个或1个数据的元组**
>
> 一个空的元组由一对空的括号如`myempty = ()`组成。 然而,只有一个对象的元组并非如此简单。你必须通过在第一个对象(唯一的一个)后紧跟一个逗号来指定它,这样Python可以区分是一个元组还是包括一个对象的表达式。例如,如果你想定义一个只含一个对象为`2`的元组,你必须使用`singleton = (2 , )`。
<!-- -->
> **Perl程序员应该注意**
>
> 在一个列表中的列表不会失去其特性,也就是说并不像在Perl中夷为平地。这同样适用于在一个元组中的一个元组,或在一个列表中的元组,或在一个元组中的列表等。就Python而言,他们只是存储在另一个对象中的一个对象。
## 字典
字典就像一个地址簿,在那里你可以通过他/她的名字,找到地址或联系人详细信息。也就是说,我们使用**键**(姓名)与**值**(细节)相联系。注意,键必须是独一无二的,就好像如果有两个完全重名的人,你会困惑找不到正确的信息一样。
注意,字典的键你只能使用不可变的对象(比如字符串),字典的值可以使用不可变或可变的对象。这意味着,对于键你只能使用简单对象。
在字典中的一对键和值是通过使用冒号指定的,如,`d = {key1 : value1, key2 : value2 }`。注意,键值对用冒号分隔,彼此之间以逗号分隔,所有这些都是包含在一对大括号中。
记住,在字典中键-值对不以任何方式排序。如果你想要一个特定的顺序,那么你将不得不在使用前自己排序。
你将要使用的字典是`dict`类的对象或实例。
例子 (保存为 ds_using_dict.py):
```python
# 'ab'是英文address book(地址簿)的首个字母
ab = {
'Swaroop': 'swaroop@swaroopch.com',
'Larry': 'larry@wall.org',
'Matsumoto': 'matz@ruby-lang.org',
'Spammer': 'spammer@hotmail.com'
}
print("Swaroop的地址是", ab['Swaroop'])
# 删除一个键-值对
del ab['Spammer']
print('\n地址薄中有 {} 个联系人\n'.format(len(ab)))
for name, address in ab.items():
print('联系人 {} 的地址是 {}'.format(name, address))
# 添加一个键-值对
ab['Guido'] = 'guido@python.org'
if 'Guido' in ab:
print("\nGuido的地址是", ab['Guido'])
```
输出:
```shell
C:\> python ds_using_dict.py
Swaroop的地址是 swaroop@swaroopch.com
地址薄中有 3 个联系人
联系人 Swaroop 的地址是 swaroop@swaroopch.com
联系人 Larry 的地址是 larry@wall.org
联系人 Matsumoto 的地址是 matz@ruby-lang.org
Guido的地址是 guido@python.org
```
**它是如何工作的:**
我们使用已经讨论过的符号创建字典`ab`。然后我们通过使用在列表和元组中讨论过的索引运算符--指定关键字来访问键-值对,遵守语法简洁原则。
我们可以使用我们的老朋友——`del`语句删除键值对,我们使用索引运算符,指定字典和要删除的键,将它传递给`del`语句。对于这个操作,我们没有必要知道对应于键的值。
接下来,我们使用字典的`items`方法,访问字典的每个键-值对的。它返回一个元组的列表,每个元组包含一个键-值对。我们获取这对值并使用`for..in`循环把每一个键值对分配给相应的变量`name`和`address`,然后在for代码块中打印这些值。
我们可以通过简单地使用索引运算符来访问一个键并分配值的方式添加新的键值对,像上面的例子中我们所做的添加Guido。
我们可以使用`in`运算符来检查一个键值对是否存在。
想要查看字典`dict`类的全部方法,请参考`help(dict)`。
> **关键字参数和字典**
>
> 如果你已经在函数中使用过了关键字参数,那么你已经接触过字典了。想象一下,这个键值对是在函数定义的参数列表中指定的,而当你在函数中访问变量,它只是访问字典的一个键(在编译器设计术语中称为**符号表**)。
## 序列
列表、元组和字符串都是序列。那么什么是序列,它为什么如此特殊呢?
序列的主要特点是**成员测试**,即`in`(在)和`not in`(不在)表达式中和**索引操作**,这使我们在一个序列中能够获取一个特定的对象。
上面提到的——列表、元组和字符串这三种类型的序列,也有允许我们从一个序列中获取其一部分的**切片**操作。
例子 (保存为ds_seq.py):
```python
shoplist = ['苹果', '芒果', '胡萝卜', '香蕉']
name = 'swaroop'
# 索引或下标运算 #
print('第0项是', shoplist[0])
print('第1项是', shoplist[1])
print('第2项是', shoplist[2])
print('第3项是', shoplist[3])
print('第-1项是', shoplist[-1])
print('第-2项是', shoplist[-2])
print('第0个字符是', name[0])
# 一个列表的切片 #
print('第1项到第3项是', shoplist[1:3])
print('第2项到末尾是', shoplist[2:])
print('第1到-1项是', shoplist[1:-1])
print('开头到结尾是', shoplist[:])
# 字符串的切片 #
print('第1到第3个字符是', name[1:3])
print('第2到末尾的字符是', name[2:])
print('第1到-1的字符是', name[1:-1])
print('从头到尾的字符是', name[:])
```
输出:
```shell
C:\> python ds_seq.py
第0项是 苹果
第1项是 芒果
第2项是 胡萝卜
第3项是 香蕉
第-1项是 香蕉
第-2项是 胡萝卜
第0个字符是 s
第1项到第3项是 ['芒果', '胡萝卜']
第2项到末尾是 ['胡萝卜', '香蕉']
第1到-1项是 ['芒果', '胡萝卜']
开头到结尾是 ['苹果', '芒果', '胡萝卜', '香蕉']
第1到第3个字符是 wa
第2到末尾的字符是 aroop
第1到-1的字符是 waroo
从头到尾的字符是 swaroop
```
**它是如何工作的:**
首先,我们看看如何使用索引来获得一个序列的个别项,这也称为**下标运算**。如上所示,当你在方括号中为序列指定一个数字的时候,Python会为你取得序列中相应位置的值。记住,Python从0开始计数。因此,在序列`shoplist`中, `shoplist[0]`获取第一项,`shoplist[3]`获取第四项。
索引也可以是负数,在这种情况下,这个位置从序列的结尾开始计算。因此, `shoplist[-1]`指的是序列的最后一项, `shoplist[-2]`取倒数第二个项。
切片操作是通过指定序列的名称后面加上一个方括号,方括号中有一对用冒号分隔的数。这非常类似于你到现在一直在使用的索引操作,记住这些数字是可选的,但冒号必须有。
在切片操作中的第一个数字(在冒号前)是切片开始的位置,第二个数字(在冒号后)是切片停止的位置。如果第一个数字没有指定,Python会从序列开头开始,如果没有第二个数字,Python会在序列的末尾停止。注意,返回的切片在**开始位置**开始,在**结束位置前**结束。也就是说,返回的切片包含开始位置,但不包含结束位置。
因此, `shoplist[1:3]` 返回序列的切片从位置1开始,包括位置2,但是在位置3停止,因此,返回两个元素的切片。同样,`shoplist[:]`返回整个序列的一个副本。
你也可以使用负值做切片。负数用于从序列的结尾开始。例如,`shoplist[:-1]` 将返回一个不包括序列最后一项,但包含了其它一切的切片。
你也可以为切片提供第三个参数,这是切片的**步长**(默认情况下,步长为1):
```python
>>> shoplist = ['苹果', '芒果', '胡萝卜', '香蕉']
>>> shoplist[::1]
['苹果', '芒果', '胡萝卜', '香蕉']
>>> shoplist[::2]
['苹果', '胡萝卜']
>>> shoplist[::3]
['苹果', '香蕉']
>>> shoplist[::-1]
['香蕉', '胡萝卜', '芒果', '苹果']
```
注意,当步长是2时,我们获得位置0、2、……的元素,当步长是3时,我们获得位置是0、3等元素。
使用Python解释器提示符,尝试指定切片的不同组合,以便你可以立刻看到结果。序列的一大好处是,你可以以同样的方式访问元组、列表和字符串!
## 集合
集合是简单对象的**无序**集合,用于一个集合中对象是否存在比它的顺序或有多少个更重要的时候。
集合的运算,包括成员测试、是否是另一个集合的子集、获取两个集合的交集等等。
```python
>>> bri = set(['巴西', '俄罗斯', '印度'])
>>> '印度' in bri
True
>>> '美国' in bri
False
>>> bric = bri.copy()
>>> bric.add('中国')
>>> bric.issuperset(bri)
True
>>> bri.remove('俄罗斯')
>>> bri & bric # 或者 bri.intersection(bric)
{'巴西', '印度'}
```
**它是如何工作的:**
如果你还记得学校教的集合论的原理,那么这个例子是非常容易理解的。如果你不记得了,百度一下“集合论”或者“文氏图”,可以更好的帮助你理解Python中的集合。
## 引用
当你创建一个对象,并赋给它一个值,该变量只是一个指向对象的**引用**,并不代表对象本身!也就是说,变量名称指向你电脑的内存中的存储对象的地址,这就是所谓的把**命名绑定**。
一般来说,你不需要担心这个,但是关于引用有一个需要你注意的小问题。
例子 (保存为ds_reference.py):
```python
print('简单的分配')
shoplist = ['苹果', '芒果', '胡萝卜', '香蕉']
# mylist是指向同一对象的另一个名字!
mylist = shoplist
# 我买到了第一项物品,因此我从清单中移除它
del shoplist[0]
print('shoplist是', shoplist)
print('mylist是', mylist)
# 注意shoplist和mylist都打印没有‘苹果’的相同的清单
# 证明它们指向相同的对象
print('通过制作完整的切片复制')
# 通过制作完整的切片复制
mylist = shoplist[:]
# 移除第一项
del mylist[0]
print('shoplist是', shoplist)
print('mylist是', mylist)
# 注意,现在两个清单不同
```
输出:
```shell
C:\> python ds_reference.py
简单的分配
shoplist是 ['芒果', '胡萝卜', '香蕉']
mylist是 ['芒果', '胡萝卜', '香蕉']
通过制作完整的切片复制
shoplist是 ['芒果', '胡萝卜', '香蕉']
mylist是 ['胡萝卜', '香蕉']
```
**它是如何工作的:**
在注释中有更多有用的解释。
记住,如果你想要复制一个列表或其他类型的序列或复杂的对象(而不是简单的**对象**如整数),那么您必须使用切片操作复制。如果你只是赋值给另一个变量名,两个变量将“引用”到同一个对象,如果你不小心,这可能会引起麻烦。
> **Per程序员需要注意**
>
> 记住,列表的赋值并**不**创建一个副本。你必须使用切片操作复制序列。
## 更多关于字符串的特性
之前我们已经详细讨论了字符串,在这里我们要了解更多关于字符串的特性。你知道吗,字符串也是对象,也有很多对象的方法--从检查的字符串是否包含一个子串,到从字符串前后去掉空格。实际上你已经使用过一个字符串的方法了--`format`函数
在程序中你使用的字符串都是`str`类的对象,在下面的例子中将演示这个类的一些有用的函数,想要查看字符串函数的完整列表,请看`help(str)`。
例子 (保存为 ds_str_methods.py):
```python
# 这是一个字符串对象
name = 'Swaroop'
if name.startswith('Swa'):
print('是的,字符串以"Swa"开始')
if 'a' in name:
print('是的,它包含字符串"a"')
if name.find('war') != -1:
print('是的,它包含字符串"war"')
delimiter = '_*_'
mylist = ['巴西', '俄罗斯', '印度', '中国']
print(delimiter.join(mylist))
```
输出:
```shell
C:\> python ds_str_methods.py
是的,字符串以"Swa"开始
是的,它包含字符串"a"
是的,它包含字符串"war"
巴西_*_俄罗斯_*_印度_*_中国
```
**它是如何工作的:**
在这里,我们看到字符串的很多函数在起作用。`startswith`方法是用来找出字符串是否以给定的字符串开始的。`in`运算符是用来检查一个给定的字符串是否是一个字符串的一部分。
`find`方法用于定位指定的子串在字符串内的位置,如果不能成功找到子串它返回-1。`str`类也有一个整洁的方法来`join`(连接)一个字符串的序列,用作为分隔符的字符串连接序列中每个元素,返回一个由它生成的巨大的字符串。
## 小结
我们详细探索了Python各种内建的数据结构。写程序的时候,这些数据结构是至关重要的。
现在,我们有很多Python的基本知识已经就位。下面,我们看看如何设计和编写一个真实的Python程序。
- 开始学习
- 搭建Python开发环境
- 简明Python教程
- 致敬
- 前言
- 关于Python
- 安装
- 第一步
- 基础
- 运算符和表达式
- 控制流
- 函数
- 模块
- 数据结构
- 实战案例
- 面向对象编程
- 输入与输出
- 异常处理
- 标准库
- 更多
- 继续学习
- 附录:免费/自由和开放源码软件
- 附录: 关于
- 附录: 版本历史
- 附录: 翻译
- 附录: 参与翻译工作
- 反馈
- Django Step Sy Step
- 第一讲 从简单到复杂
- 第二讲 做加法的例子
- 第三讲 使用Template
- 第四讲 生成csv格式文件
- 第五讲 session示例
- 第六讲 wiki的例子
- 第七讲 通讯录的例子
- 第八讲 文件导入和导出
- 第九讲 通讯录的美化
- 第十讲 扩展django的模板
- 第十一讲 用户管理
- 第十二讲 搜索和部署
- 第十三讲 Ajax的实现(一)
- 第十四讲 Ajax的实现(二)
- 第十五讲 i18n的一个简单实现
- 第十六讲 自定义Calendar Tag
- 第十七讲 View,Template和Tag
- Django开发实战
- Python开发规范
- Django项目的gitignore
- 怎样配置开发环境的settings
- 如何使用Django和Vue.js构建项目
- 使用WebSocket开发网页聊天室
- 怎样使Django Admin显示中文
- 怎样使Model在Admin界面中显示中文
- 使用Django Admin怎样上传并显示图片
- 解决Django模板和Vue指令花括号冲突的问题
- 使用Django和Vue开发微信公众号
- 使用Django和Vue调用微信JSSDK开发微信支付