编程 Python,第二部分

作者:José P. E. "Pupeno" Fernandez

上个月刊登的教程涵盖了安装 Python、运行 Python 和使用 Python 的基础知识。然后,我们继续使用 Python 构建一个基本的博客。这个博客非常简单——只是一个 Python 列表。我们专注于文章,并构建了一个 Post 类

class Post(object):
    def __init__(self, title, body):
        self.set_title(title)
        self.set_body(body)

    def set_title(self, title):
        self._title = title

    def get_title(self):
         return self._title

    def set_body(self, body):
        self._body = body

    def get_body(self):
         return self._body

    def __repr__(self):
        return "Blog Post: %s" % self.get_title()

在这篇后续文章中,让我们专注于博客本身,并深入探讨。

博客

现在我们有了 Post 类,我们可以创建 Blog 类。一个初始的实现可能看起来像这样

class Blog(object):
    def __init__(self):
        self._posts = []

    def add_post(self, post):
        self._posts.append(post)

    def get_posts(self):
        return self._posts

我们使用列表来维护文章,但接口完全抽象在 Blog 类的一组方法之后。这有一个巨大的优势:明天我们可以用 SQL 后端替换这个简单的列表,而使用 Blog 的代码几乎不需要任何更改。

请注意,没有删除文章的方法。我们可以直接篡改 _posts,但只要我们做这个类应该做的事情,我们就不能删除文章。这可能是好事也可能是坏事,但重要的是,通过定义一组方法,我们暴露了该类应该如何使用的设计。

发布或不发布

get_posts 方法返回所有文章。当我们正在撰写一篇新文章时,我们不希望全世界都能在文章完成之前阅读它。文章需要一个新的成员来告知它是否已发布。在 Post 的初始化器 __init__ 中,我们添加以下行

self._published = False

这使得每篇新文章默认都是私有的。要切换状态,我们添加以下方法

def publish(self):
    self._published = True

def hide(self):
    self._published = False

def is_public(self):
    return self._published

在这些方法中,我引入了一种新的变量类型——布尔值。布尔值很简单;它们可以是 true 或 false。让我们稍微玩一下

>>> cool = blog.Post("Cool", "Python is cool")
>>> cool.is_public()
False
>>> cool.publish()
>>> cool.is_public()
True
>>> cool.hide()
>>> cool.is_public()
False
>>> 

如果当您运行 is_public 时,您得到

Traceback (most recent call last):
  File "<stdin>", line 1, in ?
  File "blog.py", line 25, in is_public
    return self._published
AttributeError: 'Post' object has no attribute 
'_published'

那是因为 _published 没有被创建,它不能被使用,而 is_public 想要使用它。如果您想成为一名成功的程序员,理解工具中的错误非常重要。

在这短短的一组消息中,最后一行是错误本身。错误有多种类型,而这个错误是 AttributeError。回溯中给出了很多重要信息。回溯是“谁调用了谁”的列表,提供了错误发生时正在执行的操作的想法。

回溯的第一行没有提供太多信息。它可能与我们在 REPL 中键入的行有关。第二行告诉我们错误发生在文件 blog.py 的第 25 行,方法是 is_public。现在我们有了引发问题的行。

这个回溯很简单。在实际应用中,您会有方法调用方法,方法再调用方法,依此类推。在这些情况下,看到 25 行或更多的回溯并不少见。我见过超过 150 行的回溯,但那些是极端情况下发生的极端情况。

下一步是对 Blog 类进行修改,只选取已发布的文章。因此,我们添加一个新的方法

def get_public_posts(self):
    published_posts = []
    for post in self._posts:
        if port.is_public():
            published_posts.append(post)

Python 试图尽可能地具有可读性,但是该方法引入了太多新事物,因此需要一些仔细的解释。

循环

Python 的循环结构之一是 for。它被设计为迭代列表、集合、映射和其他可迭代对象。在这种情况下,它获取 self._posts 中的所有项目,并逐个将它们分配给变量 post。在 for 的主体中,它在每次迭代时执行,我们可以使用变量 post。

for 的主体,与其他需要一段代码的结构一样,仅由缩进分隔。这是一个例子

>>> the_list = [1,2,3,"a","b"]
>>> for item in the_list:
...     print item
...
1
2
3
a
b
>>>

各种任务都可以通过循环来解决。其中一项任务是对集合的每个成员执行某些操作,就像我们在前面的示例中所做的那样。对于这些类型的任务,for 结构非常出色。

另一个常见的做法是执行给定次数的操作——例如,打印 “Hello, world” 三次。为此,我们可以使用

