Python-Class-Object

exercise40.模块, 类和对象


Python 是一门“面向对象编程语言”。这意味着在 Python 中有一个叫做类的概念,你能通过类用一种特殊的方式构建你的软件。使用类的概念,能给你的程序增添一致性,这样你可以用一种很轻松方便的方式调用他们。至少,这是面向对象的理论。


模块就像字典


你知道字典是如何被创建以及使用的,它用来将一个事物对应到另一个事物。意思就是说如果你有一个字典,字典中包括一个 key "apple",那么你就可以实现:
mystuff = {'apple': "I AM APPLES!"}
print mystuff['apple']

记住“从 X 获取 Y”的说法,然后想一想模块(module)。你之前已经写过一些模块,你应该知道它们:
1.包含一些函数和变量的 python 文件 2.你可以导入这个文件 3.你可以用.操作符访问这个模块的函数和变量

想象一下我有一个叫做 mystuff.py 的模块,在这个模块中有一个叫做 apple 的函数,下面是 mystuff.py 模块的代码:
# this goes in mystuff.py
def apple():
    print ("I AM APPLES!")

当我写完以上代码,我就可以通过 import 来调用 mystuff 模块,并访问 apple 函数:
import mystuff
mystuff.apple()

我还可以增加一个叫做 tangerine 的变量:
def apple():
    print ("I AM APPLES!")

# this is just a variable
tangerine = "Living reflection of a dream"

可以用相同的方法来访问:
import mystuff

mystuff.apple()
print mystuff.tangerine

返回再看字典,你应该发现模块的使用跟字典是相似的,只不过语法不同,我们比较一下:
mystuff['apple'] # get apple from dict
mystuff.apple() # get apple from the module
mystuff.tangerine # same thing, it's just a variable

也就是说我们在 Python 中有一套通用的模式:
1.有一个 key=value 模式的容器 2.通过 key 从容器中获取数据

在字典中,key 是一个字符串,写法为[key],在模块中,key 是一个标识符,写法为.key,在其余的地方,他们几乎是一样的。

类就像模块


你可以认为一个模块就是一个可以存放 Python 代码的特殊字典,这样你就可以通过.操作符访问它。Python 还有一个另外的结构提供相似的功能,就是类(class)。类的作用是组织一系列的函数和数据并将它们放在一个容器里,这样你可以通过.操作符访问到它们。

如果我想创建一个类似 mystuff 的类,我需要这样做:
class MyStuff(object):

    def __init__(self):
        self.tangerine = "And now a thousand years between"

    def apple(self):
        print "I AM CLASSY APPLES!"

这相比于模块复杂一些,对比之下肯定会有不同,但是你应该能返现这个类就像写一个包含 apple()方法的“迷你”mystuff 模块一样.让你困惑的地方可能是init()这个方法以及使用 self.tangerine 给 tangerine 赋值。

这正是为什么要使用类而不是仅有模块的原因:你可以使用 Mystuff 这个类,还可以用它来创建更多个 Mystuff,而他们之间也不会互相冲突。当你导入一个模块时,你的整个项目也就只有一个这个模块。

在你理解这些之前,你需要明白什么是“对象”,以及如何像使用 Mystuff.py 模块一样使用 Mystuff 这个类。

对象就像导入


如果一个类就像一个迷你模块,那么类也会有一个类似 import 的概念,这个概念被称为实例化,这只是对创建一种更虚幻更聪明的叫法。当一个类被实例化,你就得到一个类的对象。

实例化类的方法就是像调用函数一样调用这个类:
thing = MyStuff()
thing.apple()
print thing.tangerine

第一行就是实例化的操作,这个操作多像是在调用一个函数啊。当然了,python 在幕后帮你做了一系列的事情,我带你来看看具体的调用步骤:
1.python 查找 MyStuff() 并确认它是你已经定义过的类 2.python 创建一个空的对象,该对象拥有你在类中用 def 创建的所有函数 3.python 看你是否创建了init函数,如果有,调用该方法初始化你新创建的空对象 4.在 MyStuff 中,init方法有一个额外的变量 self,这是 python 为我创建的一个空的对象,我可以在其上设置变量。 5.然后,我给 self.tangerine 设置一首歌词,然后初始化这个对象 6.python 可以使用这个新创建好的对象,并将其分配给我可以使用的变量 thing。

这就是当你调用一个类的时候,python 做的事情。记住,这并不是把类给你,而是把类作为蓝本来创建这种类型东西的副本。

