请使用最新版本浏览器访问此演示文稿以获得更好体验。
在本章以前,我们在编程时都将程序看作是一系列函数的集合,通过调用函数来进行流程控制,或者直接对电脑下达指令,这种传统的编程范式被称为面向过程编程。
面向对象程序设计(Object-oriented programming,OOP)是另一种具有对象(object)概念的编程范式,同时也是一种程序开发的抽象方针。它将对象作为程序的基本单元,将程序和数据封装其中,以提高软件的重用性、灵活性和扩展性。对象指的是类(class)的实例,它可能包含数据、属性、代码与方法。在面向对象程序编程里,计算机程序会被设计成彼此相关的对象。
面向对象程序设计中的每一个对象都应该能够接受数据、处理数据并将数据传达给其它对象,因此它们都可以被看作一个小型的“机器”,即对象。面向对象程序设计提高了程序的灵活性和可维护性,在大型项目设计中被广为应用。
用史书来类比程序设计的范式:面向过程程序设计就像是编年体史书,而面向对象程序设计则像是纪传体史书。前者按事情的发展过程叙述,后者围绕特定的主角来叙述。
问题:洗衣机里有脏衣服,怎么洗干净?
(1)面向过程的解决方法:
(2)面向对象的解决方法:
执行:
例如,整数可以看作是一个类,而 33 则可以被看作是一个对象。
再如,一只名字叫阿黄的小狗就可以看作是一个对象,它是犬类的一个实例。
同函数定义(def 语句)类似,类的定义通过 class 语句实现:
            class ClassName:
                """类的文档字符串"""  # 类的帮助信息
                statement             # 类体
        
        ClassName 用于指定类名,一般使用“驼峰式命名法”;"""类的文档字符串"""可以通过 ClassName.__doc__ 查看;statement 主要由类变量(或称类成员)、方法和属性等定义语句组成。示例:
            class Student:
                """所有学生的基类"""
                count = 0  # 类变量,表示学生总数
            
                def __init__(self, id, name):  # 特殊的构造方法
                    self.id = id
                    self.name = name
                    self.scores = {}
                    Student.count += 1
            
                def set_score(self, course, score):  # 方法
                    self.scores[course] = score
        
    上页代码中:
count 变量是一个类变量,又称类属性,类变量定义在类中且在函数体之外,它的值将在这个类的所有实例之间共享。类属性可以通过类名称(如 Student.count)或实例名访问。__init__ 和 set_score 这些在类中定义的函数,称为方法,方法是从属于对象的函数。self 是方法的一个特殊参数,代表类的实例,它在方法的参数列表中首先给出,但在调用时不必传入相应的参数。id、name 和 scores 这种使用 self.name 形式赋值的变量称为实例变量,又称实例属性,一般应该在 __init__() 函数中定义所有实例变量。可以通过属性引用的方式(obj.name)访问类变量,如下所示:
            print(Student.count)  # 0
        
    定义好的类相当于一栋房屋的设计图,它告诉我们房屋的模型,但本身不是房屋。要创建真实的房屋,需要把类实例化。
类的实例化使用函数表示法。如下所示:
            class MyClass:  # 定义 MyClass 类
                pass
            
            obj1 = MyClass()  # MyClass 类的实例化
            obj2 = MyClass()  # MyClass 类的实例化
        
        obj1 = MyClass() 创建类 MyClass 的新实例并将其赋值给变量 obj1。
__init__() 方法是一种特殊的方法,称为构造函数或构造器。每当创建一个类的新实例时,都会自动执行该方法。该方法必须包含一个 self 参数,且其必须是第一个参数。示例如下:
            class Dog:
                def __init__(self):
                    print('汪!汪!')
            
            my_dog = Dog()  # 汪!汪!
        
        说明:在 __init__() 方法的名称中,开头和结尾处是两个下划线(中间没有空格),这是一种约定,旨在区分 Python 的默认方法和普通方法。
