Python3 cookbook学习笔记-数据结构与算法6

时间:2022-10-18 16:06:11

映射名称到序列元素

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

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

>>> from collections import namedtuple
>>>
>>> Subscriber = namedtuple('Subscriber', ['addr', 'joined'])
>>> sub = Subscriber('jonesy@example.com', '2012-10-19')
>>> sub
Subscriber(addr='jonesy@example.com', joined='2012-10-19')
>>> sub.addr
'jonesy@example.com'
>>> sub.joined
'2012-10-19'
>>>

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

>>> len(sub)
2
>>> addr, joined = sub
>>> addr
'jonesy@example.com'
>>> joined
'2012-10-19'
>>>

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

对比如下两份代码:

使用普通元组:

def compute_cost(records): 
total = 0.0
for rec in records:
total += rec[1] * rec[2]
return total

使用命名元组:

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

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

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

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

>>> s = s._replace(shares=75)
>>> s
Stock(name='ACME', shares=75, price=123.45)
>>>

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

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)
>>> 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方法的类(参考8.4小节)。

转换并同时计算数据

问题:

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

解决方案:
一个非常优雅的方式去结合数据计算与转换就是使用一个生成器表达式参数。如:

>>> nums = [1, 2, 3, 4, 5]
>>> s = sum(x*x for x in nums)
>>> s
55
>>>

更多的例子:

# 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)

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

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

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

nums = [1, 2, 3, 4, 5]
s = sum([x * x for x in nums])

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

在使用一些聚集函数比如min()和max()的时候,你可能更加倾向于使用生成器版本,它们接受的一个key关键字参数或许对你很有帮助。

# 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'])

合并多个字典或映射

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

解决方案:
假如你有如下两个字典:

>>> a = {'x':1, 'z':3}
>>> b = {'y':2, 'z':4}

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

>>> from collections import ChainMap
>>> c = ChainMap(a,b)
>>> c
ChainMap({'x': 1, 'z': 3}, {'y': 2, 'z': 4})
>>> c['x']
1
>>> c['y']
2
>>> c['z']
3
>>>

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

>>> len(c)
3
>>> list(c.keys())
['z', 'x', 'y']
>>> list(c.values())
[3, 1, 2]
>>>

如果出现重复键,那么第一次出现的映射会被返回。

对字典的更新或删除操作勇士影响列表中的第一个字典:

>>> c['z'] = 10
>>> c['w'] = 40
>>> c
ChainMap({'x': 1, 'z': 10, 'w': 40}, {'y': 2, 'z': 4})
>>> del c['x']
>>> c
ChainMap({'z': 10, 'w': 40}, {'y': 2, 'z': 4})
>>> a
{'z': 10, 'w': 40}
>>> del c['y']
Traceback (most recent call last):
...
KeyError: "Key not found in the first mapping: 'y'"

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

>>> values = ChainMap()
>>>
>>> values['x'] = 1
>>>
>>> values = values.new_child()
>>> values['x'] = 2
>>>
>>> values = values.new_child()
>>> values['x'] = 3
>>> values
ChainMap({'x': 3}, {'x': 2}, {'x': 1})
>>>
>>> values['x']
3
>>>
>>> values = values.parents
>>> values['x']
2
>>>
>>> values = values.parents
>>> values['x']
1
>>> values
ChainMap({'x': 1})
>>>

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

>>> a = {'x' : 1, 'z' : 3}
>>> b = {'y' : 2, 'z' : 4}
>>> merged = dict(b)
>>> merged.update(a)
>>> merged
{'y': 2, 'z': 3, 'x': 1}
>>>

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

>>> a['x'] = 13
>>> a
{'x': 13, 'z': 3}
>>> merged
{'y': 2, 'z': 3, 'x': 1}
>>>

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

>>> a = {'x': 1, 'z': 3 }
>>> b = {'y': 2, 'z': 4 }
>>>
>>> merged = ChainMap(a, b)
>>> merged
ChainMap({'x': 1, 'z': 3}, {'y': 2, 'z': 4})
>>> a['x'] = 42
>>> merged
ChainMap({'x': 42, 'z': 3}, {'y': 2, 'z': 4})
>>> a
{'x': 42, 'z': 3}
>>>