我给了你一个类的简单工作原理,这样你就可以基于你所了解的模块,建立你对类的理解。事实上,在这一点上,类和对象与模块是有区别的:


  • 类是用来创建迷你模块的蓝本或定义

  • 实例化是如何创建这些小模块,并在同一时间将其导入。实例化仅仅是指通过类创建一个对象。

  • 由此产生的迷你模块被称为对象,你可以将其分配给一个变量,让它开始运行


在这一点上,对象和模块的表现不同,这只是提供一种让你理解类和对象的方法。

从一个事物中获取事物


现在我提供给你 3 中方法从一个事物中获取另一个:
# dict style
mystuff['apples']

# module style
mystuff.apples()
print mystuff.tangerine

# class style
thing = MyStuff()
thing.apples()
print thing.tangerine

类的举例


你应该可以看到在这三个 key=value 的容器类型有一定的相似性,你也可能有一堆问题.记住这些问题,下一节练习会训练你关于“面向对象的词汇”。这节练习中,我只想让你输入这些代码并保证它能正常运行,这样能让你再继续下一节练习之前获得一些经验。
class Song(object):
 def __init__(self,lyric):
  self.lyric = lyric
 def sing(self):
  for content in self.lyric:
   print (content)

happy_bday = Song(["Happy birthday to you",
                   "I don't want to get sued",
                   "So I'll stop right there"])

bulls_on_parade = Song(["They rally around tha family",
                        "With pockets full of shells"])

happy_bday.sing()

bulls_on_parade.sing()

你应该看到的内容


Happy birthday to you
I don't want to get sued
So I'll stop right there
They rally around tha family
With pockets full of shells


exercise41.学会说面向对象


单词解释


class(类):告诉 python 去创建一个新类型。 object(对象):有两种意思,事物的基本类型,或者事物的实例化。 instance(实例):你通过 python 创建一个类所获得的。 def:用来在类中定义一个函数。 self:在一个类包含的函数中,self 是一个用来访问实例或对象的变量。 inheritance:概念,表示一个类可以继承另一个类的特征,就像你和你的父母。 composition:概念,表示一个类可以包含其他类,就像汽车轮子。 attribute:类所拥有的特性,通常是变量。 is-a:惯用语,表示一个东西继承自另一个东西(a),像在“鲑鱼”是“鱼”。 has-a:惯用语,表示由其他事情或有一个特征(a),如“鲑鱼有嘴。”

短语解释


接下来,在左边有一个 Python 代码片段列表,右面是他们的解释

class X(Y):创建一个叫 X 的类,并继承 Y。
class X(object): def init(self, J):类 X 有一个init方法,该方法有 self 和 J 两个参数。
class X(object): def M(self, J):类 X 有一个叫 M 的函数,该函数有 self 和 J 两个参数。
foo = X():给 foo 赋值为类 X 的一个实例。 foo.M(J):从 foo 里调用 M 函数,传递的参数为 self 和 J。
foo.K = Q:从 foo 里调用 K 属性,并将其设置为 Q。

你可以把上面看到的所有的 X, Y, M, J, K, Q, 以及 foo 看做空白的坑,比如,我还可以这么写:
1.创建一个叫??的类继承 Y
2.类??有一个init方法,该方法有 self 和??两个参数。
3.类??有一个叫??的函数,该函数有 self 和??两个参数。
4.给 foo 赋值为类??的一个实例。
5.从 foo 里调用??函数,传递的参数为 self 和??。
6.从 foo 里调用??属性,并将其设置为??。

同样的,把这些写到卡片上,牢牢记住它们。卡片的前面写上 python 的小段代码,背面写上它们的解释,你要做到每当你看到正面的代码段的时候,能立即说出后面的解释。

# -- coding: utf-8 -- 

import random
from urllib.request import urlopen
import sys

WORD_URL="http://learncodethehardway.org/words.txt"
WORDS = []

PHRASES = {
    "class %%%(%%%):":
      "Make a class named %%% that is-a %%%.",
    "class %%%(object):\n\tdef __init__(self, ***)" :
      "class %%% has-a __init__ that takes self and *** parameters.",
    "class %%%(object):\n\tdef ***(self, @@@)":
      "class %%% has-a function named *** that takes self and @@@ parameters.",
    "*** = %%%()":
      "Set *** to an instance of class %%%.",
    "***.***(@@@)":
      "From *** get the *** function, and call it with parameters self, @@@.",
    "***.*** = '***'":
      "From *** get the *** attribute and set it to '***'."
}

# do they want to drill phrases first
if len(sys.argv) == 2 and sys.argv[1] == "english":
    PHRASE_FIRST = True
else:
    PHRASE_FIRST = False

# load up the words from the website
for word in urlopen(WORD_URL).readlines():
 WORDS.append(word.strip())

