【笔记】流畅的Python


1. 一摞Python风格的纸牌

import collections

Card = collections.namedtuple('Card', ['rank', 'suit'])

class FrenchDeck:
    ranks =[str(n) for n in range (2, 11)] + list('JQKA')
    suits =spades diamonds clubs hearts.split()

    def __init__(self):
        self._cards = [Card(rank, suit) for suit in self suits
                                        for rank in self ranks]

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

    def __getitem__(self, position):
        return self._cards[position]

collections.namedtuple : 用以构建只有少数属性但是没有方法的对象,比如数据库条目。

>>> beer_card = Card('7', 'diamonds')
>>> beer_card
Card(rank='7', suit='diamonds')

__len__() : 提供 len() 函数对该类的访问。

__getitem__() : 用于按照某一规则得到相关属性,例如 random.choice()

>>> from random import choice
>>>choice(deck)
Card(rank='3', suit='hearts')
>>> choice(deck)
Card(rank='K', suit='spades')
>>>choice(deck)
Card(rank='2', suit="clubs")

现在已经可以体会到通过实现 魔法方法 来利用 Python数据模型的两个好处:

  • 作为你定义的类的用户,他们不必去记住标准操作的各式名称(“怎么得到元素的总数?是 .size() 还是 .length() 还是别的什么?”)

  • 可以更加方便地利用Python的标准库,比如 random.choice() 函数,从而不用重新发明轮子(通过 __getitem__() 获得)。

而且好戏还在后面。

因为 __getitem__ 方法把 [] 操作交给了 self._cards 列表,所以我们的 deck 类自动支持切片(slicing)操作。下面列出了查看一摞牌最上面 3 张和只看牌面是 A 的牌的操作。其中第二种操作的具体方法是,先抽出索引是 12 的那张牌,然后每隔 13 张牌拿 1 张:

>>> deck[:3]
[Card(rank='2', suit='spades'), Card(rank='3', suit='spades'),
Card(rank='4', suit='spades')]
>>> deck[12::13]
[Card(rank='A', suit='spades'), Card(rank='A', suit='diamonds'),
Card(rank='A', suit='clubs'), Card(rank='A', suit='hearts')]

另外,仅仅实现了 __getitem__ 方法,这一摞牌就变成可迭代的了:

>>> for card in deck:
...     print(card)
Card(rank='2', suit='spades')
Card(rank='3', suit='spades')
Card(rank='4', suit='spades')
...

反向迭代也没关系:

>>> for card in reversed(deck): # doctest: +ELLIPSIS
...     print(card)
Card(rank='A', suit='hearts')
Card(rank='K', suit='hearts')
Card(rank='Q', suit='hearts')
...

2. 不要使用可变类型作为参数的默认值

可选参数可以有默认值,这是 Python 函数定义的一个很棒的特性,这样我们的 API 在进化的同时能保证向后兼容。然而,我们应该避免使用可变的对象作为参数的默认值。

下面在示例 8-12 中说明这个问题。我们以示例 8-8 中的 Bus 类为基础定义一个新类,HauntedBus,然后修改 init 方法。这一次,passengers 的默认值不是 None,而是 [],这样就不用像之前那样使用 if 判断了。这个“聪明的举动”会让我们陷入麻烦。

class HauntedBus:
    """备受幽灵乘客折磨的校车"""
    def __init__(self, passengers=[]):
        self.passengers = passengers

    def pick(self, name):
        self.passengers.append(name)

    def drop(self, name):
        self.passengers.remove(name)

HauntedBus 的诡异行为如示例 8-13 所示。

>>> bus1 = HauntedBus(['Alice', 'Bill'])
>>> bus1.passengers
['Alice', 'Bill']
>>> bus1.pick('Charlie')
>>> bus1.drop('Alice')
>>> bus1.passengers ➊
['Bill', 'Charlie']
>>> bus2 = HauntedBus() ➋
>>> bus2.pick('Carrie')
>>> bus2.passengers
['Carrie']
>>> bus3 = HauntedBus() ➌
>>> bus3.passengers ➍
['Carrie']
>>> bus3.pick('Dave')
>>> bus2.passengers ➎
['Carrie', 'Dave']
>>> bus2.passengers is bus3.passengers ➏
True
>>> bus1.passengers ➐
['Bill', 'Charlie']

❶ 目前没什么问题,bus1 没有出现异常。

❷ 一开始,bus2 是空的,因此把默认的空列表赋值给 self.passengers。

❸ bus3 一开始也是空的,因此还是赋值默认的列表。

❹ 但是默认列表不为空!

❺ 登上 bus3 的 Dave 出现在 bus2 中。

❻ 问题是,bus2.passengers 和 bus3.passengers 指代同一个列表。

❼ 但 bus1.passengers 是不同的列表。

问题在于,没有指定初始乘客的 HauntedBus 实例会共享同一个乘客列表。

这种问题很难发现。如示例 8-13 所示,实例化 HauntedBus 时,如果传入乘客,会按预期运作。但是不为 HauntedBus 指定乘客的话,奇怪的事就发生了,这是因为self.passengers 变成了 passengers 参数默认值的别名。出现这个问题的根源是,默认值在定义函数时计算(通常在加载模块时),因此默认值变成了函数对象的属性。因此,如果默认值是可变对象,而且修改了它的值,那么后续的函数调用都会受到影响。

可变默认值导致的这个问题说明了为什么通常使用 None 作为接收可变值的参数的默认值。在示例 8-8 中,init 方法检查 passengers 参数的值是不是 None,如果是就把一个新的空列表赋值给 self.passengers。

posted @ 2020-09-25 21:13  brt2  阅读(266)  评论(0编辑  收藏  举报