简介
在这个实验中,你将学习 Python 编程中迭代的基本概念。迭代使你能够高效地处理列表、元组和字典等序列中的元素。掌握迭代技术可以显著提升你的 Python 编码技能。
你将探索几种强大的 Python 迭代技术,包括基本的 for 循环迭代、序列解包、使用 enumerate() 和 zip() 等内置函数,以及利用生成器表达式来提高内存使用效率。
在这个实验中,你将学习 Python 编程中迭代的基本概念。迭代使你能够高效地处理列表、元组和字典等序列中的元素。掌握迭代技术可以显著提升你的 Python 编码技能。
你将探索几种强大的 Python 迭代技术,包括基本的 for 循环迭代、序列解包、使用 enumerate() 和 zip() 等内置函数,以及利用生成器表达式来提高内存使用效率。
在这一步中,我们将探索使用 for 循环进行基本迭代以及 Python 中的序列解包。迭代是编程中的一个基本概念,它允许你逐个遍历序列中的每个元素。而序列解包则能让你以一种便捷的方式将序列中的单个元素赋值给变量。
让我们从从 CSV 文件加载一些数据开始。CSV(逗号分隔值)是一种用于存储表格数据的常见文件格式。首先,你需要在 WebIDE 中打开一个终端并启动 Python 解释器。这样你就可以交互式地运行 Python 代码。
cd ~/project
python3
现在你已经进入了 Python 解释器,可以执行以下 Python 代码来从 portfolio.csv 文件中读取数据。首先,我们导入 csv 模块,它提供了处理 CSV 文件的功能。然后,我们打开文件并创建一个 csv.reader 对象来读取数据。我们使用 next 函数获取列标题,并将剩余的数据转换为列表。最后,我们使用 pprint 模块中的 pprint 函数以更易读的格式打印行。
import csv
f = open('portfolio.csv')
f_csv = csv.reader(f)
headers = next(f_csv) ## Get the column headers
rows = list(f_csv) ## Convert the remaining data to a list
from pprint import pprint
pprint(rows) ## Pretty print the rows
你应该会看到类似以下的输出:
[['AA', '100', '32.20'],
['IBM', '50', '91.10'],
['CAT', '150', '83.44'],
['MSFT', '200', '51.23'],
['GE', '95', '40.37'],
['MSFT', '50', '65.10'],
['IBM', '100', '70.44']]
for 循环进行基本迭代Python 中的 for 语句用于遍历任何数据序列,如列表、元组或字符串。在我们的例子中,我们将使用它来遍历从 CSV 文件加载的数据行。
for row in rows:
print(row)
这段代码将遍历 rows 列表中的每一行并打印出来。你会看到 CSV 文件中的每一行数据逐个打印出来。
['AA', '100', '32.20']
['IBM', '50', '91.10']
['CAT', '150', '83.44']
['MSFT', '200', '51.23']
['GE', '95', '40.37']
['MSFT', '50', '65.10']
['IBM', '100', '70.44']
Python 允许你在 for 循环中直接解包序列。当你知道序列中每个元素的结构时,这非常有用。在我们的例子中,rows 列表中的每一行包含三个元素:名称、股票数量和价格。我们可以在 for 循环中直接解包这些元素。
for name, shares, price in rows:
print(name, shares, price)
这段代码将把每一行解包到变量 name、shares 和 price 中,然后打印出来。你会看到数据以更易读的格式打印出来。
AA 100 32.20
IBM 50 91.10
CAT 150 83.44
MSFT 200 51.23
GE 95 40.37
MSFT 50 65.10
IBM 100 70.44
如果你不需要某些值,可以使用 _ 作为占位符来表示你不关心这些值。例如,如果你只想打印名称和价格,可以使用以下代码:
for name, _, price in rows:
print(name, price)
这段代码将忽略每一行中的第二个元素,只打印名称和价格。
AA 32.20
IBM 91.10
CAT 83.44
MSFT 51.23
GE 40.37
MSFT 65.10
IBM 70.44
* 运算符进行扩展解包对于更高级的解包,你可以使用 * 运算符作为通配符。这允许你将多个元素收集到一个列表中。让我们使用这种技术按名称对数据进行分组。
from collections import defaultdict
byname = defaultdict(list)
for name, *data in rows:
byname[name].append(data)
## Print the data for IBM
print(byname['IBM'])
## Iterate through IBM's data
for shares, price in byname['IBM']:
print(shares, price)
在这段代码中,我们首先从 collections 模块导入 defaultdict 类。defaultdict 是一种字典,如果键不存在,它会自动创建一个新值(在这种情况下是一个空列表)。然后,我们使用 * 运算符将除第一个元素之外的所有元素收集到一个名为 data 的列表中。我们将这个列表存储在 byname 字典中,按名称分组。最后,我们打印 IBM 的数据并遍历它以打印股票数量和价格。
输出:
[['50', '91.10'], ['100', '70.44']]
50 91.10
100 70.44
在这个例子中,*data 将除第一个元素之外的所有元素收集到一个列表中,然后我们将其存储在按名称分组的字典中。这是一种处理可变长度序列数据的强大技术。
enumerate() 和 zip() 函数在这一步中,我们将探索 Python 中两个非常有用的内置函数,它们对于迭代操作至关重要:enumerate() 和 zip()。当你处理序列时,这些函数可以显著简化你的代码。
enumerate() 进行计数当你遍历一个序列时,你可能经常需要跟踪每个元素的索引或位置。这时,enumerate() 函数就派上用场了。enumerate() 函数接受一个序列作为输入,并为该序列中的每个元素返回 (索引,值) 对。
如果你在上一步的 Python 解释器中继续操作,可以直接使用。如果没有,请开启一个新的会话。如果你要重新开始,可以按以下方式设置数据:
## If you're starting a new session, reload the data first:
## import csv
## f = open('portfolio.csv')
## f_csv = csv.reader(f)
## headers = next(f_csv)
## rows = list(f_csv)
## Use enumerate to get row numbers
for rowno, row in enumerate(rows):
print(rowno, row)
当你运行上述代码时,enumerate(rows) 函数会生成索引(从 0 开始)和 rows 序列中对应行的对。然后,for 循环将这些对解包到变量 rowno 和 row 中,并将它们打印出来。
输出:
0 ['AA', '100', '32.20']
1 ['IBM', '50', '91.10']
2 ['CAT', '150', '83.44']
3 ['MSFT', '200', '51.23']
4 ['GE', '95', '40.37']
5 ['MSFT', '50', '65.10']
6 ['IBM', '100', '70.44']
我们可以通过将 enumerate() 与解包结合使用,让代码更具可读性。解包允许我们直接将序列的元素分配给各个变量。
for rowno, (name, shares, price) in enumerate(rows):
print(rowno, name, shares, price)
在这段代码中,我们在 (name, shares, price) 周围使用了额外的一对括号,以正确解包行数据。enumerate(rows) 仍然为我们提供索引和行,但现在我们将行解包到 name、shares 和 price 变量中。
输出:
0 AA 100 32.20
1 IBM 50 91.10
2 CAT 150 83.44
3 MSFT 200 51.23
4 GE 95 40.37
5 MSFT 50 65.10
6 IBM 100 70.44
zip() 配对数据zip() 函数是 Python 中的另一个强大工具。它用于组合多个序列中对应的元素。当你将多个序列传递给 zip() 时,它会创建一个迭代器,生成元组,每个元组包含输入序列中相同位置的元素。
让我们看看如何将 zip() 与我们一直在处理的 headers 和 row 数据结合使用。
## Recall the headers variable from earlier
print(headers) ## Should show ['name', 'shares', 'price']
## Get the first row
row = rows[0]
print(row) ## Should show ['AA', '100', '32.20']
## Use zip to pair column names with values
for col, val in zip(headers, row):
print(col, val)
在这段代码中,zip(headers, row) 接受 headers 序列和 row 序列,并将它们对应的元素配对。然后,for 循环将这些对解包到 col(用于 headers 中的列名)和 val(用于 row 中的值)中,并将它们打印出来。
输出:
['name', 'shares', 'price']
['AA', '100', '32.20']
name AA
shares 100
price 32.20
zip() 的一个非常常见的用途是从键值对创建字典。在 Python 中,字典是键值对的集合。
## Create a dictionary from headers and row values
record = dict(zip(headers, row))
print(record)
在这里,zip(headers, row) 创建列名和值的对,dict() 函数将这些对转换为字典。
输出:
{'name': 'AA', 'shares': '100', 'price': '32.20'}
我们可以扩展这个想法,将 rows 序列中的所有行转换为字典。
## Convert all rows to dictionaries
for row in rows:
record = dict(zip(headers, row))
print(record)
在这个循环中,对于 rows 中的每一行,我们使用 zip(headers, row) 创建键值对,然后使用 dict() 将这些对转换为字典。这种技术在数据处理应用中非常常见,特别是在处理第一行包含标题的 CSV 文件时。
输出:
{'name': 'AA', 'shares': '100', 'price': '32.20'}
{'name': 'IBM', 'shares': '50', 'price': '91.10'}
{'name': 'CAT', 'shares': '150', 'price': '83.44'}
{'name': 'MSFT', 'shares': '200', 'price': '51.23'}
{'name': 'GE', 'shares': '95', 'price': '40.37'}
{'name': 'MSFT', 'shares': '50', 'price': '65.10'}
{'name': 'IBM', 'shares': '100', 'price': '70.44'}
在这一步中,我们将探索生成器表达式。当你在 Python 中处理大型数据集时,它们非常有用。它们可以让你的代码在内存使用上更加高效,这在处理大量数据时至关重要。
生成器表达式与列表推导式类似,但有一个关键区别。当你使用列表推导式时,Python 会一次性创建一个包含所有结果的列表。这可能会占用大量内存,特别是在处理大型数据集时。而生成器表达式会根据需要逐个生成结果。这意味着它不需要一次性将所有结果存储在内存中,从而可以节省大量内存。
让我们看一个简单的例子,了解它是如何工作的:
## Start a new Python session if needed
## python3
## List comprehension (creates a list in memory)
nums = [1, 2, 3, 4, 5]
squares_list = [x*x for x in nums]
print(squares_list)
## Generator expression (creates a generator object)
squares_gen = (x*x for x in nums)
print(squares_gen) ## This doesn't print the values, just the generator object
## Iterate through the generator to get values
for n in squares_gen:
print(n)
当你运行这段代码时,你会看到以下输出:
[1, 4, 9, 16, 25]
<generator object <genexpr> at 0x7f...>
1
4
9
16
25
关于生成器,有一点需要注意:它们只能被迭代一次。一旦你遍历完生成器中的所有值,它就会耗尽,你无法再次获取这些值。
## Try to iterate again over the same generator
for n in squares_gen:
print(n) ## Nothing will be printed, as the generator is already exhausted
你还可以使用 next() 函数手动逐个从生成器中获取值。
## Create a fresh generator
squares_gen = (x*x for x in nums)
## Get values one by one
print(next(squares_gen)) ## 1
print(next(squares_gen)) ## 4
print(next(squares_gen)) ## 9
当生成器中没有更多值时,调用 next() 会引发 StopIteration 异常。
yield 的生成器函数对于更复杂的生成器逻辑,你可以使用 yield 语句编写生成器函数。生成器函数是一种特殊类型的函数,它使用 yield 逐个返回值,而不是一次性返回单个结果。
def squares(nums):
for x in nums:
yield x*x
## Use the generator function
for n in squares(nums):
print(n)
当你运行这段代码时,你会看到以下输出:
1
4
9
16
25
现在,让我们看看在处理大型数据集时,生成器表达式如何节省内存。我们将使用 CTA 公交数据文件,它相当大。
首先,让我们尝试一种内存密集型的方法:
import tracemalloc
tracemalloc.start()
import readrides
rows = readrides.read_rides_as_dicts('ctabus.csv')
rt22 = [row for row in rows if row['route'] == '22']
max_row = max(rt22, key=lambda row: int(row['rides']))
print(max_row)
## Check memory usage
current, peak = tracemalloc.get_traced_memory()
print(f"Current memory usage: {current / 1024 / 1024:.2f} MB")
print(f"Peak memory usage: {peak / 1024 / 1024:.2f} MB")
现在,退出 Python 并重新启动,以便与基于生成器的方法进行比较:
exit() python3
import tracemalloc
tracemalloc.start()
import csv
f = open('ctabus.csv')
f_csv = csv.reader(f)
headers = next(f_csv)
## Use generator expressions for all operations
rows = (dict(zip(headers, row)) for row in f_csv)
rt22 = (row for row in rows if row['route'] == '22')
max_row = max(rt22, key=lambda row: int(row['rides']))
print(max_row)
## Check memory usage
current, peak = tracemalloc.get_traced_memory()
print(f"Current memory usage: {current / 1024 / 1024:.2f} MB")
print(f"Peak memory usage: {peak / 1024 / 1024:.2f} MB")
你应该会注意到这两种方法在内存使用上有显著差异。基于生成器的方法会逐步处理数据,而不是一次性将所有数据加载到内存中,这样在内存使用上更加高效。
当生成器表达式与 sum()、min()、max()、any() 和 all() 等处理整个序列并产生单个结果的函数结合使用时,特别有用。
让我们看一些例子:
from readport import read_portfolio
portfolio = read_portfolio('portfolio.csv')
## Calculate the total value of the portfolio
total_value = sum(s['shares']*s['price'] for s in portfolio)
print(f"Total portfolio value: {total_value}")
## Find the minimum number of shares in any holding
min_shares = min(s['shares'] for s in portfolio)
print(f"Minimum shares in any holding: {min_shares}")
## Check if any stock is IBM
has_ibm = any(s['name'] == 'IBM' for s in portfolio)
print(f"Portfolio contains IBM: {has_ibm}")
## Check if all stocks are IBM
all_ibm = all(s['name'] == 'IBM' for s in portfolio)
print(f"All stocks are IBM: {all_ibm}")
## Count IBM shares
ibm_shares = sum(s['shares'] for s in portfolio if s['name'] == 'IBM')
print(f"Total IBM shares: {ibm_shares}")
生成器表达式在字符串操作中也很有用。以下是如何连接元组中的值:
s = ('GOOG', 100, 490.10)
## This would fail: ','.join(s)
## Use a generator expression to convert all items to strings
joined = ','.join(str(x) for x in s)
print(joined) ## Output: 'GOOG,100,490.1'
在这些例子中,使用生成器表达式的关键优势在于不会创建中间列表,从而使代码在内存使用上更加高效。
在本次实验中,你学习了几种强大的 Python 迭代技术。首先,你掌握了基本的迭代和序列解包,使用 for 循环遍历序列并将其解包为单个变量。其次,你探索了像 enumerate() 这样的内置函数,用于在迭代过程中跟踪索引,以及 zip() 函数,用于将不同序列中的元素配对。
这些技术是高效 Python 编程的基础。它们使你能够编写更简洁、易读且内存使用更高效的代码。通过掌握这些迭代模式,你可以在 Python 项目中更有效地处理数据处理任务。