面向对象
核心概念
面向对象编程(Object-Oriented Programming, OOP)是一种编程范式,它使用“对象”来设计软件。这种方法不仅侧重于数据(即属性),还侧重于操作这些数据的代码(即方法)。面向对象编程基于几个核心概念,包括类、对象、继承、多态和封装。
- 类(Class):类是创建对象的蓝图或模板。它定义了对象的属性和方法。属性是类中的变量,方法是类中的函数。类本身不占用内存空间,它只是定义了对象的结构。
- 对象(Object):对象是类的实例。当类被实例化时,会创建一个对象。对象具有类定义的属性和方法。每个对象都有自己的属性值,但共享相同的方法。
- 继承(Inheritance):继承允许一个类继承另一个类的属性和方法。这有助于代码重用,并实现多层次的分类。在继承关系中,有基类(父类)和派生类(子类)。
- 多态(Polymorphism):多态是指即使不同类的对象可能有不同的实现,但它们可以通过相同的接口进行交互。这意味着,可以用统一的方式来处理不同类的对象,即使它们具有不同的内部结构。
- 封装(Encapsulation):封装是隐藏对象内部实现细节的过程,仅公开必要的接口。这有助于防止外部代码随意更改对象内部状态,从而保护对象的完整性并简化接口。
- 抽象(Abstraction):抽象是将复杂的现实世界问题简化为模型的过程。在面向对象编程中,这通常意味着创建类,这些类代表了现实世界中的实体或概念,但只包括与当前问题相关的部分。
优点
- 模块化:OOP 促进了代码的模块化,使得不同模块可以独立开发和测试,提高了代码的可维护性和复用性。
- 可扩展性:通过继承和多态,可以在现有代码的基础上添加新功能,而不必大幅修改现有代码。
- 可维护性:封装提供了一种方式,使得代码更易于理解和维护。
缺点
- 性能:相较于面向过程的编程,面向对象的程序可能在性能上稍微逊色,因为它需要额外的内存和处理。
- 复杂性:面向对象的系统可能比简单的面向过程的系统更复杂。
应用
面向对象编程在软件工程中广泛应用,从桌面和移动应用程序到大型企业级系统。众多流行的编程语言,如 Java、C#、Python、Ruby 和 C++,都支持面向对象的编程范式。
类 (Class)
类(Class)是面向对象编程(OOP)中的一个核心概念。在很多现代编程语言中,类作为一种构造,提供了一种封装数据和行为(即代码)的方式。它们允许开发者创建复杂的数据结构
类的定义
首先,我们定义一个名为Car
的类:
1 | class Car: |
在这个Car
类中:
- 成员属性:
brand
(品牌)、color
(颜色)和speed
(速度)是类的属性。它们用来描述这个类的状态。 - 成员方法:
start
、accelerate
和brake
是类的方法。它们描述了Car
可以执行的操作。
实例化类
接下来,我们创建几个Car
类的实例:
1 | first_car = Car() # 实例化第一辆车 |
每次调用Car()
时,我们都创建了一个新的Car
类的实例(即一个新的汽车对象)。通过为不同的实例分别设置属性(如brand
和color
),每辆车都有自己的特点。
QA
问:
通过类名()
可以实例化一个对象吗?
答:
在Python中,你可以通过调用类名并传递所需的参数(如果有的话)来创建(实例化)一个类的实例。例如,如果有一个名为 MyClass
的类,可以通过 my_instance = MyClass()
来创建这个类的一个实例。
问:
类具有属性和方法吗?
答:
在Python中,类可以拥有属性(有时称为数据成员)和方法(类内定义的函数)。属性用于保存数据值,而方法用于定义对象可以执行的操作。
问:
一个类只能创建一个对象吗?
答:
这个说法是错误的。一个类可以用来创建任意数量的对象。每个对象都是类的实例,拥有自己的属性集和方法。例如,如果有一个 Dog
类,可以用它来创建许多 Dog
实例,每个实例代表一个不同的狗。
问:
对象是一个实体吗?
答:
在编程中,对象通常指的是一个具体的实例,它由类创建。对象是类定义的实体的具体表现。它拥有类中定义的属性和方法。
成员方法中的self
在Car
类的成员方法中,self
关键字是对当前对象实例的引用。它用于访问对象的属性和调用其他方法。例如,在accelerate
方法中,self.speed += increase
表示将当前车辆的速度增加increase
单位。
_ _init_ _初始化方法(魔术方法/构造器)
__init__
是一个在 Python 中特别重要的方法,它是类的一个特殊方法(有时被称为“魔术方法”或“构造器”)。当创建类的新实例时,__init__
方法被自动调用,用于为新创建的对象初始化其属性或执行其他必要的设置。以下是关于 __init__
方法的一些关键点:
- 构造器:
__init__
方法可以看作是类的“构造器”。它在对象创建时立即执行,允许类接受参数并据此初始化对象的属性。 - self 参数:
__init__
方法的第一个参数总是self
,它是对类实例本身的引用。通过self
,可以在方法内部访问类的属性和其他方法。 - 初始化属性:可以在
__init__
方法中定义并初始化对象的属性。这样做的好处是,可以为不同的对象实例设置不同的初始状态。 - 接受参数:
__init__
方法可以接受参数(除了自身self
参数外),这些参数在创建类实例时传递给它,用于设置对象的属性或进行其他操作。
1 | class Car: |
在这个例子中, Car
类中添加了 __init__
方法,它接受 brand
和 color
作为参数,并将它们分别赋值给实例的 brand
和 color
属性。现在,在创建 Car
类的实例时,我们需要提供品牌和颜色作为参数。这样一来,每个 Car
实例在创建时都会有自己的品牌和颜色属性。
QA
问:为什么accelerate不在car(brand, color)
里直接写?
答:将 accelerate
方法放在 Car
类的构造方法 (__init__
) 中与放在类的外部作为一个独立的方法有不同的含义和用途。理解这一点,需要对面向对象编程中的一些基本概念有所了解:
- 构造方法 (
__init__
):__init__
方法用于初始化新创建的对象的状态。它通常用于设置对象的初始属性值。构造方法在对象被创建时自动调用一次,用于设置对象的初始状态。 - 成员方法: 类中定义的其他方法(如
accelerate
)用于描述对象的行为。这些方法可以在对象的整个生命周期内被多次调用,并且通常用于执行特定的操作,比如修改对象的状态或者与其他对象进行交互。 - 职责分离: 在面向对象设计中,通常推荐遵循单一职责原则。这意味着每个部分(如方法)应该只负责一项功能。将
accelerate
方法放在类的外部作为一个独立的方法,而不是在__init__
中定义,可以使类的结构更加清晰,每个方法的职责更加明确。 - 灵活性与可重用性: 将
accelerate
作为独立的方法可以在任何时候调用以改变对象的速度属性。如果它被放在__init__
中,它只能在对象创建时执行,这限制了方法的用途。
总结来说,accelerate
不放在 __init__
中是为了保持代码的清晰性和组织性,确保每个方法只负责一项特定的功能,并允许在对象的生命周期中多次调用 accelerate
方法。这是一种更灵活、可维护和符合面向对象设计原则的做法。
问:self.speed = 0
为什么在(__init__
) 里?
答:将 self.speed = 0
放在 __init__
方法中是为了初始化 Car
类的每个实例的速度属性。这是一种常见的做法,用于设定对象的初始状态。以下是放置这个语句在 __init__
中的几个理由:
- 初始状态设定:在面向对象编程中,构造方法(如 Python 中的
__init__
)用于设定对象的初始状态。对于汽车来说,初始速度设为 0 是合理的,因为当汽车被“创建”(或实例化)时,它通常是静止的。 - 统一属性管理:将所有属性的初始化集中在
__init__
方法中可以使代码更加清晰和易于维护。这样,可以在一个地方看到对象所有属性的初始值。 - 确保属性存在:在
__init__
方法中初始化speed
确保了每个Car
实例都具有speed
属性,并且有一个明确的初始值。这避免了在其他方法(如accelerate
或brake
)中使用speed
属性之前需要检查其是否存在的情况。 - 避免未初始化的属性:如果不在
__init__
中初始化speed
,那么在调用accelerate
方法之前,尝试访问speed
属性可能会导致错误,因为 Python 不会自动创建这个属性。
综上所述,将 self.speed = 0
放在 __init__
方法中是为了初始化对象的速度属性,保证对象的完整性,并为后续的操作(如加速或刹车)提供一个明确和稳定的起点。
案例
类定义与初始化
首先,我们定义一个名为Car
的类,并在其中使用__init__
方法来初始化类的实例。__init__
是一个特殊的方法,它在类的实例化时自动调用。我们可以通过它来设置初始的成员属性。
1 | class Car: |
实例化类
当我们实例化这个类时,我们需要提供品牌、颜色和重量这三个参数。
1 | # 实例化 Car 类 |
这里,first_car
是 Car
类的一个实例,代表了一辆特定的汽车。我们给它赋予了“Toyota”品牌、“红色”颜色和1500公斤重量的属性,并通过display_info
方法打印了这些信息。
总结
通过修改这个例子,我们可以看到如何在类中使用初始化方法(__init__
)来设置对象的初始状态,并通过实例化这个类来创建具体的对象。每个对象都有自己的属性值,但它们共享相同的方法(如display_info
)。
QA
问:以女娲造人为例子
- 什么是类?
- 什么是实例化类?
- 什么是面向对象?
答:
什么是类?
在女娲造人的故事中,女娲的想法或设计可以被看作是一个“类”(Class)。这个类是一个抽象的概念,它定义了人类将拥有的特征(如眼睛、鼻子、嘴巴)和行为(如走路、说话)。类是面向对象编程中定义数据结构和行为的蓝图或模板。
什么是实例化类?
当女娲根据她的想法创造出一个具体的人时,这个过程就像是在面向对象编程中的“实例化”(Instantiation)。实例化类的过程就是根据类的定义创建一个具体的对象(在这个故事中就是一个具体的人)。每个人(对象)都有类定义的特征和行为,但每个人也有自己的个性(即对象的状态可以独立于其他对象)。
什么是面向对象?
面向对象编程(Object-Oriented Programming, OOP)是一种编程范式,它使用对象来模拟现实世界的实体。在女娲造人的故事中,整个造人过程(设计-创造-赋予行为)就类似于面向对象的过程。面向对象编程的核心是类和对象,以及它们之间的交互。
问:面向对象的好处是什么?我们以后开发代码需要怎么做?
答:
好处
- 封装:面向对象允许封装数据和方法,提高数据安全性。
- 继承:通过继承,新的类可以扩展现有类的功能。
- 多态:相同的接口可以用在不同的对象上,提高代码的灵活性和可重用性。
- 代码组织和可读性:OOP 提高了代码的组织性和可读性,使大型项目更易于管理。
开发时的做法
- 使用类和对象来模拟现实世界的实体和概念。
- 封装相关的属性和方法,确保数据安全性和减少外部干扰。
- 通过继承和多态性来重用代码和提高代码的灵活性。
- 关注对象之间的交互,而不仅仅是代码的执行流程。
问: self
在初始化__init__
的作用是什么? 在普通方法中的作用是什么?
答:
在 __init__
方法中
self
在__init__
方法中代表新创建的对象本身。- 它用于初始化对象的属性。
- 通过
self
,可以将对象的属性与局部变量区分开来。
在普通方法中
self
在类的其他方法中也代表对象本身。- 它用于访问和修改对象的属性。
- 同样,
self
用于调用对象的其他方法。
简而言之,self
是类方法中的一个参数,它提供了对类实例本身的引用。这允许类的方法在操作对象时访问和修改对象的属性,以及调用其他方法。
问:请根据闹钟 来写一个类 并创造三个闹钟
1 | from datetime import datetime |
面向对象的特点
封装 (Encapsulation)
封装是面向对象编程中的一个核心概念,它指的是将对象的数据(属性)和行为(方法)组合在一起,形成一个独立的单元。在封装过程中,可以对对象的成员进行访问限制。
私有属性 (Private Attributes)
属性名前加了两个下划线表示它是私有属性,只能在类的内部被访问。
作用:
不能被外部直接访问
self.__资金 = 0
私有属性,代表车辆的资金
私有方法 (Private Methods)
方法名前加了两个下划线表示它是私有方法,只能在类的内部被调用。
作用:
不能被外部直接访问
1
2def __start_engine(self):
print('引擎启动,准备行驶')
以汽车为例:
1 | class Car: |
在类中,成员属性和成员方法可以通过在名称前加上两个下划线 __
来设为私有。这样做的好处是能够保证这些成员不会被外界直接访问,但它们仍然可以在类的内部通过 self
被访问。这种方式有助于隐藏数据和实现细节,提高了代码的安全性和健壮性。
QA
问:为什么class AdvancedCar(Car)
不用super()
答:AdvancedCar
类继承自 Car
类。在这个特定的情况下,super()
没有被使用,原因如下:
- 不需要覆盖父类的方法: 在
AdvancedCar
类中,我们添加了新的方法(autopilot
和advanced_navigation
),而没有修改或覆盖任何继承自Car
类的方法。因此,没有必要使用super()
来调用父类Car
的任何方法。 - 构造函数未被重写: 如果子类不重写父类的构造函数(
__init__
方法),则默认调用父类的构造函数。在例子中,AdvancedCar
没有定义自己的__init__
方法,因此它会自动使用父类Car
的构造函数来初始化对象。如果在子类中重写了构造函数并且想要执行父类的构造函数,那么在子类的构造函数中使用super().__init__()
是必要的。 - 独立的新方法:
AdvancedCar
类中新增的方法(autopilot
和advanced_navigation
)是独立的,它们不依赖于Car
类中定义的任何方法,因此没有必要使用super()
。
总结一下,super()
主要用于需要调用或扩展父类中定义的方法时。如果只是在子类中添加新的独立方法或属性,并且没有重写父类的构造函数,就不需要使用 super()
。在例子中,由于这些条件都未满足,因此不使用 super()
是合理的。
继承 (Inheritance)
在面向对象编程中,继承允许我们从一个已存在的类(称为父类)派生出一个新的类(称为子类)。子类会继承父类的所有属性和方法,并可以添加新的属性和方法或重写某些方法。
以汽车为例,我们可以从一个基本的汽车类派生出一个具有更多功能的特殊汽车类。
首先是父类,表示基本的汽车:
1 | class Car: |
然后是子类,这里我们假设有一个增强版的汽车,它除了有基本的汽车功能外,还增加了自动驾驶和先进的导航系统:
1 | class AdvancedCar(Car): # 继承自 Car 类 |
在这个例子中,AdvancedCar
类继承了 Car
类的所有属性和方法(如 start_engine
和 drive
),并新增了 autopilot
和 advanced_navigation
方法。这表明 AdvancedCar
不仅保留了基本汽车的特性,还加入了新的功能,充分展示了继承的实用性和灵活性。
多态 (Polymorphism)
多态是面向对象编程中的一个重要概念,它允许不同类的对象对同一消息做出响应。即不同的对象可以通过相同的接口(方法或函数)执行不同的操作。这增加了程序的灵活性和可扩展性。
1 | class Dog: |
在这个例子中,Dog
和 Cat
类都实现了同一个方法 speak
,但它们以不同的方式响应(狗叫声和猫叫声)。函数 animal_sound
接受任何有 speak
方法的对象作为参数,然后调用这个方法。这就是多态的核心:通过同一接口,不同的对象可以执行各自不同的操作。
总结来说,多态允许我们编写更通用和可重用的代码,因为我们可以设计函数或方法,它们可以与任何符合特定接口(即实现特定方法)的对象交互,而不关心对象的具体类型。这样可以提高代码的灵活性和可扩展性。
学生管理系统案例
这个学员管理系统示例演示了面向对象编程的几个关键概念,如类的定义、初始化、封装、私有属性和方法的使用。
1 | # 定义一个名为StudentManagementSystem的类 |
为了在现有的 StudentManagementSystem
类中引入继承和多态的特点,我们可以创建一个基类来表示一般的用户,然后让 StudentManagementSystem
类成为这个基类的子类。此外,我们可以定义一个接口(一个具有共同方法但没有具体实现的基类),来展示多态。
首先是基类和接口的定义:
1 | # 基类 User,表示一般的用户 |
然后是修改后的 StudentManagementSystem
类,它继承自 User
并实现了 CourseConsumer
接口:
1 | # StudentManagementSystem 类继承自 User 类并实现了 CourseConsumer 接口 |
在这个修改后的版本中,StudentManagementSystem
类继承了 User
类,并且实现了 CourseConsumer
接口的 purchase_course
方法。这样的设计不仅引入了继承,还通过实现接口展示了多态。继承允许 StudentManagementSystem
类重用 User
类的代码,而多态则体现在 StudentManagementSystem
类能够以不同方式实现 CourseConsumer
接口中定义的方法。这样的设计使得代码更加灵活和可扩展。
信息
在面向对象编程中,super()
函数的使用通常出现在继承体系中,尤其是在子类需要调用父类的方法或构造函数时。以下是一些使用 super()
的典型情况:
- 在子类的构造函数中调用父类的构造函数: 当创建一个子类时,可能需要在子类中初始化一些继承自父类的属性。使用
super()
调用父类的构造函数可以确保这些属性被适当地初始化。
1 | class Parent: |
- 在子类中覆盖父类的方法时调用父类的实现: 如果在子类中重写了父类的方法,并且还想保留父类方法的行为,可以在子类方法中使用
super()
调用父类的方法。
1 | class Parent: |
- 多重继承中解决方法解析顺序(Method Resolution Order, MRO)问题: 在使用多重继承时,
super()
被用来确保每个父类都被适当地初始化或调用相应的方法。它按照方法解析顺序遍历所有的基类。
1 | class Base: |
总之,super()
的使用有助于确保父类的适当初始化和方法调用,尤其是在有继承关系的类之间。这有助于减少代码重复,并确保继承体系中的逻辑一致性。
- 标题: 面向对象
- 作者: Yiuhang Chan
- 创建于 : 2018-09-08 12:13:23
- 更新于 : 2024-02-28 18:49:07
- 链接: https://www.yiuhangblog.com/2018/09/08/20180908面向对象/
- 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。