当 __init__() 方法除了包含 self 参数外,还有额外的参数时,在类实例化时也应该提供这些参数,他们将被传递给__init__() 方法。如前面定义的 Student 类,可以按如下方式进行实例化和使用:
            zhangsan = Student(2001, '张三')
            lisi = Student(2002, '李四')
            print(Student.count)
        
    在创建了类的实例后,可以通过属性引用访问实例的属性(实例变量和方法)。例如:
            zhangsan = Student(2001, '张三')
            zhangsan.set_score('数学', 79.0)  # 调用实例方法时不必给定 self 参数
            print(zhangsan.scores)  # 访问其 scores 数据属性,{'数学': 79.0}
            print(zhangsan.count)  # 甚至可以通过实例访问类变量,1
            
            lisi = Student(2002, '李四')
            lisi.set_score('数学', 88.5)
            print(lisi.name)  # 李四
            print(lisi.count)  # 2
            
            print(Student.count)  # 2
        
       
    类属性、实例属性都可以自由地添加、修改和删除:
            class Dog:
                bark_mode = '汪!汪!'
                def __init__(self):
                    print(Dog.bark_mode)
        
            my_dog1 = Dog()  # 汪!汪!
            Dog.bark_mode = '嗷...呜...'
            my_dog2 = Dog()  # 嗷...呜...
            Dog.name = '狗狗'
            print(Dog.name)  # 狗狗
            my_dog1.name = '阿黄'
            print(my_dog1.name)  # 阿黄,同样的属性名称同时出现在实例和类中,则属性查找会优先选择实例
            del Dog.bark_mode
            del my_dog1.name
        
        一般来说,实例变量用于每个实例的唯一数据,而类变量用于类的所有实例共享的属性和方法。
也可以使用以下函数的方式来访问属性:
getattr(object, name, /) 或 getattr(object, name, default, /): 返回 object 对象的 name 属性,等价于 object.name。hasattr(object, name, /):检查是否存在一个属性,如果字符串 name 是对象 object 的属性之一的名称,则返回 True,否则返回 False。setattr(object, name, value, /):设置一个属性。如果属性不存在,会创建一个新属性,等价于 object.name = value。delattr(object, name, /):删除属性,等价于 del object.name。所有类具有一些内置属性:
__dict__: 类的属性(包含一个字典,由类的数据属性组成)。__doc__:类的文档字符串。__name__:类名。__module__:类定义所在的模块(类的全名是 __main__.ClassName,如果类位于一个导入模块 mymod 中,那么 ClassName.__module__ 等于 mymod)。__bases__:类的所有父类构成元素(包含了一个由所有父类组成的元组)。为了保证类内部的某些属性或方法不被外部所访问,可以在属性或方法名前面添加双下划线 __private_attrs 声明该属性为私有,只允许定义这个类本身进行访问。如下所示:
            class Student:
                """所有学生的基类"""
                __count = 0  # 私有类变量,表示学生总数
            
                def __init__(self, id, name):  # 特殊的构造方法
                    self.__avg_score = None  # 私有示例变量
                    self.id = id
                    self.name = name
                    self.scores = {}
                    Student.__count += 1
            
                def set_score(self, course, score):  # 方法
                    self.scores[course] = score
                    self.__avg_score = sum(self.scores.values()) / len(self.scores)
            
                def avg_score(self):
                    return self.__avg_score
            
            zhangsan = Student(2001, '张三')
            zhangsan.set_score('数学', 79.0)
            print(zhangsan.avg_score())
        
    单下划线、双下划线、头尾双下划线说明:
_foo:以单下划线开头的表示的是保护(protected)类型的变量,即保护类型只能允许其本身与子类进行访问,不能用于 from module import *。__foo:以双下划线开头的表示的是私有类型(private)类型的变量,只能是允许这个类本身进行访问。__foo__:头尾双下划线定义的是特殊属性,一般是系统定义名字,类似 __init__() 之类的。Python 通过不同的下划线命名方式在一定程度上实现了封装(Encapsulation)。封装是面向对象编程的三大基本特征之一。封装思想保证了类内部数据结构的完整性,使用该类的用户无法直接看到类中的数据结构,只能使用类允许公开的数据,很好地避免了外部对内部数据的影响,提高了程序的可维护性。
在某种情况下,一个类会有子类(或称派生类)。子类比原本的类(称为父类或基类)要更加具体化。例如,Student 这个类可能会有它的子类 MiddleSchoolStudent 和 CollegeStudent 。子类会继承父类的属性(变量和方法),并且也可包含它们自己的。我们假设 Dog 这个类有一个方法叫做 bark() 和一个属性叫做 color。它的子类(如 ChineseRuralDog 或 GermanShepherdDog)会继承这些成员。这意味着只需要将相同的代码写一次。
这种子类具有父类的属性和方法的机制,称为继承(Inheritance)。
继承可避免相同代码的重复书写,减少代码的数量。继承是面向对象编程的三大基本特征之二。
子类定义的语法如下:
            class DerivedClassName(BaseClassName):
                ...
        
        即需要将父类 BaseClassName 列在子类 DerivedClassName 头部的括号内。
