西安科技大学

安全工程专业课程

安全仿真与模拟基础


金洪伟 & 闫振国 & 王延平

西安科技大学安全科学与工程学院


返回目录⇡

如何浏览?

  1. 从浏览器地址栏打开 https://zimo.net/aqmn/
  2. 点击章节列表中的任一链接,打开相应的演示文稿;
  3. 点击链接打开演示文稿,使用空格键或方向键导航;
  4. f键进入全屏播放,再按Esc键退出全屏;
  5. Alt键同时点击鼠标左键进行局部缩放;
  6. Esco键进入幻灯片浏览视图。

请使用最新版本浏览器访问此演示文稿以获得更好体验。

第 2 部分  Python 基础

第 6 章
面向对象程序设计

目 录

  1. 概述
  2. 类的定义和使用
  3. 类的继承
  4. 方法重写
  5. 特征属性

1. 面向对象介绍

1.1 什么是面向对象编程?

在本章以前,我们在编程时都将程序看作是一系列函数的集合,通过调用函数来进行流程控制,或者直接对电脑下达指令,这种传统的编程范式被称为面向过程编程

面向对象程序设计(Object-oriented programming,OOP)是另一种具有对象(object)概念的编程范式,同时也是一种程序开发的抽象方针。它将对象作为程序的基本单元,将程序和数据封装其中,以提高软件的重用性、灵活性和扩展性。对象指的是(class)的实例,它可能包含数据、属性、代码与方法。在面向对象程序编程里,计算机程序会被设计成彼此相关的对象。

1.1 什么是面向对象编程?

面向对象程序设计中的每一个对象都应该能够接受数据、处理数据并将数据传达给其它对象,因此它们都可以被看作一个小型的“机器”,即对象。面向对象程序设计提高了程序的灵活性和可维护性,在大型项目设计中被广为应用。

用史书来类比程序设计的范式:面向过程程序设计就像是编年体史书,而面向对象程序设计则像是纪传体史书。前者按事情的发展过程叙述,后者围绕特定的主角来叙述。

1.2 示例

问题:洗衣机里有脏衣服,怎么洗干净?

(1)面向过程的解决方法:

  1. 执行加洗衣粉函数;
  2. 执行加水函数;
  3. 执行洗衣服函数;
  4. 执行清洗函数;
  5. 执行烘干函数。

1.2 示例

(2)面向对象的解决方法:

  1. 创建两个对象:“洗衣机”对象和“人”对象
  2. 为“洗衣机”对象加入属性和方法:“洗衣服”、“清洗”、“烘干”
  3. 为“人”对象加入属性和方法:“加洗衣粉”、“加水”
  4. 执行:

    • 人.加洗衣粉
    • 人.加水
    • 洗衣机.洗衣服
    • 洗衣机.清洗
    • 洗衣机.烘干

1.3 类和对象

类(Class)
用来描述具有相同的属性和方法的对象的集合,是多个实例的工厂。它定义了该集合中每个对象所共有的属性和方法。
对象(Object)
通过类定义的数据结构实例,代表程序中具体的元素。对象包括两个数据成员(类变量和实例变量)和方法。
实例化
创建一个类的实例(类的具体对象)。

例如,整数可以看作是一个类,而 33 则可以被看作是一个对象。

再如,一只名字叫阿黄的小狗就可以看作是一个对象,它是类的一个实例。

2. 类的定义和使用

2.1 定义类

同函数定义(def 语句)类似,类的定义通过 class 语句实现:


            class ClassName:
                """类的文档字符串"""  # 类的帮助信息
                statement             # 类体
        
  • 其中 ClassName 用于指定类名,一般使用“驼峰式命名法”;
  • """类的文档字符串"""可以通过 ClassName.__doc__ 查看;
  • statement 主要由类变量(或称类成员)、方法和属性等定义语句组成。

2.1 定义类

示例:


            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
        

2.1 定义类

上页代码中:

  • count 变量是一个类变量,又称类属性,类变量定义在类中且在函数体之外,它的值将在这个类的所有实例之间共享。类属性可以通过类名称(如 Student.count)或实例名访问。
  • __init__set_score 这些在类中定义的函数,称为方法,方法是从属于对象的函数。
  • self 是方法的一个特殊参数,代表类的实例,它在方法的参数列表中首先给出,但在调用时不必传入相应的参数。
  • idnamescores 这种使用 self.name 形式赋值的变量称为实例变量,又称实例属性,一般应该在 __init__() 函数中定义所有实例变量。
  • 所有这些类变量、实例变量、方法都可统称为属性(Attribute)。

可以通过属性引用的方式(obj.name)访问类变量,如下所示:


            print(Student.count)  # 0
        

2.2 创建类的实例

定义好的类相当于一栋房屋的设计图,它告诉我们房屋的模型,但本身不是房屋。要创建真实的房屋,需要把类实例化。

