您当前位置: 首页 深入 Python 3

难度级别: ♦♦♢♢♢

列表推导

我们的想象力被扩展到极致,不是像在虚构中那样去想象那些不存在的东西,而是去理解那些存在的东西。
— 理查德·费曼

 

深入

每种编程语言都有一个特性,一个刻意简化的复杂事物。如果您来自其他语言,您可能会很容易错过它,因为您的旧语言没有简化那个事物(因为它正忙着简化其他事物)。本章将向您介绍列表推导、字典推导和集合推导:围绕一种非常强大的技术的三个相关概念。但首先,我想先绕个弯,介绍两个模块,它们将帮助您浏览本地文件系统。

使用文件和目录

Python 3 带有一个名为 os 的模块,它代表“操作系统”。os 模块包含大量函数,用于获取有关本地目录、文件、进程和环境变量的信息,在某些情况下,还可以操作这些信息。Python 尽其所能为所有支持的操作系统提供统一的 API,以便您的程序可以在任何计算机上运行,尽可能减少平台特定的代码。

当前工作目录

当您刚开始使用 Python 时,您将在 Python Shell 中花费大量时间。在本书中,您将看到如下示例

  1. 导入 examples 文件夹 中的其中一个模块
  2. 调用该模块中的函数
  3. 解释结果

如果您不了解当前工作目录,步骤 1 可能会因 ImportError 而失败。为什么?因为 Python 会在 导入搜索路径 中查找示例模块,但它不会找到它,因为 examples 文件夹不是搜索路径中的目录之一。要解决这个问题,您可以执行以下两种操作之一

  1. examples 文件夹添加到导入搜索路径
  2. 将当前工作目录更改为 examples 文件夹

当前工作目录是 Python 始终在内存中持有的一个不可见属性。始终存在一个当前工作目录,无论您是在 Python Shell 中,从命令行运行自己的 Python 脚本,还是在某个 Web 服务器上运行 Python CGI 脚本。

os 模块包含两个用于处理当前工作目录的函数。

>>> import os 
>>> print(os.getcwd()) 
C:\Python31
>>> os.chdir('/Users/pilgrim/diveintopython3/examples') 
>>> print(os.getcwd()) 
C:\Users\pilgrim\diveintopython3\examples
  1. os 模块随 Python 提供;您可以随时随地导入它。
  2. 使用 os.getcwd() 函数获取当前工作目录。当您运行图形化 Python Shell 时,当前工作目录从 Python Shell 可执行文件所在的目录开始。在 Windows 上,这取决于您安装 Python 的位置;默认目录为 c:\Python31。如果您从命令行运行 Python Shell,则当前工作目录从您运行 python3 时所在的目录开始。
  3. 使用 os.chdir() 函数更改当前工作目录。
  4. 当我调用 os.chdir() 函数时,我使用了 Linux 样式的路径名(正斜杠,没有驱动器号),即使我在 Windows 上。这是 Python 试图掩盖不同操作系统之间差异的地方之一。

使用文件名和目录名

说到目录,我想指出 os.path 模块。os.path 包含用于操作文件名和目录名的函数。

