15. PY4E - 面向所有人的 Python¶
切换导航
第 1 章:简介 第 2 章:变量 第 3 章:条件语句 第 4 章:函数 第 5 章:迭代 第 6 章:字符串 第 7 章:文件 第 8 章:线性表 第 9 章:字典 第 10 章:元组 第 11 章:正则表达式 第 12 章:网络程序 第 13 章:Python 与网络服务 第 14 章:Python 对象 第 15 章:Python 与数据库 第 16 章:数据可视化
15.1. 面向对象编程¶
15.1.1. 管理更大的程序¶
在本书开头,我们提出了四种基本编程模式,用于构建程序:
顺序代码
条件代码(if 语句)
重复代码(循环)
存储与复用(函数)
在后续章节中,我们探讨了简单变量以及集合数据结构,如线性表、元组和字典。
随着我们构建程序,我们设计数据结构并编写代码来操作这些数据结构。编写程序有许多方法,到目前为止,您可能已经编写了一些“不够优雅”的程序,也编写了一些“更优雅”的程序。即使您的程序可能很小,您也开始看到编写代码中有一点艺术和美学。
随着程序变得长达数百万行,编写易于理解的代码变得越来越重要。如果你正在开发一个百万行的程序,你无法同时将整个程序牢记于心。我们需要将大型程序分解为多个较小的部分,这样在解决问题、修复错误或添加新功能时,需要查看的内容就会减少。
在某种意义上,面向对象编程是一种组织代码的方式,使你可以聚焦于代码中的 50 行并理解它们,同时暂时忽略其他 999,950 行代码。
15.1.2. 开始使用¶
如同编程的许多方面一样,在能够有效地使用它们之前,有必要学习面向对象编程的概念。你应该将本章视为学习一些术语和概念,并通过几个简单的示例来为未来的学习奠定基础。
本章的关键成果是对对象如何构建、如何运作以及如何利用 Python 和 Python 库提供的对象功能具备基本理解。
15.1.3. 使用对象¶
事实证明,我们在这本书中一直一直在使用对象。Python 为我们提供了许多内置对象。这里有一些简单的代码,其中前几行应该让你感觉非常简单和自然。
stuff = list()
stuff.append('python')
stuff.append('chuck')
stuff.sort()
print (stuff[0])
print (stuff.__getitem__(0))
print (list.__getitem__(stuff,0))
# Code: http://www.py4e.com/code3/party1.py
与其关注这些行完成了什么,不如从面向对象编程的角度来看真正发生了什么。如果以下段落在你第一次阅读时没有意义,请不要担心,因为我们尚未定义所有这些术语。
第一条 构造 一个类型为 list 的对象,第二和第三条 调用 append() 方法,第四条 调用 sort() 方法,第五条 检索 位置 0 处的项。
第六行在 stuff 线性表中调用 __getitem__() 方法,参数为零。
print (stuff.__getitem__(0))
第七行是一种更为冗长的获取线性表中第 0 项的方式。
print (list.__getitem__(stuff,0))
在此代码中,我们在 list 类中调用 __getitem__ 方法,并将我们要从线性表中检索的线性表和项作为参数 pass。
程序的最后三行是等价的,但使用方括号语法来查找线性表中特定位置的项更为方便。
我们可以通过查看 dir() 函数的输出来了解对象的能力:
>>> stuff = list()
>>> dir(stuff)
['__add__', '__class__', '__contains__', '__delattr__',
'__delitem__', '__dir__', '__doc__', '__eq__',
'__format__', '__ge__', '__getattribute__', '__getitem__',
'__gt__', '__hash__', '__iadd__', '__imul__', '__init__',
'__iter__', '__le__', '__len__', '__lt__', '__mul__',
'__ne__', '__new__', '__reduce__', '__reduce_ex__',
'__repr__', '__reversed__', '__rmul__', '__setattr__',
'__setitem__', '__sizeof__', '__str__', '__subclasshook__',
'append', 'clear', 'copy', 'count', 'extend', 'index',
'insert', 'pop', 'remove', 'reverse', 'sort']
>>>
本章其余部分将定义上述所有术语,因此请确保在完成本章后返回并重新阅读上述段落以检查您的理解。
15.1.4. 从程序开始¶
程序在最基本的形式下会接收一些输入,执行一些处理,并产生一些输出。我们的电梯转换程序演示了一个非常简短但完整的程序,展示了这三个步骤。
usf = input('Enter the US Floor Number: ')
wf = int(usf) - 1
print('Non-US Floor Number is',wf)
# Code: http://www.py4e.com/code3/elev.py
如果我们再思考一下这个程序,就有“外部世界”和程序。输入和输出方面是程序与外部世界交互的地方。在程序内部,我们有代码和数据来完成程序旨在解决的任务。
程序
面向对象编程的一种思考方式是将程序划分为多个“区域”。每个区域包含一些代码和数据(就像一个程序),并且与外部世界以及程序内的其他区域具有明确定义的交互。
如果我们回顾使用 BeautifulSoup 库进行链接提取的应用程序,我们可以看到一个由连接不同的对象来完成任务的程序:
# To run this, download the BeautifulSoup zip file
# http://www.py4e.com/code3/bs4.zip
# and unzip it in the same directory as this file
import urllib.request, urllib.parse, urllib.error
from bs4 import BeautifulSoup
import ssl
# Ignore SSL certificate errors
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
url = input('Enter - ')
html = urllib.request.urlopen(url, context=ctx).read()
soup = BeautifulSoup(html, 'html.parser')
# Retrieve all of the anchor tags
tags = soup('a')
for tag in tags:
print(tag.get('href', None))
# Code: http://www.py4e.com/code3/urllinks.py
我们将 URL 读入字符串,然后将其传入 urllib 以从网络获取数据。urllib 库使用 socket 库来建立实际的网络连接以获取数据。我们取 urllib 返回的字符串并将其交给 BeautifulSoup 进行解析。BeautifulSoup 使用对象 html.parser:sup:`1 <https://www.py4e.com/html3/14-objects#fn1>`__ 并返回一个对象。我们在返回的对象上调用 tags() 方法,该方法返回标签对象的字典。我们遍历标签并为每个标签调用 get() 方法以打印出 href 属性。
我们可以绘制一张程序图以及对象如何协同工作的图。
程序作为对象网络
关键在于并非要完全理解该程序如何工作,而是要看到我们如何构建一个相互作用的对象网络,并协调对象间信息的流动以创建程序。同样重要的是要注意,当你查看几章前的那个程序时,你能够完全理解程序中发生的事情,甚至没有意识到该程序正在“协调对象间数据的流动”。它只是完成工作的代码行。
15.1.5. 将问题细分¶
面向对象方法的优势之一是它可以隐藏复杂性。例如,虽然我们需要知道如何使用 urllib 和 BeautifulSoup 代码,但我们不需要知道这些库的内部工作原理。这使我们能够专注于需要解决的问题部分,而忽略程序的其他部分。
忽略使用对象时的细节
这种能够专注于程序与我们关心的部分并忽略其余部分的能力,也有助于我们所用对象的开发者。例如,开发 BeautifulSoup 的程序员不需要知道或关心我们如何获取 HTML 页、希望读取哪些部分,或者打算对从网页中提取的数据做什么。
忽略对象构建时的细节
15.1.6. 我们的第一个 Python 对象¶
在基本层面上,对象仅仅是比整个程序小的代码加数据结构。定义函数允许我们存储一些代码并给它一个名称,然后稍后使用函数名称来调用该代码。
对象可以包含若干函数(我们称之为 方法)以及被这些函数使用的数据。我们将对象的一部分数据项称为 属性。
我们使用 class 关键字来定义将构成每个对象的数据和代码。class 关键字包含类的名称,并开始一个缩进代码块,我们在其中包含属性(数据)和方法(代码)。
class PartyAnimal:
x = 0
def party(self) :
self.x = self.x + 1
print("So far",self.x)
an = PartyAnimal()
an.party()
an.party()
an.party()
PartyAnimal.party(an)
# Code: http://www.py4e.com/code3/party2.py
每个方法看起来都像是一个函数,以关键字 def 开头,并由一段缩进的代码块组成。该对象拥有一个属性(x)和一个方法(party)。这些方法有一个特殊的第一个参数,我们按惯例将其命名为 self。
就像 def 关键字不会导致函数代码执行一样,class 关键字也不会创建对象。相反,class 关键字定义了一个模板,指明每种类型 PartyAnimal 的对象中将包含哪些数据和代码。类就像一个饼干模具,而使用该类创建的对象就是饼干:sup:`2 <https://www.py4e.com/html3/14-objects#fn2>`__。你不会在饼干模具上抹糖霜;你是在饼干上抹糖霜,并且可以在每个饼干上抹不同的糖霜。

