《Effective Python》Chapter3总结

Date:
Categories: python
Author: sysublackbear
Tags:

Chapter3 的整理细节如下:

Chapter3.类与继承

第22条:尽量用辅助类来维护程序的状态,而不要用字典和元组

要点:

  • 不要使用包含其他字典的字典,也不要使用过长的元组。
  • 如果容器中包含简单而又不可变的数据,那么可以先使用namedtuple来表示,待稍后有需要时,在修改为完整的类。
  • 保存内部状态的字典如果变得比较复杂,那就应该把这些代码拆解为多个辅助类。

举个例子:本来记录每次学生考试的成绩的一个简单例子,由于不停地添加需求,比如增加记录此成绩占该科目总成绩的权重,等等,代码膨胀成如下这样:

class WeightedGradebook(object):
    def __init__(self):
        self._grades = {}

    def add_student(self, name):
        self._grades[name] = {}

    def report_grade(self, name, subject, score, weight):
        by_subject = self._grades[name]
        grade_list = by_subject.setdefault(subject, [])
        grade_list.append((score, weight))

    def average_grade(self, name):
        by_subject = self._grades[name]
        score_sum, score_count = 0, 0
        for subject, scores in by_subject.items():
            subject_avg, total_weight = 0, 0
            for score, weight in scores:
                # ...
        return score_sum / score_count

现在代码已经变得如此复杂了,那么我们就该从字典和元组迁移到类体系了。

与字典嵌套层级逐渐变深一样,元组长度逐步扩张,也意味着代码逐渐趋复杂。元组里的元素一旦超过两项,就应该考虑用其他办法来实现了。

collections模块中的namedtuple(具名元组)类型非常适合实现这种需求。它很容易就能定义出精简而又不可变的数据类。

import collections
Grade = collections.namedtuple('Grade', ('score', 'weight'))

接下来编写表示科目的类,该类包含一系列考试成绩。

class Subject(object):
    def __init__(self):
        self._grades = []

    def report_grade(self, score, weight):
        self._grades.append(Grade(score, weight))

    def average_grade(self):
        total, total_weight = 0, 0
        for grade in self._grades:
            total += grade.score * grade.weight
            total_weight = += grade.weight
        return total / total_weight

然后,可以编写表示学生的类,该类包含此学生正在学习的各项课程。

class Student(object):
    def __init__(self):
        self._subjects = {}

    def subject(self, name):
        if name not in self._subjects:
            self._subjects[name] = Subject()
        return self._subjects[name]

    def average_grade(self):
        total, count = 0, 0
        for subject in self._subjects.values():
            total += subject.average_grade()
            count += 1
        return total / count

最后,编写包含所有学生考试成绩的容器类,该容器以学生的名字为键,并且可以动态地添加学生。

class Gradebook(object):
    def __init__(self):
        self._students = {}

    def student(self, name):s
        if name not in self._students:
            self._students[name] = Student()
        return self._students[name]

这些类的代码量,基本上是刚才那种实现方式的两倍。但这种程序理解起来要比原来容易许多,而且使用这些类的范例代码写起来也比原来更清晰,更易扩展。

第23条:简单的接口应该接受函数,而不是类的实例

要点:

  • 对于连接各种Python 组件的简单接口来说,通常应该给其直接传入函数,而不是先定义某个类,然后再传入该类的实例。
  • Python 中的函数和方法都可以像一级类那样引用,因此,它们与其他类型的对象一样,也能够放在表达式里面。

在Python 中,很多挂钩只是无状态的函数,这些函数有明确的参数及返回值。用函数做挂钩是比较合适的,因为它们很容易就能描述这个挂钩的功能,而且比定义一个类要简单。Python 中的函数之所以能充当挂钩,原因就在于,它是一级对象,也就是说,函数与方法可以像语言中的其他值那样传递和引用。

例如,现在要给defaultdict 传入一个产生默认值得挂钩,并令其统计出该字典一共遇到了多少个缺失的键。一种实现方式是使用带状态的闭包。

def increment_with_report(current, increments):
    added_count = 0

    def missing():
        nonlocal added_count  # Stateful closure
        added_count += 1
        return 0

    result = defaultdict(missing, current)
    for key, amount in increments:
        result[key] += amount

    return result, added_count

类方法实现:

class CountMissing(object):
    def __init__(self):
        self.added = 0

    def missing(self):
        self.added += 1
        return 0

# 调用
counter = CountMissing()
result = defaultdict(counter.misssing, currents)  # Method ref 

for key, amount in increments:
    result[key] = += amount
assert counter.added == 2
  • 通过名__call的特殊方法,可以使类的实例能够像普通的Python 函数那样得到调用。

