geekdoc-python-zh/docs/realpython/python3-object-oriented-pro...

28 KiB
Raw Permalink Blame History

Python 3 中的面向对象编程(OOP)

原文:https://realpython.com/python3-object-oriented-programming/

*立即观看**本教程有真实 Python 团队创建的相关视频课程。和文字教程一起看,加深理解:Python 中的面向对象编程(OOP)介绍

面向对象编程 (OOP)是一种通过将相关属性和行为捆绑到单独的对象中来构建程序的方法。在本教程中,您将学习 Python 中面向对象编程的基础知识。

从概念上讲,对象就像系统的组件。把一个程序想象成某种工厂装配线。在装配线的每一步,一个系统组件处理一些材料,最终将原材料转化为成品。

对象包含数据(如装配线上每个步骤的原材料或预处理材料)和行为(如每个装配线组件执行的操作)。

在本教程中,您将学习如何:

  • 创建一个,这就像是创建一个对象的蓝图
  • 使用类来创建新对象
  • 具有类继承的模型系统

**注:**本教程改编自 Python 基础知识:Python 实用入门 3 中“面向对象编程(OOP)”一章。

这本书使用 Python 内置的 IDLE 编辑器来创建和编辑 Python 文件,并与 Python shell 进行交互,因此在整个教程中你会偶尔看到对 IDLE 的引用。但是,从您选择的编辑器和环境中运行示例代码应该没有问题。

免费奖励: 点击此处获取免费的 Python OOP 备忘单,它会为你指出最好的教程、视频和书籍,让你了解更多关于 Python 面向对象编程的知识。

Python 中的面向对象编程是什么?

面向对象编程是一种编程范式,它提供了一种结构化程序的方法,从而将属性和行为捆绑到单独的对象中。

例如,一个对象可以代表一个具有属性如姓名、年龄和地址以及行为如走路、说话、呼吸和跑步的人。或者它可以代表一封电子邮件,具有收件人列表、主题和正文等属性,以及添加附件和发送等行为。

换句话说面向对象编程是一种对具体的、真实世界的事物建模的方法如汽车以及事物之间的关系如公司和雇员、学生和教师等等。OOP 将现实世界中的实体建模为软件对象,这些对象有一些与之相关的数据,并且可以执行某些功能。

另一个常见的编程范例是过程化编程,它像菜谱一样构建程序,以函数和代码块的形式提供一组步骤,这些步骤按顺序流动以完成任务。

关键的一点是,在 Python 中,对象是面向对象编程的核心,不仅像在过程编程中一样表示数据,而且在程序的整体结构中也是如此。

Remove ads

在 Python 中定义一个类

原始的数据结构——像数字、字符串和列表——被设计用来表示简单的信息,比如一个苹果的价格、一首诗的名字或者你最喜欢的颜色。如果你想表现更复杂的东西呢?

例如,假设您想要跟踪某个组织中的员工。您需要存储每个员工的一些基本信息,比如他们的姓名、年龄、职位以及他们开始工作的年份。

一种方法是将每个雇员表示为一个列表:

kirk = ["James Kirk", 34, "Captain", 2265]
spock = ["Spock", 35, "Science Officer", 2254]
mccoy = ["Leonard McCoy", "Chief Medical Officer", 2266]

这种方法有许多问题。

首先,它会使较大的代码文件更难管理。如果在声明kirk列表的地方引用几行之外的kirk[0],你会记得索引为0的元素是雇员的名字吗?

第二,如果不是每个雇员在列表中有相同数量的元素,它可能会引入错误。在上面的mccoy列表中,缺少年龄,所以mccoy[1]将返回"Chief Medical Officer"而不是麦考伊博士的年龄。

让这类代码更易于管理和维护的一个好方法是使用

类与实例

类用于创建用户定义的数据结构。类定义了名为方法的函数,这些方法标识了从类创建的对象可以对其数据执行的行为和动作。

在本教程中,您将创建一个Dog类来存储一些关于单只狗的特征和行为的信息。

一个类是应该如何定义的蓝图。它实际上不包含任何数据。Dog类指定名字和年龄是定义狗的必要条件,但它不包含任何特定狗的名字或年龄。