def convert(snippet, phrase):
    class_names = [w.capitalize() for w in
                   random.sample(WORDS, snippet.count("%%%"))]
    other_names = random.sample(WORDS, snippet.count("***"))
    results = []
    param_names = []

    for i in range(0, snippet.count("@@@")):
        param_count = random.randint(1,3)
        param_names.append(', '.join(random.sample(WORDS, param_count)))

    for sentence in snippet, phrase:
        result = sentence[:]

        # fake class names
        for word in class_names:
            result = result.replace("%%%", word, 1)

        # fake other names
        for word in other_names:
            result = result.replace("***", word, 1)

        # fake parameter lists
        for word in param_names:
            result = result.replace("@@@", word, 1)

        results.append(result)

    return results

# keep going until they hit CTRL-D
try:
    while True:
        snippets = PHRASES.keys()
  print (snippets)
  random.shuffle(list(snippets))

        for snippet in snippets:
   print (snippet)
   phrase = PHRASES[snippet]
   question, answer = convert(snippet, phrase)
   if PHRASE_FIRST:
    question, answer = answer, question
            print (question)

            input("> ")
            print ("ANSWER:  %s\n\n" % answer)
except EOFError:
    print ("\nBye")

exercise42.对象、类、以及从属关系


有一个重要的概念你需要弄明白,那就是“类(class)”和“对象(object)”的区别。问题在于,class 和 object 并没有真正的不同。它们其实是同样的东西,只是在不同的时间名字不同罢了。我用禅语来解释一下吧:
鱼和三文鱼有什么区别?

这个问题有没有让你有点晕呢?说真的,坐下来想一分钟。我的意思是说,鱼和三文鱼是不一样,不过它们其实也是一样的是不是?三文鱼是鱼的一种,所以说没什么不同,不过三文鱼又有些特别,它和别的种类的鱼的确不一样,比如三文鱼和大比目鱼就不一样。所以三文鱼和鱼既相同又不同。怪了。

这个问题让人晕的原因是大部分人不会这样去思考问题,其实每个人都懂这一点,你无须去思考鱼和三文鱼的区别,因为你知道它们之间的关系。你知道三文鱼是鱼的一种,而且鱼还有别的种类,根本就没必要去思考这类问题。

让我们更进一步,假设你有一只水桶,里边有三条三文鱼。假设你的好人卡多到没地方用,于是你给它们分别取名叫 Frank, Joe,和 Mary。现在想想这个问题:
Mary 和三文鱼有什么区别?

这个问题一样的奇怪,但比起鱼和三文鱼的问题来还好点。你知道 Mary 是一条三文鱼,所以他并没什么不同,他只是三文鱼的一个“实例(instance)”。Frank 和 Joe 一样也是三文鱼的实例。我的意思是说,它们是由三文鱼创建出来的,而且代表着和三文鱼一样的属性。

所以我们的思维方式是(你可能会有点不习惯):鱼是一个“类(class)”,三文鱼是一个“类(class)”,而 Mary 是一个“对象(object)”。仔细想想,然后我再一点一点慢慢解释给你。

鱼是一个“类”,表示它不是一个真正的东西,而是一个用来描述具有同类属性的实例的概括性词汇。 你有鳍?你有鳔?你住在水里?好吧那你就是一条鱼。

后来河蟹养殖专家路过,看到你的水桶,于是告诉你:“小伙子,你这些鱼是三文鱼。” 并且专家还定义了一个新的叫做“三文鱼”的“类”,而这个“类”又有它特定的属性。长鼻子?浅红色的肉?生活在海洋里?吃起来味道还可以?那你就是一条三文鱼。

最后一个厨师过来了,他跟专家说:“非也非也,你看到的是三文鱼,我看到的是 Mary,而且我要把 Mary 和剁椒配一起做一道小菜。”于是你就有了一只叫做 Mary 的三文鱼的“实例(instance)”(三文鱼也是鱼的一个“实例”),并且你使用了它,这样它就是一个“对象(object)”。

这会你应该了解了:Mary 是三文鱼的成员,而三文鱼又是鱼的成员。这里的关系式:对象属于某个类,而某个类又属于另一个类。

写成代码是什么样子


这个概念有点绕,不过实话说,你只要在创建和使用 class 的时候操心一下就可以了。我来给你两个区分 Class 和 Object 的小技巧。

首先针对类和对象,你需要学会两个说法,“is-a(是啥)”和“has-a(有啥)”。“是啥”要用在谈论“两者以类的关系互相关联”的时候,而“有啥”要用在“两者无共同点,仅是互为参照”的时候。

接下来,通读这段代码,将每一个注释为##?? 的位置标明他是“is-a”还是“has-a”的关系,并讲明白这个关系是什么。在代码的开始我还举了几个例子,所以你只要写剩下的就可以了。