>>> import os
>>> print(os.path.join('/Users/pilgrim/diveintopython3/examples/', 'humansize.py')) 
/Users/pilgrim/diveintopython3/examples/humansize.py
>>> print(os.path.join('/Users/pilgrim/diveintopython3/examples', 'humansize.py')) 
/Users/pilgrim/diveintopython3/examples\humansize.py
>>> print(os.path.expanduser('~')) 
c:\Users\pilgrim
>>> print(os.path.join(os.path.expanduser('~'), 'diveintopython3', 'examples', 'humansize.py')) 
c:\Users\pilgrim\diveintopython3\examples\humansize.py
  1. os.path.join() 函数使用一个或多个部分路径名构造路径名。在本例中,它只是简单地连接字符串。
  2. 在这种略微不那么平凡的情况下,调用 os.path.join() 函数将在路径名之前添加一个额外的斜杠,然后将其与文件名连接。它是一个反斜杠而不是正斜杠,因为我在 Windows 上构建了这个示例。如果您在 Linux 或 Mac OS X 上复制此示例,您将看到一个正斜杠。不要纠结于斜杠;始终使用 os.path.join(),让 Python 做正确的事情。
  3. os.path.expanduser() 函数将扩展使用 ~ 表示当前用户主目录的路径名。这适用于所有用户都有主目录的平台,包括 Linux、Mac OS X 和 Windows。返回的路径没有尾部斜杠,但 os.path.join() 函数不介意。
  4. 结合这些技术,您可以轻松地为用户主目录中的目录和文件构造路径名。os.path.join() 函数可以接受任意数量的参数。当我发现这一点时,我欣喜若狂,因为 addSlashIfNecessary() 是我在新语言中构建工具箱时总是需要编写的那些愚蠢的小函数之一。不要在 Python 中编写这个愚蠢的小函数;聪明的人已经为你解决了这个问题。

os.path 还包含用于将完整路径名、目录名和文件名拆分为其组成部分的函数。

>>> pathname = '/Users/pilgrim/diveintopython3/examples/humansize.py'
>>> os.path.split(pathname) 
('/Users/pilgrim/diveintopython3/examples', 'humansize.py')
>>> (dirname, filename) = os.path.split(pathname) 
>>> dirname 
'/Users/pilgrim/diveintopython3/examples'
>>> filename 
'humansize.py'
>>> (shortname, extension) = os.path.splitext(filename) 
>>> shortname
'humansize'
>>> extension
'.py'
  1. split 函数拆分完整路径名,并返回包含路径和文件名的元组。
  2. 还记得我说您可以使用 多变量赋值 从函数返回多个值吗?os.path.split() 函数正是这样做的。您将 split 函数的返回值分配给两个变量的元组。每个变量接收返回的元组的相应元素的值。
  3. 第一个变量 dirname 接收从 os.path.split() 函数返回的元组的第一个元素的值,即文件路径。
  4. 第二个变量 filename 接收从 os.path.split() 函数返回的元组的第二个元素的值,即文件名。
  5. os.path 还包含 os.path.splitext() 函数,该函数拆分文件名并返回包含文件名和文件扩展名的元组。您使用相同的技术将它们分别分配给不同的变量。

列出目录

glob 模块是 Python 标准库中的另一个工具。它是一种通过编程方式获取目录内容的简单方法,它使用您可能已经从命令行工作中熟悉的通配符。

>>> os.chdir('/Users/pilgrim/diveintopython3/')
>>> import glob
>>> glob.glob('examples/*.xml') 
['examples\\feed-broken.xml',
 'examples\\feed-ns0.xml',
 'examples\\feed.xml']
>>> os.chdir('examples/') 
>>> glob.glob('*test*.py') 
['alphameticstest.py',
 'pluraltest1.py',
 'pluraltest2.py',
 'pluraltest3.py',
 'pluraltest4.py',
 'pluraltest5.py',
 'pluraltest6.py',
 'romantest1.py',
 'romantest10.py',
 'romantest2.py',
 'romantest3.py',
 'romantest4.py',
 'romantest5.py',
 'romantest6.py',
 'romantest7.py',
 'romantest8.py',
 'romantest9.py']
  1. glob 模块接受一个通配符,并返回与通配符匹配的所有文件和目录的路径。在此示例中,通配符是一个目录路径加上“*.xml”,它将匹配 examples 子目录中的所有 .xml 文件。
  2. 现在将当前工作目录更改为 examples 子目录。os.chdir() 函数可以接受相对路径名。
  3. 您可以在 glob 模式中包含多个通配符。此示例查找当前工作目录中以 .py 扩展名结尾且文件名中包含 test 的所有文件。

获取文件元数据

