类的继承

从已有类中衍生出新的类,添加或修改部分功能,能提高代码复用。

子类是从父类派生出来的新类。

子类继承了父类的属性和方法,并且可以添加自己的属性和方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class A():
def foo(self):
print('A.foo')
def zoo(self):
print('A.zoo')

class B(A): # B 类将继承 A 类的所有属性和方法。
def fooo(self):
super().foo() # 使用 super() 函数调用父类 A 的 foo方法。
print('B.foo')
def bar(self):
print('B.bar')

a = A()
b = B()
b.bar()

a = A()
b = B()
# a.foo()
# b.fooo()
# b.zoo() # 继承自类 A,可以直接使用类 A 的所有方法。
# b.foo()
# b.bar()

使用super( )从父类得到帮助

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Person():
def __init__(self, name):
print("Initializing Person")
self.name = name

class EmailPerson(Person):
def __init__(self, name, email):
print("Initializing EmailPerson")
super().__init__(name)
self.email = email

zhangsan = EmailPerson("zhangsan", "123@aaa.com")
print(zhangsan.email)
# Initializing EmailPerson
# Initializing Person
# 123@aaa.com
  • class EmailPerson(Person): 定义了 EmailPerson 类,它继承自 Person 类,这意味着 EmailPerson 类拥有 Person 类的属性和方法。
  • __init__ 是子类的构造方法,接收 nameemail 两个参数。首先打印子类的初始化信息,然后使用 super().__init__(name) 调用父类的 __init__ 方法,将 name 参数传递给父类构造函数,完成父类部分的初始化工作,最后将 email 赋值给子类特有的实例属性 self.email 。这里 super() 函数的作用是获取父类的定义,从而调用父类的方法,避免在子类中重复编写父类已有的初始化逻辑。

方法重写

是指子类可以对从父类中继承过来的方法进行重新定义,从而使得子类对象可以表现出与父类对象不同的行为。

例:创建一个名为”Person”的父类,具有属性”name”和”age”。添加一个名为”get_info”的方法,打印人的姓名和年龄。

然后,创建一个名为”Student”的子类,继承自Person类,并添加一个额外的属性”grade”。在Student类中重写”get_info”方法,也打印出成绩。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Person:
def __init__(self, name, age):
self.name = name
self.age = age

def get_info(self):
print("姓名:", self.name)
print("年龄:", self.age)

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

def get_info(self):
super().get_info()
print("成绩:", self.grade)

多态

多态,是指在执行同样代码的情况下,系统会根据对象实际所属的类去调用相应类中的方法。

在 Python 中编写一个函数,传递实参前其参数的类型并不确定,在函数中使用形参进行操作时只要传入的对象能够支持该操作程序就能正常执行 。

例:鸭子类型

在程序设计中,鸭子类型(duck typing)是动态类型的一种风格。在这种风格中,一个对象有效的语义,不是由继承自特定的类或实现特定的接口,而是由”当前方法和属性的集合”决定。支持“鸭子类型”的语言的解释器/编译器会在解释或编译时推断对象的类型。在鸭子类型中,关注的不是对象所属的类,而是一个对象能够如何使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Person: # 定义 Person 类
def CaptureImage(self): # 定义 CaptureImage 方法
print('Person 类中的 CaptureImage 方法被调用!')

class Camera: # 定义 Camera 类
def CaptureImage(self): # 定义 CaptureImage 方法
print('Camera 类中的 CaptureImage 方法被调用!')

def CaptureImageTest(arg): # 定义 CaptureImageTest 方法
arg.CaptureImage() # 通过 arg 调用 CaptureImage

p = Person() # 定义 Person 类对象 p
c = Camera() # 定义 Camera 类对象 c
CaptureImageTest (p)
CaptureImageTest (c)

通过统一的方法接口,可以方便地调用不同类中的相同方法。

实例

创建两个不相关的类,分别命名为 ”Book” 和 “DVD”,它们都具有一个方法 play,但 Book 的 play 方法返回 “Reading the book” 而 DVD 的 play 方法返回 “Playing the DVD”。创建一个函数 start_playing,接受一个对象并调用其 play 方法,展示鸭子类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Book:
def play(self):
return "Reading the book"