记住,“是啥”指的是鱼和三文鱼的关系,而“有啥”指的是三文鱼和鳃的关系。
## Animal is-a object (yes, sort of confusing) look at the extra credit
class Animal(object):
    pass

## ??
class Dog(Animal):

    def __init__(self, name):
        ## ??
        self.name = name

## ??
class Cat(Animal):

    def __init__(self, name):
        ## ??
        self.name = name

## ??
class Person(object):

    def __init__(self, name):
        ## ??
        self.name = name

        ## Person has-a pet of some kind
        self.pet = None

## ??
class Employee(Person):

    def __init__(self, name, salary):
        ## ?? hmm what is this strange magic?
        super(Employee, self).__init__(name)
        ## ??
        self.salary = salary

## ??
class Fish(object):
    pass

## ??
class Salmon(Fish):
    pass

## ??
class Halibut(Fish):
    pass

## rover is-a Dog
rover = Dog("Rover")

## ??
satan = Cat("Satan")

## ??
mary = Person("Mary")

## ??
mary.pet = satan

## ??
frank = Employee("Frank", 120000)

## ??
frank.pet = rover

## ??
flipper = Fish()

## ??
crouse = Salmon()

## ??
harry = Halibut()

关于 class Name(object)


记得我曾经强迫让你使用 class Name(object) 却没告诉你为什么吧,现在你已经知道了“类”和“对象”的区别,我就可以告诉你原因了。如果我早告诉你的话,你可能会晕掉,也学不会这门技术了。

真正的原因是在 Python 早期,它对于 class 的定义在很多方面都是严重有问题的。当他们承认这一点的时候已经太迟了,所以逼不得已,他们需要支持这种有问题的 class。为了解决已有的问题,他们需要引入一种“新类”,这样的话“旧类”还能继续使用,而你也有一个新的正确的类可以使用了。

这就用到了“类即是对象”的概念。他们决定用小写的“object”这个词作为一个类,让你在创建新类时从它继承下来。有点晕了吧?一个类从另一个类继承,而后者虽然是个类,但名字却叫“object”……不过在定义类的时候,别忘记要从 object 继承就好了。

的确如此。一个词的不同就让这个概念变得更难理解,让我不得不现在才讲给你。现在你可以试着去理解“一个是对象的类”这个概念了,如果你感兴趣的话。

不过我还是建议你别去理解了,干脆完全忘记旧格式和新格式类的区别吧,就假设 Python 的 class 永远都要求你加上 (object) 好了,你的脑力要留着思考更重要的问题。

附加题


1.研究一下为什么 Python 添加了这个奇怪的叫做 object 的类,它究竟有什么含义呢? 2.有没有办法把 Class 当作 Object 使用呢? 3.在习题中为 animals、fish、还有 people 添加一些函数,让它们做一些事情。看看当函数在 Animal 这样的“基类(base class)”里和在 Dog 里有什么区别。 4.找些别人的代码,理清里边的“是啥”和“有啥”的关系。 5.使用列表和字典创建一些新的一对应多的“has-many”的关系。 6.你认为会有一种“has-many”的关系吗?阅读一下关于“多重继承(multiple inheritance)”的资料,然后尽量避免这种用法。


exercise43.基本的面向对象的分析和设计


在这节练习,我想给你介绍一个使用 python 创建某类东西的过程,也就是“面向对象编程”(OOP)。我把它叫做一个过程,是因为我将给出一系列按顺序进行的步骤,但是你也不应该死板的遵循这个步骤,企图用它解决所有难题。它们对于许多编程问题只是一个良好的开头,而不应该被认为是解决这些问题的唯一方法。这个过程只是一个你可以遵循的方法:
1.写出或画出你的问题 2.从 1 中提炼关键问题并搜索相关资料 3.为 2 中的问题创建一个有层次结构的类和对象映射 4.编写类和测试代码,并保证他们运行 5.重复并精炼

按照这个顺序执行流程,叫做“自顶向下”的方式,意思是说,它从非常抽象宽松的想法开始,然后慢慢提炼,直到想法是坚实的东西,然后你再开始编码。

首先我只是写出这个问题,并试图想出任何我想要的东西就可以了。也许我会画一两个图表,或者某种可能的地图,甚至给自己写了一系列的电子邮件描述这个问题。这给了我表达了问题的关键概念的方法,并探测出我对于这个游戏有什么具体的想法和了解。

接下来,我浏览这些笔记,图表以及描述,通过这些记录我找到我需要的关键问题点。有一个简单的技巧:简单地列出你的笔记和图表中所有的名词和动词,然后写出他们是如何相关的。这一步其实也我为我下一步要写的类、对象、以及函数等提供了命名列表。我利用这个概念列表,研究任何我不明白的地方,如果我需要,我还可以进一步完善它们。