类是蓝图,而实例是从类构建的包含真实数据的对象。Dog类的实例不再是蓝图。这是一只真正的狗,它有一个名字,像四岁的迈尔斯。

换句话说,一个类就像一个表格或问卷。实例就像一个已经填写了信息的表单。就像许多人可以用他们自己的独特信息填写同一个表单一样,许多实例可以从单个类中创建。

如何定义一个类

所有的类定义都以关键字class开始,后面是类名和冒号。缩进到类定义下面的任何代码都被认为是类体的一部分。

下面是一个Dog类的例子:

class Dog:
    pass

Dog类的主体由一条语句组成:关键字passpass通常被用作占位符,表示代码最终的去向。它允许您在 Python 不抛出错误的情况下运行这段代码。

注意: Python 类名按照惯例是用 CapitalizedWords 符号写的。例如,一个特定品种的狗(如杰克罗素梗)的类可以写成JackRussellTerrier

Dog类现在不是很有趣,所以让我们通过定义所有Dog对象应该具有的一些属性来稍微修饰一下它。有许多属性可供我们选择,包括名字、年龄、毛色和品种。为了简单起见,我们只使用姓名和年龄。

所有Dog对象必须具有的属性在一个叫做.__init__()的方法中定义。每次创建一个新的Dog对象,.__init__()通过分配对象的属性值来设置对象的初始状态。也就是说,.__init__()初始化该类的每个新实例。

你可以给.__init__()任意数量的参数,但是第一个参数总是一个叫做self变量。当一个新的类实例被创建时,该实例被自动传递给.__init__()中的self参数,这样就可以在对象上定义新的属性

让我们用一个创建.name.age属性的.__init__()方法来更新Dog类:

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

注意,.__init__()方法的签名缩进了四个空格。该方法的主体缩进八个空格。这个缩进非常重要。它告诉 Python,.__init__()方法属于Dog类。

.__init__()的主体中,有两条语句使用了self变量:

  1. self.name = name 创建一个名为name的属性,并赋予它name参数的值。
  2. self.age = age 创建一个名为age的属性,并赋予它age参数的值。

.__init__()中创建的属性称为实例属性。实例属性的值特定于类的特定实例。所有的Dog对象都有名称和年龄,但是nameage属性的值会根据Dog实例的不同而不同。

另一方面,类属性是对所有类实例具有相同值的属性。您可以通过在.__init__()之外给变量赋值来定义一个类属性。

例如,下面的Dog类有一个名为species的类属性,其值为"Canis familiaris":

class Dog:
    # Class attribute
    species = "Canis familiaris"

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

类属性直接定义在类名的第一行下面,缩进四个空格。它们必须总是被赋予一个初始值。当创建类的实例时,会自动创建类属性并将其赋给初始值。

使用类属性为每个类实例定义应该具有相同值的属性。对于因实例而异的属性,请使用实例属性。

现在我们有了一个Dog类,让我们创建一些狗吧!

Remove ads

用 Python 实例化一个对象

打开 IDLE 的交互窗口,键入以下内容:

>>> class Dog:
...     pass

这创建了一个没有属性或方法的新的Dog类。

从一个类创建一个新对象叫做实例化一个对象。您可以通过键入类名,后跟左括号和右括号来实例化一个新的Dog对象:

>>> Dog()
<__main__.Dog object at 0x106702d30>

您现在在0x106702d30有了一个新的Dog对象。这个看起来很有趣的字母和数字串是一个内存地址,它指示了Dog对象在你的计算机内存中的存储位置。请注意,您在屏幕上看到的地址会有所不同。

现在实例化第二个Dog对象:

>>> Dog()
<__main__.Dog object at 0x0004ccc90>

新的Dog实例位于不同的内存地址。这是因为它是一个全新的实例,与您实例化的第一个Dog对象完全不同。

要从另一个角度看这个问题,请键入以下内容:

>>> a = Dog()
>>> b = Dog()
>>> a == b
False

在这段代码中,您创建了两个新的Dog对象,并将它们分配给变量ab。当您使用==运算符比较ab时,结果是False。尽管ab都是Dog类的实例,但它们在内存中代表两个不同的对象。

