上篇文章中的练习参考答案:
class User:
def __init__(self, name, age, sport):
self.name = name
self.age = age
self.sport = sport
def show(self):
print("My name is %s" % self.name)
print("I'm %d years old." % self.age)
print("%s is my favorite sport." % self.sport)
xiaohong = User('xiaohong', 18, 'badminton')
xiaoming = User('xiaoming', 18, 'Esports')
xiaohong.show()
xiaoming.show()
今日学习目标
掌握面向对象的三要素:
1.封装
2.继承
3.多态
一、封装
定义
封装是面向对象编程中的重要概念之一,它是将数据和操作封装在对象中的机制。
封装的目的是隐藏对象的内部细节和实现细节,只暴露必要的接口供外界访问和使用。通过封装,可以实现以下几个重要的目标:
1.数据隐藏:封装可以隐藏对象的内部数据,使其对外部不可见。只有通过对象的公共接口(方法)才能访问和修改对象的数据,这样可以防止外部直接修改对象内部的数据,提高了数据的安全性和完整性。
2.实现细节隐藏:封装也可以隐藏对象的具体实现细节。对象对外部只公开一些抽象的方法,而不暴露内部的实现细节,这样可以在不影响外部使用的前提下,随时改变和优化对象的实现方式。
3.接口定义:封装通过定义对象的公共接口(方法)来提供对对象的访问和操作。对象的用户不需要知道对象内部的实现细节,只需要使用接口提供的方法来完成所需的操作。
4.代码组织:封装可以将相关的数据和操作组织在一个对象中,使代码更加模块化和可维护。对象拥有自己的数据和方法,这样可以将相关的功能集中在一个对象中,便于开发、测试和理解。
在实践中,封装可以通过以下方式来实现:
- 定义类:类是封装的基本单位,可以将相关的数据和方法封装在一个类中,并提供类的接口供外界使用。
- 访问修饰符:通过使用访问修饰符(如公有、私有和受保护),可以控制属性和方法的访问范围,从而实现对对象的封装。
- Getter和Setter方法:可以通过定义Getter和Setter方法来控制对象属性的访问和修改。Getter方法用于获取属性的值,Setter方法用于设置属性的值,可以在方法中添加逻辑来对属性进行验证和控制。
封装是面向对象编程的核心特性之一,它提供了一种更安全、可靠和模块化的方式来设计和组织代码。封装可以提高代码的可维护性、可重用性和安全性,使得我们能够更加专注于对象的使用和功能的实现。
请看下方代码:
class Person:
def __init__(self, name, age):
self._name = name # 使用单个下划线作为属性的约定,表示属性是受保护的,但仍可访问
self._age = age
# Getter方法
def get_name(self):
return self._name
# Setter方法
def set_name(self, name):
self._name = name
def speak(self):
print(f"I am {self._name}, {self._age} years old.")
# 创建对象
person = Person("John", 25)
# 访问属性
print(person.get_name()) # 输出:John
# 修改属性
person.set_name("Michael")
print(person.get_name()) # 输出:Michael
# 使用方法
person.speak() # 输出:I am Michael, 25 years old.
在上面的代码中,我们定义了一个名为Person
的类,它具有_name
和_age
两个属性。这些属性被命名为以单个下划线开头,这是一种约定,表示这些属性是受保护的,应该在类的外部进行访问。虽然它们仍然可以通过外部访问,但是作为开发者,我们应该将其视为不应直接访问的属性。
为了访问和修改这些属性,我们提供了Getter和Setter方法。这些方法允许外部代码通过调用方法来获取和设置属性的值。在Getter方法中,我们简单地返回相应的属性值。在Setter方法中,我们将传入的新值分配给属性。通过这种方式,我们可以在Getter和Setter方法中添加逻辑来验证和控制属性的访问和修改。
在主程序中,我们创建了一个Person
对象,传入名称和年龄参数。通过get_name()
方法,我们可以获取名称属性的值,并进行输出。然后,我们通过set_name()
方法将名称属性修改为新值。最后,我们使用speak()
方法来展示该对象的行为。
这个示例展示了Python中如何使用Getter和Setter方法实现封装,以及通过属性名称的约定来标识受保护的属性。封装使我们能够控制属性的访问和修改,并提供统一的接口来与对象交互。
这里提及了受保护的属性,其实除了属性之外,方法同样也可以声明为保护方法和私有方法,二者之间是存在着一定的区别的。
保护属性
保护属性就是在属性名前面使用一个下划线(_
)表示属性是保护的。保护属性可以被访问和修改,但它们被视为私有属性,应该在类的外部进行访问。
class Person:
def __init__(self, name, age):
self._name = name # 保护属性
self._age = age
def speak(self):
print(f"I am {self._name}, {self._age} years old.")
# 创建对象
person = Person("John", 25)
# 访问保护属性
print(person._name) # 输出:John
# 修改保护属性
person._name = "Michael"
print(person._name) # 输出:Michael
# 使用方法
person.speak() # 输出:I am Michael, 25 years old.
在上面的示例中,我们使用_name
和_age
作为保护属性。尽管它们具有保护属性的约定,仍然可以通过对象的实例访问和修改这些属性。person._name
语法仍然是有效的,但作为开发者,我们应该将其视为不应直接访问的属性。
私有属性
在属性名前面使用两个下划线(__
)表示属性是私有的。私有属性不能直接从类的外部访问和修改。
class Person:
def __init__(self, name, age):
self.__name = name # 私有属性
self.__age = age
def speak(self):
print(f"I am {self.__name}, {self.__age} years old.")
# 创建对象
person = Person("John", 25)
# 访问私有属性(错误示例)
print(person.__name) # 报错:'Person' object has no attribute '__name'
# 使用方法
person.speak() # 输出:I am John, 25 years old.
在上面的示例中,我们使用__name
和__age
作为私有属性。私有属性不能直接通过对象的实例访问,因为Python会对它们进行名称修饰(name mangling)。如果尝试直接访问,会引发AttributeError
错误。
为了在类的内部访问私有属性,可以使用名称修饰的属性名,以_类名
作为前缀。例如,self.__name
可以通过self._Person__name
来访问。
总结来说,保护属性和私有属性都用于指示属性的访问权限。保护属性可以被访问和修改,但是被视为私有属性,应该在类的外部进行访问。私有属性不能直接从类的外部访问,可以通过名称修饰的方式在类的内部访问。虽然Python并没有严格限制私有属性的访问,但是按照约定,我们应该将其视为不应直接访问的属性。
二、继承
继承是面向对象编程中的一种重要概念,它允许一个类(称为子类或派生类)从另一个类(称为父类或基类)继承属性和方法。
继承的主要优势在于代码的重用。通过继承,子类可以继承父类的属性和方法,并可以添加新的属性和方法,或覆盖(重写)父类的方法,从而实现代码的扩展和定制。
在Python中,可以通过以下方式来实现继承:
# 定义父类
class Animal:
def __init__(self, name):
self.name = name
def sound(self):
print("This is the sound of an animal.")
# 定义子类,继承父类
class Cat(Animal):
def __init__(self, name, color):
super().__init__(name) # 调用父类的初始化方法
self.color = color
def sound(self): # 重写父类的方法
print("Meow!")
# 创建子类对象
cat = Cat("Tom", "black")
# 访问继承的属性
print(cat.name) # 输出:Tom
# 调用继承的方法
cat.sound() # 输出:Meow!
在上述示例中,我们首先定义了一个父类Animal
,它具有name
属性和sound()
方法。然后,我们定义了一个子类Cat
,并使用括号中的父类名称指明该子类继承自Animal
。
子类Cat
具有自己的构造函数__init__()
,它使用super()
函数调用父类Animal
的构造函数,以继承父类的属性name
。子类还添加了自己的属性color
。
子类Cat
还重写了父类Animal
的方法sound()
。当我们调用cat.sound()
时,子类对象的方法会被执行,而不是父类的方法。这就是方法重写的概念,在子类中可以根据需要重新定义父类的方法实现。
继承的关键点:
- 子类通过在类定义中指定父类名称来继承父类。
- 子类继承了父类的属性和方法,并可以添加自己的属性和方法。
- 子类可以重写父类的方法,以实现自己的行为。
通过继承,可以实现多级继承(子类继承自其他子类)和多重继承(子类继承自多个父类)。继承在面向对象编程中扮演着重要的角色,它提高了代码的可重用性、扩展性和可维护性。
注:在Python中,一个子类可以继承多个父类
三、多态
多态(Polymorphism)是面向对象编程中的一个重要概念,它允许使用相同的接口来处理不同类型的对象,从而实现代码的灵活性和可扩展性。
多态性可以分为编译时多态(静态多态)和运行时多态(动态多态):
编译时多态:也称为静态多态,它发生在编译时期,通过函数的重载和运算符的重载来实现。编译器会根据函数或运算符的参数类型和数量,选择合适的函数或运算符来执行。
def add(a, b):
return a + b
def add(a, b, c):
return a + b + c
result1 = add(2, 3)
print(result1) # 输出:5
result2 = add(2, 3, 4)
print(result2) # 输出:9
在上述示例中,我们定义了两个具有相同函数名的函数add
,但是它们具有不同的参数数量。当我们调用add
函数时,编译器会根据传递的参数数量选择合适的函数实现。
运行时多态:也称为动态多态,它发生在运行时期,通过继承和方法重写来实现。在运行时,对象的实际类型决定了调用哪个方法。
class Animal:
def sound(self):
print("This is the sound of an animal.")
class Cat(Animal):
def sound(self):
print("Meow!")
class Dog(Animal):
def sound(self):
print("Woof!")
# 创建对象
cat = Cat()
dog = Dog()
# 调用方法
cat.sound() # 输出:Meow!
dog.sound() # 输出:Woof!
在上述示例中,我们定义了一个基类Animal
,并创建了两个子类Cat
和Dog
。这两个子类都重写了基类中的sound()
方法。
当我们调用cat.sound()
和dog.sound()
时,由于对象的实际类型是Cat
和Dog
,所以会调用各自子类中的方法实现。这就是运行时多态,对象的实际类型决定了方法的调用。
多态提供了代码的灵活性和可扩展性。通过使用相同的接口处理不同类型的对象,我们可以将代码设计得更加通用,降低了代码的耦合度,并提高了代码的可维护性和可读性。
小结
关于面向对象,在上篇文章中说过其是一个很抽象的概念,所以希望大家能够结合文章去花费大量的时间来进行练习,通过代码来辅助理解其定义是一个不错的学习方式。
**练习:**现有一个图书管理系统,目前已实现的功能有用户借书与还书两个功能,请以空闲的时间来完成对该图书管理系统的功能,功能包括如下:用户注册、用户信息修改、用户删除、添加图书、图书信息修改、删除图书。除此外,也可以根据个人能力进行功能的添加。练习代码如需要审查的话可私信本人进行评改。
已实现的代码如下:
class User:
"""用户类"""
def __init__(self, name):
self.name = name
def borrow_book(self, library, book):
"""借书"""
library.remove_book(book)
print(f"{self.name} borrowed the book: {book.title}")
def return_book(self, library, book):
"""还书"""
library.add_book(book)
print(f"{self.name} returned the book: {book.title}")