一旦我完成这个列表,我可以创建的一个简单的轮廓/树用来说明这些概念之间的关系。你还可以对着你的名词列表并询问“这个名词和其他的是一个概念吗?或者它们有一个通用的父类吗,那它们的父类是什么?”持续检查,直到你得到一个有层次结构的类,它应该像一棵简单的树或者图表。然后检查你的动词列表,看它们是否可以作为函数的名字添加到你的类树种。

随着这棵类树的生成,我坐下来写一些基本的框架代码,代码中只包括刚才提到的类,类中包含的函数。然后我再写一个测试用例,用来检验我刚才写的类是正确的。有时候我可能只需要在开始写一段测试代码,但是更多的时候,我需要写一段测试,再写一段代码,再写一段测试...直到整个项目完成。

最后,在我完成更多的工作之前,我周期性的重复和精炼这个流程,是他变得清楚和易于理解。如果我在一个没有预料到的地方被一些概念或问题卡住,我会停下来在这一小部分上投入更大的精力来分析解决,直到我解决了这问题,再继续下去。

这节练习中,我将通过建造一个游戏引擎带大家学习这一流程。

分析一个简单的游戏引擎


我打算制作一个叫做 "Gothons from Planet Percal #25"的游戏,这个一个小型的太空冒险游戏。

写或画出这个问题


我为这个游戏写了一小段描述:

“外星人乘坐一个宇宙飞船入侵,我们的英雄需要通过一个迷宫似的房间击败他们,然后他才能逃入一个逃生舱到达下面的行星。游戏将更像一个有着文本输出和有趣的死法的 Zork 或冒险类型游戏。游戏将包括一个引擎运行充满房间或场景的地图。 当玩家进入房间,每个房间将打印自己的描述,然后告诉引擎运行下一个地图。”

我先描述每个场景:

Death : 这是玩家死亡的场景,应该是有趣的。

Central Corridor : 这是游戏的起点,在这里已经有一个外星人等待英雄的到来,它们需要被一个玩笑打败。

Laser Weapon Armory : 这是英雄得到了中子弹获得的逃生舱之前要炸毁的船。它有一个需要英雄猜数的键盘。

The Bridge : 和外星人战斗的另一个场景,英雄防止炸弹的地方。

Escape Pod : 英雄逃脱的场景,但是需要英雄找对正确的逃生舱

现在,我可以绘制出它们的地图,或者对每个房间再多写一些描述,或者其他一些我脑中出现的想法。

提取关键概念并研究他们


现在我已经有足够多的信息还提取出名词,并分析它们的类结构。首先我列出所有的名词:


  • Alien

  • Player

  • Ship

  • Maze

  • Room

  • Scene

  • Gothon

  • Escape Pod

  • Planet

  • Map

  • Engine

  • Death

  • Central Corridor

  • Laser Weapon Armory

  • The Bridge


我也可能要列出所有的动词,看它们能否作为函数的名字,不过现在我要先跳过这一步。

你需要研究所有你现在还不知道的概念。例如,我可能会玩几个这种类型的游戏,并确保知道他们是如何工作的。我可能会研究船舶和炸弹的设计和工作原理。也许我会研究怎么样将游戏中的数据或状态存储在数据库等一些技术上的问题。我做完这个研究之后,我可能会回到第 1 步重新开始,根据新的信息,重写描述和提取新的概念。

创建一个层次结构的类和对象映射的概念


当我完成上面的步骤,我通过思考“哪些是类似于其他东西的”,把名词列表转换成一个有层次结构的类树,同样,我也会思考,哪些单词是其他东西的基础呢?

马上我发现,"Room" 和 "Scene"对于我要做的事情来说基本上是一样的事情。在这个游戏中,我选择使用"Scene" 。接下来我发现,所有的特殊房间比如"Central Corridor"基本上也跟"Scene"是一样的。我发现"Death"也是一个"Scene", 由于我选择了"Scene"而不是"Room",你可以有一个“死亡场景”,而是不一个很奇怪的“死亡房间”。"Maze"和"Map"基本一样,所以我选择使用"Map",因为我更多的时候都用它。我不想做一个战斗系统,所以我会先忽略"Alien"和 "Player",但是会把他们保存下来以供以后使用。"Planet"也可能仅仅是另一个场景,而不是什么具体的事情。

在此之后,我开始创建一个层次结构的类:


  • Map

  • Engine

  • Scene

  • Death

  • Central Corridor

  • Laser Weapon Armory

  • The Bridge

  • Escape Pod