类别和实例属性

现在创建一个新的Dog类,它有一个名为.species的类属性和两个名为.name.age的实例属性:

>>> class Dog:
...     species = "Canis familiaris"
...     def __init__(self, name, age):
...         self.name = name
...         self.age = age

要实例化这个Dog类的对象,您需要为nameage提供值。如果没有Python 就会抛出一个TypeError:

>>> Dog()
Traceback (most recent call last):
  File "<pyshell#6>", line 1, in <module>
    Dog()
TypeError: __init__() missing 2 required positional arguments: 'name' and 'age'

要将参数传递给nameage参数,请将值放入类名后的括号中:

>>> buddy = Dog("Buddy", 9)
>>> miles = Dog("Miles", 4)

这创建了两个新的Dog实例——一个是九岁的狗 Buddy另一个是四岁的狗 Miles。

Dog类的.__init__()方法有三个参数,那么为什么在示例中只有两个参数传递给它呢?

当实例化一个Dog对象时Python 会创建一个新的实例,并将其传递给.__init__()的第一个参数。这实质上移除了self参数,因此您只需要担心nameage参数。

在您创建了Dog实例之后,您可以使用点符号来访问它们的实例属性:

>>> buddy.name
'Buddy'
>>> buddy.age
9

>>> miles.name
'Miles'
>>> miles.age
4

您可以用同样的方式访问类属性:

>>> buddy.species
'Canis familiaris'

使用类来组织数据的一个最大的优点是实例保证具有您期望的属性。所有的Dog实例都有.species.name.age属性,所以您可以放心地使用这些属性,因为它们总是会返回值。

虽然属性被保证存在,但是它们的值可以被动态地改变:

>>> buddy.age = 10
>>> buddy.age
10

>>> miles.species = "Felis silvestris"
>>> miles.species
'Felis silvestris'

在这个例子中,您将buddy对象的.age属性更改为10。然后你将miles对象的.species属性改为"Felis silvestris",这是一种猫。这使得迈尔斯成为一只非常奇怪的狗,但它是一条有效的蟒蛇!

这里的关键是定制对象在默认情况下是可变的。如果一个对象可以动态改变,那么它就是可变的。例如,列表和字典是可变的,但是字符串和元组是不可变的

Remove ads

实例方法

实例方法是定义在类内部的函数,只能从该类的实例中调用。就像.__init__()一样,实例方法的第一个参数总是self

在空闲状态下打开一个新的编辑器窗口,键入下面的Dog类:

class Dog:
    species = "Canis familiaris"

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

    # Instance method
    def description(self):
        return f"{self.name} is {self.age} years old"

    # Another instance method
    def speak(self, sound):
        return f"{self.name} says {sound}"

这个Dog类有两个实例方法:

  1. .description() 返回显示狗的名字和年龄的字符串。
  2. .speak() 有一个名为sound的参数,返回一个包含狗的名字和狗发出的声音的字符串。

将修改后的Dog类保存到名为dog.py的文件中,按 F5 运行程序。然后打开交互式窗口并键入以下内容,查看实例方法的运行情况:

>>> miles = Dog("Miles", 4)

>>> miles.description()
'Miles is 4 years old'

>>> miles.speak("Woof Woof")
'Miles says Woof Woof'

>>> miles.speak("Bow Wow")
'Miles says Bow Wow'

在上面的Dog类中,.description()返回一个包含关于Dog实例miles信息的字符串。在编写自己的类时,最好有一个方法返回一个字符串,该字符串包含关于类实例的有用信息。然而,.description()并不是最的做法。

当您创建一个list对象时,您可以使用print()来显示一个类似于列表的字符串:

>>> names = ["Fletcher", "David", "Dan"]
>>> print(names)
['Fletcher', 'David', 'Dan']

让我们看看当你print()这个miles对象时会发生什么:

>>> print(miles)
<__main__.Dog object at 0x00aeff70>