在使用子类及其实例化对象时,如果引用了其属性,并且在子类中找不到此属性,会转往基类中查找此属性,如果基类也继承自其它某个类,此搜索将被递归地应用。
示例:
            class Dog:
                def __init__(self):
                    self.name = None
                    self.breed = None
                    self.bark_mode = '汪…汪…'
                def bark(self):
                    print(f'一只名为{self.name}的{self.breed}在{self.bark_mode}地叫着。')
            
            class ChineseRuralDog(Dog): pass
            my_dog = ChineseRuralDog()
            my_dog.name = '大黄'
            my_dog.breed = '中华田园犬'
            my_dog.bark()  # 一只名为大黄的中华田园犬在汪…汪…地叫着。
        
    以下示例无法运行,请分析原因:
            class Dog:
                def __init__(self):
                    self.name = None
                    self.breed = None
                    self.bark_mode = '汪…汪…'
                def bark(self):
                    print(f'一只名为{self.name}的{self.breed}在{self.bark_mode}地叫着。')
            class ChineseRuralDog(Dog):
                def __init__(self):
                    self.breed = '中华田园犬'
            my_dog = ChineseRuralDog()
            my_dog.name = '大黄'
            my_dog.bark()  # AttributeError: 'ChineseRuralDog' object has no attribute 'bark_mode'
        
    又一个示例:
            class Shape:
                def draw(self): ...
                def edge_length(self): ...
            
            class Polygon(Shape):
                def area(self): ...
            
            class Rectangle(Polygon):
                def width(self): ...
                def height(self): ...
            rect = Rectangle()
            rect.draw()
            rect.area()
            rect.width()
        
    一个子类可以有多个父类,此种继承关系称为多重继承。如下所示:
            class DerivedClassName(Base1, Base2, Base3):
                ...
        
        在搜索从父类所继承的属性属性时,会按照深度优先、从左至右的顺序进行,当层次结构中存在重叠时不会在同一个类中搜索两次。因此,如果某一属性在 DerivedClassName 中未找到,则会到 Base1 中搜索它,然后(递归地)到 Base1 的基类中搜索,如果还未找到,再到 Base2 中搜索,依此类推。
示例:
            class HeadingMachine:
                """掘进机"""
                def driving(self): ...
            
            class RockDrill:
                """凿岩机"""
                def drill(self): ...
            
            class BolterMiner(HeadingMachine, RockDrill):
                """掘锚机"""
                def other(self): ...
        
    另一个示例:
            class Reader:
                def read(self): ...
            
            class Writer:
                def write(self): ...
            
            class File(Reader, Writer):
                def other(self): ...
        
    Python 有两个内置函数可被用于继承机制:
isinstance()检查一个实例的类型,isinstance(obj, int) 仅会在 obj.__class__ 为 int 或某个派生自 int 的类时为 True。issubclass()检查类的继承关系,issubclass(bool, int)  为 True,因为 bool 是 int 的子类。 但是,issubclass(float, int) 为 False,因为 float 不是 int 的子类。子类可以包含与父类相同签名的方法,即子类中的方法将重写(Override)父类的方法,从而表现出与父类不同的行为,而其他方法仍然继承父类。
由继承而产生的相关的不同的类,其对象对同一方法会做出不同的响应,这种机制被称为多态(Polymorphism)。多态是面向对象编程的三大基本特征之三。
在下面示例中,子类 Car、Train 和 Bicycle 都重写了父类的 whistle 方法:
            class Vehicle:
                def whistle(self):
                    print('鸣笛声')
            
            class Car(Vehicle):
                def whistle(self):
                    print('嘀……嘀……')
            
            class Train(Vehicle):
                def whistle(self):
                    print('呜……呜……')
            
            class Bicycle(Vehicle):
                def whistle(self):
                    print('叮铃铃……')
        
    上例中三个子类的实例对象可以嵌入到一个更大的容器中,通过循环语句进行遍历,并调用他们同名的方法:
            vehicles = [Car(), Train(), Bicycle()]
            for v in vehicles:
                v.whistle()
            # 嘀……嘀……
            # 呜……呜……
            # 叮铃铃……
        
        这些同名的方法表现出不同的行为,这就是多态。
