详解 Python 中的面向对象编程(1)

Python Python 226 人阅读 | 0 人回复 | 2024-08-05

引言

面向对象编程(OOP)是一种编程范式,它通过将属性和行为整合到对象中来构建程序。本教程将带你了解Python语言中面向对象编程的基本概念。

想象一下,对象就像是系统中的各个部件。可以把程序比作一条工厂流水线。在流水线的每一个环节,部件都会对材料进行处理,最终将原材料变成成品。

对象内部存储着数据,类似于流水线上各个环节所需的原材料或经过初步处理的材料。同时,对象还具有行为,即流水线上每个部件执行的具体操作。

通过本教程,你将学会:

  • 如何定义一个类,这可以看作是创建对象的模板。
  • 如何利用类来生成新的对象。
  • 如何利用类继承来构建和模拟复杂的系统。

面向对象编程是什么?

面向对象编程是一种编程模式,它允许我们将程序中的属性和行为整合到独立的“对象”中。

举个例子,一个对象可以模拟一个人,拥有姓名、年龄和住址等属性,以及走路、说话、呼吸和跑步等行为。或者,它也可以模拟一封电子邮件,拥有收件人列表、主题和内容等属性,以及添加附件和发送邮件等行为。

换言之,面向对象编程是将现实世界中的具体事物,比如汽车,以及事物间的关系,如公司与员工、学生与教师,抽象成软件对象的一种方法。这些对象不仅关联着数据,还能执行特定的操作。

提示:您还可以观看“Python基础:面向对象编程”视频教程,以加深对本节教程所学技能的理解和应用。

核心要点在于,在Python的面向对象编程中,对象扮演着核心角色。与其他编程模式不同,在OOP中,对象不仅代表数据,还决定了程序的整体架构。

如何创建一个类?

在Python语言中,我们通过关键字 class来创建一个类,紧接着是类名和冒号。之后,我们利用 __init__()方法来指定这个类的所有实例应该包含哪些属性。

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

这些内容究竟指的是什么?你为何一开始要用到类呢?让我们换个角度,考虑使用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]时,得到的将是"首席医疗官"这个职位名称,而不是McCoy博士的年龄。

一个更好的解决方案是使用类来组织代码。这样做不仅可以使代码更加易于管理,还可以提高其可维护性。

  • 类与实例的区别

类让你能够定义自己的数据结构。类中包含了方法,这些方法定义了对象能够使用其数据执行哪些行为和动作。

在本教程中,我们将创建一个Dog类,它记录了一只狗可能具有的特征和行为。

类是一个定义事物的模板。它本身并不包含数据。Dog类规定了定义一只狗需要一个名字和一个年龄,但它本身并不包含任何特定狗的名字或年龄。

类作为模板,而实例则是根据这个模板创建的,包含了实际数据的对象。Dog类的一个实例不再是模板,而是一只具体的狗,比如名叫Miles,四岁的狗。

换个说法,类就像是一个表格或问卷。实例则像是你填写了个人信息的表格。正如许多人可以根据自己的信息填写相同的表格,你也可以根据一个类创建多个实例。

  • 类的定义

类的定义以 class关键字开始,接着是类名和冒号。Python会将类定义下的所有缩进代码视为类体的一部分。

这里有一个Dog类的例子:

# dog.py

class Dog:
    pass

Dog类目前只包含一个 pass语句,这是Python程序员常用的一个占位符,用来标记将来要添加代码的位置。使用 pass可以避免在开发过程中出现错误。

为了让Dog类更有趣,我们将为其定义一些基本属性,比如所有Dog对象都应具备的名字和年龄。这些属性将在 __init__()方法中定义,这个方法在每次创建新的Dog对象时被调用,用于初始化对象的状态。

__init__()方法可以接受任意数量的参数,但第一个参数总是名为 self的变量,它代表当前的实例对象。Python会在创建类的新实例时自动将这个新对象传递给 self参数,使得我们可以在对象上设置属性。

接下来,我们将为Dog类添加一个 __init__()方法,用以创建 .name.age这两个属性:

# dog.py

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

请确保 __init__()方法的声明部分缩进四个空格,方法体部分缩进八个空格。这种缩进对于Python来说是至关重要的,因为它表明 __init__()方法是Dog类的一部分。

__init__()方法的内部,有两个语句使用了 self变量:

self.name = name会创建一个名为name的属性,并将传入的name参数值赋给它。 self.age = age会创建一个名为age的属性,并将传入的age参数值赋给它。 在 __init__()中定义的属性被称为实例属性,它们为每个类实例所独有。所有Dog类的实例都会有name和age这两个属性,但是每个实例的name和age属性值都是不同的。

相对地,类属性则是所有类实例共享的属性。你可以通过在 __init__()方法之外直接给变量赋值来定义一个类属性。

比如,下面的Dog类定义了一个名为species的类属性,其固定值为"Canis familiaris":

# dog.py

class Dog:
    species = "Canis familiaris"

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

