Python 序列基础

Intermediate

This tutorial is from open-source community. Access the source code

简介

Python 序列是有序的项集合。它们由整数索引。

这是一个实验(Guided Lab),提供逐步指导来帮助你学习和实践。请仔细按照说明完成每个步骤,获得实际操作经验。根据历史数据,这是一个 中级 级别的实验,完成率为 80%。获得了学习者 100% 的好评率。

序列数据类型

Python 有三种序列数据类型。

  • 字符串:'Hello'。字符串是字符序列。
  • 列表:[1, 4, 5]
  • 元组:('GOOG', 100, 490.1)

所有序列都是有序的,由整数索引,并且有一个长度。

a = 'Hello'               ## 字符串
b = [1, 4, 5]             ## 列表
c = ('GOOG', 100, 490.1)  ## 元组

## 索引顺序
a[0]                      ## 'H'
b[-1]                     ## 5
c[1]                      ## 100

## 序列长度
len(a)                    ## 5
len(b)                    ## 3
len(c)                    ## 3

序列可以复制:s * n

>>> a = 'Hello'
>>> a * 3
'HelloHelloHello'
>>> b = [1, 2, 3]
>>> b * 2
[1, 2, 3, 1, 2, 3]
>>>

相同类型的序列可以连接:s + t

>>> a = (1, 2, 3)
>>> b = (4, 5)
>>> a + b
(1, 2, 3, 4, 5)
>>>
>>> c = [1, 5]
>>> a + c
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: can only concatenate tuple (not "list") to tuple

切片

切片是指从一个序列中取出一个子序列。语法是 s[start:end]。其中 startend 是你想要的子序列的索引。

a = [0,1,2,3,4,5,6,7,8]

a[2:5]    ## [2,3,4]
a[-5:]    ## [4,5,6,7,8]
a[:3]     ## [0,1,2]
  • 索引 startend 必须是整数。
  • 切片包括结束值。这类似于数学中的半开区间。
  • 如果省略索引,它们默认为列表的开头或结尾。

切片重新赋值

对于列表,切片可以重新赋值和删除。

## 重新赋值
a = [0,1,2,3,4,5,6,7,8]
a[2:4] = [10,11,12]       ## [0,1,10,11,12,4,5,6,7,8]

注意:重新赋值的切片不需要具有相同的长度。

## 删除
a = [0,1,2,3,4,5,6,7,8]
del a[2:4]                ## [0,1,4,5,6,7,8]

序列归约

有一些常用函数可将序列归约为单个值。

>>> s = [1, 2, 3, 4]
>>> sum(s)
10
>>> min(s)
1
>>> max(s)
4
>>> t = ['Hello', 'World']
>>> max(t)
'World'
>>>

遍历序列

for 循环会遍历序列中的元素。

>>> s = [1, 4, 9, 16]
>>> for i in s:
...     print(i)
...
1
4
9
16
>>>

在循环的每次迭代中,你会得到一个新的元素来处理。这个新值会被赋给迭代变量。在这个例子中,迭代变量是 x

for x in s:         ## `x` 是一个迭代变量
  ...statements

在每次迭代中,迭代变量的前一个值会被覆盖(如果有的话)。循环结束后,该变量会保留最后一个值。

break 语句

你可以使用 break 语句提前退出循环。

for name in namelist:
    if name == 'Jake':
        break
  ...
  ...
statements

break 语句执行时,它会退出循环并继续执行下一个 statementsbreak 语句仅适用于最内层的循环。如果这个循环在另一个循环内部,它不会中断外层循环。

continue 语句

要跳过一个元素并移动到下一个元素,请使用 continue 语句。

for line in lines:
    if line == '\n':    ## 跳过空行
        continue
    ## 更多语句
 ...

当当前项目不感兴趣或在处理过程中需要忽略时,这很有用。

遍历整数

如果你需要进行计数,请使用 range()

for i in range(100):
    ## i = 0,1,...,99

其语法为 range([start,] end [,step])

for i in range(100):
    ## i = 0,1,...,99
for j in range(10,20):
    ## j = 10,11,..., 19
for k in range(10,50,2):
    ## k = 10,12,...,48
    ## 注意它是以 2 为步长计数,而非 1。
  • 结束值永远不包含在内。这与切片的行为一致。
  • start 是可选的。默认值为 0
  • step 是可选的。默认值为 1
  • range() 根据需要计算值。它实际上并不存储大量数字。

enumerate() 函数

enumerate 函数会在迭代时额外添加一个计数器值。

