8.面向对象编程

1 面向对象编程思想

1.1 什么是面向过程

传统的面向过程的编程思想总结起来就八个字——自顶向下,逐步细化

  1. 将要实现的功能描述为一个从开始到结束按部就班的连续的“步骤”。

  2. 依次逐步完成这些步骤,如果某一个步骤的难度较大,又可以将该步骤再次细化为若干个子步骤,以此类推,一直到结尾并得到我们想要的结果。

  3. 就是把要开发的系统分解为若干个步骤,每个步骤就是函数,当所有步骤全部完成以后,则这个系统就开发完毕了!

举个栗子:大家以来教育机构报名学习这件事情,可以分成哪些步骤?

开始 -> 学员提出报名,提供相关材料 -> 学生缴纳学费,获得缴费凭证 -> 教师凭借学生缴费凭证进行分配班级 -> 班级增加学生信息 -> 结束

所谓的面向过程,就是将上面分析好了的步骤,依次执行就行了!

1.2 什么是面向对象

思考:上面的整个报名过程,都有哪些动词?

提出、提供、缴纳、获得、分配、增加

有动词就一定有实现这个动作的实体!

所谓的模拟现实世界,就是使计算机的编程语言在解决相关业务逻辑的时候,与真实业务逻辑的发生保持一致,需要使任何一个动作的发生都存在一个支配给该动作的一个实体(主体),因为在现实世界中,任何一个功能的实现都可以看做是一个一个的实体在发挥其各自的“功能”(能力)并在内部进行协调有序的调用过程!

1.3 举个栗子:使用面向对象实现报名系统开发

☆ 第一步:分析哪些动作是由哪些实体发出的

  • 学生提出报名

  • 学生提供相关资料

  • 学生缴费

  • 机构收费

  • 教师分配教室

  • 班级增加学生信息

于是,在整个过程中,一共有四个实体:学生、机构、教师、班级!在现实中的一个具体的实体,就是计算机编程中的一个对象

☆ 第二步:定义这些实体,为其增加相应的属性和功能

属性就是实体固有的某些特征特性信息,在面向对象的术语中,属性就是以前的变量。

比如:

一个人的属性有:身高、体重、三围、姓名、年龄、学历、电话、籍贯、毕业院校等……

一个手机的属性有:价格、品牌、操作系统、颜色、尺寸等……

功能就是就是实体可以完成的动作,在面向对象的术语中,功能就是封装成了函数或方法。

image-20210316122158360

☆ 第三步:让实体去执行相应的功能或动作

  • 学生提出报名

  • 学生提供相关资料

  • 教师登记学生信息

  • 学生缴费

  • 机构收费

  • 教师分配教室

  • 班级增加学生信息

1.4 面向对象编程思想迁移

以前写代码,首先想到的是需要实现什么功能——调用系统函数,或者自己自定义函数,然后按部就班的执行就行了!

以后写代码,首先想到的是应该由什么样的主体去实现什么样的功能,再把该主体的属性和功能统一的进行封装,最后才去实现各个实体的功能。

注意:面向对象并不是一种技术,而是一种思想,是一种解决问题的最基本的思维方式!

所以,面向对象的核心思想是:不仅仅是简单的将功能进行封装(封装成函数),更是对调用该功能的主体进行封装,实现某个主体拥有多个功能,在使用的过程中,先得到对应的主体,再使用主体去实现相关的功能!

1.5 面向对象要比面向过程好?

一个面试题:面向过程和面向对象的区别?

  1. 都可以实现代码重用和模块化编程,面向对象的模块化更深,数据也更封闭和安全。

  2. 面向对象的思维方式更加贴近现实生活,更容易解决大型的复杂的业务逻辑。

  3. 从前期开发的角度来看,面向对象比面向过程要更复杂,但是从维护和扩展的角度来看,面向对象要远比面向过程简单!

  4. 面向过程的代码执行效率比面向对象高。

1.6 面向对象编程简介

面向对象(Object oriented Programming,OOP)编程的思想主要是针对大型软件设计而来的。面向对象编程使程序的扩展性更强、可读性更好,使的编程可以像搭积木一样简单。

面向对象编程将数据和操作数据相关的方法封装到对象中,组织代码和数据的方式更加接近人的思维,从而大大提高了编程的效率。

Python 完全采用了面向对象的思想,是真正面向对象的编程语言,完全支持面向对象的基本功能,例如:继承、多态、封装等。

Python 中,一切皆对象。我们在前面学习的数据类型、函数等,都是对象。

注意
Python 支持面向过程、面向对象、函数式编程等多种编程范式。

1.7 对象的进化

随着编程面临的问题越来越复杂,编程语言本身也在进化,从主要处理简单数据开始,随着数据变多进化数组;数据类型变复杂,进化出了结构体;处理数据的方式和逻辑变复杂,进化出了对象

  1. 简单数据
    30, 40, 50.4 等这些数字,可以看做是简单数据。最初的计算机编程,都是像这样的数字。

  2. C 语言中的数组
    将同类型的数据放到一起。比如:整数数组 [20, 30, 40],浮点数数组 [10.2, 11.3, 12.4],字符串数组:["aa", "bb", "cc"]

    [!tip] 注意
    上面的 [20, 30, 40] 不是 Python 中的列表,是 C 语言中的数组。

  3. C 语言中的结构体
    将不同类型的数据放到一起,是 C 语言中的数据结构。比如:

    struct resume{
    		int age;
    		char name[10];
    		double salary;
    	   };
    
  4. 对象
    将不同类型的数据、方法(即函数)放到一起,就是对象。比如:

     class Student:
         company = "SXT"  # 类属性
         count = 0  # 类属性
     
         def __init__(self, name, score):
             self.name = name  # 实例属性
             self.score = score
             Student.count = Student.count + 1
     
         def say_score(self):  # 实例方法
             print("我的公司是:", Student.company)
             print(self.name, '的分数是:', self.score)
    

我们前面学习的数字也是对象。比如:整数 9,就是一个包含了加法、乘法等方法的对象。

2 面向对象的基本概念

2.1 面向对象中两个比较重要概念

☆ 类

对象如何产生?又是如何规定对象的属性和方法呢?

答:在 Python 中,采用类(class)来生产对象,用类来规定对象的属性和方法!也就是说,在 Python 中,要想得到对象,必须先有类!

为什么要引入类的概念?

类本来就是对现实世界的一种模拟,在现实生活中,任何一个实体都有一个类别,类就是具有相同或相似属性和动作的一组实体的集合!所以,在 Python 中,对象是指现实中的一个具体的实体,而既然现实中的实体都有一个类别,所以,OOP 中的对象也都应该有一个类!

☆ 对象

对象,object,现实业务逻辑的一个动作实体就对应着 OOP 编程中的一个对象!

image-20210316143602528

一个对象所有应该具有的特征和特性信息,都是由其所属的类来决定的,但是每个对象又可以具有不同的特征和特性信息,比如,我自己(人类)这个对象,名字叫老王,性别男,会写代码,会教书;另一个对象(人类)可能叫赵薇,性别女,会演戏,会唱歌!

所以:

  1. 对象使用属性(property)保存数据!
  2. 对象使用方法(method)管理数据!

☆ 类与对象举例

我们把对象比作一个“饼干”,类就是制造这个饼干的“模具”。

image-20220701174635240image-20220701174643734

我们通过类定义数据类型的属性(数据)和方法(行为),也就是说,“类将行为和状态打包在一起”。

image-20220701174658238

注意
类:我们叫做 `class`。对象:我们叫做 `object` 或 `instance`(实例)。以后我们说某个类的对象,某个类的实例。是一样的意思。

对象是类的具体实体,一般称为“类的实例”。类看做“饼干模具”,对象就是根据这个“模具”制造出的“饼干”。

从一个类创建对象时,每个对象会共享这个类的行为(类中定义的方法),但会有自己的属性值(不共享状态)。更具体一点:“方法代码是共享的,属性数据不共享”。

image-20220701174729591

注意
Python 中,**一切皆对象**。**类**也称为**类对象**,**类的实例**也称为**实例对象**。

2.2 类的定义


在 Python 中,我们可以有两种类的定义方式:Python2(经典类)和 Python3(新式类)。

Python3 中已经不存在经典类了,所有类都是“新式类”。没有显式继承的都默认继承 object 类。

经典类:不由任意内置类型派生出的类,称之为经典类。

class 类名:
    # 属性
    # 方法

新式类:

class 类名():
    # 属性
    # 方法

在 Python 2 及以前的版本中,由任意内置类型派生出的类(只要一个内置类型位于类树的某个位置),都属于“新式类”,都会获得所有“新式类”的特性;反之,即不由任意内置类型派生出的类,则称之为“经典类”。


“新式类”和“经典类”的区分在 Python 3 之后就已经不存在,在 Python 3.x 之后的版本,因为所有的类都派生自内置类型 object(即使没有显示的继承 object 类型),即所有的类都是“新式类”

基本语法:

class 类名:
    类体

要点如下:

  1. 类名必须符合标识符的规则;一般规定类名使用大驼峰原则
  2. 类体中我们可以定义属性和方法。
  3. 属性用来描述数据,方法(即函数)用来描述这些数据相关的操作。

案例:一个典型的类的定义。