类属性的声明位于类名所在行的下方,并且需要缩进四个空格。声明时必须为它们指定一个初始值。当你根据类创建一个新实例时,Python会自动为这些类属性设置它们的初始值。

类属性用于定义所有实例共有的属性值,而实例属性则用于定义每个实例独有的属性值。

既然你已经定义了Dog类,接下来就是创建一些狗的实例了!

如何实例化一个类?

在Python中,根据类创建一个新对象的过程被称为类的实例化。你可以通过指定类名后跟一对圆括号来实例化一个类,从而创建一个新的对象。

>>> class Dog:
...     pass
...
>>> Dog()
<__main__.Dog object at 0x106702d30>

首先,你定义了一个全新的Dog类,它目前还没有任何属性或方法。接着,你通过实例化这个Dog类来创建了一个Dog对象。

在上述的输出结果中,你会发现在内存地址0x106702d30处生成了一个新的Dog对象。这个由字母和数字组成的奇怪字符串实际上是一个内存地址,它告诉了我们Python是在你的计算机内存中哪个位置存储这个Dog对象的。请注意,你看到的地址可能与这里显示的不同。

现在,再次实例化Dog类,以创建第二个Dog对象:

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

新创建的Dog实例存在于一个全新的内存地址。这是因为它是一个全新的个体,与您之前创建的第一个Dog对象是完全独立的。

要换个方式理解这一点,请尝试输入以下代码:

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

这段代码演示了如何创建两个Dog类的新实例,并将它们分别存储在变量a和b中。当你使用等号(==)来比较这两个变量时,返回的结果是False。这说明即便a和b都是根据Dog类生成的实例,它们在内存中是两个完全独立的实体。

  • 类和实例属性

接下来,我们将定义一个新的Dog类。这个类将包含一个类属性 .species,用于在整个类的所有实例中共享相同的属性值,以及两个实例属性 .name.age,用于存储每个Dog实例特有的信息。

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

要实例化此 Dog 类,您需要提供名称和年龄的值。如果不这样做,Python 会引发 TypeError:

>>> Dog()
Traceback (most recent call last):
  ...
TypeError: __init__() missing 2 required positional arguments: 'name' and 'age'

要将参数传递给 name 和age 参数,请将值放入类名称后面的括号中:

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

这里创建了两个Dog类的新的实例——一只名叫Miles的四岁小狗,和一只名叫Buddy的九岁大狗。

Dog类的构造函数 __init__()有三个参数,但在示例中为什么只传递了两个值呢?

当你根据Dog类创建一个新实例时,Python会自动创建一个新的Dog对象,并将这个对象作为第一个参数传递给 __init__()方法。这样,self这个参数就被自动处理了,你只需要提供 nameage这两个参数。

创建了Dog实例之后,你可以通过点操作符来访问它们的实例属性:

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

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

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

>>> 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",这是猫的科学名称。虽然这让Miles看起来像一只非常不寻常的狗,但在Python中这样的操作是完全合法的!

这里要传达的核心观点是,自定义对象默认情况下是允许更改的。如果一个对象允许你动态地对其进行修改,那么它就是可变的。例如,列表和字典就是可变的数据结构,而字符串和元组则是不可变的。

  • 实例方法

实例方法是指在类内部定义的函数,它们只能在该类的实例上被调用。与 __init__()方法类似,实例方法的第一个参数始终是 self

现在,请在IDLE的编辑器中打开一个新窗口,并编写如下的Dog类:

# dog.py

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()方法并不是最符合Python编程习惯的方式。

举例来说,当你创建了一个列表对象,你可以利用print()函数来输出一个看起来与列表结构相似的字符串:

>>> names = ["Miles", "Buddy", "Jack"]
>>> print(names)
['Miles', 'Buddy', 'Jack']

继续打印miles对象来看看你得到什么输出:

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

当你尝试打印miles对象时,会显示一条难以理解的信息,它告诉你miles是一个Dog实例,位于内存地址0x00aeff70。这样的信息对我们来说帮助不大。为了让打印输出更加友好,你可以在Dog类中定义一个特殊的实例方法 .__str__()

在编辑器中,将Dog类里原本的 .description()方法改名为 .__str__():这个方法在Python中被特殊调用,当使用print()函数打印对象时,.__str__()方法会决定对象将以何种格式显示。

# dog.py

class Dog:
    # ...

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

保存文件并按 F5。现在,当您打印里程时,您会得到更友好的输出:

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

我们把 __init__()__str__()这类方法称作魔法方法(dunder,即“double underscore”的谐音),因为它们前后都带有双下划线。Python中有许多这样的魔法方法,它们可以用来定制类的行为。深入理解这些魔法方法是精通Python面向对象编程的关键。不过,在你的初次探索中,我们将只专注于这两个基础的魔法方法。

微信扫一扫分享文章

+12
无需登陆也可“点赞”支持作者

最近谁赞过

分享到:
评论

使用道具 举报

2548 积分
226 主题
+ 关注
热门推荐