编写高质量Python (第15条) 不要过分依赖给字典添加条目时所用的顺序

第15条 不要过分依赖给字典添加条目时所用的顺序

​ 在 Python3.5 与之前的版本中,迭代字典(dict)时所看到的顺序好像是任意的,不一定与当初把这些键值对添加到字典时的顺序时相同。也就是说,字典不保证迭代顺序与插入顺序一致。下面创建这样一个字典,把每种动物的名字跟这种动物的幼兽的称呼关联起来(参见 第75条)。
# Python 3.5
baby_name = {
		'cat': 'kitten',
		'dog':  'puppy',
}
print(baby_name)

>>>
{'dog': 'puppy', 'cat': 'kitten'}

​ 创建字典时,先添加的时 ‘cat’,后添加的是’dog’,但在打印的时候,'dog’却出现在了 ‘cat’ 的前面。这样的效果令人惊讶,而且看到的顺序并不固定,因此很难在测试实例使用。

​ 之所以出现这种情况,是因为字典类型以前是用哈希表来实现的(这个算法通过内置的 hash 函数与一个随机种子数来运行,而该种子数会在每次启动 Python 解释器时确定)。所以,这样的机制导致这些键值对在字典中的存放顺序不一定会与添加时的顺序相同,而且每次运行程序的时候,存放顺序困难都不一样。

从 Python 3.6 开始,字典会保留这些键值对在添加时所用的顺序,而且 Python 3.7 版的语言规范正式确立了这条规则。 于是,在小新版的 Python 里,总是能够按照当初创建字典时的那套顺序来遍历这些键值对。

baby_names = {
    'cat': 'kitten',
    'dog': 'puppy',
}
print(baby_names)

>>>
{'cat': 'kitten', 'dog': 'puppy'}

​ 在 Python 3.5 与之前的版本, dict 所提供的许多方法(包括 keys、values、items与 popitem)都不保证固定的顺序,所以让人觉得好像是随机处理的。

# Python 3.5
print(list(baby_names.keys()))
print(list(baby_names.values()))
print(list(baby_names.items()))
print(baby_names.popitem())   # Randomly chooses an item

>>>
['dog', 'cat']
['pubby', 'kitten']
[('dog', 'puppy'), ('cat', 'kitten')]
('dog', 'puppy')

​ 在新版的 Python 中,这些方法已经可以按照当初添加键值对的顺序来处理了。

print(list(baby_names.keys()))
print(list(baby_names.values()))
print(list(baby_names.items()))
print(baby_names.popitem()) # last item inserted
 
>>>
['cat', 'dog']
['kitten', 'puppy']
[('cat', 'kitten'), ('dog', 'puppy')]
('dog', 'puppy')

​ 这项变化对 Python 中那些依赖字典类型及其实现细节的特性产生了很多影响。

​ 函数的关键字参数(包括万能的 **kwargs参数,参见 第23条),以前是按照近乎随机的顺序出现的,这使函数调用操作变得很难调试。

# python 3.5
def my_func(**kwargs):
    for key, value in kwargs.items():
        print('%s = %s' % (key, value))


my_func(goose='gosling', kangaroo='joey')

>>>
kangaroo = joey
goose = gosling

​ 现在,那些关键字参数总是能够保留调用函数所指定的那套顺序。

def my_func(**kwargs):
    for key, value in kwargs.items():
        print('%s = %s' % (key, value))


my_func(goose='gosling', kangaroo='joey')

>>>
goose = gosling
kangaroo = joey

​ 另外,类也会利用字典来保存这个类的实例所具备的一些数据。在早前版本的 Python 中,对象(object)中的字段看上去好像是随机出现的。

# python 3.5
class MyClass:
    def __init__(self):
        self.alligator = 'hatchling'
        self.elephant = 'calf'

a = MyClass()
for key, value in a.__dict__.items():
    print('%s = %s' % (key, value))
    
>>>
elephant = calf
alligator = hatchling

​ 同样,在新版的 Python 中,我们就可以认为这些字段在 __ dict __ 中出现的顺序应该与当初赋值时的顺序一样。

class MyClass:
    def __init__(self):
        self.alligator = 'hatchling'
        self.elephant = 'calf'
        
a = MyClass()
for key, value in a.__dict__.items():
    print('%s = %s' % (key, value))

>>>
alligator = hatchling
elephant = calf

​ 现在的 Python 语言规范已经要求,字典必须保留添加键值对时所依照的顺序。所以,我们可以利用这样的特征来实现一些功能,而且可以把它融入自己给类和函数所设计的 API 中。

其实,内置的 collections 模块早就提供了这种能够保留插入顺序的字典,叫做 Orderdict。它的行为跟(Python 3.7 以来的)标准dict 类型很像,但性能上有很大区别。如果要频繁插入或弹出键值对(例如要实现 least-recently-used 缓存),那么 orderdict 可能比标准的 Python dict 类型更合适(如何判断是否应该换用这种类型,请参见 第70条)。

​ 处理字典的时候,不能总是假设所有的字典都能保留键值对插入时的顺序。在 Python 中,我们很容易就能定义出特制的容器类型,并且让这些容器也像标准的 list 与 dict 等类型遵守相关的协议(参见 第43条)。Python 不是静态的语言,大多数代码都以鸭子类型(duck typing)机制运作(也就是说,对象支持什么样的行为,就可以当作什么样的数据来使用,而不用执着于它在类体系中的地位)。这种类型可能会产生意想不到的问题。