>>> a = 0
>>> while a < 3:
...     print "Hello world"
...     a = a + 1
...
Hello world
Hello world
Hello world
>>>

另一个循环结构是 while,它将继续运行其主体,直到检查——即 while 之后和冒号之前的表达式——变为 false。

我们可以将之前的循环重新思考为迭代包含数字 0-9 的列表。有一种使用 for 结构来实现它的方法

>>> for a in range(0,3):
...     print "Hello world"
...
Hello world
Hello world
Hello world
>>>>

这更短,并且可以说更具可读性。那么 while 有什么用呢?当您不真正知道何时停止循环时,它就很有用。以下是一些示例

  • 从文件中读取字符,直到遇到文件结束符 (EOF)。

  • 从用户读取命令,直到用户输入 quit 命令。

  • 从传感器读取温度,直到温度过高。

  • 从用户界面读取事件,直到用户按下窗口顶部的 X 按钮关闭程序。

这里正在形成一种模式——做某事直到发生其他事情。这就是 while 的用武之地。

在很久以前,当我们编程语言的选择不多,最终大部分时间都在使用 C 语言时,while 结构往往比 for 结构更有用。但是今天,凭借强大的 for 结构、诸如 range 之类的优秀函数以及围绕任何事物放置迭代器的可能性,for 的使用频率远高于 while。

这是最后一个例子,供您欣赏

>>> for l in "Hello World":
...     print l + " ",
...
H  e  l  l  o     W  o  r  l  d
条件语句

在前面一些示例代码的第四行中,if post.is_public(),我们有另一个新结构——if。这允许程序根据数据做出选择。它需要一个布尔值和一段代码。代码仅在布尔值为 True 时运行。如果您提供的内容不是布尔值,Python 会尽力将其解释为布尔值。例如,数字 0 被解释为 False,但所有其他数字都被解释为 True。以下是一些例子

>>> if True:
...     print "It is true!"
...
It is true!
>>> if False:
...     print "Is it false?"
...
>>> 

我们可以在不同类型的对象上执行许多不同类型的比较。请注意,相等运算符是 ==,而不是 =(即,两个等号)

>>> a = 10
>>> if a == 10:
...     print "Ten!"
...
Ten!

还有其他比较,例如大于 (>)、小于 (<) 和不等于 (!=)。您可以直接在 REPL 上尝试比较

>>> 3 == 4
False
>>> 10 != 5
True
>>> 4 >= 1
True

常见的情况是,如果某个条件为真,则运行一段代码,如果为假,则运行另一段代码。例如,我们可以这样做

if a == 10:
    print "A is ten."
if a != 10:
    print "A is not ten."

这有一个大问题。如果我们在第一种情况下将 a 更改为 b,我们必须记住在第二种情况下也更改它。并且,对于我们做的任何其他小改动,都应该这样做。解决方案是 if 结构的扩展

if a == 10:
    print "A is ten."
else:
    print "A is not ten."

如果第一段代码没有执行,则会执行 else 之后的代码段。

另一种常见情况是针对不同情况有各种条件。在那种情况下,我们使用一系列 if 语句

if a == 10:
    print "A is ten."
elif a == 0:
    print "A is zero."
elif a != 30:
    print "A is not thirty."
else:
    print "Who cares about a ?"

elif 是 “else if” 的缩写,实际上,之前的代码可以写成

if a == 10:
    print "A is ten."
else:
    if a == 0:
        print "A is zero."
    else:
        if a != 30:
            print "A is not thirty."
        else:
            print "Who cares about a ?"

但是,这很丑陋并且容易出错。如果您有 10 或 15 种不同的情况,您将需要一个 29 英寸的宽屏显示器才能查看它。(并不是说我反对这样的显示器。我很想拥有一个。)

如果您来自其他具有 switch 或 select 或 case 结构的语言,并且想知道它们在 Python 中的位置,我很抱歉让您失望了。Python 没有这样的结构。有一个包含它们的提议,但尚未实现。目前,解决方案是使用 if、elif 和 else 的链。在您使用几次之后,它就没有那么糟糕了。

既然您了解了 else,这里有一个有趣的花絮:for 和 while 也可以有 else。它们是做什么的?运行 Python,并尝试一下,直到您自己发现为止。在编程时,您需要运行大量代码来找出有多少未记录的、晦涩的、几乎是黑魔法的东西在工作,因此从一些简单的东西开始将有助于您获得一些训练。

继承