class Student:
    def __init__(self, name, score):  # 构造方法第一个参数必须为 self
        self.name = name  # 实例属性
        self.score = score

    def say_score(self):  # 实例方法
        print("{0}的分数是{1}".format(self.name, self.score))


if __name__ == '__main__':
    # s1 是实例对象,自动调用 __init__() 方法
    s1 = Student('张三', 80)
    s1.say_score()

2.3 类的实例化(创建对象)

类的实例化就是把抽象的事务具体为现实世界中的实体。

类的实例化就是通过类得到对象

类只是对象的一种规范,类本身基本上什么都做不了,必须利用类得到对象,这个过程就叫作类的实例化

类是抽象的,也称之为“对象的模板”。我们需要通过类这个模板,创建类的实例对象,然后才能使用类定义的功能。

我们前面说过一个 Python 对象包含三个部分:id(identity 识别码)、type(对象类型)、value(对象的值)。

现在,我们可以更进一步的说,一个 Python 对象包含如下部分:

  1. id(identity 识别码)
  2. type(对象类型)
  3. value(对象的值)
    (1)属性(attribute)
    (2)方法(method)

基本语法:

对象名 = 类名()
> 在其他的编程语言中,类的实例化一般是通过 `new` 关键字实例化生成的,但是在 Python 中,我们不需要 `new` 关键字,只需要 `类名()` 就代表创建类的实例。
**Python 中实例化经历了两个阶段**:
  1. 构造实例过程,实例化。通过 __new__ 魔术方法生成并返回一个实例。

    生产线上生成一辆车。

  2. 初始化过程,初始化。通过魔术方法 __init__ 方法完成初始化。

    对新造的车做出场配置,如软装,车载系统等。

案例:把 Person 类实例化为为对象 p1。

# 1. 定义一个类
class Person():
    # 定义相关方法
    def eat(self):
        print('我喜欢吃零食')

    def drink(self):
        print('我喜欢喝可乐')


# 2. 实例化对象
p1 = Person()
# 3. 调用类中的方法
p1.eat()
p1.drink()

2.4 类中的 self 关键字

self 也是 Python 内置的关键字之一,其指向了类实例对象本身

Python 中的 self 相当于 C++ 中的 self 指针,JAVA 和 C# 中的 this 关键字。Python 中,self 必须为构造函数的第一个参数,名字可以任意修改。但一般遵守惯例,都叫做 self

# 1. 定义一个类
class Person():
    # 定义一个方法
    def speak(self):
        print(self)
        print('Nice to meet you!')


# 2. 类的实例化(生成对象)
p1 = Person()
print(p1)
p1.speak()

p2 = Person()
print(p2)
p2.speak()

> 一句话总结:类中的 `self` 就是谁实例化了对象,其就指向谁。
## 3 对象的属性添加与获取

3.1 什么是属性

在 Python 中,任何一个对象都应该由两部分组成:属性 + 方法

属性即是特征,比如:人的姓名、年龄、身高、体重……都是对象的属性。

​车的品牌、型号、颜色、载重量……都是对象的属性。

对象属性既可以在类外面添加和获取,也能在类里面添加和获取。

3.2 在类的外面添加属性和获取属性

☆ 设置

对象名.属性 = 属性值

案例:

# 1. 定义一个 Person 类
class Person:
    pass


# 2. 实例化 Person 类,生成 p1 对象
p1 = Person()
# 3. 为 p1 对象添加属性
p1.name = '老王'
p1.age = 18
p1.address = '北京市顺义区京顺路99号'

☆ 获取

在 Python 中,对象的属性值我们可以通过 对象名.属性 来获取。

# 1. 定义一个 Person 类
class Person():
    pass


# 2. 实例化 Person 类,生成 p1 对象
p1 = Person()
# 3. 为 p1 对象添加属性
p1.name = '老王'
p1.age = 18
p1.address = '北京市顺义区京顺路99号'

# 4. 获取 p1 对象的属性
print(f'我的姓名:{p1.name}')
print(f'我的年龄:{p1.age}')
print(f'我的住址:{p1.address}')

3.3 在类的内部获取外部定义的属性

# 1. 定义一个 Person 类
class Person:
    def speak(self):
        print(f'我的名字:{self.name},我的年龄:{self.age},我的住址:{self.address}')


# 2. 实例化 Person 类,生成 p1 对象
p1 = Person()
# 3. 添加属性
p1.name = '孙悟空'
p1.age = 500
p1.address = '花果山水帘洞'
# 4. 调用 speak 方法
p1.speak()

思考
> 目前我们的确可以通过 `对象.属性` 的方式设置或获取对象的属性,但是这种设置属性的方式有点繁琐,每次定义一个对象,就必须手工设置属性,在我们面向对象中,对象的属性能不能在实例化对象时,直接进行设置呢? ---
可以,但是需要使用魔术方法。

4 魔术方法

4.1 什么是魔术方法

在 Python 中,__xxx__() 的函数叫做魔法方法,指的是具有特殊功能的函数。

4.2 __init__() 方法(初始化方法或构造方法)

思考
人的姓名、年龄等信息都是与生俱来的属性,可不可以在创建对象的过程中就赋予这些属性呢?
---
可以,使用 `__init__()` 方法。

__init__() 的作用:实例化对象时,连带其中的参数,会一并传给 __init__ 函数自动并执行它。__init__() 函数的参数列表会在开头多出一项,它永远指向新建的那个实例对象,Python 语法要求这个参数必须要有,习惯命名为 self

构造方法用于执行实例对象的初始化工作,即对象创建后,初始化当前对象的相关属性,不能有返回值,也就是只能是 return None

__init__() 的要点如下:

  1. 名称固定,必须为:__init__()

  2. 第一个参数固定,必须为:selfself 指的就是刚刚创建好的实例对象。

  3. 构造函数通常用来初始化实例对象的实例属性,如下代码就是初始化实例属性:namescore

    def init (self, name, score):
        # 实例属性
        self.score = score
        self.name = name
    
  4. 通过 类名(参数列表) 来调用构造函数。调用后,将创建好的对象返回给相应的变量。

    s1 = Student('张三', 80)
    
  5. __init__() 方法:初始化创建好的对象。

    初始化指的是:给实例属性赋值

  6. __new__() 方法:用于创建对象,但我们一般无需重定义该方法。

  7. 如果我们不定义 __init__ 方法,系统会提供一个默认的 __init__ 方法。如果我们定义了带参的 __init__ 方法,系统不创建默认的 __init__ 方法。

# 1. 定义一个类
class Person():
    # 初始化实例对象属性
    def __init__(self, name, age):
        # 赋予 name 属性、age 属性给实例化对象本身
        # self.实例化对象属性 = 参数
        self.name = name
        self.age = age


# 2. 实例化对象并传入初始化属性值
p1 = Person('孙悟空', 500)
# 3. 调用 p1 对象自身属性 name 与 age
print(p1.name)
print(p1.age)

总结
1. `__init__()` 方法,在创建一个对象时默认被调用,不需要手动调用。
> > 2. ` __init__(self)` 中的 `self` 参数,不需要开发者传递,Python 解释器会自动把当前的对象引用传递过去。

4.3 __str__() 方法

当使用 print 输出对象的时候,默认打印对象的内存地址。如果类定义了 __str__ 方法,那么就会打印从在这个方法中 return 的数据。

没有定义 __str__() 方法的类:

# 1. 定义一个类
class Car:
    # 首先定义一个 __init__ 方法,用于初始化实例对象属性
    def __init__(self, brand, model, color):
        self.brand = brand
        self.model = model
        self.color = color

    # 2. 实例化对象 c1


c1 = Car('奔驰', 'S600', '黑色')
print(c1)

使用 __str__() 方法的类:

# 1. 定义一个类
class Car:
    # 首先定义一个 __init__ 方法,用于初始化实例对象属性
    def __init__(self, brand, model, color):
        self.brand = brand
        self.model = model
        self.color = color

    # 定义一个 __str__ 内置魔术方法,用于输出小汽车的相关信息
    def __str__(self):
        return f'汽车品牌:{self.brand},汽车型号:{self.model},汽车颜色:{self.color}'


# 2. 实例化对象 c1
c1 = Car('奔驰', 'S600', '黑色')
print(c1)

总结
1. `__str__` 这个魔术方法是在类的外部,使用 `print(对象)` 时,自动被调用的。
> > 2. 在类的内部定义 `__str__` 方法时,必须使用 `return` 返回一个字符串类型的数据。

4.4 __del__() 方法(删除方法或析构方法)

当删除对象时,Python 解释器也会默认调用 __del__() 方法。

__del__() 方法称为析构方法,用于实现对象被销毁时所需的操作。比如:释放对象占用的资源,例如:打开的文件资源、网络连接等。

Python 实现自动的垃圾回收,当对象没有被引用时(引用计数为 0),由垃圾回收器调用 __del__() 方法。

我们也可以通过 del 语句删除对象,从而保证调用 __del__() 方法。

系统会自动提供 __del__() 方法,一般不需要自定义析构方法。

class Person():
    # 构造函数 __init__
    def __init__(self, name, age):
        self.name = name
        self.age = age

    # 析构方法 __del__
    def __del__(self):
        print(f'{self} 对象已经被删除')


# 实例化对象
p1 = Person('白骨精', 100)
# 删除对象 p1
del p1