类的实例化使用函数表示法。如下所示:


            class MyClass:  # 定义 MyClass 类
                pass
            
            obj1 = MyClass()  # MyClass 类的实例化
            obj2 = MyClass()  # MyClass 类的实例化
        

obj1 = MyClass() 创建类 MyClass 的新实例并将其赋值给变量 obj1

2.2 创建类的实例

__init__() 方法是一种特殊的方法,称为构造函数构造器。每当创建一个类的新实例时,都会自动执行该方法。该方法必须包含一个 self 参数,且其必须是第一个参数。示例如下:


            class Dog:
                def __init__(self):
                    print('汪!汪!')
            
            my_dog = Dog()  # 汪!汪!
        

说明:__init__() 方法的名称中,开头和结尾处是两个下划线(中间没有空格),这是一种约定,旨在区分 Python 的默认方法和普通方法。

2.2 创建类的实例

__init__() 方法除了包含 self 参数外,还有额外的参数时,在类实例化时也应该提供这些参数,他们将被传递给__init__() 方法。如前面定义的 Student 类,可以按如下方式进行实例化和使用:


            zhangsan = Student(2001, '张三')
            lisi = Student(2002, '李四')

            print(Student.count)
        

2.3 访问属性

在创建了类的实例后,可以通过属性引用访问实例的属性(实例变量和方法)。例如:


            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
        

2.3 访问属性

类属性、实例属性都可以自由地添加、修改和删除:


            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
        

一般来说,实例变量用于每个实例的唯一数据,而类变量用于类的所有实例共享的属性和方法。

2.3 访问属性

也可以使用以下函数的方式来访问属性:

  • 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

2.4 内置属性

所有类具有一些内置属性:

  • __dict__: 类的属性(包含一个字典,由类的数据属性组成)。
  • __doc__:类的文档字符串。
  • __name__:类名。
  • __module__:类定义所在的模块(类的全名是 __main__.ClassName,如果类位于一个导入模块 mymod 中,那么 ClassName.__module__ 等于 mymod)。
  • __bases__:类的所有父类构成元素(包含了一个由所有父类组成的元组)。

2.5 访问限制

为了保证类内部的某些属性或方法不被外部所访问,可以在属性或方法名前面添加双下划线 __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())
        

2.5 访问限制

单下划线、双下划线、头尾双下划线说明:

  • _foo:以单下划线开头的表示的是保护(protected)类型的变量,即保护类型只能允许其本身与子类进行访问,不能用于 from module import *
  • __foo:以双下划线开头的表示的是私有类型(private)类型的变量,只能是允许这个类本身进行访问。
  • __foo__:头尾双下划线定义的是特殊属性,一般是系统定义名字,类似 __init__() 之类的。

Python 通过不同的下划线命名方式在一定程度上实现了封装(Encapsulation)。封装是面向对象编程的三大基本特征之一。封装思想保证了类内部数据结构的完整性,使用该类的用户无法直接看到类中的数据结构,只能使用类允许公开的数据,很好地避免了外部对内部数据的影响,提高了程序的可维护性。

3. 类的继承

在某种情况下,一个类会有子类(或称派生类)。子类比原本的类(称为父类基类)要更加具体化。例如,Student 这个类可能会有它的子类 MiddleSchoolStudentCollegeStudent 。子类会继承父类的属性(变量和方法),并且也可包含它们自己的。我们假设 Dog 这个类有一个方法叫做 bark() 和一个属性叫做 color。它的子类(如 ChineseRuralDogGermanShepherdDog)会继承这些成员。这意味着只需要将相同的代码写一次。

这种子类具有父类的属性和方法的机制,称为继承(Inheritance)。

继承可避免相同代码的重复书写,减少代码的数量。继承是面向对象编程的三大基本特征之二。

3.1 定义子类

子类定义的语法如下:


            class DerivedClassName(BaseClassName):
                ...
        

即需要将父类 BaseClassName 列在子类 DerivedClassName 头部的括号内。

在使用子类及其实例化对象时,如果引用了其属性,并且在子类中找不到此属性,会转往基类中查找此属性,如果基类也继承自其它某个类,此搜索将被递归地应用。

3.1 定义子类

示例:


            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()  # 一只名为大黄的中华田园犬在汪…汪…地叫着。
        

3.1 定义子类

以下示例无法运行,请分析原因:


            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'
        

3.1 定义子类

又一个示例:


            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()
        

3.2 多重继承

一个子类可以有多个父类,此种继承关系称为多重继承。如下所示:


            class DerivedClassName(Base1, Base2, Base3):
                ...
        

在搜索从父类所继承的属性属性时,会按照深度优先、从左至右的顺序进行,当层次结构中存在重叠时不会在同一个类中搜索两次。因此,如果某一属性在 DerivedClassName 中未找到,则会到 Base1 中搜索它,然后(递归地)到 Base1 的基类中搜索,如果还未找到,再到 Base2 中搜索,依此类推。

3.2 多重继承