当你print(miles)时,你会得到一个看起来很神秘的消息,告诉你miles是一个位于内存地址0x00aeff70Dog对象。这条消息没什么帮助。您可以通过定义一个名为.__str__()的特殊实例方法来改变打印的内容。

在编辑器窗口中,将Dog类的.description()方法的名称改为.__str__():

class Dog:
    # Leave other parts of Dog class as-is

    # Replace .description() with __str__()
    def __str__(self):
        return f"{self.name} is {self.age} years old"

保存文件,按 F5 。现在,当你print(miles)时,你会得到一个更友好的输出:

>>> miles = Dog("Miles", 4)
>>> print(miles)
'Miles is 4 years old'

.__init__().__str__()这样的方法被称为 dunder 方法,因为它们以双下划线开始和结束。在 Python 中,有许多 dunder 方法可以用来定制类。虽然对于一本初级 Python 书籍来说,这是一个过于高级的主题,但是理解 dunder 方法是掌握 Python 中面向对象编程的重要部分。

在下一节中,您将看到如何更进一步,从其他类创建类。

Remove ads

检查你的理解能力

展开下面的方框,检查您的理解程度:

创建一个具有两个实例属性的Car类:

  1. .color,它将汽车颜色的名称存储为一个字符串
  2. .mileage,以整数形式存储汽车的里程数

然后实例化两个Car对象——一辆行驶 20000 英里的蓝色汽车和一辆行驶 30000 英里的红色汽车——并打印出它们的颜色和里程。您的输出应该如下所示:

The blue car has 20,000 miles.
The red car has 30,000 miles.

您可以展开下面的方框查看解决方案:

首先,创建一个具有.color.mileage实例属性的Car类:

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

.__init__()colormileage参数被分配给self.colorself.mileage,这就创建了两个实例属性。

现在您可以创建两个Car实例:

blue_car = Car(color="blue", mileage=20_000)
red_car = Car(color="red", mileage=30_000)

通过将值"blue"传递给color参数并将20_000传递给mileage参数来创建blue_car实例。类似地,red_car用值"red"30_000创建。

要打印每个Car对象的颜色和里程,您可以在包含两个对象的tuple上循环:

for car in (blue_car, red_car):
    print(f"The {car.color} car has {car.mileage:,} miles")

上述for循环中的 f 字符串.color.mileage属性插入到字符串中,并使用:, 格式说明符打印以千为单位分组并以逗号分隔的里程。

最终输出如下所示:

The blue car has 20,000 miles.
The red car has 30,000 miles.

当你准备好了,你可以进入下一部分。

从 Python 中的其他类继承

继承是一个类继承另一个类的属性和方法的过程。新形成的类称为子类,子类派生的类称为父类

**注:**本教程改编自 Python 基础知识:Python 实用入门 3 中“面向对象编程(OOP)”一章。如果你喜欢你正在阅读的东西,那么一定要看看这本书的其余部分。

子类可以重写或扩展父类的属性和方法。换句话说,子类继承父类的所有属性和方法,但也可以指定自己独有的属性和方法。

虽然这个类比并不完美,但是你可以把对象继承想象成类似于基因继承。

你可能从你母亲那里遗传了你的发色。这是你与生俱来的属性。假设你决定把头发染成紫色。假设你的母亲没有紫色的头发,你只是覆盖了你从你母亲那里继承的头发颜色属性。

从某种意义上说,你也从父母那里继承了你的语言。如果你的父母说英语,那么你也会说英语。现在想象你决定学习第二语言,比如德语。在这种情况下,您已经扩展了您的属性,因为您添加了一个您的父母没有的属性。

狗狗公园的例子

假设你在一个狗狗公园。公园里有很多不同品种的狗,都在从事各种狗的行为。

假设现在您想用 Python 类来建模 dog park。您在上一节中编写的Dog类可以通过名字和年龄来区分狗,但不能通过品种来区分。

您可以通过添加一个.breed属性来修改编辑器窗口中的Dog类:

class Dog:
    species = "Canis familiaris"

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

这里省略了前面定义的实例方法,因为它们对于本次讨论并不重要。

F5 保存文件。现在,您可以通过在交互式窗口中实例化一群不同的狗来模拟狗公园:

>>> miles = Dog("Miles", 4, "Jack Russell Terrier")
>>> buddy = Dog("Buddy", 9, "Dachshund")
>>> jack = Dog("Jack", 3, "Bulldog")
>>> jim = Dog("Jim", 5, "Bulldog")

每种狗的行为都略有不同。例如,牛头犬低沉的叫声听起来像汪汪叫,但是腊肠犬的叫声更高,听起来更像 T2 吠声。

仅使用Dog类,每次在Dog实例上调用.speak()sound参数时,必须提供一个字符串:

>>> buddy.speak("Yap")
'Buddy says Yap'

>>> jim.speak("Woof")
'Jim says Woof'

>>> jack.speak("Woof")
'Jack says Woof'

向每个对.speak()的调用传递一个字符串是重复且不方便的。此外,代表每个Dog实例发出的声音的字符串应该由它的.breed属性决定,但是这里您必须在每次调用它时手动将正确的字符串传递给.speak()

您可以通过为每种狗创建一个子类来简化使用Dog类的体验。这允许您扩展每个子类继承的功能,包括为.speak()指定一个默认参数。

Remove ads

父类 vs 子类

让我们为上面提到的三个品种分别创建一个子类:杰克罗素梗、腊肠犬和牛头犬。

作为参考,下面是Dog类的完整定义:

class Dog:
    species = "Canis familiaris"

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

    def __str__(self):
        return f"{self.name} is {self.age} years old"

    def speak(self, sound):
        return f"{self.name} says {sound}"

记住,要创建一个子类,你要创建一个有自己名字的新类,然后把父类的名字放在括号里。将以下内容添加到dog.py文件中,以创建Dog类的三个新子类:

class JackRussellTerrier(Dog):
    pass

class Dachshund(Dog):
    pass

class Bulldog(Dog):
    pass

F5 保存并运行文件。定义子类后,现在可以在交互窗口中实例化一些特定品种的狗:

>>> miles = JackRussellTerrier("Miles", 4)
>>> buddy = Dachshund("Buddy", 9)
>>> jack = Bulldog("Jack", 3)
>>> jim = Bulldog("Jim", 5)

子类的实例继承父类的所有属性和方法:

>>> miles.species
'Canis familiaris'

>>> buddy.name
'Buddy'

>>> print(jack)
Jack is 3 years old

>>> jim.speak("Woof")
'Jim says Woof'

要确定给定对象属于哪个类,可以使用内置的type():

>>> type(miles)
<class '__main__.JackRussellTerrier'>

如果你想确定miles是否也是Dog类的一个实例呢?你可以通过内置的isinstance()来实现:

>>> isinstance(miles, Dog)
True

注意isinstance()有两个参数,一个对象和一个类。在上面的例子中,isinstance()检查miles是否是Dog类的实例,并返回True

milesbuddyjackjim对象都是Dog实例,但是miles不是Bulldog实例,jack也不是Dachshund实例:

>>> isinstance(miles, Bulldog)
False

>>> isinstance(jack, Dachshund)
False

更一般地说,从子类创建的所有对象都是父类的实例,尽管它们可能不是其他子类的实例。

现在你已经为一些不同品种的狗创建了子类,让我们给每个品种赋予它自己的声音。

扩展父类的功能

由于不同品种的狗的叫声略有不同,所以您希望为它们各自的.speak()方法的sound参数提供一个默认值。为此,你需要在每个品种的类定义中覆盖.speak()

要重写在父类上定义的方法,需要在子类上定义一个同名的方法。下面是JackRussellTerrier类的情况:

class JackRussellTerrier(Dog):
    def speak(self, sound="Arf"):
        return f"{self.name} says {sound}"

现在.speak()被定义在JackRussellTerrier类上,sound的默认参数被设置为"Arf"

用新的JackRussellTerrier类更新dog.py并按 F5 保存并运行文件。现在,您可以在一个JackRussellTerrier实例上调用.speak(),而无需向sound传递参数:

>>> miles = JackRussellTerrier("Miles", 4)
>>> miles.speak()
'Miles says Arf'

有时狗会发出不同的叫声,所以如果迈尔斯生气了,你仍然可以用不同的声音呼叫.speak():

