《Effective Python》Chapter3总结
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__
。