本文第一部分对面向对象编程 (OOP) 的简短介绍遗漏了一个重要主题——继承。此功能使 OOP 真正有用,并且由于 OOP 试图模仿现实生活,因此我在此处使用现实生活中的示例解释继承。

想想椅子。椅子是由某种材料制成的,有两个扶手、一个靠背、一种颜色、一种风格,甚至可能还有保修。现在,想想桌子。它是由某种材料制成的,可能有几个抽屉、一种颜色、一种风格,也可能有保修。它们有很多共同点!如果我们制作 Chair 和 Table 两个类,很多代码会被重复。在编程中,当您两次编写相同的代码行时,您可能做错了什么——继承来救援。

椅子是一种家具。桌子也是。这种相似之处可以在 Furniture 类中。让我们让 Furniture 类具有默认材料和设置其他材料的能力

class Furniture(object):
    def __init__(self):
        self._material = "wood"
    
    def set_material(self, material):
        self._material = material

现在,一个继承 Furniture 的 Chair 类

class Chair(Furniture):
    def __init__(self):
        self._backrest_height = 30
    
    def set_backrest_height(self, height):
        self._backrest_height = height

现在,您知道类头中的括号内是什么了:被继承的类的名称,也称为超类或父类。让我们稍微玩一下,这样您就可以看到会发生什么

>>> c = Chair()
>>> c.set_backrest_height(50)
>>> c._backrest_height
50
>>> c.set_material("plastic")
>>> c._material
'plastic'
>>>

正如您所见,Furniture 的方法也在 Chair 上。我将 Table 类的定义留给读者作为练习。但首先,这是另一个交互

>>> d = Chair()
>>> d._backrest_height
30
>>> d._material
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
AttributeError: 'Chair' object has no attribute '_material'
>>>

我敢打赌这不是您所期望的。让我们仔细看看发生了什么。我们创建了一个 Chair,方法 Chair.__init__ 被运行,设置了 _backrest_height。哦!没有人调用 Furniture.__init__,它从未设置 _material。对此有两种解决方案。

在 Chair.__init__ 中设置 _material 不是 解决方案。如果我们这样做,这些类将耦合在一起,这意味着一个类的实现将取决于另一个类的实现。如果我们将 _material 的名称更改为 _materials,Chair 突然将停止工作。如果您有数百个由数百个不同的人开发的类,那么跟踪这些更改是很困难的。此外,Furniture 将增长以拥有更多成员,因此我们必须记住在 Chair.__init__ 中将所有这些成员设置为相同的默认值。我只是想想就头疼。

一个真正的解决方案是调用 Furniture.__init__ 并以这种方式重写 Chair.__init__

def __init__(self):
    Furniture.__init__(self)
    self._backrest_height = 30

我们必须将 self 传递给 __init__,因为如果我们使用类而不是对象调用它,它将不知道在哪个对象上执行其操作。

我个人不喜欢这种解决方案,因为它意味着在两个或更多地方编写类的名称。如果您更改了名称,您必须记住运行搜索和替换。另一种解决方案比它应该的更神秘,但它没有我刚才提到的问题

def __init__(self):
    super(Chair, self).__init__()
    self._backrest_height = 30

在这个解决方案中,我调用 super,传递当前类和当前对象,它允许我使用当前对象调用父类。如果我们更改类本身的名称,这里可能会出现问题,但是当进行这种更改时,在文件中运行搜索和替换是一个好主意。您还需要更改文档。此解决方案的真正问题很难理解和解释——它与多重继承有关。有关更多信息,请阅读 “Python's Super Considered Harmful”。就我个人而言,我一直在使用第二种解决方案,没有任何问题。

您会看到我定义的所有类都继承自 object。那是最基本的类——根(或顶部)类。除非您的类继承自另一个类,否则让您的所有类都继承自它是一个好主意。如果您不这样做,您的类将是旧式类,并且某些东西将无法工作,例如 super。了解这一点很重要,因为您可能会在任何地方遇到旧式类,并且您应该做好准备。

Python 2.5

在撰写本文的过程中,Python 2.5 在万众瞩目和欢呼声中发布了。这是近两年内最重要的版本,它带来了许多承诺。

由于 Python 开发团队使用的测试程序有所改进,它承诺会更加可靠。它现在有了 Buildbot,这是一个持续构建和测试 Python 的程序,每当出现问题时,它都会向全世界发出警报。成为犯错的开发者的耻辱会让所有开发者更加小心——至少,当我有 Buildbot 监视我的代码时,我就是这样做的。

对于某些人来说,例如这位在最糟糕的时间发布新版本的作者,最重要的是 Python 2.5 是向后兼容的。您在这里学到的一切都将有效。而且,它不仅会有效,而且仍然是正确的方法。