思考
`__del__()` 方法在使用过程中,比较简单,但是其在实际开发中,有何作用呢?
---
主要用于关闭文件操作、关闭数据库连接等等。

4.5 __call__ 方法和可调用对象

定义了 __call__ 方法的对象,称为可调用对象,即该对象可以像函数一样被调用。

  1. Python 中,凡是可以将 () 直接应用到自身并执行,都称为可调用对象。
  2. 可调用对象包括自定义的函数、Python 内置函数、以及本节所讲的实例对象。
  3. 定义了 __call__() 的对象,称为可调用对象,即该对象可以像函数一样被调用。
  4. 该方法使得实例对象可以像调用普通函数那样,以 对象名() 的形式使用。

案例:测试可调用对象。

class SalaryAccount:
    """工资计算类"""

    def __call__(self, salary):
        year_salary = salary * 12
        day_salary = salary // 30
        hour_salary = day_salary // 8
        return dict(monthSalary=salary,
                    yearSalary=year_salary,
                    daySalary=day_salary,
                    hourSalary=hour_salary)


s = SalaryAccount()
# 可以像调用函数一样调用对象的 __call__ 方法
print(s(5000))

案例:类装饰器的使用案例。

class MyLogDecorator:
    def __init__(self, func):
        self.func = func

    def __call__(self, *args, **kwargs):
        print("日志纪录...")
        return self.func(*args, **kwargs)


@MyLogDecorator  # 等价于 fun2 = MyLogDecorator(fun2)
def fun2():
    print("使用功能2")


if __name__ == '__main__':
    fun2()

4.6 总结

提到魔术方法:

  1. 这个方法在什么情况下被触发。
  2. 这个方法有什么实际的作用。

__new__:创建一个类的实例。在创建对象时自动调用,其作用就是返回一个新创建的类的实例。一般我们无需重新定义此方法。

__init__():初始化方法或者称之构造函数,在对象初始化时执行,其主要作用就是在对象初始化时,对对象进行初始化操作(如赋予属性)。

__str__():对象字符串方法,当我们在类的外部,使用 print 方法输出对象时被触发,其主要功能就是对对象进行打印输出操作,要求方法必须使用 return 返回字符串格式的数据

__del__():删除方法或者称之为析构方法,在对象被 del 删除时触发,其主要作用就是适用于关闭文件、关闭数据库连接等等。

__call__:让一个对象成为可调用对象。在使用 对象名() 时触发,自动执行 __call__ 中的代码。让对象可以向函数那样被调用。

[[魔法方法详解]]

5 实例属性和实例方法

5.1 实例属性

实例属性是从属于实例对象的属性,也称为实例变量。他的使用有如下几个要点:

  1. 实例属性一般在 __init__() 方法中通过如下代码定义:

    self.实例属性名 = 初始值
    
  2. 在本类的其他实例方法中,也是通过 self 进行访问:

    self.实例属性名
    
  3. 创建实例对象后,通过实例对象访问:

    # 创建对象,调用 __init__() 初始化属性
    obj = 类名()
    
    # 可以给已有属性赋值,也可以新加属性
    obj.实例属性名 = 值

案例:实例属性测试。

class Student:
    def __init__(self, name, score):
        self.name = name  # 增加 name 属性
        self.score = score  # 增加 score 属性

    def say_score(self):
        self.age = 18  # 增加 age 属性
        print(f"{self.name} 的分数是 {self.score}")


s1 = Student("张三", 80)
s1.say_score()
print(f"{s1.name} 的年龄是 {s1.age}")
s1.salary = 3000  # s1对象增加salary属性
s2 = Student("李四", 90)
s2.say_score()
print(f"{s2.name} 的年龄是 {s2.age}")

5.2 实例方法

实例方法是从属于实例对象的方法。实例方法的定义格式如下:

def	方法名(self [, 形参列表]):
	函数体

方法的调用格式如下:

对象.方法名([实参列表])

要点:

  1. 定义实例方法时,第一个参数必须为 self。和前面一样,self 指当前的实例对象。
  2. 调用实例方法时,不需要也不能给 self 传参。self 由解释器自动传参。

案例:实例方法测试。

class Student:
    def __init__(self, name, age, score):
        self.name = name
        self.age = age
        self.score = score

    def say_score(self):
        print(f"{self.name} 的分数是 {self.score}")


a = Student("小龙女", 18, 90)
a.say_score()

函数和实例方法的区别

  1. 都是用来完成一个功能的语句块,本质一样。
  2. 实例方法调用时,通过对象来调用。实例方法从属于特定实例对象,普通函数没有这个特点。
  3. 直观上看,实例方法定义时需要传递 self,函数不需要。

实例对象的方法调用本质

image-20220701185245323

class Student:
    def __init__(self, name, age, score):
        self.name = name
        self.age = age
        self.score = score

    def say_score(self):
        print(f"{self.name} 的分数是 {self.score}")


a = Student("小龙女", 18, 90)
a.say_score()
# 本质上是
Student.say_score(a)

其他操作

  1. dir(obj) 可以获得对象的所有属性、方法。
  2. obj.__dict__ 对象的实例属性字典。
  3. pass 空语句。
  4. isinstance(对象, 类型) 判断对象是不是指定类型
class Student:
    def __init__(self, name, score):
        self.name = name  # 增加 name 属性
        self.score = score  # 增加 score 属性

    def say_score(self):
        self.age = 18  # 增加 age 属性
        print("{0}的分数是{1}".format(self.name, self.score))


stu = Student("小龙女", 90)
# dir(obj) 可以获得对象的所有属性、方法
print(f"dir方法:{dir(stu)}")
# obj.__dict__ 对象的属性字典
print(f"__dict__属性:{stu.__dict__}")
# isinstance(对象, 类型)  判断“对象”是不是“指定类型”
print(f"isinstance方法:{isinstance(stu, Student)}")

"""
运行结果:
dir方法:['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__firstlineno__', '__format__', '__ge__', '__getattribute__', '__getstate__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__static_attributes__', '__str__', '__subclasshook__', '__weakref__', 'name', 'say_score', 'score']

__dict__属性:{'name': '小龙女', 'score': 90}

isinstance方法:True
"""

6 类对象、类属性、类方法、静态方法

6.1 类对象

我们在前面讲的类定义格式中,class 类名:。实际上,当解释器执行 class 语句时,就会创建一个类对象

案例:测试类对象的生成。

class Student:
    # 空语句
    pass


print(type(Student))
print(id(Student))

Stu2 = Student
s1 = Stu2()
print(s1)

我们可以看到实际上生成了一个变量名就是类名 Student 的对象。我们通过赋值给新变量 Stu2,也能实现相关的调用。说明,确实创建了类对象

> `pass` 为空语句。就是表示什么都不做,只是作为一个占位符存在。当你写代码时,遇到暂时不知道往方法或者类中加入什么时,可以先用 `pass` 占位,后期再补上。

6.2 类属性

类属性是从属于类对象的属性,也称为类变量。由于类属性从属于类对象,所以可以被所有实例对象共享。

类属性的定义方式:

class 类名:
	类变量名 = 初始值

在类中或者类的外面,我们可以通过:类名.类变量名 来读写。

案例:类属性的使用测试。

class Student:
    # 类属性
    company = "古墓派"
    # 类属性
    count = 0

    def __init__(self, name, score):
        # 实例属性
        self.name = name
        self.score = score
        Student.count = Student.count + 1

    def say_score(self):
        # 实例方法
        print("我的门派是:", Student.company)
        print(self.name, "的分数是:", self.score)


# s1 是实例对象,自动调用 __init__()方法
s1 = Student("小龙女", 99)
s1.say_score()
print(f"一共创建 {Student.count} 个 Student 对象")
print("=" * 40)
s1 = Student("杨过", 80)
s1.say_score()
print(f"一共创建 {Student.count} 个 Student 对象")

6.3 类方法

类方法是从属于类对象的方法。类方法通过装饰器 @classmethod 来定义。

格式如下:

@classmethod
def	类方法名(cls[, 形参列表]):
    函数体

要点如下:

  1. @classmethod 必须位于方法上面一行。
  2. 第一个参数 cls 必须有;cls 指的就是类对象本身。
  3. 调用类方法格式:类名.类方法名(参数列表)。参数列表中,不需要也不能给 cls 传值。
  4. 类方法中访问实例属性和实例方法会导致错误。
  5. 子类继承父类方法时,传入 cls 是子类的类对象,而非父类的类对象。

案例:类方法使用测试。

class Student:
    # 类属性
    company = "古墓派"

    # 类方法
    @classmethod
    def print_company(cls, person):
        print(f"{cls.company}--{person}")


Student.print_company("小龙女")
Student.print_company("杨过")

6.4 静态方法

Python 中允许定义与类对象无关的方法,称为静态方法

既不操作类属性,也不操作实例属性的方法可以考虑定义成静态方法

静态方法和在模块中定义普通函数没有区别,只不过静态方法放到了类的名字空间里面,需要通过类调用

静态方法通过装饰器 @staticmethod 来定义。

格式如下:

@staticmethod
def	静态方法名([形参列表]):
    函数体

要点如下:

  1. @staticmethod 必须位于方法上面一行。
  2. 调用静态方法格式:类名.静态方法名(参数列表)
  3. 静态方法中访问实例属性和实例方法会导致错误。

