Python for Everybody 中文版

Chapter 0 Base Material

| 关于   «  14. PY4E - 面向所有人的 Python   ::   目录   ::   16. PY4E - 面向所有人的 Python  »

15. PY4E - 面向所有人的 Python

切换导航

PY4E

第 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

如果我们再思考一下这个程序,就有“外部世界”和程序。输入和输出方面是程序与外部世界交互的地方。在程序内部,我们有代码和数据来完成程序旨在解决的任务。

A Program

程序

面向对象编程的一种思考方式是将程序划分为多个“区域”。每个区域包含一些代码和数据(就像一个程序),并且与外部世界以及程序内的其他区域具有明确定义的交互。

如果我们回顾使用 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 属性。

我们可以绘制一张程序图以及对象如何协同工作的图。

A Program as Network of Objects

程序作为对象网络

关键在于并非要完全理解该程序如何工作,而是要看到我们如何构建一个相互作用的对象网络,并协调对象间信息的流动以创建程序。同样重要的是要注意,当你查看几章前的那个程序时,你能够完全理解程序中发生的事情,甚至没有意识到该程序正在“协调对象间数据的流动”。它只是完成工作的代码行。

15.1.5. 将问题细分

面向对象方法的优势之一是它可以隐藏复杂性。例如,虽然我们需要知道如何使用 urllib 和 BeautifulSoup 代码,但我们不需要知道这些库的内部工作原理。这使我们能够专注于需要解决的问题部分,而忽略程序的其他部分。

Ignoring Detail When Using an Object

忽略使用对象时的细节

这种能够专注于程序与我们关心的部分并忽略其余部分的能力,也有助于我们所用对象的开发者。例如,开发 BeautifulSoup 的程序员不需要知道或关心我们如何获取 HTML 页、希望读取哪些部分,或者打算对从网页中提取的数据做什么。

Ignoring Detail When Building an Object

忽略对象构建时的细节

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>`__。你不会在饼干模具上抹糖霜;你是在饼干上抹糖霜,并且可以在每个饼干上抹不同的糖霜。

A Class and Two Objects

对象

如果我们继续运行这个示例程序,我们会看到第一行可执行代码:

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__ ),在对象即将被销毁的时刻调用。析构函数很少使用。 继承 通过扩展现有类(父类)来创建新类(子类)的过程。子类拥有父类的所有属性和方法,以及由子类定义的额外属性和方法。 方法 包含在类中以及由该类构造的对象中的函数。一些面向对象模式使用“消息”而非“方法”来描述此概念。 对象 类的已构造实例。对象包含由类定义的所有属性和方法。一些面向对象文档将术语“实例”与“对象”互换使用。 父类 被扩展以创建新子类的类。父类将其所有方法和属性贡献给新的子类。


  1. https://docs.python.org/3/library/html.parser.html ↩︎

  2. Cookie 图像版权 CC-BY https://www.flickr.com/photos/dinnerseries/23570475099↩︎

  3. 如果你好奇 `list` 类是在哪里定义的,请查看(希望 URL 不会改变)https://github.com/python/cpython/blob/master/Objects/listobject.c ——该线性表类是用一种名为“C"的语言编写的。如果你查看该源代码并觉得好奇,你可能想探索几门计算机科学课程。↩︎


如果您在本书中发现错误,欢迎使用 Github 向我发送修正。

   «  14. PY4E - 面向所有人的 Python   ::   目录   ::   16. PY4E - 面向所有人的 Python  »

关闭窗口