names = ['Elwood', 'Jake', 'Curtis']
for i, name in enumerate(names):
    ## 循环时 i = 0,name = 'Elwood'
    ## i = 1,name = 'Jake'
    ## i = 2,name = 'Curtis'

一般形式为 enumerate(sequence [, start = 0])start 是可选的。使用 enumerate() 的一个很好的例子是在读取文件时跟踪行号:

with open(filename) as f:
    for lineno, line in enumerate(f, start=1):
     ...

最终,enumerate 只是以下代码的一个便捷写法:

i = 0
for x in s:
    statements
    i += 1

使用 enumerate 可以减少输入量,并且运行速度稍快一些。

针对元组的 for 循环

你可以使用多个迭代变量进行迭代。

points = [
  (1, 4),(10, 40),(23, 14),(5, 6),(7, 8)
]
for x, y in points:
    ## 循环时 x = 1,y = 4
    ##            x = 10,y = 40
    ##            x = 23,y = 14
    ##           ...

当使用多个变量时,每个元组会被“解包”成一组迭代变量。变量的数量必须与每个元组中的元素数量相匹配。

zip() 函数

zip 函数接受多个序列,并创建一个将它们组合在一起的迭代器。

columns = ['name','shares', 'price']
values = ['GOOG', 100, 490.1 ]
pairs = zip(columns, values)
## ('name', 'GOOG'), ('shares', 100), ('price', 490.1)

要获得结果,你必须进行迭代。如前所示,你可以使用多个变量来解包元组。

for column, value in pairs:
  ...

zip 的一个常见用途是创建用于构建字典的键/值对。

d = dict(zip(columns, values))

练习 2.13:计数

尝试一些基本的计数示例:

>>> for n in range(10):            ## 从 0 计数到 9
        print(n, end=' ')

0 1 2 3 4 5 6 7 8 9
>>> for n in range(10,0,-1):       ## 从 10 计数到 1
        print(n, end=' ')

10 9 8 7 6 5 4 3 2 1
>>> for n in range(0,10,2):        ## 从 0 开始,以 2 为步长计数到 8
        print(n, end=' ')

0 2 4 6 8
>>>

练习 2.14:更多序列操作

交互式地试验一些序列归约操作。

>>> data = [4, 9, 1, 25, 16, 100, 49]
>>> min(data)
1
>>> max(data)
100
>>> sum(data)
204
>>>

尝试遍历数据。

>>> for x in data:
        print(x)

4
9
...
>>> for n, x in enumerate(data):
        print(n, x)

0 4
1 9
2 1
...
>>>

有时新手会在一些糟糕的代码片段中使用for语句、len()range(),这些代码片段看起来就像是从一个生锈的 C 程序深处冒出来的。

>>> for n in range(len(data)):
        print(data[n])

4
9
1
...
>>>

别这么做!不仅读起来会让每个人都眼睛难受,而且它在内存使用上效率低下,运行速度也慢得多。如果你想遍历数据,就用普通的for循环。如果你出于某种原因需要索引,就使用enumerate()

练习 2.15:一个实用的 enumerate() 示例

回想一下,文件missing.csv包含一个股票投资组合的数据,但有一些行的数据缺失。使用enumerate(),修改你的pcost.py程序,使其在遇到错误输入时打印出行号和警告信息。

>>> cost = portfolio_cost('/home/labex/project/missing.csv')
第4行:无法转换:['MSFT', '', '51.23']
第7行:无法转换:['IBM', '', '70.44']
>>>

要做到这一点,你需要修改代码的几个部分。

...
for rowno, row in enumerate(rows, start=1):
    try:
     ...
    except ValueError:
        print(f'第{rowno}行:错误的行:{row}')

练习 2.16:使用 zip() 函数

在文件portfolio.csv中,第一行包含列标题。在之前所有的代码中,我们都忽略了它们。

>>> f = open('/home/labex/project/portfolio.csv')
>>> rows = csv.reader(f)
>>> headers = next(rows)
>>> headers
['name','shares', 'price']
>>>

然而,如果你能将这些标题用于一些有用的事情呢?这就是zip()函数发挥作用的地方。首先尝试这样做,将文件标题与一行数据配对:

>>> row = next(rows)
>>> row
['AA', '100', '32.20']
>>> list(zip(headers, row))
[ ('name', 'AA'), ('shares', '100'), ('price', '32.20') ]
>>>

