【Python Cookbook】数据结构和算法(四)

目录 案例 目录 案例
数据结构和算法(一) 1.将序列分解为单独的变量 2.解压可迭代对象赋值给多个变量 3.保留最后 N 个元素 4.查找最大或最小的 N 个元素 5.实现一个优先级队列 数据结构和算法(三) 11.命名切片 12.序列中出现次数最多的元素 13.通过某个关键字排序一个字典列表 14.排序不支持原生比较的对象 15.通过某个字段将记录分组
数据结构和算法(二) 6.字典中的键映射多个值 7.字典排序 8.字典的运算 9.查找两字典的相同点 10.删除序列相同元素并保持顺序 数据结构和算法(四) 16.过滤序列元素 17.从字典中提取子集 18.映射名称到序列元素 19.转换并同时计算数据 20.合并多个字典或映射

数据结构和算法(四)

16.过滤序列元素

你有一个数据序列,想利用一些规则从中提取出需要的值或者是缩短序列。

最简单的过滤序列元素的方法就是使用列表推导。比如:

python 复制代码
>>> mylist = [1, 4, -5, 10, -7, 2, 3, -1]
>>> [n for n in mylist if n > 0]
[1, 4, 10, 2, 3]
>>> [n for n in mylist if n < 0]
[-5, -7, -1]

使用列表推导的一个潜在缺陷就是如果输入非常大的时候会产生一个非常大的结果集,占用大量内存。如果你对内存比较敏感,那么你可以使用生成器表达式迭代产生过滤的元素。比如:

python 复制代码
>>> pos = (n for n in mylist if n > 0)
>>> pos
<generator object <genexpr> at 0x1006a0eb0>
>>> for x in pos:
... print(x)
...
1
4
10
2
3

有时候,过滤规则比较复杂,不能简单的在列表推导或者生成器表达式中表达出来。比如,假设过滤的时候需要处理一些异常或者其他复杂情况。这时候你可以将过滤代码放到一个函数中,然后使用内建的 filter() 函数。示例如下:

python 复制代码
values = ['1', '2', '-3', '-', '4', 'N/A', '5']
def is_int(val):
    try:
        x = int(val)
        return True
    except ValueError:
        return False
ivals = list(filter(is_int, values))
print(ivals)
# Outputs ['1', '2', '-3', '4', '5']

🚀 为什么是 return True,不是 return x

filter() 函数需要的是一个布尔值来决定是否保留当前元素。如果返回整数值,可能会导致逻辑错误,因为非零整数会被视为 True,而 0 0 0 会被视为 False

filter() 函数创建了一个迭代器,因此如果你想得到一个列表的话,就得像示例那样使用 list() 去转换。

列表推导和生成器表达式通常情况下是过滤数据最简单的方式。其实它们还能在过滤的时候转换数据。比如:

python 复制代码
>>> mylist = [1, 4, -5, 10, -7, 2, 3, -1]
>>> import math
>>> [math.sqrt(n) for n in mylist if n > 0]
[1.0, 2.0, 3.1622776601683795, 1.4142135623730951, 1.7320508075688772]

过滤操作的一个变种就是将不符合条件的值用新的值代替,而不是丢弃它们。比如,在一列数据中你可能不仅想找到正数,而且还想将不是正数的数替换成指定的数。通过将过滤条件放到条件表达式中去,可以很容易的解决这个问题,就像这样:

python 复制代码
>>> clip_neg = [n if n > 0 else 0 for n in mylist]
>>> clip_neg
[1, 4, 0, 10, 0, 2, 3, 0]
>>> clip_pos = [n if n < 0 else 0 for n in mylist]
>>> clip_pos
[0, 0, -5, 0, -7, 0, 0, -1]

另外一个值得关注的过滤工具就是 itertools.compress(),它以一个 iterable 对象和一个相对应的 Boolean 选择器序列作为输入参数。然后输出 iterable 对象中对应选择器为 True 的元素。当你需要用另外一个相关联的序列来过滤某个序列的时候,这个函数是非常有用的。比如,假如现在你有下面两列数据:

python 复制代码
addresses = [
    '5412 N CLARK',
    '5148 N CLARK',
    '5800 E 58TH',
    '2122 N CLARK',
    '5645 N RAVENSWOOD',
    '1060 W ADDISON',
    '4801 N BROADWAY',
    '1039 W GRANVILLE',
]
counts = [ 0, 3, 10, 4, 1, 7, 6, 1]