但是上面类方法实现的功能有点鸡肋,可读性不高。为了理清这些问题,我们可以在Python 代码中定义名为__call__的特殊方法。该方法使相关对象能够像函数那样得到调用。此外,如果把这样的实例传给内置的callable函数,那么callable函数会返回True。

class BetterCountMissing(object):
    def __init__(self):
        self.added = 0

    def __call__(self):
        self.added += 1
        return 0

counter = BetterCountMissing()
counter()
assert callable(counter)

# 调用
result = defaultdict(counter, current)  # Relies on __call__
for key, amount in increments:
    result[key] += amount 
assert counter.added == 2

由此得到最后一点:

  • 如果要用函数来保存状态,那就应该定义新的类,并令其实现__call__方法,而不要定义带状态的闭包。

第24条:以@classmethod 形式的多态去通用地构建对象

要点:

  • 在Python 程序中,每个类只能有一个构造器,也就是__init__方法。
  • 通过@classmethod机制,可以用一种与构造器相仿的方式来构造类的对象。
  • 通过类方法多态机制,我们能够以更加通用的方式来构建并拼接具体的子类。

例子,我们想要实现一套MapReduce 流程,我们需要定义公共基类来表示输入的数据。如下:

class InputData(object):s
    def read(self):
        raise NotImplementedError

class PathInputData(InputData):
    def __init__(self, path):
        super().__init__()
        self.path = path

    def read(self):
        return open(self.path).read()

# 定义工作线程
class Worker(object):
    def __init__(self, input_data):
        self.input_data
        self.result = None

    def map(self):
        raise NotImplementedError

class LineCountWorker(Worker):
    def map(self):
        data = self.input_data.read()
        self.result = data.count('\n')

    def reduce(self, other):
        self.result += other.result

def generate_input(data_dir):
    for name in os.listdir(data_dir):
        yield PathInputData(os.path.join(data_dir, name))

def create_workers(input_list):
    workers = []
    for input_data in input_list:
        workers.append(LineCountWorker(input_data))
    return  workers

def execute(workers):
    threads = [Thread(target=w.map) for w in workers]
    for thread in threads: thread.start()
    for thread in threads: thread.join()

    first, rest = workers[0], workers[1:]
    for worker in rest:
        first.reduce(worker)
    return first.result

# 最后,拼装函数
def mapreduce(data_dir):
    inputs = generate_inputs(data_dir)
    workers = create_workers(inputs)
    return execute(workers)

但是,上面这种写法有个大问题,那就是MapReduce 函数不够通用。如果要编写其他的InputData 或 Worker 子类,那就得重写 generate_inputs, create_workers 和 mapreduce 函数,以便与之匹配。

class GenericInputData(object):
    def read(self):
        raise NotImplementedError

    @classmethod
    def generate_inputs(cls, config):
        raise NotImplementedError

class PathInputData(GenericInputData):
    # ...
    def read(self):
        return open(self.path).read()

    @classmethod
    def generate_inputs(cls, config):
        data_dir = config['data_dir']
        for name in os.listdir(data_dir):
            yield cls(os.path.join(data_dir, name))


class GenericWorker(object):
    # ...
    def map(self):
        raise NotImplementedError

    def reduce(self, other):
        raise NotImplementedError

    @classmethod
    def create_workers(cls, input_class, config):
        workers = []
        for input_data in input_class.generate_inputs(config):
            workers.append(cls(input_data))
        return workers

class LineCountWorker(GenericWorker):
    # ...

# 最后,重写mapreduce函数,令其变得完全通用。
def mapreduce(worker_cls, input_class, config):
    workers = worker_cls.create_workers(input_class, config)
    return execute(workers)

第25条:用super 初始化父类

要点:

  • Python 采用标准的方法解析顺序来解决超类初始化次序及钻石继承问题。

在Python2 里面,我们使用内置的super 函数,并且定义了方法解析顺序(MRO),MRO以标准的流程来安排超类之间的初始化顺序,它也保证钻石顶部那个公共基类的__init__方法只会运行一次。

# Python 2 
class TimesFiveCorrect(MyBaseClass):
    def __init__(self, value):
        super(TimesFiveCorrect, self).__init__(value)
        self.value *= 5

class PlusTwoCorrect(MyBaseClass):
    def __init__(self, value):
        supper(PlusTwoCorrect, self).__init__(value)
        self.value += 2

现在,对于处在钻石顶部的那个MyBaseClass 类来说,它的__init__方法只会运行一次。而其他超类的初始化顺序,则与这些超类在class 语句中出现的顺序相同。

# Python 2
class GoodWay(TimesFiveCorrect, PlusTwoCorrect):
    def __init__(self, value):
        super(GoodWay, self).__init__(value)