​ 例如,现在要写一个程序,统计各种小动物的受欢迎程度。我们可以设定一个字典,把每种动物和它得到的票数关联起来。

votes = {
    'otter': 1281,
    'polar bear': 587,
    'fox': 863,
}

​ 现在定义一个函数处理投票数据。用户可以把空的字典传给这个函数,这样的话,它就会把每个动物及其排名放到这个字典中。这种字典可以充当数据模型,给带有用户界面( UI )的元素提供数据。

def populate_ranks(votes, ranks):
    names = list(votes.items()) # 获取小动物名称
    names.sort(key=votes.get, reverse=True)
    for i, name in enumerate(names, 1):
        ranks[name] = i

​ 我们还需要写一个函数来查出人气最高的动物。这个函数假定 populate_ranks 总是会按照升序向字典写入键值对,这样第一个出现在字典里的就应该是排名最靠前的动物。

def get_winner(ranks):
    return next(iter(ranks))

​ 下面要验证刚才设计的函数,看它们能不能实现想要的结果。

ranks = {}
populate_ranks(votes, ranks)
print(ranks)
winner = get_winner(ranks)
print(winner)

>>>
{'otter': 1, 'fox': 2, 'polar bear': 3}
otter

​ 结果没有问题。但是,假设现在的需求变了,我们现在想要按照字母顺序在 UI 中显示,而不是像原来那样按照名次显示。为了实现这种效果,我们用内置的 collections.abc 模块定义这样一个类。这个类的功能跟字典一样,而且会按照字母顺序迭代其中的内容。

from collections.abc import MutableMapping


class SortedDict(MutableMapping):
    def __init__(self):
        self.data = {}

    def __getitem__(self, key):
        return self.data[key]

    def __setitem__(self, key, value):
        self.data[key] = value

    def __delitem__(self, key):
        del self.data[key]
    
    def __iter__(self):
        keys = list(self.data.keys())
        keys.sort()
        for key in keys:
            yield key
    
    def __len__(self):
        return len(self.data)

​ 原来使用标准 dict 的地方,现在可以用这个类的实例。我们这个 SortedDict 类与标准的字典遵循同一套协议,因此程序不会出错。但是,我们并没有得到预期的结果。

sorted_ranks = SortedDict()
populate_ranks(votes, sorted_ranks)
print(sorted_ranks.data)
winner = get_winner(sorted_ranks)
print(winner)

>>>
{'otter': 1, 'fox': 2, 'polar bear': 3}
fox

​ 为什么会这样呢?因为 get_winner 函数总是假设,迭代字典的时候应该跟 pupulate_ranks 函数当初面向字典中插入数据时的顺序一样。但是这次,我们用的是 SortedDict 实例,而不是标准的 dict 实例,所以这项假设不成立。因此,函数返回的数据是按照字母排列时最先出现的那个数据,也就是 ‘fox’ 。

​ 这个问题有三种解决方案。第一种就是重新实现 get_winner 函数,使它不再假设 ranks 字典总是能按照固定的顺序来迭代。

def get_winner(ranks):
    for name, rank in ranks.items():
        if rank == 1:
            return name
            
winner = get_winner(sorted_ranks)
print(winner)

>>>
otter

​ 第二种方法是在函数开头先判断 ranks 是不是预期的那种标准字典( dict )。如果不是,那就抛出异常。这个办法的运行性能比上一个好。

def get_winner(ranks):
    if not isinstance(ranks, dict):
        raise TypeError('must provide a dict instance')
    return next(iter(ranks))

winner = get_winner(sorted_ranks)

>>>
Traceback ...
TypeError: must provide a dict instance

​ 第三种是通过类型注解 ( type annotion ) 来保证传给 get_winner 函数的确实是个真证的 dict 实例,而不是那种行为跟标准字典类似的 MutableMapping ( 参见 第90条 ) 。下面就采用严格模式,针对含有注释的代码运行 mypy 工具。

class SortedDict(MutableMapping[str, int]):
    def __init__(self):
        self.data = {}

    def __getitem__(self, key):
        return self.data[key]

    def __setitem__(self, key, value):
        self.data[key] = value

    def __delitem__(self, key):
        del self.data[key]

    def __iter__(self):
        keys = list(self.data.keys())
        keys.sort()
        for key in keys:
            yield key

    def __len__(self):
        return len(self.data)


def populate_ranks(votes: Dict[str, int], ranks: Dict[str, int]) -> None:
    names = list(votes.keys())
    names.sort(key=votes.get, reverse=True)
    for i, name in enumerate(names, 1):
        ranks[name] = i


def get_winner(ranks: Dict[str, int]) -> str:
    return next(iter(ranks))


sorted_ranks = SortedDict()
populate_ranks(votes, sorted_ranks)
print(sorted_ranks.data)
winner = get_winner(sorted_ranks)
print(winner)

$ python3 -m mypy --strict 1.py
1.py:480: error: Argument 2 to "populate_ranks" has incompatible type "SortedDict"; 
expected "dict[str, int]"  [arg-type]
1.py:482: error: Argument 1 to "get_winner" has incompatible type "SortedDict"; 
expected "dict[str, int]"  [arg-type]

你可能感兴趣的:(Effective,Python,python,开发语言)