然后,我会找出说明中每个动词都需要什么行动。比如,我从说明中得知,我需要一个方法来"run"这个引擎,通过地图获得下一个场景"get the next scene",获得"opening scene",或者"enter"一个场景。我把这些加到类树里:


  • Map - next_scene - opening_scene

  • Engine - play

  • Scene - enter Death Central Corridor Laser Weapon Armory The Bridge * Escape Pod


注意一下,我只是把-enter 放到 Scene 下面,因为我知道所有的场景都会继承它并重写它。

编码类和测试代码并运行它们


当我有了这个类树和一些功能之后,我在编辑器中打开源文件,并尝试编写代码。通常,我会复制粘贴这棵类树到源文件中,然后编辑它。下面是一个如何编码的小例子,文件末尾包含一个简单的测试用例:
class Scene(object):

    def enter(self):
        pass

class Engine(object):

    def __init__(self, scene_map):
        pass

    def play(self):
        pass

class Death(Scene):

    def enter(self):
        pass

class CentralCorridor(Scene):

    def enter(self):
        pass

class LaserWeaponArmory(Scene):

    def enter(self):
        pass

class TheBridge(Scene):

    def enter(self):
        pass

class EscapePod(Scene):

    def enter(self):
        pass

class Map(object):

    def __init__(self, start_scene):
        pass

    def next_scene(self, scene_name):
        pass

    def opening_scene(self):
        pass

a_map = Map('central_corridor')
a_game = Engine(a_map)
a_game.play()

在这个文件中,你可以看到我只是复制了我想要的层次结构,然后一点点的补齐代码再运行它,看看它在这个基本结构中是否运行顺利。 在这节练习后面的部分,你会填补这段代码的其余部分,使其正常工作,以配合练习开头的游戏描述。

重复并精炼


在我提供的流程中,最后一步并不是实际意义上的一步,而是要做一个循环。在编程的世界里,你永远做不到一次通过,相反,你退回整个过程,并再次根据你从后面的步骤中了解到的信息完善它。有时候我已经到了第 3 步,但是我发现我还需要在第 1、2 步做更多工作,我会停下来并返回去完善它。有时候我也会突然灵光一闪,跳到最后,用我脑子里更好的解决方案编码实现,但是之后,我仍然会回去完成前面的步骤,以确保我的工作覆盖了所有的可能性。

在这一过程的另一个观点是,你不是仅在一个层面上使用这个流程,当你遇到某些特定问题的时候,你可以在任意一个层级上使用该流程。比方说,我不知道如何写 Engine.play 方法,我可以静下心来用这个流程只做这一种功能,直到弄清楚这个方法怎么写。

自顶向下和自下而上


因为这个流程在最抽象的概念(顶部)开始,然后再下降到实际执行过程中,因此这一流程通常标示为“自上而下”。我希望你使用这一流程,但你也应该知道,还有另一种方式来解决程序中的问题,这种方式是从代码开始,再“上升”到抽象的概念问题。这种方式被称为“自下而上”。下面是自下而下方式所遵循的步骤:
1.取一小块问题,编写一些代码,并让他勉强运行 2.完善代码,将其转换成一些更正式的包含类和自动化测试的代码。 3.提取其中的关键概念,并尝试找出研究他们。 4.写出到底发生了什么的描述。 5.继续完善代码,也可能是把它扔掉,并重新开始。 6.移动到其他问题上,重复步骤。

当你需要更优质的代码,并在代码中更自然的思考你要解决的问题时,这种方式更好一些。尤其是当你知道小块的难题,但没有足够的信息把握整个概念的时候,这个方式是一个解决问题很好的办法。将问题分解成碎片并探索代码,直到你解决这个问题。然而,你解决问题的途径可能是缓慢而曲折的,所以,我的这一流程中也包含后退并反复研究问题,直到你通过自己所为解决所有难题。

exercise44.继承 Vs.包含


什么是继承


继承是用来描述一个类从它的父类那里获得大部分甚至全部父类的功能。当你写下 class Foo(Bar)的时候,就发生了继承,这句代码的意思是“创建一个叫做 Foo 的类,并继承 Bar” 。当你执行这句的时候,编程语言使得 Foo 的实例所有的行为都跟 Bar 的实例一样。这么做,可以让你在类 Bar 中放一些通用功能,而那些需要特殊定制的函数或功能可以放在类 Foo 中。

当你需要做这些特殊化函数编写的时候,父子类之间有 3 中交互方法:
1.子类的方法隐性继承父类方法 2.子类重写父类的方法 3.对子类的操作改变父类

我将按顺序给你展示这 3 种交互方式,并给你看它们的代码。

隐性继承