class DVD:
def play(self):
return "Playing the DVD"

def start_playing(obj):
return obj.play()

# 创建Book和DVD的实例
book = Book()
dvd = DVD()

# 调用start_playing函数
print(start_playing(book))
print(start_playing(dvd))

类中的封装

在面向对象编程里,类的封装是一种重要特性。它一方面把属性和方法集合在一起,形成一个逻辑单元,让代码结构更清晰;另一方面,它还能把类里一些不希望被外部随意访问、只在类内部使用的属性和方法隐藏起来,提高代码的安全性和稳定性。

  1. 集合了对应的属性和方法
  2. 将类中私有的、只在内部使用的属性和方法进行隐藏

第一是约定任何以单下划线_开头的名字都应该是内部实现

1
2
3
4
5
6
7
8
9
10
11
12
class A():
def __init__(self):
self._internal = 0
self.public = 1

def public_method(self):
pass

def _internal_method(self):
pass
a = A()
a._internal_method()
  • 属性和方法定义:在 A 类中,_internal 是属性,_internal_method 是方法,它们都以单下划线开头,表明是内部使用的。而 public 属性和 public_method 方法没有下划线,是供外部正常访问的。
  • 访问情况:虽然 Python 不会从语法层面阻止外部访问这些带单下划线开头的属性和方法,像 a._internal_method() 这样的调用是可以执行的,但这不符合规范。这只是一种约定,提醒开发者这些是类的内部实现细节,最好不要在外部调用,否则可能破坏类的设计逻辑,让代码变得脆弱、难以维护。而且这种约定不仅适用于类,模块名(如 _private_module )和模块级别函数(如 sys._getframe() )也适用,使用时要谨慎。

第二,使用双下划线__开始

当属性或方法以双下划线 __ 开头时,Python 会对其名称进行特殊处理。

会导致访问名称变成其他形式(名称改写/name mangling)。

1
2
3
4
5
6
7
8
9
10
11
12
class B():
def __init__(self):
self.__private = 0

def __private_method(self):
print(self.__private)

def public_method(self):
self.__private_method()
pass
b = B()
b.__dict__ # Python会把属性名存入实例的__dict__属性中
  • 名称改写:在 B 类中,__private 属性和 __private_method 方法,Python 会将它们重命名为 _B__private_B__private_method 。查看实例 b__dict__ 属性(它存储了实例的属性信息),就能发现这种改写后的名称。
  • 目的:这种机制主要是为了在继承时防止子类意外覆盖父类的私有属性和方法。比如:
1
2
3
4
5
6
7
8
9
10
11
12
class C(B):
def __init__(self):
super().__init__()
self.__private = 1 # does not override B.__private

# does not override B.__private_method()

def __private_method(self):
pass
c = C()
c.__dict__
# c._B__private_method()

在子类 C 中,__private__private_method 同样会被改写,变成 _C__private_C__private_method ,和父类 B 中对应的名称不一样,所以不会覆盖父类的私有属性和方法。

Python 解释器不会对使用单个下划线的属性名做特殊处理,不过这是很多 Python 程序员严格遵守的约定,他们不会在类外部访问这种属性。

当你定义的一个变量和某个保留关键字冲突,可以使用单下划线作为后缀:

总结:

  1. ___都可以定义私有属性
  2. 使用__来定义的属性,
    调用时需要将命名方式调整为_ClassName__methodName
  3. __方法适用于需要在子类中进行隐藏的情况

创建可管理属性

可管理属性是Python面向对象编程中一个强大的特性,它允许你控制对类属性的访问、修改和删除操作。

在面向对象编程中,我们有时需要对属性的访问进行控制:

  • 在设置属性时进行类型检查或验证
  • 在获取属性时进行计算或格式化
  • 防止某些属性被删除
  • 创建只读属性

在对实例属性的获取和设定上,有时候我们希望增加一些额外的处理过程(比如类型检查或者验证)。这种机制可以用于对”私有”属性进行访问和修改。

使用property