查看MRO 顺序可以通过名为mro 的类方法来查询。

from pprint import pprint
pprint(GoodWay.mro())
>>>
[<class '__main__.GoodWay'>, <class '__main__.TimesFiveCorrect'>,
 <class '__main__.PlusTwoCorrect>', <class '__main__.MyBaseClass'>, <class 'object'>]

对于Python 2 调用super时,必须写出当前类的名称。Python 3 则没有这些问题,因为它提供了一种不带参数的super 调用方式,该方式的效果与用__class__和self 来调用super 相同。

class Explicit(MyBaseClass):
    def __init__(self, value):
        super(__class__, self).__init__(value * 2)

class Implicit(MyBaseClass):
    def __init__(self, value):
        super().__init__(value * 2)

注:Python 2 没有定义__class__

第26条:只在使用Mix-in 组件制作工具类时进行多重继承

Python 是面向对象的编程语言,它提供了一些内置的编程机制,使得开发者可以适当地实现多重继承。但是,我们仍然应该尽量避开多重继承。

若一定要利用多重继承所带来的便利及封装性,那就考虑编写mix-in 类。mix-in 是一种小型的类,它只定义了其他类可能需要提供的一套附加方法,而不定义自己的实例属性,此外,它也不要求使用者调用自己的__init__构造器。

举个例子,下列代码定义了实现该功能所用的mix-in 类,并在其中添加了一个新的public 方法,使其他类可以通过继承这个mix-in 类来具备此功能:

class ToDictMixin(object):
    def to_dict(self):
        return self._traverse_dict(self.__dict__)

    # 具体的实现代码写起来也很直观:我们只需要用hasattr 函数动态地访问属性,用isinstance 函数动态地检测对象类型,并用__dict__ 来访问实例内部的字典即可。
    def _traverse_dict(self, instance_dict):
        output = {}
        for key, value in instance_dict.items():
            output[key] = self._traverse(key, value)
        return output

    def _traverse(self, key, value):
        if isinstance(value, ToDictMixin):
            return value.to_dict()
        elif isinstance(value, dict):
            return self._traverse_dict(value)
        elif isinstance(value, list):
            return [self._traverse(key, i) for i in value]
        elif hasattr(value, '__dict__'):
            return self._traverse_dict(value.__dict__)
        else:
            return value

下面定义的这个类演示了如何用mix-in 把二叉树表示为字典:

class BinaryTree(ToDictMixin):
    def __init__(self, value, left=None, right=None):
        self.value = value
        self.left = left
        self.right = right

# 使用
tree = BinaryTree(10, 
                  left=BinaryTree(7, right=BinaryTree(9)),
                  right=BinaryTree(13, left=BinaryTree(11)))
print(tree.to_dict())

多个mix-in 之间也可以相互结合。例如,我们要提供通用的JSON 序列化功能。

class JsonMixin(object):
    @classmethod
    def from_json(cls, data):
        kwargs = json.loads(data)
        return cls(**kwargs)

    def to_json(self):
        return json.dumps(self.to_dict())

那么继承多种的拓扑结构:

class DatacenterRack(ToDictMixin, JsonMixin):
    def __init__(self, switch=None, machines=None):
        self.switch = Switch(**switch)
        self.machines = [
          Machine(**kwargs) for kwargs in machines
        ]

class Switch(ToDictMixin, JsonMixin)
    # ...

class Machine(ToDictMixin, JsonMixin)
    # ...

要点:

  • 能用mix-in组件实现的效果,就不要用多重继承来做。
  • 将各功能实现为可插拨的mix-in 组件,然后令相关的类继承自己需要的那些组件,即可定制该类实例所应具备的行为。
  • 把简单的行为封装到mix-in 组件里,然后就可以用多个mix-in 组合出复杂的行为了。

第27条:多用 public 属性,少用 private 属性

对Python 类来说,其属性的可见度只有两种,也就是 public (公开的,公共的)和 private (私密的,私有的):

由于类级别的方法仍然声明在本类的 class 代码块之内,所以,这些方法也是能够访问 private 属性的。

class MyObject(object):
    def __init__(self):
        self.public_field = 5
        self.__private_field = 10

    def get_private_field(self):
        return self.__private_field

    @classmethod
    def get_private_field_of_instance(cls, instance):
        return instance.__private_field

正如大家所料,子类无法访问父类的 private 字段。

class MyParentObject(object):
    def __init__(self):
        self.__private_field = 71

class MyChildObject(MyParentObject):
    def get_private_field(self):
        return self.__private_field

baz = MyChildObject()
baz.get_private_field()
>>>
AttributeError: 'MyChildObject' object has no attribute
'_MyChildObject__private_field'