现在你想将那些对应 count 值大于 5 的地址全部输出,那么你可以这样做:

python 复制代码
>>> from itertools import compress
>>> more5 = [n > 5 for n in counts]
>>> more5
[False, False, True, False, False, True, True, False]
>>> list(compress(addresses, more5))
['5800 E 58TH', '1060 W ADDISON', '4801 N BROADWAY']

这里的关键点在于先创建一个 Boolean 序列,指示哪些元素符合条件。然后 compress() 函数根据这个序列去选择输出对应位置为 True 的元素。

filter() 函数类似,compress() 也是返回的一个迭代器。因此,如果你需要得到一个列表,那么你需要使用 list() 来将结果转换为列表类型。

17.从字典中提取子集

你想构造一个字典,它是另外一个字典的子集。

最简单的方式是使用字典推导。比如:

python 复制代码
prices = {
    'ACME': 45.23,
    'AAPL': 612.78,
    'IBM': 205.55,
    'HPQ': 37.20,
    'FB': 10.75
}
# Make a dictionary of all prices over 200
p1 = {key: value for key, value in prices.items() if value > 200}
# Make a dictionary of tech stocks
tech_names = {'AAPL', 'IBM', 'HPQ', 'MSFT'}
p2 = {key: value for key, value in prices.items() if key in tech_names}

大多数情况下字典推导能做到的,通过创建一个元组序列然后把它传给 dict() 函数也能实现。比如:

python 复制代码
p1 = dict((key, value) for key, value in prices.items() if value > 200)

但是,字典推导方式表意更清晰,并且实际上也会运行的更快些(在这个例子中,实际测试几乎比 dict() 函数方式快整整一倍)。

有时候完成同一件事会有多种方式。比如,第二个例子程序也可以像这样重写:

python 复制代码
# Make a dictionary of tech stocks
tech_names = { 'AAPL', 'IBM', 'HPQ', 'MSFT' }
p2 = { key:prices[key] for key in prices.keys() & tech_names }

但是,运行时间测试结果显示这种方案大概比第一种方案慢 1.6 倍。如果对程序运行性能要求比较高的话,需要花点时间去做计时测试。

18.映射名称到序列元素

你有一段通过下标访问列表或者元组中元素的代码,但是这样有时候会使得你的代码难以阅读,于是你想通过名称来访问元素。

collections.namedtuple() 函数通过使用一个普通的元组对象来帮你解决这个问题。这个函数实际上是一个返回 Python 中标准元组类型子类的一个工厂方法。你需要传递一个类型名和你需要的字段给它,然后它就会返回一个类,你可以初始化这个类,为你定义的字段传递值等。代码示例:

python 复制代码
>>> from collections import namedtuple
>>> Subscriber = namedtuple('Subscriber', ['addr', 'joined'])
>>> sub = Subscriber('[email protected]', '2012-10-19')
>>> sub
Subscriber(addr='[email protected]', joined='2012-10-19')
>>> sub.addr
'[email protected]'
>>> sub.joined
'2012-10-19'

尽管 namedtuple 的实例看起来像一个普通的类实例,但是它跟元组类型是可交换的,支持所有的普通元组操作,比如索引和解压。比如:

python 复制代码
>>> len(sub)
2
>>> addr, joined = sub
>>> addr
'[email protected]'
>>> joined
'2012-10-19'

命名元组的一个主要用途是将你的代码从下标操作中解脱出来。因此,如果你从数据库调用中返回了一个很大的元组列表,通过下标去操作其中的元素,当你在表中添加了新的列的时候你的代码可能就会出错了。但是如果你使用了命名元组,那么就不会有这样的顾虑。

为了说明清楚,下面是使用普通元组的代码:

python 复制代码
def compute_cost(records):
    total = 0.0
    for rec in records:
        total += rec[1] * rec[2]
    return total

下标操作通常会让代码表意不清晰,并且非常依赖记录的结构。下面是使用命名元组的版本:

python 复制代码
from collections import namedtuple

Stock = namedtuple('Stock', ['name', 'shares', 'price'])
def compute_cost(records):
    total = 0.0
    for rec in records:
        s = Stock(*rec)
        total += s.shares * s.price
    return total

命名元组另一个用途就是作为字典的替代,因为字典存储需要更多的内存空间。如果你需要构建一个非常大的包含字典的数据结构,那么使用命名元组会更加高效。但是需要注意的是,不像字典那样,一个命名元组是不可更改的。比如:

python 复制代码
>>> s = Stock('ACME', 100, 123.45)
>>> s
Stock(name='ACME', shares=100, price=123.45)
>>> s.shares = 75
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: can't set attribute

如果你真的需要改变属性的值,那么可以使用命名元组实例的 _replace() 方法,它会创建一个全新的命名元组并将对应的字段用新的值取代。比如:

python 复制代码
>>> s = s._replace(shares=75)
>>> s
Stock(name='ACME', shares=75, price=123.45)

_replace() 方法还有一个很有用的特性就是当你的命名元组拥有可选或者缺失字段时候,它是一个非常方便的填充数据的方法。你可以先创建一个包含缺省值的原型元组,然后使用 _replace() 方法创建新的值被更新过的实例。比如:

python 复制代码
from collections import namedtuple

Stock = namedtuple('Stock', ['name', 'shares', 'price', 'date', 'time'])

# Create a prototype instance
stock_prototype = Stock('', 0, 0.0, None, None)

# Function to convert a dictionary to a Stock
def dict_to_stock(s):
    return stock_prototype._replace(**s)

下面是它的使用方法:

python 复制代码
>>> a = {'name': 'ACME', 'shares': 100, 'price': 123.45}
>>> dict_to_stock(a)
Stock(name='ACME', shares=100, price=123.45, date=None, time=None)
>>> b = {'name': 'ACME', 'shares': 100, 'price': 123.45, 'date': '12/17/2012'}
>>> dict_to_stock(b)
Stock(name='ACME', shares=100, price=123.45, date='12/17/2012', time=None)

最后要说的是,如果你的目标是定义一个需要更新很多实例属性的高效数据结构,那么命名元组并不是你的最佳选择。这时候你应该考虑定义一个包含 __slots__ 方法的类。

19.转换并同时计算数据

你需要在数据序列上执行聚集函数(比如 sum()min()max() ), 但是首先你需要先转换或者过滤数据。

一个非常优雅的方式去结合数据计算与转换就是使用一个生成器表达式参数。比如,如果你想计算平方和,可以像下面这样做:

python 复制代码
nums = [1, 2, 3, 4, 5]
s = sum(x * x for x in nums)

下面是更多的例子:

python 复制代码
# Determine if any .py files exist in a directory
import os
files = os.listdir('dirname')
if any(name.endswith('.py') for name in files):
    print('There be python!')
else:
    print('Sorry, no python.')
    
# Output a tuple as CSV
s = ('ACME', 50, 123.45)
print(','.join(str(x) for x in s))

# Data reduction across fields of a data structure
portfolio = [
    {'name':'GOOG', 'shares': 50},
    {'name':'YHOO', 'shares': 75},
    {'name':'AOL', 'shares': 20},
    {'name':'SCOX', 'shares': 65}
]
min_shares = min(s['shares'] for s in portfolio)

上面的示例向你演示了当生成器表达式作为一个单独参数传递给函数时候的巧妙语法(你并不需要多加一个括号)。比如,下面这些语句是等效的:

python 复制代码
s = sum((x * x for x in nums)) # 显式的传递一个生成器表达式对象
s = sum(x * x for x in nums) # 更加优雅的实现方式,省略了括号

使用一个生成器表达式作为参数会比先创建一个临时列表更加高效和优雅。比如,如果你不使用生成器表达式的话,你可能会考虑使用下面的实现方式:

python 复制代码
nums = [1, 2, 3, 4, 5]
s = sum([x * x for x in nums])

这种方式同样可以达到想要的效果,但是它会多一个步骤,先创建一个额外的列表。对于小型列表可能没什么关系,但是如果元素数量非常大的时候,它会创建一个巨大的仅仅被使用一次就被丢弃的临时数据结构。而生成器方案会以迭代的方式转换数据,因此更省内存。

在使用一些聚集函数比如 min()max() 的时候你可能更加倾向于使用生成器版本,它们接受的一个 key 关键字参数或许对你很有帮助。比如,在上面的证券例子中,你可能会考虑下面的实现版本:

python 复制代码
# Original: Returns 20
min_shares = min(s['shares'] for s in portfolio)
# Alternative: Returns {'name': 'AOL', 'shares': 20}
min_shares = min(portfolio, key=lambda s: s['shares'])

20.合并多个字典或映射