首先,我将给你展示的是隐性继承发生在你在父类中定义了方法,而在子类中没有:
class Parent(object):

    def implicit(self):
        print "PARENT implicit()"

class Child(Parent):
    pass

dad = Parent()
son = Child()

dad.implicit()
son.implicit()

在 class Child 下使用 pass 的目的是告诉 Python,在这里你想要一个空白的块。这里创建了一个叫做 Child 的类,但是没有在这个类中定义任何性的方法。它将从类 Parent 继承获得所有的方法。当你执行这段代码的时候,你会看到:
$ python ex44a.py
PARENT implicit()
PARENT implicit()

注意一下,尽管我再代码的 13 行调用了 son.implicit(),而类 Child 并没有一个定义叫做 implicit 的方法,代码仍然正常工作了,因为它调用了 Parent 中定义的同名方法。这个高速你的是:如果你在基类(i.e., Parent)中定义了一个方法,那么所有的子类(i.e., Child) 都可以自动的获得这个功能。对于你需要在很多类中有重复的方法来说,非常方便。

重写方法


关于有些方法是隐式调用的问题原因在于有时候你想让子类有不同的表现。这时你想重写子类中的方法且有效的覆盖父类中的方法。要做到这一点,你只需要在子类中定义一个同名的方法就行,例如
class Parent(object):

    def override(self):
        print "PARENT override()"

class Child(Parent):

    def override(self):
        print "CHILD override()"

dad = Parent()
son = Child()

dad.override()
son.override()

在这个例子中,两个类中我都有一个叫做 override 的方法,所以让我们来看看当你运行此例时都发生了什么
$ python ex44b.py
PARENT override()
CHILD override()

你可以看到,当第 14 行执行时,它执行了父类的 override 方法,因为变量 dad 是一个父类实例,但是当第 15 行执行时,打印的是子类的 override 方法,这是因为 son 是一个子类的实例,这个子类重写了那个方法,定义了自己的版本。

休息以下,在继续下面的内容之前,尝试练习这两种方法。

之前或之后改变父类


第三种使用继承的方式比较特别,你想在父类版本的方法执行行为前后给出些提示,你首先像上个例子那样重写了方法,接着你使用一个 Python 内建的叫做 super 的方法得到了父类版本的方法调用。以下这个例子就是这么做的,你可以感受一下上面的描述是什么意思。
class Parent(object):

    def altered(self):
        print "PARENT altered()"

class Child(Parent):

    def altered(self):
        print "CHILD, BEFORE PARENT altered()"
        super(Child, self).altered()
        print "CHILD, AFTER PARENT altered()"

dad = Parent()
son = Child()

dad.altered()
son.altered()

重要的地方在于 9-11 行,在 son.altered()被调用前我做了如下操作:
1.因为我已经重写了子类的 Child.altered 方法,第 9 行的运行结果应该和你的期待是一样 2.在这个例子中,我打算使用 super 来得到父类的 Parent.altered 版本。 3.在第 10 行,我调用了 super(Child, self).altered()方法, 这个方法能够意识到继承的发生,并给你获得类 Parent。你可以这样读这行代码“调用 super,参数为 Child 和 self,然后不管它返回什么,调用方法 altered ” 4.在这种情况下,父类的 Parent.altered 版本执行,并打印出父类的信息。 5.最后,代码从 Parent.altered 返回, Child.altered 方法继续打印出剩下的信息。

运行了程序之后,你应该能看到下面的内容:
$ python ex44c.py
PARENT altered()
CHILD, BEFORE PARENT altered()
PARENT altered()
CHILD, AFTER PARENT altered()

三种组合使用


为了论证上面的三种方式,我有一个最终版本的文件,该文件给你展示了每一种继承方式的交互:
class Parent(object):

    def override(self):
        print "PARENT override()"

    def implicit(self):
        print "PARENT implicit()"

    def altered(self):
        print "PARENT altered()"

class Child(Parent):

    def override(self):
        print "CHILD override()"

    def altered(self):
        print "CHILD, BEFORE PARENT altered()"
        super(Child, self).altered()
        print "CHILD, AFTER PARENT altered()"

dad = Parent()
son = Child()

dad.implicit()
son.implicit()

dad.override()
son.override()

dad.altered()
son.altered()

通读每一行代码,不管有没有被重写,写一个注释用来解释每一行实现了什么,然后将代码运行起来,确认你的结果和你的期望是否一致:
$ python ex44d.py
PARENT implicit()
PARENT implicit()
PARENT override()
CHILD override()
PARENT altered()
CHILD, BEFORE PARENT altered()
PARENT altered()
CHILD, AFTER PARENT altered()

使用 super()的原因