private属性的根本原理:

  • Python 编译器无法严格保证private 字段的私密性。转而以以下机制来保证private 属性。

Python 会对私有属性的名称做一些简单的变换,以保证private 字段的私密性。当编译器看到 MyChildObject.get_private_field 方法要访问私有属性时,它会先把 __private_field 变换为_MyChildObject__private_field,然后再进行访问。本例中,__private_field字段只在MyParentObject.__init__里面做了定义。因此,这个私有属性的真实名称,实际上是_MyParentObject__private_field。子类之所以无法访问父类的私有属性,只不过是因为变换后的属性名与待访问的属性名不相符而已。

了解这套机制之后,我们就可以从任意类中访问相关类的私有属性了。无论是从该类的子类访问,还是从外部访问,我们都不受制于private 属性的访问权限。

assert baz._MyParentObject__private_field == 71

查询该对象的属性字典,我们就能看到,私有属性实际上是按变换后的名称来保存的。

print(baz.__dict__)
>>>
{'_MyParentObject__private_field': 71}

Python 为什么不从语法上严格保证 private 字段的私密性呢?用最简单的话来说,就是:We are all consenting adults here. 这句广为流传的格言,表达了很多Python 程序员的观点,大家都认为开放要比封闭好。

另外一个原因在于:Python 语言本身就已经提供了一些属性挂钩,使得开发者能够按照自己的需要来操作对象内部的数据。

  • 不要盲目地将属性设为private ,而是应该从一开始就做好规划,并允许子类更多地访问超类的内部API。

举个例子:

class MyClass(object):
    def __init__(self):
        self.__value = value

    def get_value(self):
        return str(self.__value)

foo = MyClass(5)
assert foo.get_value() == '5'

# 子类只能通过该方法访问父类的变量
class MyIntegerSubClass(MyClass):
    def get_value(self):
        return int(self._MyClass__value)

foo = MyIntegerSubClass(5)
assert foo.get_value() == 5

可是,一旦子类后面的那套继承体系发生变化,这些对private 字段的引用代码就会傻逼了,从而导致子类出现错误:

class MyBaseClass(object):
    def __init__(self, value):
        self.__value = value
    # ...

class MyClass(MyBaseClass):
    # ...

class MyIntegerSubClass(MyClass):
    def get_value(self):
        return int(self._MyClass__value)

因此,恰当的做法:宁可叫子类更多地访问超类的 protected 属性,也别把这些属性设成private 。我们应该在文档中说明每个protected 字段的含义,解释哪些字段是可供子类使用的内部API,哪些字段是完全不应该触碰的数据。这种建议信息,不仅可以给其他程序员看,而且也能在将来扩展代码的时候提醒自己,应该如何保证数据安全。

class MyClass(object):
    def __init__(self, value):
        # 文档内容:定义_value为protected属性,方便子类访问
        self._value = value

由此,可得:

  • 应该多用protected 属性,并在文档中把这些字段的合理用法告诉子类的开发者,而不要试图用private 属性来限制子类访问这些字段。

只有一种情况是可以合理使用private 属性的,那就是用它来避免子类的属性名与超类相冲突。如果子类无意中定义了与超类同名的属性,那么程序就可能出问题。

  • 只有当子类不受自己控制时,才可以考虑用private 属性来避免名称冲突。即子类的开发者和基类的开发者不是同一个人的时候。
class ApiClass(object):
    def __init__(self):
        self.__value = 5

    def get(self):
        return self.__value

class Child(ApiClass):
    def __init__(self):
        super().__init__()
        self._value = 'hello'  # OK!

第28条:继承 collections.abc 以实现自定义的容器类型

要点:

  • 如果要定制的子类比较简单,那就可以直接从 Python 的容器类型(如 list 或 dict)中继承。

例如:要创建一种自定义的列表类型,并提供统计各元素出现频率的方法。

class FrequencyList(list):
    def __init__(self, members):
        super().__init__(members)

    def frequency(self):
        counts = {}
        for item in self:
            counts.setdefault(item, 0)
            counts[item] += 1
  • 想正确实现自定义的容器类型,可能需要编写大量的特殊方法。
  • 编写自制的容器类型时,可以从 collections.abc 模块的抽象基类中继承,那些基类能够确保我们的子类具备恰当的接口及行为。
from collections.abc import Sequence

class BadType(Sequence):
    pass

foo = BadType()

>>>
TypeError: Can't instantiate abstract class BadType with abstract methods __getitem__, __len__

如果子类已经实现了抽象基类所要求的每个方法,那么基类就会自动提供剩下的那些方法。比如index 和 count 方法实现后之后,其他方法自动会生成。主要方法有__len____getitem__