类的继承
从已有类中衍生出新的类,添加或修改部分功能,能提高代码复用。
子类是从父类派生出来的新类。
子类继承了父类的属性和方法,并且可以添加自己的属性和方法。
1 | class A(): |
使用super( )从父类得到帮助
1 | class Person(): |
class EmailPerson(Person):
定义了EmailPerson
类,它继承自Person
类,这意味着EmailPerson
类拥有Person
类的属性和方法。__init__
是子类的构造方法,接收name
和email
两个参数。首先打印子类的初始化信息,然后使用super().__init__(name)
调用父类的__init__
方法,将name
参数传递给父类构造函数,完成父类部分的初始化工作,最后将email
赋值给子类特有的实例属性self.email
。这里super()
函数的作用是获取父类的定义,从而调用父类的方法,避免在子类中重复编写父类已有的初始化逻辑。
方法重写
是指子类可以对从父类中继承过来的方法进行重新定义,从而使得子类对象可以表现出与父类对象不同的行为。
例:创建一个名为”Person”的父类,具有属性”name”和”age”。添加一个名为”get_info”的方法,打印人的姓名和年龄。
然后,创建一个名为”Student”的子类,继承自Person类,并添加一个额外的属性”grade”。在Student类中重写”get_info”方法,也打印出成绩。
1 | class Person: |
多态
多态,是指在执行同样代码的情况下,系统会根据对象实际所属的类去调用相应类中的方法。
在 Python 中编写一个函数,传递实参前其参数的类型并不确定,在函数中使用形参进行操作时只要传入的对象能够支持该操作程序就能正常执行 。
例:鸭子类型
在程序设计中,鸭子类型(duck typing)是动态类型的一种风格。在这种风格中,一个对象有效的语义,不是由继承自特定的类或实现特定的接口,而是由”当前方法和属性的集合”决定。支持“鸭子类型”的语言的解释器/编译器会在解释或编译时推断对象的类型。在鸭子类型中,关注的不是对象所属的类,而是一个对象能够如何使用。
1 | class Person: # 定义 Person 类 |
通过统一的方法接口,可以方便地调用不同类中的相同方法。
实例
创建两个不相关的类,分别命名为 ”Book” 和 “DVD”,它们都具有一个方法 play,但 Book 的 play 方法返回 “Reading the book” 而 DVD 的 play 方法返回 “Playing the DVD”。创建一个函数 start_playing,接受一个对象并调用其 play 方法,展示鸭子类型。
1 | class Book: |
类中的封装
在面向对象编程里,类的封装是一种重要特性。它一方面把属性和方法集合在一起,形成一个逻辑单元,让代码结构更清晰;另一方面,它还能把类里一些不希望被外部随意访问、只在类内部使用的属性和方法隐藏起来,提高代码的安全性和稳定性。
- 集合了对应的属性和方法
- 将类中私有的、只在内部使用的属性和方法进行隐藏
第一是约定任何以单下划线_
开头的名字都应该是内部实现。
1 | class A(): |
- 属性和方法定义:在
A
类中,_internal
是属性,_internal_method
是方法,它们都以单下划线开头,表明是内部使用的。而public
属性和public_method
方法没有下划线,是供外部正常访问的。 - 访问情况:虽然 Python
不会从语法层面阻止外部访问这些带单下划线开头的属性和方法,像
a._internal_method()
这样的调用是可以执行的,但这不符合规范。这只是一种约定,提醒开发者这些是类的内部实现细节,最好不要在外部调用,否则可能破坏类的设计逻辑,让代码变得脆弱、难以维护。而且这种约定不仅适用于类,模块名(如_private_module
)和模块级别函数(如sys._getframe()
)也适用,使用时要谨慎。
第二,使用双下划线__
开始
当属性或方法以双下划线 __
开头时,Python
会对其名称进行特殊处理。
会导致访问名称变成其他形式(名称改写/name mangling)。
1 | class B(): |
- 名称改写:在
B
类中,__private
属性和__private_method
方法,Python 会将它们重命名为_B__private
和_B__private_method
。查看实例b
的__dict__
属性(它存储了实例的属性信息),就能发现这种改写后的名称。 - 目的:这种机制主要是为了在继承时防止子类意外覆盖父类的私有属性和方法。比如:
1 | class C(B): |
在子类 C
中,__private
和
__private_method
同样会被改写,变成
_C__private
和 _C__private_method
,和父类
B
中对应的名称不一样,所以不会覆盖父类的私有属性和方法。
Python 解释器不会对使用单个下划线的属性名做特殊处理,不过这是很多 Python 程序员严格遵守的约定,他们不会在类外部访问这种属性。
当你定义的一个变量和某个保留关键字冲突,可以使用单下划线作为后缀:
总结:
_
和__
都可以定义私有属性- 使用
__
来定义的属性,
调用时需要将命名方式调整为_ClassName__methodName
__
方法适用于需要在子类中进行隐藏的情况
创建可管理属性
可管理属性是Python面向对象编程中一个强大的特性,它允许你控制对类属性的访问、修改和删除操作。
在面向对象编程中,我们有时需要对属性的访问进行控制:
- 在设置属性时进行类型检查或验证
- 在获取属性时进行计算或格式化
- 防止某些属性被删除
- 创建只读属性
在对实例属性的获取和设定上,有时候我们希望增加一些额外的处理过程(比如类型检查或者验证)。这种机制可以用于对”私有”属性进行访问和修改。
使用property
要自定义对属性的访问,一种简单的方式是将其定义为
property
property
把类中定义的函数当做一种属性来使用。
property()
函数可以创建一个属性,它允许你定义getter、setter和deleter方法。
1 | property(fget=None, fset=None, fdel=None, doc=None) |
fget
: 获取属性值的函数fset
: 设置属性值的函数fdel
: 删除属性的函数doc
: 属性的文档字符串
下面的示例代码定义了一个property
,增加了对属性的验证。
1 | class Person(): |
上例中,使用property()
定义了一个属性first_name
property()
的第一个参数是getter方法,第二个参数是setter方法,第三个参数是deleter方法property
的一个关键特征是它看上去跟普通的属性(attribute)没什么两样,但是访问它的时候会自动触发getter、setter、deleter方法。
在实现一个 property
的时候,底层数据(如果有的话)仍然需要存储在某个地方
- 在getter和setter方法中,你会看到对
_firse_name
的操作,这也是实际数据保存的地方 - 数据实际存储在
_first_name
中(注意前面的下划线表示这是内部属性)。
为什么__init__()
方法中设置了self.first_name
而不是self._first_name
- 在这个例子中,创建一个
property
的目的就是在设置attribute的时候进行检查 - 这样设置是为了在初始化的时候也进行这种类型检查
- 通过设置
self.first_name
,自动调用setter方法,这个方法里面会进行参数的检查,否则就是直接访问self._first_name
了 - 在
__init__
方法中设置self.first_name
而不是self._first_name
,这样会调用setter方法进行验证。
使用装饰器
另一种定义属性的方法是使用装饰器
装饰器语法提供了更简洁的方式来定义可管理属性。
1 | class Person(): |
- 装饰器顺序:
- 必须先定义
@property
方法(getter) - 然后才能定义
@xxx.setter
和@xxx.deleter
- 必须先定义
- 方法命名:
- 所有相关方法必须使用相同的名称
- 这是装饰器语法要求的
- 文档字符串:
- 可以在
@property
方法中添加文档字符串 - 通过
help(Person.first_name)
可以查看
- 可以在
上述代码中有三个相关联的方法,这三个方法的名字都必须一样。@property
用于指示getter方法,它使得first_name
成为一个属性。@first_name.setter
用于指示setter方法,@first_name.deleter
用于指示deleter方法。需要强调的是只有在first_name
属性被创建后,后面的两个装饰器@first_name.setter
和@first_name.deleter
才能被定义。
注意:不要写没有做任何其他额外操作的property。
另外,property还可以用于创建动态计算的属性,这些属性不会实际存储,而是在访问时计算
1 | import math |
特点
- 只读属性:如果没有定义setter,属性就是只读的
- 统一访问接口:无论是存储属性还是计算属性,访问方式都一样
- 延迟计算:只在访问时计算,节省内存
在这里,我们通过使用property,将所有的访问接口形式统一起来,对半径、周长和面积的访问都能够简单地以属性的形式进行访问,而不必将属性访问和方法调用混在一起使用了。
如果你没有指定某一特性的*setter*属性(@area.setter
),那么将无法在类的外部对它的值进行设置。这对于只读的特性非常有用:
总结:
- 类的定义中使用
@property
可以实现属性的获取(“getter”) - 类的定义中使用
@setter
可以实现属性的设置(“setter”)
可管理属性是Python面向对象编程中非常强大的特性,它允许你:
- 控制属性的访问、设置和删除行为
- 添加验证逻辑和类型检查
- 创建动态计算的属性
- 实现只读属性
- 保持统一的访问接口
无论是使用property()
函数还是装饰器语法,都能有效地增强类的封装性和安全性。选择哪种方式主要取决于个人偏好和代码的可读性,装饰器语法通常更为简洁明了。