有时候,我们需要对数据属性给出更精确的控制。按照其他语言的的实现方法,可以编写 “getter”、“setter” 或 “deleter” 一类的方法。如下示例:
            class Student:
                def __init__(self, name):
                    self.name = name
                    self.__is_male = None  # 为了演示目的,特别通过私有的布尔类型变量存储性别
                def get_gender(self):
                    return '男' if self.__is_male else '女'
                def set_gender(self, gender):
                    match gender.lower():
                        case '男' | '男性' | 'male' | 'm' | 'man':
                            self.__is_male = True
                        case '女' | '女性' | 'female' | 'f' | 'woman' | 'w':
                            self.__is_male = False
                        case _:
                            self.__is_male = None
                def del_gender(self):
                    del self.__is_male
            xiaoming = Student('小明')
            xiaoming.set_gender('男')
            print(xiaoming.get_gender())
            xiaoming.del_gender
        
    Python 有一个内置的 property 函数,用于创建一个特征属性 property 对象。其签名为:
            class property(fget=None, fset=None, fdel=None, doc=None)
        
        property 对象有三个方法:getter()、setter() 和 delete(),可以分别通过 fget、fset 和 fdel 参数指定。这里 fget 是获取属性值的函数,fset 是用于设置属性值的函数,fdel 是用于删除属性值的函数,doc 用于为属性对象创建文档字符串。
可以通过为所定义的类创建一个 property 类型的类变量来定义一个托管属性,见下页示例。
使用 property() 函数:
            class Student:
                def __init__(self, name):
                    self.name = name
                    self.__is_male = None  # 为了演示目的,特别通过私有的布尔类型变量存储性别
                def get_gender(self):
                    return '男' if self.__is_male else '女'
                def set_gender(self, gender):
                    match gender.lower():
                        case '男' | '男性' | 'male' | 'm' | 'man':
                            self.__is_male = True
                        case '女' | '女性' | 'female' | 'f' | 'woman' | 'w':
                            self.__is_male = False
                        case _:
                            self.__is_male = None
                def del_gender(self):
                    del self.__is_male
                # gender 是一个托管的属性
                gender = property(get_gender, set_gender, del_gender, "I'm the 'gender' property.")
            xiaoming = Student('小明')
            xiaoming.gender = '男'
            print(xiaoming.gender)  # 男
            del xiaoming.gender
        
        这里 xiaoming.gender 将调用其“getter”,xiaoming.gender = value 将调用其“setter”,del xiaoming.gender 将调用 “deleter”。
以上代码可进一步用 Python 的装饰器来实现,见下页代码。
装饰器为一种语法糖,它是返回值为另一个函数的函数。通常使用 @wrapper 形式的语法来进行函数变换,从而修改其他函数功能,使代码更简洁。
下页 @property 装饰器会创建一个与 gender() 方法同名的特征属性 gender,将该方法指定给其 “getter”,将 gender 的文档字符串设置为 "I'm the 'gender' property.",然后将同名的 gender() 方法分别指定给 gender 特征属性的 “setter” 和 “deleter” 方法。
使用 @property 装饰器:
            class Student:
                def __init__(self, name):
                    self.name = name
                    self.__is_male = None
                @property
                def gender(self):
                    """I'm the 'gender' property."""
                    return '男' if self.__is_male else '女'
                @gender.setter
                def gender(self, gender):
                    match gender.lower():
                        case '男' | '男性' | 'male' | 'm' | 'man':
                            self.__is_male = True
                        case '女' | '女性' | 'female' | 'f' | 'woman' | 'w':
                            self.__is_male = False
                        case _:
                            self.__is_male = None
                @gender.deleter
                def gender(self):
                    del self.__is_male
            xiaoming = Student('小明')
            xiaoming.gender = '男'
            print(xiaoming.gender)  # 男
            del xiaoming.gender
        
    编写一个存储网站注册用户信息的 User 类,要求:
name 只能使用 A~Z、a~z、0~9、- 和 _ 这些字符,只能以字母开头,长度在 5~20 字符之间;password 只能是编号范围为 32~126 之间的 ASCII 可显示字符,长度在 10~50 之间,必须至少各包含一个小写字母、大写字母和数字,以及至少包含一个除了字母和数字之外的特殊字符。User(name, password) 的形式创建用户,后期也可以进一步通过 user.name = new_value 的形式修改用户名和密码。提示:最好使用 @property 装饰器将用户名和密码设置为特征属性,并将实际的用户名和密码实例变量设置为私有的或保护的。另外请注意这里是以明文存储密码的,实际网站的密码都应该是不可逆加密存储的。
复习矿井通风课程中所学习的关于井巷通风阻力一章的知识,编写一个名称为 Roadway 的类,可以表示在矿井通风设计时井巷通风阻力计算的一些基本操作。以下已经搭建出该类的框架,请将其中标注为 TODO 的部分补充完整:
                    class Roadway:
                        """表示一段巷道,该段巷道对应通风网络图中的一个边,所有数值单位都为国际标准单位"""
                        def __init__(self, length, area, form, alpha, volume_flux):
                            self.length = length  # 巷道的长度
                            self.area = area  # 巷道的断面积
                            self.form = form  # 巷道断面形状,参见 SectionalForm 类的定义
                            self.alpha = alpha  # 巷道的摩擦阻力系数,可在通风教材最后的附录中通过查表得到
                            self.volume_flux = volume_flux  # 巷道的风量(体积通量)
                            self.air_density = 1.25  # 巷道内空气的平均密度
                            # 总阻力与摩擦阻力的比值,也是总风阻与摩擦风阻的比值,取值范围为 1.1~1.15,
                            # 一般新建矿井宜取 1.10,改扩建矿井宜取 1.15
                            self.t2f_ratio = 1.1
                        @property
                        def perimeter(self):
                            """根据巷道的断面面积、断面形状估算巷道的断面周长"""
                            match self.form:
                                case SectionalForm.Trapezoid:
                                    return 4.16 * (self.area ** 0.5)
                                case SectionalForm.ThreeCenteredArch:
                                    return 3.85 * (self.area ** 0.5)
                                case SectionalForm.SemiCircularArch:
                                    return 3.90 * (self.area ** 0.5)
                                case _:
                                    return None
                        def frictional_resistance(self):
                            """计算该段巷道的摩擦风阻 Rf"""
                            pass  # TODO: 请进一步完成该函数
                        def local_resistance(self):
                            """计算该段巷道的局部风阻 Rl"""
                            return self.frictional_resistance() * (self.t2f_ratio - 1.0)
                        def total_resistance(self):
                            """计算该段巷道的总风阻 R"""
                            return self.frictional_resistance() * self.t2f_ratio
                        def velocity(self):
                            """计算该段巷道的平均风速"""
                            pass  # TODO: 请进一步完成该函数
                        def frictional_loss(self):
                            """计算该段巷道的摩擦阻力 hf"""
                            pass  # TODO: 请进一步完成该函数
                        def local_loss(self):
                            """计算该段巷道的局部阻力 hl"""
                            return self.frictional_loss() * (self.t2f_ratio - 1.0)
                        def total_loss(self):
                            """计算该段巷道的总阻力 h"""
                            return self.frictional_loss() * self.t2f_ratio
                    class SectionalForm:
                        """这里实际上是用类变量模拟其他语言的枚举变量,即三个类变量分别代表三种断面形状。
                        Python 的标准库是有枚举(enum)模块的,不过这里为了展示类的用法而没有用该模块。"""
                        Trapezoid = 1  # 梯形
                        ThreeCenteredArch = 2  # 三心拱
                        SemiCircularArch = 3  # 半圆拱
                    # 给定一段通风路线
                    vent_path = [
                        Roadway(123.0, 16.0, SectionalForm.ThreeCenteredArch, 33.2e4, 90.0),
                        Roadway(37.4, 13.6, SectionalForm.SemiCircularArch, 82.9e4, 74.5),
                        Roadway(409.1, 13.2, SectionalForm.Trapezoid, 167.8e4, 64.5),
                    ]
                    path_total_loss = 0.0  # 给定的通风路线的总阻力初值
                    # TODO: 请进一步编写程序,计算以上给定的通风路线的总阻力 path_total_loss
                
            要求:
安模作业02-06-学号-姓名.py 的源文件中,通过电子邮件以附件形式发给任课教师。安模作业02-06-学号-姓名 的形式。