目录

Python辅助类代替字典和元组维持程序状态

概述

通常人们习惯用 Python 的内置字典类型来保存对象在生命周期里的动态内部状态。举个简单的例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
class SimpleGradebook(object):
    def __init__(self):
        self._grades = {}

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

    def report_grades(self, name, score):
        self._grades[name].append(score)

    def average_grade(self, name):
        grades = self._grades[name]
        return sum(grades) / len(grades)

从示例程序来看,SimpleGradebook 这个类代表简易的成绩册的意义,通过用内部的 _grades 字典来保存学生的姓名和成绩,并且提供方法 report_grades 了来登记学生的姓名和成绩,此外还提供一个计算所有登记在册的学生的平均成绩的方法 average_grade

字典用起来是很方便的,并且效率也很高,计算复杂度很低。然后当需求增多的时候,其实并不好扩展,比如现在需要成绩册可以记录不同科目的成绩,并且可以根据不同科目来求平均成绩。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
class BySubjectGradebook(object):
    def __init__(self):
        self._grades = {}

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

    def report_grade(self, name, subject, grade):
        by_subject = self._grades[name]
        grade_list = by_subject.setdefault(subject, [])
        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

上例中的类为 BySubjectGradebook,代表记录不同科目的成绩,可以看到,在录入成绩的方法 report_grade 增加科目参数,并且增加 grade_list 来记录不同科目的成绩列表,同时也可以看到 average_grade 计算成绩的平均分的方法代码膨胀,而已有嵌套循环,这让人头大……

然而这还不是最糟糕的,当年级长需要班主任提供一份该班的「平均成绩」,并且由于「素质教育」,各个科目都有相应权重,比如说语数英权重更高,音乐体育权重较低,所以成绩册还得加一个功能,就是计算加权平均分的功能。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
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):
        if name not in self._students:
            self._students[name] = Student()
        return self._students[name]
        
if __name__ == '__main__':
    book = Gradebook()
    oscar = book.student('oscar')
    math = oscar.subject('Math')
    math.report_grade(80, 0.1)
    print oscar.average_grade()

可以看到新的具有计算加权平均分功能的类 Grade 代码量其实并不小,但是仔细去阅读的话,可以发现代码逻辑会更加清晰。利用 collections 模块中的 namedtuple 类型可以定义出精简而有不可变的数据数据类型。

上述代码中构建了一个 Grade 结构,会记录某科目的平均分和该科目的权重 weight,并且把科目、学生各构建一个辅助类来记录数据,这样使得最后的成绩册类 Gradebook 的表达更加清晰。在使用上,只需要新建一个 Student 的对象,然后再新建一个该学生的 subject 对象,再去登记分数和权重,最后调用计算加权平均分的方法即可。

参考资料

  1. Effective Python
警告
本文最后更新于 2017年2月1日,文中内容可能已过时,请谨慎参考。