现在有多个字典或者映射,你想将它们从逻辑上合并为一个单一的映射后执行某些操作,比如查找值或者检查某些键是否存在。

假如你有如下两个字典:

python 复制代码
a = {'x': 1, 'z': 3 }
b = {'y': 2, 'z': 4 }

现在假设你必须在两个字典中执行查找操作(比如先从 a 中找,如果找不到再在 b 中找)。一个非常简单的解决方案就是使用 collections 模块中的 ChainMap 类。比如:

python 复制代码
from collections import ChainMap
c = ChainMap(a,b)
print(c['x']) # Outputs 1 (from a)
print(c['y']) # Outputs 2 (from b)
print(c['z']) # Outputs 3 (from a)

一个 ChainMap 接受多个字典并将它们在逻辑上变为一个字典。然后,这些字典并不是真的合并在一起了,ChainMap 类只是在内部创建了一个容纳这些字典的列表,并重新定义了一些常见的字典操作来遍历这个列表。大部分字典操作都是可以正常使用的,比如:

python 复制代码
>>> len(c)
3
>>> list(c.keys())
['x', 'y', 'z']
>>> list(c.values())
[1, 2, 3]

如果出现重复键,那么第一次出现的映射值会被返回。因此,示例程序中的 c['z'] 总是会返回字典 a 中对应的值,而不是 b 中对应的值。

对于字典的更新或删除操作总是影响的是列表中第一个字典。比如:

python 复制代码
>>> c['z'] = 10
>>> c['w'] = 40
>>> del c['x']
>>> a
{'w': 40, 'z': 10}
>>> del c['y']
Traceback (most recent call last):
...
KeyError: "Key not found in the first mapping: 'y'"

ChainMap 对于编程语言中的作用范围变量(比如 globalslocals 等)是非常有用的。事实上,有一些方法可以使它变得简单:

python 复制代码
>>> values = ChainMap()
>>> values['x'] = 1
>>> # Add a new mapping
>>> values = values.new_child()
>>> values['x'] = 2
>>> # Add a new mapping
>>> values = values.new_child()
>>> values['x'] = 3
>>> values
ChainMap({'x': 3}, {'x': 2}, {'x': 1})
>>> values['x']
3
>>> # Discard last mapping
>>> values = values.parents
>>> values['x']
2
>>> # Discard last mapping
>>> values = values.parents
>>> values['x']
1
>>> values
ChainMap({'x': 1})

作为 ChainMap 的替代,你可能会考虑使用 update() 方法将两个字典合并。比如:

python 复制代码
>>> a = {'x': 1, 'z': 3 }
>>> b = {'y': 2, 'z': 4 }
>>> merged = dict(b)
>>> merged.update(a)
>>> merged['x']
1
>>> merged['y']
2
>>> merged['z']
3

这样也能行得通,但是它需要你创建一个完全不同的字典对象(或者是破坏现有字典结构)。同时,如果原字典做了更新,这种改变不会反应到新的合并字典中去。比如:

python 复制代码
>>> a['x'] = 13
>>> merged['x']
1

ChainMap 使用原来的字典,它自己不创建新的字典。所以它并不会产生上面所说的结果,比如:

python 复制代码
>>> a = {'x': 1, 'z': 3 }
>>> b = {'y': 2, 'z': 4 }
>>> merged = ChainMap(a, b)
>>> merged['x']
1
>>> a['x'] = 42
>>> merged['x'] # Notice change to merged dicts
42
相关推荐
Auspemak-Derafru5 分钟前
ZIP_STORED和ZIP_LZMA没有compresslevel参数!
python
rookiefishs6 分钟前
websocket结合promise的通信协议
前端·javascript·python
deming_su10 分钟前
第七课:Python基础排序算法与比较排序原理深度解析
数据结构·算法·排序算法
姝孟27 分钟前
const应用
算法
Rhys..29 分钟前
allure结合pytest生成测试报告
开发语言·python
Phoebe鑫34 分钟前
数据结构每日一题day1
数据结构
陈天在睡觉41 分钟前
python爬虫DrissionPage的使用
开发语言·爬虫·python
带娃的IT创业者42 分钟前
《Python实战进阶》第31集:特征工程:特征选择与降维技术
开发语言·python
知舟不叙42 分钟前
机器学习之KMeans算法
算法·机器学习·kmeans
不要绝望总会慢慢变强1 小时前
python面试高频考点(深度学习大模型方向)
开发语言·python·面试