EffectivePython类与继承

用Python编程时,我们经常需要编写新的类,并且需要规定这些类的使用者应该如何通过接口与继承体系同该类相交互。Python的类和继承使得我们很容易在程序中表达出对象所应具备的行为,也使得我们能够随时改进程序并扩充其功能,以便灵活地应对不断变化的需求。善用类和继承,就可以写出易于维护的代码。

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

Python内置的字典类型可以很好地保存某个对象在其生命周期里的动态内部状态。动态(dynamic)是指这些待保存的信息,其标识符无法提前获知。例如,要把许多学生的成绩记录下来,但这些学生的名字,我们事先并不知道。于是,可以定义一个类,把学生名字全部保存到字典里面,这样就不用把每个学生都表示成对象了,也无需在每个对象中预设一个存放其名字的属性。

 1 class SimpleGradebook(object):
 2     def __init__(self):
 3         self.grades = {}
 4     def add_student(self, name):
 5         self._grades[name] = []
 6     def report_grade(self, name, score):
 7         self._grades[name].append(score)
 8     def average_grade(self, name):
 9         grades = self._grades[name]
10         return sum(grades)/len(grades)
book = SimpleGradebook()
book.add_student('Isaac Netwon')
book.report_grade('Isaac Netwon', 90)
print(book.average_grade('Isaac Netwon'))
>>> 90.0

由于字典用起来很方便,所以有可能因为功能过分膨胀而导致代码出问题。例如,要扩充SimpleGradebook类,使它能够按照科目来保存成绩,而不是像原来那样,把所有科目的成绩都保存到一起。要实现这个功能,可以修改_grades的字典结构,把每个学生的名字与另外一份字典关联起来,使得学生的名字成为_grades字典中每个条目的键,使得另外的那份字典成为该键所对应的值。然后,在另外那份字典中,把每个科目当作键,把该科目下的各项成绩当作值,建立映射关系。

class BySubjectGradebook(object):
    def __init__(self):
        self._grades = {}
    def add_student(self, name):
        self._grades[name] = {}
# 但是report_grade和average_grade方法就比较复杂了,因为它们需要处理嵌套了两层的字典
def report_grade(self, name, subject, grade):
   by_subject = self._grades[name]
   grade_list = by_subject.setdefault(subject, []) # 字典的setdefault(key, default=None)和get()类似, 但如果键不存在于字典中,将会添加键并将值设为default
   grade_list.append(grade)
def average_grade(self, name):
   by_subject = self._grades[name]
   total, count = 0, 0
   for grades in by_subject.values():
     total += sum(grades)
     count += len(grades)
   return total / count
book = BySubjectGradebook()
book.add_student('Albert Einstein')
book.report_grade('Albert Einstein', 'Math', 75)
book.report_grade('Albert Einstein', 'Math', 65)
book.report_grade('Albert Einstein', 'gym', 65)
book.report_grade('Albert Einstein', 'gym', 65)

现在假设需求又变了。除了要记录每次考试的成绩,还需要记录此成绩占科目总成绩的权重。实现该功能的方式之一,是修改内部的字典。原来我们把科目当作键,把该科目各次考试的分数当作值,而现在,则改用一系列元组作为值,每个元组都具备(score,weight)的形式。

class WeightedGradebook(object):
    # ...
    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, weight in scores:
                pass
        return score_sum/score_count

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

起初,我们并不知道后来需要实现带权重的分数统计,所以根据当时的复杂度来看,没有必要编写辅助类。我们很容易就能用Python内置的字典与元组类型构建出分层的数据结构,从而保存程序的内部状态。但是,当嵌套多于一层的时候,就应该避免这种做法了(例如,不要使用包含字典的字典)。这种多层嵌套的代码,其他程序员很难看懂,而且自己维护起来也很麻烦。

用来保存程序状态的数据结构一旦变得过于复杂,就应该将其拆解为类,以便提供更为明确的接口,并更好地封装数据。这样做也能够在接口与具体实现之间创建抽象层。

把嵌套结构重构为类

grades = []
grades.append((95, 0.45))
total = sum(score*weight for score, weight in grades)
total_weight = sum(weight for _, weight in grades)
average_grade = total / total_weight


grades = []
grades.append((95, 0.45, 'Great job'))
total = sum(score*weight for score, weight, _ in grades)
total_weight = sum(weight for _, weight, _ in grades)
average_grade = total / total_weight
# Python程序习惯用下划线表示无用的变量

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

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

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

构建这些具名元组时,既可以按位置指定其中各项,也可以采用关键字来指定。这些字段都可以通过属性名称访问。由于元组的属性都带有名称,所以当需求发生变化,以致要给简单的数据容器添加新的行为时,很容易就能从namedtuple迁移到自己定义的类。

 1 # 表示科目的类,该类包含一系列考试成绩
 2 class Subject(object):
 3     def __init__(self):
 4         self._grades = []
 5     
 6     def report_grade(self, score, weight):
 7         self._grades.append(Grade(score, weight))
 8 
 9     def average_grade(self):
10         total, total_weight = 0, 0
11         for grade in self._grades:
12             total += grade.score * grade.weight
13             total_weight += grade.weight
14         return total / total_weight
15 
16 # 表示学生的类,该类包含此学生正在学习的各项课程
17 class Student(object):
18     def __init__(self):
19         self._subjects = {}
20 
21     def subject(self, name):
22         if name not in self._subjects:
23             self._subjects[name] = Subject()
24         return self._subjects[name]
25 
26     def average_grade(self):
27         total, count = 0, 0
28         for subject in self._subjects.values():
29             total += subject.average_grade()
30             count += 1
31         return total / count
32 
33 # 包含所有学生考试成绩的容器类,该容器以学生的名字为键,并且可以动态地添加学生
34 class Gradebook(object):
35     def __init__(self):
36         self._students = []
37     
38     def student(self, name):
39         if name not in self._students:
40             self._students[name] = Student()
41         return self._students[name]

book = Gradebook()
albert = book.student('Albert Einstein')
math = albert.subject('Math')
math.report_grade(80, 0.10)
print(albert.average_grade())

要点:

不要使用包含其他字典的字典,也不要使用过长的元组

如果容器中包含简单而又不可变的数据,那么可以先使用namedtuple来表示,待稍后有需要时,再修改为完整的类

保存内部状态的字典如果变得比较复杂,那就应该把这些代码拆解为多个辅助类

posted @ 2020-02-23 12:18  LinBupt  阅读(148)  评论(0编辑  收藏  举报