这看起来应该是常识,但是随后我们即将进入一个叫做所多重继承的麻烦事。 多重继承是指当你定义一个的类的时候,从一个或多个类继承,例如:
class SuperFun(Child, BadStuff):
    pass

上述代码的意思是, "创建一个叫做 SuperFun 的类,它同时继承类 Child 和类 BadStuff ."

在这个例子中,只要你隐式的调用任何 SuperFun 的实例,Python 把必须从类 Child 和 BadStuff 查询可能的函数,但是,查找也需要一个顺序。为了做到这点,Python 使用"方法解析顺序"(MRO)和一种叫做 C3 的运算法则来直接获得。

因为 MRO 是复杂的,并使用了明确定义的算法,Python 不能让你来获得正确的 MRO,相反的,Python 提供给你 super()方法,它用来处理所有这一切你需要改变类型的行为,如同我在 Child.altered 所实现的。使用 super()你不必担心得到的是否是正确的方法,Python 会帮你找到正确的那个。

init中使用 super()


super()最常见的用途是在基类的init方法里。这通常是你需要在子类里实现什么事情,然后完成父类初始化的地方。以下是在类 Child 中这样做的一个简单的例子:
class Child(Parent):

    def __init__(self, stuff):
        self.stuff = stuff
        super(Child, self).__init__()

除了我在init中初始化父类之前定义了一些变量,这个跟上面的例子 Child.altered 几乎是一样的。

包含


继承是有用的, 但另一种方式仅仅是用其他类和模块就做到了同样的事情, 而没有使用隐性继承。如果你看一下使用继承的三种方式,其中的两种方法涉及编写新的代码来替换或改变父类功能。这可以很容易地通过调用模块函数复制。下面是一个例子:
class Other(object):

    def override(self):
        print "OTHER override()"

    def implicit(self):
        print "OTHER implicit()"

    def altered(self):
        print "OTHER altered()"

class Child(object):

    def __init__(self):
        self.other = Other()

    def implicit(self):
        self.other.implicit()

    def override(self):
        print "CHILD override()"

    def altered(self):
        print "CHILD, BEFORE OTHER altered()"
        self.other.altered()
        print "CHILD, AFTER OTHER altered()"

son = Child()

son.implicit()
son.override()
son.altered()

在这段代码中,我没有使用名字 Parent,因为没有父子的 is-a 关系了。这是一个 has-a 关系,在这个关系中 Child has-a Other 被用来保证代码的正常工作。当我运行代码时,看到以下输出:
$ python ex44e.py
OTHER implicit()
CHILD override()
CHILD, BEFORE OTHER altered()
OTHER altered()
CHILD, AFTER OTHER altered()

你可以看到 Child 和 Other 中的大部分代码实现了相同的功能。唯一的不同之处在于我必须定义一个 Child.implicit 方法去做一个动作. 然后我可以问问自己,如果我需要一个 Other 类,是不是只要把它放在一个叫做 other.py 的模块中就可以?

什么时候用继承,什么时候用包含


继承与包含的问题可以归结为试图解决可重复使用代码的问题。你不想在你的软件中有重复的代码,因为这不是高效的干净的代码。继承通过创建一种机制,让你在基类中有隐含的功能来解决这个问题。而包含则是通过给你的模块和函数可以在其他类别被调用来解决这个问题。

如果这两种方案都能解决代码复用问题的话,哪一个更合适呢?答案是主观的,这看起来是令人难以相信的,但是我会给你 3 个指导性原则:
1.不惜一切代价避免多重继承,因为它太复杂太不可靠。如果你必须要使用它,那么一定要知道类的层次结构,并花时间找到每一个类是从哪里来的。 2.将代码封装为模块,这样就可以在许多不同的地方或情况使用。 3.只有当有明显相关的可重用的代码,且在一个共同概念下时,可以使用继承。

不要变成规则的奴隶。关于面向对象编程要记住的是:这是一个程序员创建打包和共享代码的社会习俗。因为它是一个社会习俗,但在 Python 的法典中,你可能会因为跟你合作的人而被迫避开这些规则。在这种情况下,了解他们是如何使用规则的,然后去适应形势。

附加题


这节练习中只有一个附加题,因为这其实是一个很大的练习。阅读 http://www.python.org/dev/peps/pep-0008/ 并尝试将它应用到你的代码中。你会发现,有些内容跟你从这本书学到的不同,但是现在你应该能够理解他们的建议,并将其应用到自己的代码中。本书中剩余部分的代码可能会也可能不会遵循这些准则,这个要取决于这些准则是否会使代码更加混乱。我建议你也这样做,因为理解比让大家都对你深奥的知识有印象更重要。