0
点赞
收藏
分享

微信扫一扫

Python 类 - 使用辅助类维护程序状态

Mezereon 2021-09-28 阅读 30

一. 使用字典和元组保存内部状态

Python 内置的 dict 类型可以很好地保存对象在其生命周期的动态内部状态。所谓动态,即这些被保存的信息,其标识符无法提前获知。

例如,下面的简易成绩簿类中,将学生和学生成绩保存在一个字典中,但这些学生的名字,我们事先并不知道:

class SimpleGradebook:
    def __init__(self):
        self._grades = {}
        
    def add_student(self, name):
        self._grades[name] = []
        
    def report_grade(self, name, score):
        self._grades[name].append(score)
        
    def average_grade(self, name):
        grades = self._grades[name]
        return sum(grades) / len(grades)

使用示例:

>> grade_book = SimpleGradebook()
>> grade_book.add_student('Alex')
>> grade_book.report_grade('Alex', 100)
>> grade_book.report_grade('Alex', 60)
>> grade_book.average_grade('Alex')
80.0

现在,假设提出了新的需求,需要按科目来分类保存每个学生的成绩,而不是像前面那样把所有科目的成绩都保存到一起。扩充后的 SimpleGradebook 类我们使用 BySubjectGradeBook 表示:

class BySubjectGradebook:
    def __init__(self):
        self._grades = {}
        
    def add_student(self, name):
        self._grades[name] = {}
        
    def report_grade(self, name, subject, score):
        by_subject = self._grades[name]
        grade_list = by_subject.setdefault(subject, [])
        grade_list.append(score)
        
    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

使用示例:

>> grade_book = BySubjectGradebook()
>> grade_book.add_student('Alex')
>> grade_book.report_grade('Alex', 'Math', 100)
>> grade_book.report_grade('Alex', 'Math', 80)
>> grade_book.report_grade('Alex', 'English',60)
>> grade_book.report_grade('Alex', 'English',120)
>> grade_book.average_grade('Alex')
90.0

由于维护内部状态的 _grade 变成了一个嵌套字典,所以 report_gradeaverage_grade 的处理也开始变得复杂。我们不妨可以打印一下 _grade 的结构:

>> grade_book._grades
{'Alex': {'Math': [100, 80], 'English': [60, 120]}}

如果上面的类你还可以接受,那接下来的扩展,如果继续使用字典,将会变得十分复杂难懂。新的需求:除了需要记录每次考试的成绩,还需要记录每次考试的权重,比如期末和其中考试所占的权重自然是要大于月考成绩的。

下面,我们继续修改内部维护的字典,并将成绩和权重作为一个元组,放入科目对应的列表:

class WeightedGradebook:
    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, weight_sum = 0, 0
        for subject, grades in by_subject.items():
            subject_avg, subject_total, total_weight = 0, 0, 0
            for score, weight in grades:
                subject_total += score * weight
                total_weight += weight
            subject_avg = subject_total / total_weight
            score_sum += subject_avg
            weight_sum += 1
        return score_sum / weight_sum

使用示例:

>> grade_book = WeightedGradebook()
>> grade_book.add_student('Alex')
>> grade_book.report_grade('Alex', 'Math', 80, 0.1)
>> grade_book.report_grade('Alex', 'Math', 80, 0.4)
>> grade_book.report_grade('Alex', 'English',60, 0.6)
>> grade_book.report_grade('Alex', 'English',120, 0.4)
>> grade_book.average_grade('Alex')
82.0

上述的加权计算过程即:((80*0.1+80*0.4)/(0.1+0.4) + (60*0.6+120*0.4)/(0.6+0.4))/2 = 82,可见 average_grade 方法已经变得非常复杂,包含了嵌套循环,非常不利于代码的阅读和维护。

二. 把嵌套结构重构为类

起初,我们并不知道后来需要实现带权重的分数统计,根据当时的复杂度来看没必要编写辅助类。使用 Python 内置的 dicttuple 很容易就能构建出分层的数据结构,从而保存程序的内部状态。但是,当嵌套多余一层的时候,就应避免这样做了,例如,不要使用包含字典的字典。这种多层嵌套的代码,非常不易于阅读和维护。

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

下面,我们就从依赖关系的最底层 - 成绩,开始重构。成绩包含分数和权重两个属性,但这两个属性都是不会发生变化的,因此可以使用具名元组来定义精简而又不可变的数据类型:

import collections

Grade = collections.namedtuple('Grade', ['score', 'weight'])

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

class Subject:
    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:
    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:
    def __init__(self):
        self._students = {}
        
    def student(self, name):
        if name not in self._students:
            self._students[name] = Student()
        return self._students[name]

重构完成,我们来使用以下重构之后的类:
创建成绩簿 book ,添加学生 Alex ,并录入参加考试的两门课程:

>> book = Gradebook()
>> alex = book.student('Alex')
>> math = alex.subject('Math')
>> english = alex.subject('English')

记录每门课程每次的考试成绩以及权重:

>> math.report_grade(80, 0.1)
>> math.report_grade(80, 0.4)
>> english.report_grade(60, 0.6)
>> english.report_grade(120, 0.4)

计算 Alex 考生所有科目的平均成绩:

>> alex.average_grade()
82.0
举报

相关推荐

0 条评论