每个现代文件系统都存储有关每个文件的元数据:创建日期、最后修改日期、文件大小等。Python 提供了一个单独的 API 来访问这些元数据。您无需打开文件;您只需要文件名。

>>> import os
>>> print(os.getcwd()) 
c:\Users\pilgrim\diveintopython3\examples
>>> metadata = os.stat('feed.xml') 
>>> metadata.st_mtime 
1247520344.9537716
>>> import time 
>>> time.localtime(metadata.st_mtime) 
time.struct_time(tm_year=2009, tm_mon=7, tm_mday=13, tm_hour=17,
  tm_min=25, tm_sec=44, tm_wday=0, tm_yday=194, tm_isdst=1)
  1. 当前工作目录是 examples 文件夹。
  2. feed.xmlexamples 文件夹中的一个文件。调用 os.stat() 函数返回一个对象,其中包含有关该文件的几种不同类型的元数据。
  3. st_mtime 是修改时间,但它使用的是一种不太实用的格式。(从技术上讲,它是自纪元以来的秒数,纪元定义为 1970 年 1 月 1 日的第一秒。真的。)
  4. time 模块是 Python 标准库的一部分。它包含用于在不同时间表示之间进行转换、将时间值格式化为字符串以及处理时区的函数。
  5. time.localtime() 函数将时间值从秒数转换为纪元(来自 os.stat() 函数返回的 st_mtime 属性)转换为更实用的结构,例如年、月、日、时、分、秒等。该文件最后修改时间为 2009 年 7 月 13 日下午 5:25 左右。
# continued from the previous example
>>> metadata.st_size 
3070
>>> import humansize
>>> humansize.approximate_size(metadata.st_size) 
'3.0 KiB'
  1. os.stat() 函数还返回文件的尺寸,在 st_size 属性中。feed.xml 文件大小为 3070 字节。
  2. 您可以将 st_size 属性传递给 approximate_size() 函数

构造绝对路径名

上一节 中,glob.glob() 函数返回了一系列相对路径名。第一个示例的路径名类似于 'examples\feed.xml',第二个示例的相对路径名甚至更短,例如 'romantest1.py'。只要您保留在同一个当前工作目录中,这些相对路径名就可以用于打开文件或获取文件元数据。但是,如果您要构造一个绝对路径名,即包含所有目录名直至根目录或驱动器号的路径名,那么您将需要使用 os.path.realpath() 函数。

>>> import os
>>> print(os.getcwd())
c:\Users\pilgrim\diveintopython3\examples
>>> print(os.path.realpath('feed.xml'))
c:\Users\pilgrim\diveintopython3\examples\feed.xml

列表推导

一个 列表推导 提供了一种紧凑的方式,通过对列表的每个元素应用函数将一个列表映射到另一个列表。

>>> a_list = [1, 9, 8, 4]
>>> [elem * 2 for elem in a_list] 
[2, 18, 16, 8]
>>> a_list 
[1, 9, 8, 4]
>>> a_list = [elem * 2 for elem in a_list] 
>>> a_list
[2, 18, 16, 8]
  1. 为了理解这一点,请从右到左看。a_list 是您要映射的列表。Python 解释器逐个遍历 a_list,并将每个元素的值暂时分配给变量 elem。然后,Python 应用函数 elem * 2 并将该结果追加到返回的列表中。
  2. 列表推导会创建一个新列表;它不会更改原始列表。
  3. 将列表推导的结果分配给您要映射的变量是安全的。Python 在内存中构造新列表,并且当列表推导完成时,它会将结果分配给原始变量。

您可以在列表推导中使用任何 Python 表达式,包括 os 模块中用于操作文件和目录的函数。