要自定义对属性的访问,一种简单的方式是将其定义为 property

property把类中定义的函数当做一种属性来使用。

property()函数可以创建一个属性,它允许你定义getter、setter和deleter方法。

1
property(fget=None, fset=None, fdel=None, doc=None)
  • fget: 获取属性值的函数
  • fset: 设置属性值的函数
  • fdel: 删除属性的函数
  • doc: 属性的文档字符串

下面的示例代码定义了一个property,增加了对属性的验证。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Person():
def __init__(self, first_name):
self.first_name = first_name # 这里会调用setter方法

def get_first_name(self):
print("get_first_name is called.")
return self._first_name

def set_first_name(self, value):
print("set_first_name is called.")
if not isinstance(value, str):
raise TypeError('Expected a string')
self._first_name = value

def del_first_name(self):
print("del_first_name is called.")
raise AttributeError("Can't delete attribute")

first_name = property(get_first_name, set_first_name, del_first_name, "first_name property")
# 每次对 first_name 这个属性进行访问(获取值、设置值或删除属性)时,都会触发对应的方法。
# 实际的数据存储在 _first_name 属性中。

a = Person(21)
# print(a._first_name)
# a.__dict__

上例中,使用property()定义了一个属性first_name

  • property()的第一个参数是getter方法,第二个参数是setter方法,第三个参数是deleter方法
  • property 的一个关键特征是它看上去跟普通的属性(attribute)没什么两样,但是访问它的时候会自动触发gettersetterdeleter方法。

在实现一个 property 的时候,底层数据(如果有的话)仍然需要存储在某个地方

  • gettersetter方法中,你会看到对_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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Person():
def __init__(self, first_name):
self.first_name = first_name

# 只有在`first_name`属性被创建后
# 后面的两个装饰器`@first_name.setter`和`@first_name.deleter`才能被定义
@property
def first_name(self):
return self._first_name

@first_name.setter
def first_name(self, value):
print("setter")
if not isinstance(value, str):
raise TypeError('Expected a string')
self._first_name = value

@first_name.deleter
def first_name(self):
raise AttributeError("Can't delete attribute")


a = Person("Mark")
print(a.first_name)
# a.first_name = 42
  1. 装饰器顺序
    • 必须先定义@property方法(getter)
    • 然后才能定义@xxx.setter@xxx.deleter
  2. 方法命名
    • 所有相关方法必须使用相同的名称
    • 这是装饰器语法要求的
  3. 文档字符串
    • 可以在@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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import math

class Circle():
def __init__(self, radius):
self.radius = radius

@property # 实现只读,只能访问(get),无法修改(set)
def area(self):
return math.pi * self.radius ** 2

@property
def perimeter(self):
return 2 * math.pi * self.radius



c = Circle(4.0) # 创建一个 Circle 实例
print(c.radius) # 打印半径
print(c.area) # 注意这里没有()
print(c.perimeter) # 注意这里没有()

特点

  1. 只读属性:如果没有定义setter,属性就是只读的
  2. 统一访问接口:无论是存储属性还是计算属性,访问方式都一样
  3. 延迟计算:只在访问时计算,节省内存

在这里,我们通过使用property,将所有的访问接口形式统一起来,对半径、周长和面积的访问都能够简单地以属性的形式进行访问,而不必将属性访问和方法调用混在一起使用了。

如果你没有指定某一特性的*setter*属性(@area.setter),那么将无法在类的外部对它的值进行设置。这对于只读的特性非常有用:

总结:

  1. 类的定义中使用@property可以实现属性的获取(“getter”)
  2. 类的定义中使用@setter可以实现属性的设置(“setter”)

可管理属性是Python面向对象编程中非常强大的特性,它允许你:

  1. 控制属性的访问、设置和删除行为
  2. 添加验证逻辑和类型检查
  3. 创建动态计算的属性
  4. 实现只读属性
  5. 保持统一的访问接口

无论是使用property()函数还是装饰器语法,都能有效地增强类的封装性和安全性。选择哪种方式主要取决于个人偏好和代码的可读性,装饰器语法通常更为简洁明了。