示例:


            class HeadingMachine:
                """掘进机"""
                def driving(self): ...
            
            class RockDrill:
                """凿岩机"""
                def drill(self): ...
            
            class BolterMiner(HeadingMachine, RockDrill):
                """掘锚机"""
                def other(self): ...
        

3.2 多重继承

另一个示例:


            class Reader:
                def read(self): ...
            
            class Writer:
                def write(self): ...
            
            class File(Reader, Writer):
                def other(self): ...
        

3.3 检查继承关系

Python 有两个内置函数可被用于继承机制:

  • isinstance()检查一个实例的类型,isinstance(obj, int) 仅会在 obj.__class__int 或某个派生自 int 的类时为 True
  • issubclass()检查类的继承关系,issubclass(bool, int)True,因为 boolint 的子类。 但是,issubclass(float, int)False,因为 float 不是 int 的子类。

4. 方法重写

子类可以包含与父类相同签名的方法,即子类中的方法将重写(Override)父类的方法,从而表现出与父类不同的行为,而其他方法仍然继承父类。

由继承而产生的相关的不同的类,其对象对同一方法会做出不同的响应,这种机制被称为多态(Polymorphism)。多态是面向对象编程的三大基本特征之三。

4. 方法重写

在下面示例中,子类 CarTrainBicycle 都重写了父类的 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('叮铃铃……')
        

4. 方法重写

上例中三个子类的实例对象可以嵌入到一个更大的容器中,通过循环语句进行遍历,并调用他们同名的方法:


            vehicles = [Car(), Train(), Bicycle()]
            for v in vehicles:
                v.whistle()
            # 嘀……嘀……
            # 呜……呜……
            # 叮铃铃……
        

这些同名的方法表现出不同的行为,这就是多态。

5. 特征属性

有时候,我们需要对数据属性给出更精确的控制。按照其他语言的的实现方法,可以编写 “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
        

5. 特征属性

Python 有一个内置的 property 函数,用于创建一个特征属性 property 对象。其签名为:


            class property(fget=None, fset=None, fdel=None, doc=None)
        

property 对象有三个方法:getter()setter()delete(),可以分别通过 fgetfsetfdel 参数指定。这里 fget 是获取属性值的函数,fset 是用于设置属性值的函数,fdel 是用于删除属性值的函数,doc 用于为属性对象创建文档字符串。

可以通过为所定义的类创建一个 property 类型的类变量来定义一个托管属性,见下页示例。

5. 特征属性

使用 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”。

5. 特征属性

以上代码可进一步用 Python 的装饰器来实现,见下页代码。

装饰器为一种语法糖,它是返回值为另一个函数的函数。通常使用 @wrapper 形式的语法来进行函数变换,从而修改其他函数功能,使代码更简洁。

下页 @property 装饰器会创建一个与 gender() 方法同名的特征属性 gender,将该方法指定给其 “getter”,将 gender 的文档字符串设置为 "I'm the 'gender' property.",然后将同名的 gender() 方法分别指定给 gender 特征属性的 “setter” 和 “deleter” 方法。

5. 特征属性

使用 @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
        

作业

  1. 什么是面向对象程序设计?面向对象程序设计的三大基本特征是什么?各有什么好处?
  2. 编写一个存储网站注册用户信息的 User 类,要求:

    • 用户名 name 只能使用 A~Za~z0~9-_ 这些字符,只能以字母开头,长度在 5~20 字符之间;
    • 用户密码 password 只能是编号范围为 32~126 之间的 ASCII 可显示字符,长度在 10~50 之间,必须至少各包含一个小写字母、大写字母和数字,以及至少包含一个除了字母和数字之外的特殊字符。
    • 可以通过 User(name, password) 的形式创建用户,后期也可以进一步通过 user.name = new_value 的形式修改用户名和密码。

    提示:最好使用 @property 装饰器将用户名和密码设置为特征属性,并将实际的用户名和密码实例变量设置为私有的或保护的。另外请注意这里是以明文存储密码的,实际网站的密码都应该是不可逆加密存储的。

作业

  1. 复习矿井通风课程中所学习的关于井巷通风阻力一章的知识,编写一个名称为 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
                    

作业

要求:

  1. 将本章全部作业放在一个 安模作业02-06-学号-姓名.py 的源文件中,通过电子邮件以附件形式发给任课教师。
  2. 在源文件中以注释的形式醒目地写明本次作业对应的章编号、各个作业题的编号,并按要求写出解题思路、代码注释。
  3. 以上各题不能只有文字说明,而应同时有可执行的示例代码。
  4. 邮件标题统一用 安模作业02-06-学号-姓名 的形式。
  5. 所有关于作业的回答都以代码注释的形式写在源文件中,不需要再额外附加其他文档或图片,请保证代码执行不会发生错误。
  6. 待本次作业批改后,请通过此链接下载本次作业的参考答案。

  谢谢!

返回目录
返回首页