>>> import os, glob
>>> glob.glob('*.xml') 
['feed-broken.xml', 'feed-ns0.xml', 'feed.xml']
>>> [os.path.realpath(f) for f in glob.glob('*.xml')] 
['c:\\Users\\pilgrim\\diveintopython3\\examples\\feed-broken.xml',
 'c:\\Users\\pilgrim\\diveintopython3\\examples\\feed-ns0.xml',
 'c:\\Users\\pilgrim\\diveintopython3\\examples\\feed.xml']
  1. 这将返回当前工作目录中的所有 .xml 文件的列表。
  2. 此列表推导接受该 .xml 文件列表,并将其转换为完整路径名的列表。

列表推导还可以过滤项目,生成的结果可能小于原始列表。

>>> import os, glob
>>> [f for f in glob.glob('*.py') if os.stat(f).st_size > 6000] 
['pluraltest6.py',
 'romantest10.py',
 'romantest6.py',
 'romantest7.py',
 'romantest8.py',
 'romantest9.py']
  1. 要过滤列表,您可以在列表推导的末尾包含一个 if 子句。if 关键字后的表达式将针对列表中的每个项目进行评估。如果表达式计算结果为 True,则该项目将包含在输出中。此列表推导查看当前目录中所有 .py 文件的列表,并且 if 表达式通过测试每个文件的大小是否大于 6000 字节来过滤该列表。有六个这样的文件,因此列表推导返回一个包含六个文件名的列表。

到目前为止,所有关于列表推导的示例都包含简单的表达式,例如将数字乘以一个常数、调用一个函数或简单地返回原始列表项(在过滤之后)。但是,列表推导的复杂程度没有限制。

>>> import os, glob
>>> [(os.stat(f).st_size, os.path.realpath(f)) for f in glob.glob('*.xml')] 
[(3074, 'c:\\Users\\pilgrim\\diveintopython3\\examples\\feed-broken.xml'),
 (3386, 'c:\\Users\\pilgrim\\diveintopython3\\examples\\feed-ns0.xml'),
 (3070, 'c:\\Users\\pilgrim\\diveintopython3\\examples\\feed.xml')]
>>> import humansize
>>> [(humansize.approximate_size(os.stat(f).st_size), f) for f in glob.glob('*.xml')] 
[('3.0 KiB', 'feed-broken.xml'),
 ('3.3 KiB', 'feed-ns0.xml'),
 ('3.0 KiB', 'feed.xml')]
  1. 此列表推导式会找到当前工作目录中所有以 .xml 为扩展名的文件,获取每个文件的大小(通过调用 os.stat() 函数),并构造一个包含文件大小和每个文件绝对路径的元组(通过调用 os.path.realpath() 函数)。
  2. 此推导式建立在之前的推导式基础上,并调用 approximate_size() 函数,传入每个 .xml 文件的文件大小。

字典推导式

字典推导式类似于列表推导式,但它会构建一个字典而不是列表。

>>> import os, glob
>>> metadata = [(f, os.stat(f)) for f in glob.glob('*test*.py')] 
>>> metadata[0] 
('alphameticstest.py', nt.stat_result(st_mode=33206, st_ino=0, st_dev=0,
 st_nlink=0, st_uid=0, st_gid=0, st_size=2509, st_atime=1247520344,
 st_mtime=1247520344, st_ctime=1247520344))
>>> metadata_dict = {f:os.stat(f) for f in glob.glob('*test*.py')} 
>>> type(metadata_dict) 
<class 'dict'>
>>> list(metadata_dict.keys()) 
['romantest8.py', 'pluraltest1.py', 'pluraltest2.py', 'pluraltest5.py',
 'pluraltest6.py', 'romantest7.py', 'romantest10.py', 'romantest4.py',
 'romantest9.py', 'pluraltest3.py', 'romantest1.py', 'romantest2.py',
 'romantest3.py', 'romantest5.py', 'romantest6.py', 'alphameticstest.py',
 'pluraltest4.py']