新版本还承诺更快,并且具有许多新的高级功能,包括新的模块和包。Python 和 Python 程序员的未来是光明的。

接下来做什么?

这仅仅是对 Python 的简短介绍;还有很多东西要学习。一个好的起点是官方的 Python 教程。您还可以阅读 Dive Into Python,这是一本您可以在网上购买或免费阅读的书。当然,还有很多其他书籍和教程可供选择。我主要从 Python 教程中学习 Python,它非常好。

每当您在 Python 中创建程序时,永远不要,我再说一遍,永远不要在没有检查它是否已被完成的情况下做任何事情。Python 具有许多功能和许多内置库。如果这还不够,还有数百个,甚至可能是数千个第三方 Python 库。事实上,已经用 Python 编写的大量代码是使用它的原因之一。

第一站是 Python 的文档。在那里,我们有前面提到的教程、库参考和语言参考。

语言参考可能有点难用和理解。编程语言往往难以理解,它们的参考也是如此,这些参考通常具有专有的术语,例如词法分析、标记、标识符、关键字或分隔符。本文档在展示如何使用语言结构(例如 for、if、while 以及我尚未提及的更复杂的结构,例如 yield、break 或 continue)方面特别有用。

库参考让我们了解 Python 已经提供的所有类、方法和函数。它非常重要和有用,以至于我在使用 Python 编程时总是打开它。在第二章中,您可以阅读有关内置函数和类的内容。熟悉它们总是有用的。文档的其余部分非常具体,每一章都处理从运行时内部结构到字符串、从 Python 调试器到某些通用操作系统服务的主题。在该章中,记录了一个非常重要的模块:os。我不记得制作过任何一个没有使用该模块的程序。

在如此多的文档中找到您想要的内容可能是一项困难的任务。我发现非常有用的一个技巧是使用 Google 在特定站点中搜索。这是通过在搜索查询中添加 “site:python.org” 或 “site:docs.python.org” 来实现的。第一个更通用,有时会导致无数与您正在寻找的内容无关的邮件列表帖子。在这种情况下,请使用第二个。要试用一下,请搜索 “print site:python.org” 或 “options site:python.org”。

如果您的所有搜索都一无所获怎么办?然后,您需要进行更广泛的搜索以查找一些第三方库或框架。如果您想制作图形用户界面,我推荐 PyGTK 和 PyQt,两者都非常好,并且包括对其各自桌面 GNOME 和 KDE 的支持。我听说过 wxPython 的好评,但我自己没有用过。

如果您想构建 Web 应用程序,我看到了两条路径。如果您想要一些不太壮观但可以快速实现目标的东西,我推荐 Django。Django 与 Ruby on Rails 非常相似。它是一个框架,您可以在其中使用模型-视图-控制器范例和关系数据库(例如 MySQL 或 PostgreSQL);两者在 Python 上都得到了很好的支持。

构建网站的另一种方法(据我所知)是 Zope。Zope 是一个大型框架,带有 Web 服务器和面向对象的数据库。该数据库与其他关系数据库不同,并且非常强大。它允许您以更灵活的方式存储信息。Zope 3——除非您必须使用屡获殊荣的内容管理系统 Plone,否则我不推荐以前的版本——它旨在通过接口、单元测试、适配器等等来帮助您构建可靠而健壮的代码。

如果您需要构建任何类型的守护程序——那些在后台运行使地球运转的小应用程序——请查看 Twisted Matrix。Twisted Matrix 是一个基于事件的框架,它解决了构建守护程序的许多常见问题,包括协议和逻辑的分离。它带有许多已内置的协议,并且允许您创建新协议。证明其有用性的是,Zope 在多年发布自己的 Web 服务器后,已迁移到使用 Twisted Matrix HTTP 服务器。

资源

Python 教程: docs.python.org/tut/tut.html

Dive Into Python: www.diveintopython.org

Python 文档: www.python.org/doc

PyGTK: www.pygtk.org

PyQt: www.riverbankcomputing.co.uk/pyqt

Django: www.djangoproject.com

Zope: zope.org

Python 的 Super 被认为有害: fuhm.net/super-harmful

José P. E. “Pupeno” Fernández 从...什么时候孩子能够坐在椅子上并够到键盘就开始编程了?他尝试过的语言比此页面上可以列出的还要多。他的网站是 pupeno.com,除非您是垃圾邮件发送者,否则始终可以通过 pupeno@pupeno.com 与他联系。

加载 Disqus 评论