>>> miles.speak("Grrr")
'Miles says Grrr'

关于类继承要记住的一点是,对父类的更改会自动传播到子类。只要被更改的属性或方法没有在子类中被重写,就会发生这种情况。

例如,在编辑器窗口中,更改由Dog类中的.speak()返回的字符串:

class Dog:
    # Leave other attributes and methods as they are

    # Change the string returned by .speak()
    def speak(self, sound):
        return f"{self.name} barks: {sound}"

保存文件并按 F5 。现在,当您创建一个名为jim的新的Bulldog实例时,jim.speak()返回新的字符串:

>>> jim = Bulldog("Jim", 5)
>>> jim.speak("Woof")
'Jim barks: Woof'

然而,在一个JackRussellTerrier实例上调用.speak()不会显示新的输出样式:

>>> miles = JackRussellTerrier("Miles", 4)
>>> miles.speak()
'Miles says Arf'

有时完全重写父类的方法是有意义的。但是在这个实例中,我们不希望JackRussellTerrier类丢失任何可能对Dog.speak()的输出字符串格式进行的更改。

为此,您仍然需要在子类JackRussellTerrier上定义一个.speak()方法。但是不需要显式定义输出字符串,您需要使用传递给JackRussellTerrier.speak()的相同参数调用子类.speak()的内的Dog类的.speak()

您可以使用 super() 从子类的方法内部访问父类:

class JackRussellTerrier(Dog):
    def speak(self, sound="Arf"):
        return super().speak(sound)

当您在JackRussellTerrier中调用super().speak(sound)Python 会在父类Dog中搜索一个.speak()方法,并用变量sound调用它。

用新的JackRussellTerrier类更新dog.py。保存文件并按下 F5 ,这样您就可以在交互窗口中测试它了:

>>> miles = JackRussellTerrier("Miles", 4)
>>> miles.speak()
'Miles barks: Arf'

现在,当您调用miles.speak()时,您将看到输出反映了Dog类中的新格式。

**注意:**在上面的例子中,类的层次结构非常简单。JackRussellTerrier类只有一个父类Dog。在现实世界的例子中,类的层次结构会变得非常复杂。

不仅仅是在父类中搜索方法或属性。它遍历整个类层次结构,寻找匹配的方法或属性。如果不小心,super()可能会有惊人的结果。

Remove ads

检查你的理解能力

展开下面的方框,检查您的理解程度:

创建一个继承自Dog类的GoldenRetriever类。给GoldenRetriever.speak()sound参数一个默认值"Bark"。为您的父类Dog使用以下代码:

class Dog:
    species = "Canis familiaris"

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

    def __str__(self):
        return f"{self.name} is {self.age} years old"

    def speak(self, sound):
        return f"{self.name} says {sound}"

您可以展开下面的方框查看解决方案:

创建一个名为GoldenRetriever的类,它继承了Dog类并覆盖了.speak()方法:

class GoldenRetriever(Dog):
    def speak(self, sound="Bark"):
        return super().speak(sound)

GoldenRetriever.speak()中的sound参数被赋予默认值"Bark"。然后用super()调用父类的.speak()方法,传递给sound的参数与GoldenRetriever类的.speak()方法相同。

结论

在本教程中,您学习了 Python 中的面向对象编程(OOP)。大多数现代编程语言,如 JavaC#C++ ,都遵循 OOP 原则,因此无论你的编程生涯走向何方,你在这里学到的知识都将适用。

在本教程中,您学习了如何:

  • 定义一个,它是一种对象的蓝图
  • 从一个类中实例化一个对象
  • 使用属性方法来定义对象的属性行为
  • 使用继承父类创建子类
  • 使用 super() 引用父类上的方法
  • 使用 isinstance() 检查一个对象是否从另一个类继承

如果你喜欢在这个例子中从 Python 基础知识:Python 3 实用介绍中学到的东西,那么一定要看看这本书的其余部分

立即观看本教程有真实 Python 团队创建的相关视频课程。和文字教程一起看,加深理解:Python 中的面向对象编程(OOP)介绍*****