>>> metadata_dict['alphameticstest.py'].st_size 
2509
  1. 这不是一个字典推导式;它是一个 列表推导式。它会找到所有名称中包含 test.py 文件,然后构建一个包含文件名和文件元数据(来自调用 os.stat() 函数)的元组。
  2. 结果列表中的每个项目都是一个元组。
  3. 这是一个字典推导式。其语法类似于列表推导式,但有两个不同之处。首先,它用花括号而不是方括号括起来。其次,它包含两个用冒号分隔的表达式,而不是每个项目的单个表达式。冒号之前的表达式(本例中的 f)是字典键;冒号之后的表达式(本例中的 os.stat(f))是值。
  4. 字典推导式会返回一个字典。
  5. 此特定字典的键只是从调用 glob.glob('*test*.py') 返回的文件名。
  6. 与每个键关联的值是 os.stat() 函数的返回值。这意味着我们可以通过名称在该字典中“查找”一个文件以获取其文件元数据。元数据之一是 st_size,即文件大小。文件 alphameticstest.py 长度为 2509 字节。

与列表推导式一样,您可以在字典推导式中包含一个 if 子句,以根据对每个项目求值的表达式来筛选输入序列。

>>> import os, glob, humansize
>>> metadata_dict = {f:os.stat(f) for f in glob.glob('*')} 
>>> humansize_dict = {os.path.splitext(f)[0]:humansize.approximate_size(meta.st_size) \  ...  for f, meta in metadata_dict.items() if meta.st_size > 6000} 
>>> list(humansize_dict.keys()) 
['romantest9', 'romantest8', 'romantest7', 'romantest6', 'romantest10', 'pluraltest6']
>>> humansize_dict['romantest9'] 
'6.5 KiB'
  1. 此字典推导式会构造当前工作目录中所有文件的列表(glob.glob('*')),获取每个文件的文件元数据(os.stat(f)),并构建一个字典,其键为文件名,其值为每个文件的文件元数据。
  2. 此字典推导式建立在之前的推导式基础上,筛选出小于 6000 字节的文件(if meta.st_size > 6000),并使用该筛选后的列表构建一个字典,其键为文件名减去扩展名(os.path.splitext(f)[0]),其值为每个文件的近似大小(humansize.approximate_size(meta.st_size))。
  3. 如您在前面的示例中所见,有六个这样的文件,因此该字典中有六个项目。
  4. 每个键的值是 approximate_size() 函数返回的字符串。

字典推导式可以做的一些有趣的事情

以下是用字典推导式的一个技巧,您可能有一天会用得上:交换字典的键和值。

>>> a_dict = {'a': 1, 'b': 2, 'c': 3}
>>> {value:key for key, value in a_dict.items()}
{1: 'a', 2: 'b', 3: 'c'}

当然,这只有在字典的值是不可变的时才有效,例如字符串或元组。如果尝试对包含列表的字典执行此操作,它将以最糟糕的方式失败。

>>> a_dict = {'a': [1, 2, 3], 'b': 4, 'c': 5}
>>> {value:key for key, value in a_dict.items()}
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 1, in <dictcomp>
TypeError: unhashable type: 'list'

集合推导式

集合也不甘落后,它们也有自己的推导式语法。它与字典推导式的语法非常相似。唯一的区别是集合只有值,而不是键:值对。

>>> a_set = set(range(10))
>>> a_set
{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
>>> {x ** 2 for x in a_set} 
{0, 1, 4, 81, 64, 9, 16, 49, 25, 36}
>>> {x for x in a_set if x % 2 == 0} 
{0, 8, 2, 4, 6}
>>> {2**x for x in range(10)} 
{32, 1, 2, 4, 8, 64, 128, 256, 16, 512}
  1. 集合推导式可以将一个集合作为输入。此集合推导式会计算从 0 到 9 的数字集合的平方。
  2. 与列表推导式和字典推导式一样,集合推导式可以包含一个 if 子句,以筛选每个项目,然后再将其返回到结果集中。
  3. 集合推导式不需要将集合作为输入;它们可以接受任何序列。

进一步阅读

© 2001–11 马克·皮尔格里姆