对象
如果我们继续运行这个示例程序,我们会看到第一行可执行代码:
an = PartyAnimal()
这就是我们指示 Python 构造(即创建)类 PartyAnimal 的一个 对象 或 实例 的地方。它看起来像是向类本身发出的函数调用。Python 用正确的数据和构造该对象所需的方法构建该对象,然后返回该对象,该对象随后被赋值给变量 an。从某种意义上说,这与我们要一直使用的下面这一行非常相似:
counts = dict()
此处我们指示 Python 使用模板 dict 构造一个对象(该模板已在 Python 中存在),返回字典实例,并将其赋值给变量 counts。
当使用 PartyAnimal 类来构造对象时,变量 an 用于指向该对象。我们使用 an 来访问该 PartyAnimal 类特定实例的代码和数据。
每个 PartyAnimal 对象/实例内部包含一个变量 x 和一个名为 party 的方法/函数。我们在这一行调用 party 方法:
an.party()
当调用 party 方法时,第一个参数(我们按惯例称之为 self)指向 party 调用它的那个 PartyAnimal 对象实例。在 party 方法内,我们看到如下行:
self.x = self.x + 1
这种使用 *dot* 运算符的语法表示的是“self 内部的 x"。每次调用 `party()` 时,内部 `x` 值会增加 1,并且该值会被打印出来。
下面的行是在 an 对象内调用 party 方法的另一种方式:
PartyAnimal.party(an)
在此变体中,我们从类内部访问代码,并显式地将对象指针 an 作为第一个参数传递(即方法内的 self)。你可以将 an.party() 视为上述行的简写。
当程序执行时,它产生以下输出:
So far 1
So far 2
So far 3
So far 4
对象被构建,并且 `party` 方法被调用了四次,在 `an` 对象内递增并打印 `x` 的值。
15.1.7. 类作为类型¶
如前所述,在 Python 中所有变量都具有类型。我们可以使用内置的 dir 函数来检查变量的功能。我们还可以对创建的类使用 type 和 dir。
class PartyAnimal:
x = 0
def party(self) :
self.x = self.x + 1
print("So far",self.x)
an = PartyAnimal()
print ("Type", type(an))
print ("Dir ", dir(an))
print ("Type", type(an.x))
print ("Type", type(an.party))
# Code: http://www.py4e.com/code3/party3.py
当此程序执行时,它产生以下输出:
Type <class '__main__.PartyAnimal'>
Dir ['__class__', '__delattr__', ...
'__sizeof__', '__str__', '__subclasshook__',
'__weakref__', 'party', 'x']
Type <class 'int'>
Type <class 'method'>
你可以看到,使用 class 关键字,我们创建了一种新类型。从 dir 输出中,你可以看到 x 整数属性和 party 方法都在对象中可用。
15.1.8. 对象生命周期¶
在前面的例子中,我们定义一个类(模板),使用该类的实例(对象),然后使用该实例。当程序结束时,所有变量都会被丢弃。通常,我们不会过多考虑变量的创建和销毁,但往往随着我们的对象变得越来越复杂,我们需要在对象内部采取一些操作,以便在对象构建时设置一些内容,并在对象被丢弃时可能清理一些内容。
如果我们要让对象感知到这些创建和销毁的时刻,我们就向对象添加专门命名的方法:
class PartyAnimal:
x = 0
def __init__(self):
print('I am constructed')
def party(self) :
self.x = self.x + 1
print('So far',self.x)
def __del__(self):
print('I am destructed', self.x)
an = PartyAnimal()
an.party()
an.party()
an = 42
print('an contains',an)
# Code: http://www.py4e.com/code3/party4.py
当此程序执行时,它产生以下输出:
I am constructed
So far 1
So far 2
I am destructed 2
an contains 42
当 Python 构建我们的对象时,它会调用我们的 __init__ 方法,以便为我们设置对象的默认或初始值。当 Python 遇到以下行时:
an = 42
实际上,它“丢弃了我们的对象”,以便可以重用变量 an 来存储值 42。就在我们的 an 对象正在被“销毁”的那一刻,我们的析构函数代码(__del__)被调用。我们无法阻止变量被销毁,但可以在对象不再存在之前执行任何必要的清理工作。
在开发对象时,为对象添加构造函数以设置初始值的情况相当常见。为对象添加析构函数的情况则相对罕见。
15.1.9. 多个实例¶
到目前为止,我们已定义了一个类、构造了一个对象、使用该对象,然后将其丢弃。然而,面向对象编程的真正威力在于构造我们类的多个实例。
当我们从我们的类构造多个对象时,我们可能希望为每个对象设置不同的初始值。我们可以向构造函数传递数据,以便为每个对象赋予不同的初始值:
class PartyAnimal:
x = 0
name = ''
def __init__(self, nam):
self.name = nam
print(self.name,'constructed')
def party(self) :
self.x = self.x + 1
print(self.name,'party count',self.x)
s = PartyAnimal('Sally')
j = PartyAnimal('Jim')
s.party()
j.party()
s.party()
# Code: http://www.py4e.com/code3/party5.py
构造器具有一个指向对象实例的 `self` 参数,以及作为对象构造时传入构造器的额外参数:
s = PartyAnimal('Sally')
在构造函数中,第二行将传入的(nam)参数复制到对象实例内的 name 属性中。
self.name = nam
程序的输出显示,每个对象(s 和 j)都包含其自身对 x 和 nam 的独立副本:
Sally constructed
Jim constructed
Sally party count 1
Jim party count 1
Sally party count 2
15.1.10. 继承¶
面向对象编程的另一个强大功能是能够通过扩展现有类来创建新类。当扩展一个类时,我们将原始类称为 父类,将新类称为 子类。
对于本例,我们将 PartyAnimal 类移动到自己的文件中。然后,我们可以在一个新文件中“导入”PartyAnimal 类并对其进行扩展,如下所示:
from party import PartyAnimal
class CricketFan(PartyAnimal):
points = 0
def six(self):
self.points = self.points + 6
self.party()
print(self.name,"points",self.points)
s = PartyAnimal("Sally")
s.party()
j = CricketFan("Jim")
j.party()
j.six()
print(dir(j))
# Code: http://www.py4e.com/code3/party6.py
当我们定义 CricketFan 类时,我们表明我们正在扩展 PartyAnimal 类。这意味着 x 变量和 party 方法中的所有 PartyAnimal 类的内容都被 CricketFan 类 继承。例如,在 CricketFan 类中的 six 方法内,我们调用来自 PartyAnimal 类的 party 方法。
随着程序执行,我们创建 s 和 j 作为 PartyAnimal 和 CricketFan 的独立实例。j 对象具有超出 s 对象的额外能力。
Sally constructed
Sally party count 1
Jim constructed
Jim party count 1
Jim party count 2
Jim points 6
['__class__', '__delattr__', ... '__weakref__',
'name', 'party', 'points', 'six', 'x']
在 dir 输出中(针对 j 对象,即 CricketFan 类的实例),我们可以看到它拥有父类的属性与方法,以及当类被扩展以创建 CricketFan 类时添加的属性与方法。
15.1.11. 摘要¶
这是一个关于面向对象编程的快速简介,主要关注术语以及定义和使用对象的语法。让我们快速回顾本章开头所查看的代码。此时您应该完全理解其中的内容。
stuff = list()
stuff.append('python')
stuff.append('chuck')
stuff.sort()
print (stuff[0])
print (stuff.__getitem__(0))
print (list.__getitem__(stuff,0))
# Code: http://www.py4e.com/code3/party1.py
第一行构建了一个 list 对象。当 Python 创建 list 对象时,它会调用 构造器 方法(命名为 __init__)来设置用于存储线性表数据的内部数据属性。我们没有向 构造器 传递任何参数。当构造器返回后,我们使用变量 stuff 指向 list 类返回的实例。
第二行和第三行调用带有单一参数的 append 方法,通过更新 stuff 内部的属性在列表末尾添加新项。然后在第四行,我们调用不带参数的 sort 方法对 stuff 对象内的数据进行排序。
随后,我们使用方括号打印线性表中的第一个项目,方括号是调用 __getitem__ 方法于 stuff 的快捷方式。这等同于在 __getitem__ 类中调用 list 方法,并将 stuff 对象作为第一个参数传递,将我们要查找的位置作为第二个参数传递。
在程序结束时,在调用析构函数(名为 __del__)之前,stuff 对象会被丢弃,以便该对象能够清理任何必要的遗留问题。
那是面向对象编程的基础。关于如何在开发大型应用程序和库时最好地使用面向对象方法,还有许多额外的细节,这些内容超出了本章的范围。:sup:`3 <https://www.py4e.com/html3/14-objects#fn3>`__
15.1.12. 术语表¶
属性
类的一部分的变量。
类
可用于构造对象的模板。定义构成对象的属性和方法。
子类
扩展父类时创建的新类。子类继承父类的所有属性和方法。
构造函数
一个可选的特殊命名方法( __init__ ),在使用类构造对象的时刻调用。通常用于为对象设置初始值。
析构函数
一个可选的特殊命名方法( __del__ ),在对象即将被销毁的时刻调用。析构函数很少使用。
继承
通过扩展现有类(父类)来创建新类(子类)的过程。子类拥有父类的所有属性和方法,以及由子类定义的额外属性和方法。
方法
包含在类中以及由该类构造的对象中的函数。一些面向对象模式使用“消息”而非“方法”来描述此概念。
对象
类的已构造实例。对象包含由类定义的所有属性和方法。一些面向对象文档将术语“实例”与“对象”互换使用。
父类
被扩展以创建新子类的类。父类将其所有方法和属性贡献给新的子类。
Cookie 图像版权 CC-BY https://www.flickr.com/photos/dinnerseries/23570475099↩︎
如果你好奇
`list`类是在哪里定义的,请查看(希望 URL 不会改变)https://github.com/python/cpython/blob/master/Objects/listobject.c ——该线性表类是用一种名为“C"的语言编写的。如果你查看该源代码并觉得好奇,你可能想探索几门计算机科学课程。↩︎
如果您在本书中发现错误,欢迎使用 Github 向我发送修正。