注意zip()是如何将列标题与列值配对的。我们在这里使用list()将结果转换为列表,以便你能看到它。通常,zip()创建一个迭代器,必须通过for循环来使用它。

这种配对是构建字典的中间步骤。现在试试这个:

>>> record = dict(zip(headers, row))
>>> record
{'price': '32.20', 'name': 'AA','shares': '100'}
>>>

这种转换是处理大量数据文件时最有用的技巧之一。例如,假设你想让pcost.py程序适用于各种输入文件,而不考虑名称、股票数量和价格所在的实际列号。

修改pcost.py中的portfolio_cost()函数,使其如下所示:

## pcost.py

def portfolio_cost(filename):
  ...
        for rowno, row in enumerate(rows, start=1):
            record = dict(zip(headers, row))
            try:
                nshares = int(record['shares'])
                price = float(record['price'])
                total_cost += nshares * price
            ## 这捕获了上面 int() 和 float() 转换中的错误
            except ValueError:
                print(f'第{rowno}行:错误的行:{row}')
  ...

现在,在一个完全不同的数据文件portfoliodate.csv上尝试你的函数,该文件如下所示:

name,date,time,shares,price
"AA","6/11/2007","9:50am",100,32.20
"IBM","5/13/2007","4:20pm",50,91.10
"CAT","9/23/2006","1:30pm",150,83.44
"MSFT","5/17/2007","10:30am",200,51.23
"GE","2/1/2006","10:45am",95,40.37
"MSFT","10/31/2006","12:05pm",50,65.10
"IBM","7/9/2006","3:15pm",100,70.44
>>> portfolio_cost('/home/labex/project/portfoliodate.csv')
44671.15
>>>

如果你做对了,你会发现即使数据文件的列格式与之前完全不同,你的程序仍然可以工作。这很酷!

这里所做的更改很细微,但很重要。portfolio_cost()不再硬编码为读取单一固定文件格式,新版本可以读取任何 CSV 文件并从中提取感兴趣的值。只要文件有所需的列,代码就会起作用。

修改你在 2.3 节中编写的report.py程序,使其使用相同的技术来提取列标题。

尝试在portfoliodate.csv文件上运行report.py程序,看看它是否能产生与之前相同的答案。

练习 2.17:反转字典

字典将键映射到值。例如,一个股票价格的字典。

>>> prices = {
        'GOOG' : 490.1,
        'AA' : 23.45,
        'IBM' : 91.1,
        'MSFT' : 34.23
    }
>>>

如果你使用items()方法,你可以得到(键, 值)对:

>>> prices.items()
dict_items([('GOOG', 490.1), ('AA', 23.45), ('IBM', 91.1), ('MSFT', 34.23)])
>>>

然而,如果你想要得到一个(值, 键)对的列表呢?提示:使用zip()

>>> pricelist = list(zip(prices.values(),prices.keys()))
>>> pricelist
[(490.1, 'GOOG'), (23.45, 'AA'), (91.1, 'IBM'), (34.23, 'MSFT')]
>>>

你为什么要这样做呢?一方面,它允许你对字典数据进行某些类型的数据处理。

>>> min(pricelist)
(23.45, 'AA')
>>> max(pricelist)
(490.1, 'GOOG')
>>> sorted(pricelist)
[(23.45, 'AA'), (34.23, 'MSFT'), (91.1, 'IBM'), (490.1, 'GOOG')]
>>>

这也说明了元组的一个重要特性。在进行比较时,元组会从第一个元素开始逐个元素地进行比较。类似于字符串逐个字符进行比较的方式。

zip()经常用于像这样需要将来自不同地方的数据配对的情况。例如,将列名与列值配对,以便创建一个命名值的字典。

请注意,zip()不限于配对。例如,你可以将它与任意数量的输入列表一起使用:

>>> a = [1, 2, 3, 4]
>>> b = ['w', 'x', 'y', 'z']
>>> c = [0.2, 0.4, 0.6, 0.8]
>>> list(zip(a, b, c))
[(1, 'w', 0.2), (2, 'x', 0.4), (3, 'y', 0.6), (4, 'z', 0.8))]
>>>

另外,要注意一旦最短的输入序列耗尽,zip()就会停止。

>>> a = [1, 2, 3, 4, 5, 6]
>>> b = ['x', 'y', 'z']
>>> list(zip(a,b))
[(1, 'x'), (2, 'y'), (3, 'z')]
>>>

总结

恭喜你!你已经完成了序列实验。你可以在 LabEx 中练习更多实验来提高你的技能。