案例:静态方法使用测试。

class Student:
    # 类属性
    company = "SXT"

    @staticmethod
    def add(a, b):
        # 静态方法
        print(f"{a} + {b} = {a + b}")
        return a + b


Student.add(20, 30)

7 内存分析实例对象和类对象创建过程(重要)

以下面代码为例,分析整个创建过程,让大家对面向对象概念掌握更加深刻:

class Student:
    # 类属性
    company = "尚学堂"
    # 类属性
    count = 0

    def __init__(self, name, score):
        # 实例属性
        self.name = name
        self.score = score
        Student.count = Student.count + 1

    # 实例方法
    def say_score(self):
        print("我的公司是:", Student.company)
        print(self.name, '的分数是:', self.score)


# s1 是实例对象,自动调用 __init__()方法
s1 = Student('高淇', 80)
s1.say_score()
print(f'一共创建 {Student.count} 个 Student 对象')
print("=" * 40)
# s2 是实例对象,自动调用 __init__()方法
s2 = Student('张三', 70)
s2.say_score()
print(f'一共创建 {Student.count} 个 Student 对象')

注意
上图中栈的顺序应该反过来,先执行的在下面,后执行的在上面。

8 方法没有重载

在其他语言中,可以定义多个重名的方法,只要保证方法签名唯一即可。方法签名包含 3 个部分:方法名、参数数量、参数类型

Python 中,方法的的参数没有声明类型(调用时确定参数的类型),参数的数量也可以由可变参数控制。因此,Python 中是没有方法的重载的。定义一个方法即可有多种调用方式,相当于实现了其他语言中的方法的重载。

如果我们在类体中定义了多个重名的方法,只有最后一个方法有效

建议
不要使用重名的方法!Python 中方法没有重载。

案例:Python 中没有方法的重载。定义多个同名方法,只有最后一个有效。

class Person:
    def say_hi(self):
        print("hello")

    def say_hi(self, name):
        print("{0},hello".format(name))


p1 = Person()
p1.say_hi("高淇")
# 不带参,报错:
# TypeError: Person.say_hi() missing 1 required positional argument: 'name'
p1.say_hi()

9 方法的动态性

Python 是动态语言,我们可以动态的为类添加新的方法,或者动态的修改类已有的方法。

案例:测试方法的动态性。

class Person:
    def __init__(self, name):
        self.name = name

    def work(self):
        print(f"{self.name} 努力上班!")


def play_game(self):
    print(f"{self.name} 玩游戏")


# s 就是为了在调用时接收 self 参数
def work2(s):
    print(f"{s.name} 好好工作,努力上班!")


Person.play = play_game
Person.work = work2

p = Person("小龙女")
p.play()
p.work()

我们可以看到,Person 动态的新增了 play_game 方法,以及用 work2 替换了 work 方法。

10 面向对象的三大特性——封装

10.1 面向对象有哪些特性

三种:封装性、继承性、多态性。

10.2 私有属性和私有方法(实现封装)

Python 对于类的成员没有严格的访问控制限制,这与其他面向对象语言有区别。

关于私有属性和私有方法,有如下要点:

  1. 通常我们约定,两个下划线开头的属性是私有的(private)。其他为公共的(public)。
  2. 类内部可以访问私有属性(方法)。
  3. 类外部不能直接访问私有属性(方法)。
  4. 类外部可以通过 对象._类名__私有属性(方法)名 访问私有属性(方法)。
注意
**方法本质上也是属性**!只不过是可以通过 `()` 执行而已。
> > 所以,此处讲的私有属性和公有属性,也同时讲解了私有方法和公有方法的用法。 > > 如下测试中,同时也包含了私有方法和公有方法的例子。

案例:测试私有属性、私有方法。

class Employee:
    # 私有类属性。通过 dir 可以查到 _Employee__company
    __company = "古墓派"

    def __init__(self, name, age):
        self.name = name
        # 私有实例属性
        self.__age = age

    def say_company(self):
        # 类内部可以直接访问私有属性
        print(f"我的门派是:{Employee.__company}")
        print(f"{self.name} 的年龄是:{self.__age}")
        self.__work()

    # 私有实例方法 通过 dir 可以查到 _Employee__work
    def __work(self):
        print("练武!好好练武!")


p1 = Employee("杨过", 18)
print(p1.name)
# 通过 dir 可以查到属性:_Employee__age
print(dir(p1))
p1.say_company()
# 通过这种方式可以直接访问到私有属性。
print(p1._Employee__age)
# 直接访问私有属性,报错
# print(p1.__age)
# 直接访问私有方法,报错
# p1.__sleep()

结果:

杨过
['_Employee__age', '_Employee__company', '_Employee__work', '__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getstate__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'name', 'say_company']
我的门派是:古墓派
杨过 的年龄是:18
练武!好好练武!
18

从打印的 Employee 对象所有属性我们可以看出。私有属性 age 在实际存储时是按照 _Employee__age 这个属性来存储的。这也就是为什么我们不能直接使用 __age 而可以使用 _Employee__age 的根本原因。

思考
> 由以上代码运行可知,私有属性不能在类的外部被直接访问。但是出于种种原因,我们想在外部对私有属性进行访问,该如何操作呢? ---
我们可以定义一个专门的访问**接口(函数)**,专门用于实现私有属性的访问。

10.3 私有属性设置与访问接口

在 Python 中,一般定义函数名 get_xx 用来获取私有属性,定义 set_xx 用来修改私有属性值。

class Girl:
    def __init__(self, name, age):
        self.name = name
        self.__age = age

    # 公共方法:提供给外部的访问接口
    def get_age(self):
        # 本人访问:允许直接访问
        # 外人访问:加上限制条件
        return self.__age

    # 公共方法:提供给外部的设置接口
    def set_age(self, age):
        self.__age = age


girl = Girl("小龙女", 18)
# 通过接口获取私有属性
print(f"{girl.name} {girl.get_age()} 岁了!!")
# 通过接口设置私有属性
girl.set_age(19)
# 通过接口获取私有属性
print(f"{girl.name} {girl.get_age()} 岁了!!")

10.4 封装性到底有何意义

  1. 以面向对象的编程思想进行项目开发。

  2. 封装数据属性:明确的区分内外,控制外部对隐藏的属性的操作行为(过滤掉异常数据)。

    class People:
        def __init__(self, name, age):
            self.__name = name
            self.__age = age
    
        def tell_info(self):
            print(f"Name:{self.__name} Age:{self.__age}")
    
        # 对私有属性的访问接口
        def set_info(self, name, age):
            if not isinstance(name, str):
                print("名字必须是字符串类型")
                return
            if not isinstance(age, int):
                print("年龄必须是数字类型")
                return
            # 过滤异常数据
            if not 0 <= age <= 125:
                print("年龄必须在 0~125 之间")
                return
    
            self.__name = name
            self.__age = age
    
    
    p = People("jack", 38)
    p.tell_info()
    
    p.set_info("jennifer", 18)
    p.tell_info()
    
    p.set_info(123, 35)
    p.tell_info()
    
    p.set_info("tom", 135)
    p.tell_info()
    

  3. 私有方法封装的意义:降低程序的复杂度。

    class ATM:
        def __card(self):
            print('插卡')
    
        def __auth(self):
            print('用户认证')
    
        def __input(self):
            print('输入取款金额')
    
        def __print_bill(self):
            print('打印账单')
    
        def __take_money(self):
            print('取款')
    
        # 定义一个对外提供服务的公共方法
        def withdraw(self):
            self.__card()
            self.__auth()
            self.__input()
            self.__print_bill()
            self.__take_money()
    
    
    atm = ATM()
    atm.withdraw()
    

10.5 @property 装饰器

@property 可以将一个方法的调用方式变成属性调用

下面是一个简单的示例,让大家体会一下这种转变。

案例:简单测试 @property

class Employee:
    @property
    def salary(self):
        return 30000


emp1 = Employee()
print(emp1.salary)
print(type(emp1.salary))

# 报错:TypeError: 'int' object is not callable
# emp1.salary()

# @property 修饰的属性,如果没有加 setter 方法,则为只读属性。
# 此处修改报错:AttributeError: can't set attribute
# emp1.salary = 1000

> `@property` 主要用于帮助我们处理属性的读操作、写操作。
对于某一个属性,我们可以直接通过:
# 读取 salary 属性
emp.salary

# 设置 salary 属性
emp.salary = 30000

如上可以实现属性的读操作、写操作。但是,这种做法不安全。比如,我需要限制薪水必须为 1~10000 的数字。这时候,我们就需要通过 gettersetter 方法来处理。

# 方法一:使用 getter 和 setter 方法
class Employee:
    def __init__(self, name, salary):
        self.name = name
        self.__salary = salary

    # salary 属性的 getter 方法
    def get_salary(self):
        return self.__salary

    # salary 属性的 setter 方法
    def set_salary(self, salary):
        if 0 < salary < 1000000:
            self.__salary = salary
        else:
            print("薪水录入错误!薪水只能在 0~1000000 之间")


if __name__ == '__main__':
    emp = Employee("小龙女", 50000)
    print(f"月薪为 {emp.get_salary()}, 年薪为{emp.get_salary()}")
    print("=========更改薪水=========")
    emp.set_salary(200999)
    print(f"月薪为 {emp.get_salary()}, 年薪为{emp.get_salary()}")
    print("=========更改薪水=========")
    emp.set_salary(-200)
    print(f"月薪为 {emp.get_salary()}, 年薪为{emp.get_salary()}")

上面的方式使用函数来操作属性,我们还可以通过 propertysetter 装饰器来像操作属性那样进行处理。

# 方法二:使用 @property 和 setter 装饰器
class Employee:
    def __init__(self, name, salary):
        self.name = name
        self.__salary = salary

    @property
    # 相当于 salary 属性的 getter 方法
    def salary(self):
        return self.__salary

    # @ 后面的名字一定要和 @property 修饰的函数名保持一致
    @salary.setter
    # 相当于 salary 属性的 setter 方法
    def salary(self, salary):
        if 0 < salary < 1000000:
            self.__salary = salary
        else:
            print("薪水录入错误!薪水只能在 0-1000000 之间")


if __name__ == '__main__':
    emp = Employee("小龙女", 50000)
    print(f"月薪为 {emp.salary}, 年薪为{emp.salary}")
    print("=========更改薪水=========")
    emp.salary = 200999
    print(f"月薪为 {emp.salary}, 年薪为{emp.salary}")
    print("=========更改薪水=========")
    emp.salary = -200
    print(f"月薪为 {emp.salary}, 年薪为{emp.salary}")

10.6 属性和方法命名总结

_xxx:保护成员,不能用 from module import * 导入,只有类对象和子类对象能访问这些成员。

__xxx__:系统定义的特殊成员。

__xxx:类中的私有成员,只有类对象自己能访问,子类对象也不能访问。(但,在类外部可以通过 对象名._类名 __xxx 这种特殊方式访问。Python 不存在严格意义的私有成员)。

注意
再次强调,方法和属性都遵循上面的规则。

10.7 类编码风格

  1. 类名首字母大写,多个单词之间采用大驼峰原则。
  2. 实例名、模块名采用小写,多个单词之间采用下划线隔开。
  3. 每个类,应紧跟文档字符串,说明这个类的作用。
  4. 可以用空行组织代码,但不能滥用。在类中,使用一个空行隔开方法;模块中,使用两个空行隔开多个类。

11 面向对象三大特征介绍

Python 是面向对象的语言,也支持面向对象编程的三大特性:继承、封装(隐藏)、多态。

11.1 封装(隐藏)

隐藏对象的属性和实现细节,只对外提供必要的方法。相当于将细节封装起来,只对外暴露相关调用方法

通过前面学习的“私有属性、私有方法”的方式,实现“封装”。Python 追求简洁的语法,没有严格的语法级别的“访问控制符”,更多的是依靠程序员自觉实现。

11.2 继承

继承可以让子类具有父类的特性,提高了代码的重用性。

从设计上是一种增量进化,原有父类设计不变的情况下,可以增加新的功能,或者改进已有的算法。

11.3 多态

多态是指同一个方法调用由于对象不同会产生不同的行为。生活中这样的例子比比皆是:同样是休息方法,人不同休息方法不同。张三休息是睡觉,李四休息是玩游戏,程序员休息是“敲几行代码”。

12 面向对象三大特征——继承

继承是面向对象程序设计的重要特征,也是实现“代码复用”的重要手段。

如果一个新类继承自一个设计好的类,就直接具备了已有类的特征,就大大降低了工作难度。已有的类,我们称为父类或者基类,新的类,我们称为子类或者派生类

12.1 什么是继承

我们接下来来聊聊 Python 代码中的继承:类是用来描述现实世界中同一组事务的共有特性的抽象模型,但是类也有上下级和范围之分,比如:生物 => 动物 => 哺乳动物 => 灵长型动物 => 人类 => 黄种人。

从哲学上说,就是共性与个性之间的关系,比如:白马和马!所以,我们在 OOP 代码中,也一样要体现出类与类之间的共性与个性关系,这里就需要通过类的继承来体现。简单来说,如果一个类 A 使用了另一个类 B 的成员(属性和方法),我们就可以说 A 类继承了 B 类,同时这也体现了 OOP 中代码重用的特性

12.2 继承的基本语法

Python 支持多重继承,一个子类可以继承多个父类。继承的语法格式如下:

class 子类类名(父类1[, 父类2, ...]):
	类体

如果在类定义中没有指定父类,则默认父类是 object 类。也就是说,object 是所有类的父类,里面定义了一些所有类共有的默认实现,比如:__new__()

假设 A 类要继承 B 类中的所有属性和方法(私有属性和私有方法除外)。

class B:
    pass


clss A(B):
    pass


a = A()
a.B中的所有公共属性
a.B中的所有公共方法

案例:Person 类与 Teacher、Student 类之间的继承关系。

class Person:
    def eat(self):
        print('i can eat food!')

    def speak(self):
        print('i can speak!')


class Teacher(Person):
    pass


class Student(Person):
    pass


teacher = Teacher()
teacher.eat()
teacher.speak()
print("=" * 40)

student = Student()
student.eat()
student.speak()

12.3 与继承相关的几个概念

  • 继承:一个类从另一个已有的类获得其成员的相关特性,就叫作继承!

  • 派生:从一个已有的类产生一个新的类,称为派生!

很显然,继承和派生其实就是从不同的方向来描述的相同的概念而已,本质上是一样的!

  • 父类:也叫作基类,就是指已有被继承的类!

  • 子类:也叫作派生类或扩展类!

  • 扩展:在子类中增加一些自己特有的特性,就叫作扩展,没有扩展,继承也就没有意义了!

  • 单继承:一个类只能继承自一个其他的类,不能继承多个类,单继承也是大多数面向对象语言的特性!

  • 多继承:一个类同时继承了多个父类,(C++、Python 等语言都支持多继承)。

12.4 单继承

单继承:一个类只能继承自一个其他的类,不能继承多个类。这个类会有具有父类的属性和方法。

基本语法:

# 1、定义一个共性类(父类)
class Person:
    pass


# 2、定义一个个性类(子类)
class Teacher(Person):
    pass

案例:比如汽车可以分为两种类型(汽油车、电动车)。

# 1、定义一个共性类(车类)
class Car:
    def run(self):
        print('I can run')


# 2、定义汽油车
class GasolineCar(Car):
    pass


# 3、定义电动车
class ElectricCar(Car):
    pass


bwm = GasolineCar()
bwm.run()

byd = ElectricCar()
byd.run()

12.5 单继承特性:传递性

在 Python 继承中,如 A 类继承了 B 类,B 类又继承了 C 类。则根据继承的传递性,则 A 类也会自动继承 C 类中所有属性和方法(公共)。

class C:
    def func(self):
        print('我是C类中的相关方法func')


class B(C):
    pass


class A(B):
    pass


a = A()
a.func()
print(a)

12.6 编写面向对象代码中的常见问题

问题 1
在定义类时,其没有遵循类的命名规则。
---
在 Python 中,要遵循一定的命名规范:首字母必须是字母或下划线,其中可以包含字母、数字和下划线,而且要求其命名方式采用大驼峰。
> > 例如: > > - 电动汽车:`EletricCar` > > - 父类:`Father` > > - 子类:`Son`
问题 2
父类一定要继承 object 么?`Car(object)`
---
在 Python 面向对象代码中,所有类都继承了 object 类,就算不写也会,因为默认情况下,Python 中的所有类都继承自 object。

image-20210318104228654

问题 3:打印属性和方法时,都喜欢用 print

class Person():
    def __init__(self, name):
        self.name = name

    def speak(self):
        print('i can speak')

# 创建对象,打印属性和方法
p = Person('Tom')
print(p.name)
p.speak()

12.7 多继承

什么是多继承?

Python 语言是少数支持多继承的一门编程语言,所谓的多继承就是允许一个类同时继承自多个类的特性。

语法格式:

class 子类类名(父类 1[,父类 2,...]):
	类体

image-20210318105709652

基本语法:

class B:
    pass


class C:
    pass


class A(B, C):
    pass


a = A()
a.B中的所有属性和方法
a.C中的所有属性和方法

案例:汽油车、电动车 => 混合动力汽车(汽车 + 电动)。

class GasolineCar(object):
    def run_with_gasoline(self):
        print('I can run with gasoline')


class EletricCar(object):
    def run_with_eletric(self):
        print('I can run with eletric')


class HybridCar(GasolineCar, EletricCar):
    pass


tesla = HybridCar()
tesla.run_with_gasoline()
tesla.run_with_eletric()

注意
虽然多继承允许我们同时继承自多个类,但是实际开发中,应尽量避免使用多继承,因为如果两个类中出现了相同的属性和方法就会产生命名冲突。

案例:多重继承。

class A:
    def aa(self):
        print("aa")


class B:
    def bb(self):
        print("bb")


class C(B, A):
    def cc(self):
        print("cc")


c = C()
c.aa()
c.bb()
c.cc()

12.8 子类扩展:重写父类属性和方法

扩展特性:继承让子类继承父类的所有公共属性和方法,但是如果仅仅是为了继承公共属性和方法,继承就没有实际的意义了,应该是在继承以后,子类应该有一些自己的属性和方法。

什么是重写

重写也叫作覆盖,就是当子类成员与父类成员名字相同的时候,从父类继承下来的成员会重新定义!

此时,通过子类实例化出来的对象访问相关成员的时候,真正起作用的是子类中定义的成员!

  1. 成员继承:子类继承了父类除构造方法之外的所有成员。

    [!tip] 注意
    Python 中没有严格的访问控制,私有属性其实也会继承。私有属性、私有方法被继承时,名称为 _类名__属性名

  2. 方法重写:子类可以重新定义父类中的方法,这样就会覆盖父类的方法,也称为重写

上面单继承例子中 Animal 的子类 Cat 和 Dog 继承了父类的属性和方法,但是我们狗类 Dog 有自己的叫声 '汪汪叫',猫类 Cat 有自己的叫声 '喵喵叫',这时我们需要对父类的 call() 方法进行重构。如下:

class Animal:
    def eat(self):
        print('I can eat')

    def call(self):
        print('I can call')


class Dog(Animal):
    pass


class Cat(Animal):
    pass


dog = Dog()
dog.eat()
dog.call()
print("=" * 40)

cat = Cat()
cat.eat()
cat.call()

Dog、Cat 子类重写父类 Animal 中的 call 方法。

class Animal:
    def eat(self):
        print('I can eat')

    # 公共方法
    def call(self):
        print('I can call')


class Dog(Animal):
    # 重写父类的 call 方法
    # @override
    def call(self):
        print('I can wang wang wang')


class Cat(Animal):
    # 重写父类的 call 方法
    # @override
    def call(self):
        print('I can miao miao miao')


dog = Dog()
dog.eat()
dog.call()
print("=" * 40)

cat = Cat()
cat.eat()
cat.call()

思考
重写父类中的 `call` 方法以后,此时父类中的 `call` 方法还在不在?
---
还在,只不过是在其子类中找不到了。
> > 因为类方法的调用顺序,当我们在子类中重构父类的方法后,Cat 子类的实例先会在自己的类 Cat 中查找该方法,当找不到该方法时才会去父类 Animal 中查找对应的方法。
注意
Python 3.12 引入了 `@override` 装饰器,可以用来指定该方法是用来覆盖基类方法的。
> > 程序会检查被装饰的方法是否在基类中存在。如果被装饰的方法并未在基类中定义,程序会报错,从而帮助我们发现错误。

12.9 super() 调用父类属性和方法

super():调用父类属性或方法,完整写法:super(当前类名, self).属性super(当前类名, self).方法(),在 Python3 以后版本中,调用父类的属性和方法我们只需要使用 super().属性super().方法名() 就可以完成调用了。

super() 代表父类的定义,不是父类对象。即 super().方法名 相当于 父类名.方法名

定义子类时,如果重写 __init__ 方法后还想使用父类 __init__ 中定义的属性,必须在其构造函数中调用父类的构造函数。

关于构造函数:

  1. 子类不重写 __init__,实例化子类时,会自动调用父类定义的 __init__
  2. 子类重写了 __init__ 时,实例化子类,就不会调用父类已经定义的 __init__
  3. 如果重写了 __init__ 时,要使用父类的构造方法,可以使用 super 关键字,也可以使用如下格式调用:super().__init__(参数列表)父类名.__init__(self, 参数列表)

调用格式如下:

父类名.__init__(self, 参数列表)
# 或
super().__init__(参数列表)
注意
构造函数中包含调用父类构造函数。根据需要,不是必须。
> > 子类并不会自动调用父类的 `__init__()`,我们必须显式的调用它。

案例:Car 汽车类、GasolineCar 汽油车、ElectricCar 电动车。

class Car:
    def __init__(self, brand, model, color):
        self.brand = brand
        self.model = model
        self.color = color

    def run(self):
        print('I can run')


class GasolineCar(Car):
    def __init__(self, brand, model, color):
        super().__init__(brand, model, color)

    def run(self):
        print('I can run with gasoline')


class ElectricCar(Car):
    def __init__(self, brand, model, color):
        super().__init__(brand, model, color)
        # 电池属性
        self.battery = 70

    def run(self):
        print(f'I can run with electric,remain:{self.battery}')


bwm = GasolineCar('宝马', 'X5', '白色')
bwm.run()

tesla = ElectricCar('特斯拉', 'Model S', '红色')
tesla.run()

12.10 MRO 属性或 MRO 方法:方法解析顺序

Python 支持多继承,如果父类中有相同名字的方法,在子类没有指定父类名时,解释器将从左向右按顺序搜索。

MRO(Method Resolution Order):方法解析顺序,我们可以通过类的属性 类名.__mro__ 或类的方法 类名.mro() 获得类的层次结构方法解析顺序也是按照这个类的层次结构寻找的

class Car(object):
    def __init__(self, brand, model, color):
        self.brand = brand
        self.model = model
        self.color = color

    def run(self):
        print('i can run')


class GasolineCar(Car):
    def __init__(self, brand, model, color):
        super().__init__(brand, model, color)

    def run(self):
        print('i can run with gasoline')


class ElectricCar(Car):
    def __init__(self, brand, model, color):
        super().__init__(brand, model, color)
        # 电池属性
        self.battery = 70

    def run(self):
        print(f'i can run with electric,remain:{self.battery}')


print("=" * 20 + "GasolineCar" + "=" * 20)
print(GasolineCar.__mro__)
print(GasolineCar.mro())
print("=" * 20 + "ElectricCar" + "=" * 20)
print(ElectricCar.__mro__)
print(ElectricCar.mro())

说明
由 MRO 方法解析顺序可知,在类的继承中,当某个类创建了一个对象时,调用属性或方法,首先在自身类中去寻找,如找到,则直接使用,停止后续的查找。如果未找到,继续向上一级继承的类中去寻找,如找到,则直接使用,没有找到则继续向上寻找……直到 object 类,这就是 Python 类继承中,其方法解析顺序。
> 综上 object 类是所有类的基类(因为这个查找关系到 object 才终止)。
案例:多继承 MRO 测试。
class A:
    def aa(self):
        print("aa")

    def say(self):
        print("say AAA!")


class B:
    def bb(self):
        print("bb")

    def say(self):
        print("say BBB!")


class C(B, A):
    def cc(self):
        print("cc")


c = C()
# 打印类的层次结构
print(C.mro())
# 解释器寻找方法是“从左到右”的方式寻找,此时会执行 B 类中的 say()
c.say()

12.11 object 根类

object 类是所有类的父类,因此所有的类都有 object 类的属性和方法。我们显然有必要深入研究一下 object 类的结构。对于我们继续深入学习 Python 很有好处。

dir() 查看对象属性

为了深入学习对象,我们先学习内置函数 dir(),他可以让我们方便的看到指定对象所有的属性。

案例:查看对象所有属性以及和 object 进行比对。

class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def say_age(self):
        print(f"{self.name} 的年龄是 {self.age}")


obj = object()
print(dir(obj))

s2 = Person("小龙女", 19)
print(dir(s2))

执行结果:

['__class__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getstate__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__']

['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getstate__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'age', 'name', 'say_age']

从上面我们可以发现这样几个要点:

  1. Person 对象增加了六个属性:

    __dict__
    __module__
    __weakref__
    age
    name
    say_age
    
  2. object 的所有属性,Person 类作为 object 的子类,显然包含了所有的属性。

  3. 我们打印 agenamesay_age,发现 say_age 虽然是方法,实际上也是属性。只不过,这个属性的类型是 method 而已。

    age <class 'int'>
    name <class 'str'>
    say_age <class 'method'>
    

重写 __str__() 方法

  1. object 有一个 __str__() 方法,用于返回一个对于对象的描述,内置函数 str(对象),调用的就是 __str__()
  2. __str__() 经常用于 print() 方法,帮助我们查看对象的信息。__str__() 可以重写。
class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def __str__(self):
        """将对象转化成一个字符串描述,一般用于 print 方法"""
        return f"名字是:{self.name},年龄是 {self.age}"


p = Person("小龙女", 19)
print(p)

13 面向对象三大特征——多态

13.1 什么是多态

多态指的是一类事物有多种形态。

定义:多态(polymorphism)是指调用同一个方法,但由于调用对象不同可能会产生不同的行为。

> 比如:现实生活中,同一个方法,具体实现会完全不同。
> 比如:同样是调用人“吃饭”的方法,中国人用筷子吃饭,英国人用刀叉吃饭,印度人用手吃饭。 > > ![](images/8.面向对象编程/Pasted-image-20251006150242.png)

关于多态要注意以下 2 点:

  1. 多态是方法的多态,属性没有多态。
  2. 多态的存在有 2 个必要条件:继承、方法重写。
> 首先定义一个父类,其可能拥有多个子类对象。当我们调用一个公共方法时,传递的对象不同,则返回的结果不同。
多态:可以基于继承也可以不基于继承(可以借由模板方法等方式实现多态,但是继承是一种非常方便且严谨的实现多态的方式)。

好处:调用灵活,有了多态,更容易编写出通用的代码,做出通用的编程,以适应需求的不断变化!

13.2 多态原理图

image-20210318122417602

公共接口 service 就是多态的体现,随着传入水果对象的不同,能返回不同的结果。

13.3 多态代码实现

class Fruit(object):
    # 公共方法
    def makejuice(self):
        print('I can make juice')


class Apple(Fruit):
    def makejuice(self):
        print('I can make apple juice')


class Banana(Fruit):
    def makejuice(self):
        print('I can make banana juice')


class Orange(Fruit):
    def makejuice(self):
        print('I can make orange juice')


class Peach(Fruit):
    def makejuice(self):
        print('I can make peach juice')


apple = Apple()
banana = Banana()
orange = Orange()
peach = Peach()

for i in (apple, banana, orange, peach):
    i.makejuice()

14 特殊方法和运算符重载

14.1 特殊方法

Python 的运算符实际上是通过调用对象的特殊方法实现的。比如:

a = 20
b = 30
c = a + b
d = a.__add__(b)

print("c=", c)
print("d=", d)

常见的特殊方法统计如下:

方法 说明 例子
__init__ 构造方法 对象创建:p = Person()
__del__ 析构方法 对象回收
__repr____str__ 打印,转换 print(a)
__call__ 函数调用 a()
__getattr__ 点号运算 a.xxx
__setattr__ 属性赋值 a.xxx = value
__getitem__ 索引运算 a[key]
__setitem__ 索引赋值 a[key] = value
__len__ 长度 len(a)

每个运算符实际上都对应了相应的方法,统计如下:

运算符 特殊方法 说明
+ __add__ 加法
- __sub__ 减法
<,<=,== __lt____le____eq__ 比较运算符
>,>=,!= __gt____ge____ne__ 比较运算符
|,^,& __or____xor____and__ 或、异或、与
<<,>> __lshift____rshift__ 左移、右移
*,/,%,// __mul____truediv____mod____floordiv__ 乘、浮点除、模运算(取余)、整数除
** __pow__ 指数运算

我们可以重写上面的特殊方法,即实现了运算符的重载

# 测试运算符的重载
class Person:
    def __init__(self, name):
        self.name = name

    # 重写加法
    def __add__(self, other):
        if isinstance(other, Person):
            return f"{self.name}--{other.name}"
        else:
            return "不是同类对象,不能相加"

    # 重写乘法
    def __mul__(self, other):
        if isinstance(other, Person):
            return f"{self.name} * {other.name}"
        else:
            return "不是同类对象,不能相乘"


p1 = Person("赵敏")
p2 = Person("周芷若")
print(p1 + p2)
print(p1 + 1)
print("=" * 40)

print(p1 * 3)
print(p1 * p2)

14.2 特殊属性

Python 对象中包含了很多双下划线开始和结束的属性,这些是特殊属性,有特殊用法。

这里我们列出常见的特殊属性:

特殊方法 含义
obj.__dict__ 对象的属性字典
obj.__class__ 对象所属的类
class.__bases__ 类的基类元组(多继承)
class.__base__ 类的基类
class.__mro__ 类层次结构
class.__subclasses__() 子类列表
# 测试特殊属性
class A:
    pass


class B:
    pass


class C(B, A):
    def __init__(self, nn):
        self.nn = nn

    def cc(self):
        print("cc")


class D(C, B, A):
    def __init__(self, nn, ndd):
        super().__init__(nn)
        self.ndd = ndd

    def dd(self):
        print("dd")


d = D(3, 4)
print(f"{'=' * 30}对象的所有属性{'=' * 30}")
print(dir(d))
print(f"{'=' * 30}对象的属性字典{'=' * 30}")
print(d.__dict__)
print(f"{'=' * 30}对象所属的类{'=' * 30}")
print(d.__class__)
print(f"{'=' * 30}类的基类元组(多继承){'=' * 30}")
print(D.__bases__)
print(f"{'=' * 30}类层次结构{'=' * 30}")
# print(D.mro())
print(D.__mro__)
print(f"{'=' * 30}子类列表{'=' * 30}")
print(A.__subclasses__())

运行结果:

==============================对象的所有属性==============================
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getstate__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'cc', 'dd', 'ndd', 'nn']
==============================对象的属性字典==============================
{'nn': 3, 'ndd': 4}
==============================对象所属的类==============================
<class '__main__.D'>
==============================类的基类元组(多继承)==============================
(<class '__main__.C'>, <class '__main__.B'>, <class '__main__.A'>)
==============================类层次结构==============================
(<class '__main__.D'>, <class '__main__.C'>, <class '__main__.B'>, <class '__main__.A'>, <class 'object'>)
==============================子类列表==============================
[<class '__main__.C'>, <class '__main__.D'>]

15 对象的浅拷贝和深拷贝

15.1 变量的赋值操作

只是形成两个变量,实际还是指向同一个对象。

15.2 浅拷贝

Python 拷贝一般都是浅拷贝。拷贝时,对象包含的子对象内容不拷贝。因此,源对象和拷贝对象会引用同一个子对象。

15.3 深拷贝

使用 copy 模块的 deepcopy 函数,递归拷贝对象中包含的子对象。源对象和拷贝对象所有的子对象也不同。

# 测试对象的引用赋值、浅拷贝、深拷贝
import copy


class MobilePhone:
    def __init__(self, cpu, screen):
        self.cpu = cpu
        self.screen = screen


class CPU:
    def calculate(self):
        print("计算,算个 12345")
        print("CPU 对象:", self)


class Screen:
    def show(self):
        print("显示一个好看的画面,亮瞎你的钛合金大眼")
        print("屏幕对象:", self)


c = CPU()
s = Screen()
m = MobilePhone(c, s)
m.cpu.calculate()

print(f"{'=' * 30}引用赋值测试{'=' * 30}")
# 两个变量,但是指向了同一个对象
n = m
print('m, n:', m, n)

print(f"{'=' * 30}浅拷贝测试{'=' * 30}")
# m2 是新拷贝的另一个手机对象
m2 = copy.copy(m)
print('m, m2:', m, m2)
m.cpu.calculate()
# m2 和 m 拥有了一样的 cpu 对象和 screen 对象
m2.cpu.calculate()

print(f"{'=' * 30}深拷贝测试{'=' * 30}")
m3 = copy.deepcopy(m)
print('m, m3:', m, m3)
m.cpu.calculate()
# m3 和 m 拥有不一样的 cpu 对象和 screen 对象
m3.cpu.calculate()

内存分析:

image-20220702162610275

16 组合

结婚就是组合。两人组合后,可以复用对方的属性和方法!

除了继承,组合也能实现代码的复用!组合核心是将其它类的对象作为本类的属性

注意
> “is-a”关系,我们可以使用“继承”。从而实现子类拥有的父类的方法和属性。“is-a” 关系指的是类似这样的关系:狗是动物,dog is animal。狗类就应该继承动物类。 > > “has-a”关系,我们可以使用“组合”,也能实现一个类拥有另一个类的方法和属性。“has-a”关系指的是这样的关系:手机拥有 CPU。MobilePhone has a CPU。手机类拥有 CPU 类。
# 组合测试
class MobilePhone:
    def __init__(self, cpu, screen):
        self.cpu = cpu
        self.screen = screen


class CPU:
    def calculate(self):
        print("计算,算个 12345")


class Screen:
    def show(self):
        print("显示一个好看的画面,亮瞎你的钛合金大眼")


c = CPU()
s = Screen()
m = MobilePhone(c, s)
# 通过组合,我们也能调用 cpu 对象的方法。相当于手机对象间接拥有了 “cpu 的方法”
m.cpu.calculate()
m.screen.show()

17 设计模式——工厂模式实现

设计模式是面向对象语言特有的内容,是我们在面临某一类问题时候固定的做法,设计模式有很多种,比较流行的是:GOF(Goup Of Four)23 种设计模式。当然,我们没有必要全部学习,学习几个常用的即可。

对于初学者,我们学习两个最常用的模式:工厂模式单例模式

工厂模式实现了创建者和调用者的分离,使用专门的工厂类将选择实现类、创建对象进行统一的管理和控制。

# 工厂模式

class CarFactory:
    def create_car(self, brand):
        if brand == "奔驰":
            return Benz()
        elif brand == "宝马":
            return BMW()
        elif brand == '比亚迪':
            return BYD()
        else:
            return "未知品牌,无法创建"


class Benz:
    def run(self):
        print(f"奔驰正在奔驰!!!")


class BMW:
    def run(self):
        print(f"宝马正在驰骋!!!")


class BYD:
    def run(self):
        print(f"比亚迪正在飞驰!!!")


factory = CarFactory()

c1 = factory.create_car("奔驰")
c2 = factory.create_car("宝马")
c3 = factory.create_car("比亚迪")
c4 = factory.create_car("迈巴赫")

print(c1)
c1.run()
print(c2)
c2.run()
print(c3)
c3.run()
print(c4)

18 设计模式——单例模式

单例模式是一种常见的设计模式!

单例模式(Singleton Pattern)的核心作用是确保一个类只有一个实例,并且提供一个访问该实例的全局访问点。

单例模式只生成一个实例对象,减少了对系统资源的开销。当一个对象的产生需要比较多的资源,如读取配置文件、产生其他依赖对象时,可以产生一个单例对象,然后永久驻留内存中,从而极大的降低开销。

image-20210318162425553

应用场景:音乐播放器对象、回收站对象、打印机对象。

注意
单例模式有多种实现的方式,我们这里推荐重写 `__new__()` 的方法。

18.1 __new__() 方法

在 Python 中,我们目前已经学了几个魔术方法了,分别是 __init__()__str__()__del__(),接下来介绍一下 __new__() 方法。

使用 类名() 创建对象时,Python 的解释器首先会调用 __new__方法 为对象分配空间。

__new__ 是一个由 object 基类提供的内置的静态方法,主要作用有两个:

  1. 在内存中为对象分配空间。

  2. 返回对象的引用。

Python 解析器获得对象的引用后,将引用作为第一个参数,传递给 __init__方法

重写 __new__方法 的代码非常固定,一定要在函数最后写上 return super().__new__(cls) 语句,否则 Python 解释器得不到分配了空间的对象引用,就不会调用对象的初始化方法。

> `__new__方法` 是一个静态方法,在调用时,要求将自身类信息 cls 作为参数传递到这个方法中,这个方法属于 object 类中的一个静态方法。
案例:没有使用单例模式,每次实例化对象时,都需要重新创建,开销较大。
# ==========未使用单例模式==========
# 定义一个播放器类
class MusicPlayer(object):
    # 重写 __new__() 魔术方法
    def __new__(cls, *args, **kwargs):
        print('1、开辟内存空间')
        print('2、返回实例化对象引用地址')
        return super().__new__(cls)

    def __init__(self, name):
        self.name = name


# 1. 实例 mp1 对象
mp1 = MusicPlayer('红色的高跟鞋')
print(mp1)

# 2. 实例化 mp2 对象
mp2 = MusicPlayer('春夏秋冬')
print(mp2)

18.2 单例模式的代码实现

# ==========使用单例模式==========
class MySingleton:
    __obj = None
    # 是否已经初始化
    __init_flag = False

    def __new__(cls, *args, **kwargs):
        if cls.__obj is None:
            print("正在开辟内存空间……")
            cls.__obj = super().__new__(cls)
        return cls.__obj

    def __init__(self, name):
        # 判断是否需要进行初始化操作
        if MySingleton.__init_flag:
            return

        print("init....")
        self.name = name
        MySingleton.__init_flag = True


a = MySingleton("红色的高跟鞋")
print(a)
print(f"a.name:{a.name}")
b = MySingleton("春夏秋冬")
print(b)
print(f"b.name:{b.name}")
> 类属性在内存中是一个特殊的存在,其不同于以前讲过的局部变量(局部变量当函数执行完毕后,其会被内存所销毁)。但是类属性一旦定义,除非对象以及这个类在内存中被销毁了,否则其不会自动销毁。

19 工厂和单例模式结合

设计模式称之为“模式”,就是一些固定的套路。我们很容易用到其他场景上,比如前面讲的工厂模式,我们需要将工厂类定义成“单例”,只需要简单的套用即可实现:

# 测试工厂模式和单例模式的整合使用
class CarFactory:
    __obj = None
    __init_flag = False

    def __new__(cls):
        if cls.__obj is None:
            print('1、开辟内存空间')
            print('2、返回实例化对象引用地址')
            CarFactory.__obj = super().__new__(cls)

        return CarFactory.__obj

    def __init__(self):
        if CarFactory.__init_flag:
            return

        print("初始化工厂……")
        CarFactory.__init_flag = True

    def create_car(self, brand):
        if brand == "奔驰":
            return Benz()
        elif brand == "宝马":
            return BMW()
        elif brand == '比亚迪':
            return BYD()
        else:
            return "未知品牌,无法创建"


class Benz:
    def run(self):
        print(f"奔驰正在奔驰!!!")


class BMW:
    def run(self):
        print(f"宝马正在驰骋!!!")


class BYD:
    def run(self):
        print(f"比亚迪正在飞驰!!!")


if __name__ == '__main__':
    factory = CarFactory()
    c1 = factory.create_car("奔驰")
    c2 = factory.create_car("比亚迪")
    c1.run()
    c2.run()

    factory2 = CarFactory()
    print(factory)
    print(factory2)
    print(factory2 is factory)

20 工厂模式和单例模式练习

20.1 练习一

定义发动机类 Motor、底盘类 Chassis、座椅类 Seat,车辆外壳类 Shell,并使用组合关系定义汽车类。其他要求如下:

  • 定义汽车的 run() 方法,里面需要调用 Motor 类的 work() 方法,也需要调用座椅类 Seat 的 work() 方法,也需要调用底盘类 Chassis 的 work() 方法。
class Motor:
    def work(self):
        print("发动机正在工作……")


class Chassis:
    def work(self):
        print("底盘正在工作……")


class Seat:
    def work(self):
        print("座椅正在工作……")


class Shell:
    pass


class Car:
    def __init__(self, motor, chassis, seat, shell):
        self.motor = motor
        self.chassis = chassis
        self.seat = seat
        self.shell = shell

    def run(self):
        print("汽车开始运行……")
        self.motor.work()
        self.chassis.work()
        self.seat.work()


motor = Motor()
chassis = Chassis()
seat = Seat()
shell = Shell()

car = Car(motor, chassis, seat, shell)
car.run()

20.2 练习二

使用工厂模式、单例模式实现如下需求:

  1. 电脑工厂类 ComputerFactory 用于生产电脑 Computer。工厂类使用单例模式,也就是说只能有一个工厂对象。
  2. 工厂类中可以生产各种品牌的电脑:联想、华硕、神舟。
  3. 各种品牌的电脑使用继承实现。
  4. 父类是 Computer 类,定义了 calculate 方法
  5. 各品牌电脑类需要重写父类的 calculate 方法。
class ComputerFactory:
    __obj = None
    __init_flag = False
    __id = 1000

    def __new__(cls):
        if ComputerFactory.__obj is None:
            print("开辟内存空间……")
            ComputerFactory.__obj = super().__new__(cls)

        return ComputerFactory.__obj

    def __init__(self):
        if ComputerFactory.__init_flag:
            return

        print("初始化工厂……")
        ComputerFactory.__init_flag = True

    def create_computer(self, brand):
        if brand == "联想":
            ComputerFactory.__id += 1
            return Lenovo(ComputerFactory.__id)
        elif brand == "华硕":
            ComputerFactory.__id += 1
            return ASUS(ComputerFactory.__id)
        elif brand == "神舟":
            ComputerFactory.__id += 1
            return Hasee(ComputerFactory.__id)
        else:
            print("未知品牌,无法创建")
            return None


class Computer:
    def __init__(self, computer_id):
        self.computer_id = computer_id

    def calculate(self):
        print(f"计算机 {self.computer_id} 正在计算……")


class Lenovo(Computer):
    def calculate(self):
        print(f"联想计算机 {self.computer_id} 正在计算……")


class ASUS(Computer):
    def calculate(self):
        print(f"华硕计算机 {self.computer_id} 正在计算……")


class Hasee(Computer):
    def calculate(self):
        print(f"神舟计算机 {self.computer_id} 正在计算……")


if __name__ == '__main__':
    factory = ComputerFactory()
    lenovo = factory.create_computer("联想")
    asus = factory.create_computer("华硕")
    hasee = factory.create_computer("神舟")
    lenovo.calculate()
    asus.calculate()
    hasee.calculate()
    print("=" * 40)

    factory1 = ComputerFactory()
    print(factory)
    print(factory1)
    print(factory1 is factory)

20.3 练习三

定义一个 Employee 雇员类,要求如下:

  1. 属性有:idnamesalary
  2. 运算符重载 +:实现两个对象相加时,默认返回他们的薪水和。
  3. 构造方法要求:输入 namesalary,不输入 idid 采用自增的方式,从 1000 开始自增,第一个新增对象是 1001,第二个新增对象是 1002。
  4. 根据 salary 属性,使用 @property 设置属性的 get 和 set 方法。set 方法要求输入:1000~50000 范围的数字。
"""
定义一个 Employee 雇员类,要求如下:

1. 属性有:`id`、`name`、`salary`。
2. 运算符重载 `+`:实现两个对象相加时,默认返回他们的薪水和。
3. 构造方法要求:输入 `name`、`salary`,不输入 `id`。
    `id` 采用自增的方式,从 1000 开始自增,第一个新增对象是 1001,第二个新增对象是 1002。
4. 根据 salary 属性,使用 `@property` 设置属性的 get 和 set 方法。
    set 方法要求输入:1000~50000 范围的数字
"""


class Employee:
    __id = 1000

    def __init__(self, name, salary):
        Employee.__id += 1
        self.id = Employee.__id
        self.name = name
        self.__salary = salary

    @property
    def salary(self):
        return self.__salary

    @salary.setter
    def salary(self, salary):
        if 1000 < salary < 50000:
            self.__salary = salary
        else:
            raise ValueError("salary 必须为 1000~50000 范围的数字")

    def __add__(self, other):
        if isinstance(other, Employee):
            return self.salary + other.salary
        else:
            raise TypeError("类型不同不能相加")

    def __str__(self):
        return f"{self.name} 的 id 为 {self.id},薪水为 {self.salary}。"


if __name__ == '__main__':
    emp1 = Employee("小龙女", 20000)
    emp2 = Employee("赵敏", 15000)
    print(emp1)
    print(emp2)
    print(f"emp1 + emp2:{emp1 + emp2}")
    # print(f"emp1 + 1:{emp1 + 1}")

    emp1.salary = 50000
    print(emp1.salary)
    # emp1.salary = 500000
    # print(emp1.salary)

posted @ 2026-04-11 02:00  挖掘鱼  阅读(4)  评论(0)    收藏  举报