Python-入门指南第二版-四-

60 阅读50分钟

Python 入门指南第二版(四)

原文:annas-archive.org/md5/4b0fd2cf0da7c8edae4b5ecfd40159bf

译者:飞龙

协议:CC BY-NC-SA 4.0

第十章:哦哦:对象和类

没有神秘的对象。神秘的是你的眼睛。

伊丽莎白·鲍文

拿一个对象。对它做点什么。再对它做点别的事。

贾斯珀·约翰斯

正如我在各个页面中提到的,Python 中的所有东西,从数字到函数,都是对象。但是,Python 通过特殊语法隐藏了大部分对象机制。你可以输入num = 7来创建一个类型为整数、值为 7 的对象,并将对象引用分配给名称num。只有当你想要创建自己的对象或修改现有对象的行为时,你才需要查看对象的内部。在本章中,你将看到如何执行这两个操作。

什么是对象?

一个对象是一个自定义的数据结构,包含数据(变量,称为属性)和代码(函数,称为方法)。它代表某个具体事物的唯一实例。把对象看作名词,它的方法是动词。一个对象代表一个个体,它的方法定义了它与其他事物的交互方式。

例如,值为7的整数对象是一个对象,可以执行加法和乘法等方法,就像你在第三章中看到的那样。8是另一个对象。这意味着 Python 中某处内置了一个整数类,78都属于这个类。字符串'cat''duck'也是 Python 中的对象,具有你在第五章中看到的字符串方法,如capitalize()replace()

与模块不同,你可以同时拥有多个对象(通常称为实例),每个对象可能具有不同的属性。它们就像是超级数据结构,内含代码。

简单的对象

让我们从基本对象类开始;我们将在几页后讨论继承。

使用 class 定义一个类

要创建一个以前从未创建过的新对象,首先需要定义一个,指示它包含什么内容。

在第二章中,我将对象比作一个塑料盒。一个就像是制造那个盒子的模具。例如,Python 有一个内置类,用来创建字符串对象如'cat''duck',以及其他标准数据类型——列表、字典等等。要在 Python 中创建自定义对象,首先需要使用class关键字定义一个类。让我们通过一些简单的示例来详细了解。

假设你想定义对象来表示关于猫的信息。¹ 每个对象将代表一只猫。你首先需要定义一个名为Cat的类作为模板。在接下来的示例中,我们将尝试多个版本的这个类,从最简单的类逐步构建到真正有用的类。

注意

我们遵循 Python 的命名约定PEP-8

我们的第一个尝试是最简单的类,一个空类:

>>> class Cat():
...     pass

你也可以说:

>>> class Cat:
...     pass

就像函数一样,我们需要使用pass来指示这个类是空的。这个定义是创建对象的最低要求。

通过像调用函数一样调用类名,你可以从类创建一个对象:

>>> a_cat = Cat()
>>> another_cat = Cat()

在这种情况下,调用Cat()创建了两个来自Cat类的单独对象,并将它们分配给了名称a_catanother_cat。但是我们的Cat类没有其他代码,所以我们从它创建的对象只是存在,不能做太多其他事情。

嗯,它们可以做一点点。

属性

属性是类或对象内部的变量。在创建对象或类期间以及之后,你可以给它赋予属性。属性可以是任何其他对象。让我们再次创建两个猫对象:

>>> class Cat:
...     pass
...
>>> a_cat = Cat()
>>> a_cat
<__main__.Cat object at 0x100cd1da0>
>>> another_cat = Cat()
>>> another_cat
<__main__.Cat object at 0x100cd1e48>

当我们定义Cat类时,并没有指定如何打印来自该类的对象。Python 则会打印类似 <__main__.Cat object at 0x100cd1da0> 的东西。在“魔术方法”中,你会看到如何改变这个默认行为。

现在给我们的第一个对象分配一些属性:

>>> a_cat.age = 3
>>> a_cat.name = "Mr. Fuzzybuttons"
>>> a_cat.nemesis = another_cat

我们能访问它们吗?我们当然希望如此:

>>> a_cat.age
3
>>> a_cat.name
'Mr. Fuzzybuttons'
>>> a_cat.nemesis
<__main__.Cat object at 0x100cd1e48>

因为nemesis是指向另一个Cat对象的属性,我们可以使用a_cat.nemesis来访问它,但是这个其他对象还没有name属性:

>>> a_cat.nemesis.name
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'Cat' object has no attribute 'name'

让我们给我们的大猫起个名字:

>>> a_cat.nemesis.name = "Mr. Bigglesworth"
>>> a_cat.nemesis.name
'Mr. Bigglesworth'

即使是像这样的最简单的对象也可以用来存储多个属性。因此,你可以使用多个对象来存储不同的值,而不是使用类似列表或字典的东西。

当你听到attributes(属性)时,通常指的是对象的属性。还有class attributes(类属性),稍后你会在“类和对象属性”中看到它们的区别。

方法

方法是类或对象中的函数。方法看起来像任何其他函数,但可以用特殊的方式使用,你将在“属性访问的属性”和“方法类型”中看到。

初始化

如果你想在创建时分配对象属性,你需要使用特殊的 Python 对象初始化方法__init__()

>>> class Cat:
...     def __init__(self):
...         pass

这就是你在真实的 Python 类定义中会看到的内容。我承认__init__()self看起来很奇怪。__init__()是 Python 中一个特殊的方法名,用于初始化一个类定义中的单个对象。² self参数指定它是指向个体对象本身。

当你在类定义中定义__init__()时,它的第一个参数应该命名为self。虽然self在 Python 中不是一个保留字,但它是常见的用法。如果你使用self,日后阅读你的代码的人(包括你自己!)不会猜测你的意图。

但是即使是这第二个Cat类定义也没有创建一个真正做任何事情的对象。第三次尝试是真正展示如何在 Python 中创建简单对象并分配其属性的方法。这次,我们将name参数添加到初始化方法中:

>>> class Cat():
...     def __init__(self, name):
...         self.name = name
...
>>>

现在,我们可以通过为name参数传递一个字符串来从Cat类创建一个对象:

>>> furball = Cat('Grumpy')

下面是这行代码的作用:

  • 查找Cat类的定义

  • 实例化(创建)一个新的内存对象

  • 调用对象的__init__()方法,将这个新创建的对象作为self传递,并将另一个参数('Grumpy')作为name传递

  • name的值存储在对象中

  • 返回新对象

  • 将变量furball附加到对象上

这个新对象像 Python 中的任何其他对象一样。你可以将它用作列表、元组、字典或集合的元素,可以将它作为参数传递给函数,或将它作为结果返回。

那么我们传入的name值呢?它以属性的形式保存在对象中。你可以直接读取和写入它:

>>> print('Our latest addition: ', furball.name)
Our latest addition: Grumpy

记住,在Cat类定义的内部,你通过self.name访问name属性。当你创建一个实际对象并将其赋给像furball这样的变量时,你可以使用furball.name来引用它。

并非每个类定义都必须有一个__init__()方法;它用于执行任何需要区分此对象与同类其他对象的操作。它并不是某些其他语言所称的“构造函数”。Python 已经为你构造好了对象。将__init__()视为初始化方法

注意

你可以从一个类创建许多个体对象。但要记住,Python 将数据实现为对象,因此类本身也是一个对象。但是,在你的程序中只有一个类对象。如果像我们这里定义了class Cat,它就像《猎魔人》一样——只能有一个。

继承

当你试图解决某个编程问题时,通常会发现一个现有的类可以创建几乎符合你需求的对象。你能做什么?

你可以修改这个旧类,但会使它变得更加复杂,并且可能会破坏一些曾经工作的功能。

或者你可以编写一个新类,从旧类中剪切和粘贴代码并合并新代码。但这意味着你需要维护更多的代码,并且原来和新类中曾经相同的部分可能会因为它们现在位于不同的位置而有所不同。

一个解决方案是继承:从现有类创建一个新类,并进行一些添加或更改。这是代码重用的一个很好的方式。使用继承时,新类可以自动使用旧类的所有代码,而不需要你复制任何代码。

继承自父类

你只需定义新类中需要添加或更改的内容,这样就可以覆盖旧类的行为。原始类称为父类超类基类;新类称为子类子类派生类。这些术语在面向对象编程中是可以互换使用的。

所以,让我们继承一些东西。在下一个例子中,我们定义一个空类称为 Car。接下来,我们定义 Car 的一个子类称为 Yugo。³ 您可以使用相同的 class 关键字,但在括号中使用父类名称(这里是 class Yugo(Car))来定义子类:

>>> class Car():
...     pass
...
>>> class Yugo(Car):
...     pass
...

您可以使用 issubclass() 来检查一个类是否派生自另一个类:

>>> issubclass(Yugo, Car)
True

接下来,从每个类创建一个对象:

>>> give_me_a_car = Car()
>>> give_me_a_yugo = Yugo()

子类是父类的一种特殊化;在面向对象的术语中,Yugo 是一个 Car。名为 give_me_a_yugo 的对象是 Yugo 类的实例,但它也继承了 Car 的所有功能。在这种情况下,CarYugo 就像潜水艇上的水手一样有用,因此让我们尝试实际做点事的新类定义:

>>> class Car():
...     def exclaim(self):
...         print("I'm a Car!")
...
>>> class Yugo(Car):
...     pass
...

最后,分别从每个类创建一个对象并调用 exclaim 方法:

>>> give_me_a_car = Car()
>>> give_me_a_yugo = Yugo()
>>> give_me_a_car.exclaim()
I'm a Car!
>>> give_me_a_yugo.exclaim()
I'm a Car!

不做任何特殊处理,Yugo 继承了 Carexclaim() 方法。事实上,Yugo 表示它一辆 Car,这可能导致身份危机。让我们看看我们能做些什么。

注意

继承很吸引人,但可能被滥用。多年的面向对象编程经验表明,过多使用继承会使程序难以管理。相反,通常建议强调其他技术,如聚合和组合。我们在本章中介绍这些替代方法。

覆盖一个方法

正如你刚才看到的,一个新类最初会从其父类继承所有东西。接下来,您将看到如何替换或覆盖父类方法。Yugo 可能在某种方式上应该与 Car 不同;否则,定义一个新类有什么意义?让我们改变 exclaim() 方法在 Yugo 中的工作方式:

>>> class Car():
...     def exclaim(self):
...         print("I'm a Car!")
...
>>> class Yugo(Car):
...     def exclaim(self):
...         print("I'm a Yugo! Much like a Car, but more Yugo-ish.")
...

现在从这些类中创建两个对象:

>>> give_me_a_car = Car()
>>> give_me_a_yugo = Yugo()

他们说什么?

>>> give_me_a_car.exclaim()
I'm a Car!
>>> give_me_a_yugo.exclaim()
I'm a Yugo! Much like a Car, but more Yugo-ish.

在这些示例中,我们覆盖了 exclaim() 方法。我们可以覆盖任何方法,包括 __init__()。这里有另一个使用 Person 类的例子。让我们创建代表医生(MDPerson)和律师(JDPerson)的子类:

>>> class Person():
...     def __init__(self, name):
...         self.name = name
...
>>> class MDPerson(Person):
...     def __init__(self, name):
...         self.name = "Doctor " + name
...
>>> class JDPerson(Person):
...     def __init__(self, name):
...         self.name = name + ", Esquire"
...

在这些情况下,初始化方法 __init__() 接受与父类 Person 相同的参数,但在对象实例内部以不同的方式存储 name 的值:

>>> person = Person('Fudd')
>>> doctor = MDPerson('Fudd')
>>> lawyer = JDPerson('Fudd')
>>> print(person.name)
Fudd
>>> print(doctor.name)
Doctor Fudd
>>> print(lawyer.name)
Fudd, Esquire

添加一个方法

子类还可以添加在其父类中不存在的方法。回到 CarYugo 类,我们将为仅 Yugo 类定义新方法 need_a_push()

>>> class Car():
...     def exclaim(self):
...         print("I'm a Car!")
...
>>> class Yugo(Car):
...     def exclaim(self):
...         print("I'm a Yugo! Much like a Car, but more Yugo-ish.")
...     def need_a_push(self):
...         print("A little help here?")
...

接下来,创建一个 Car 和一个 Yugo

>>> give_me_a_car = Car()
>>> give_me_a_yugo = Yugo()

Yugo 对象可以对 need_a_push() 方法调用做出反应:

>>> give_me_a_yugo.need_a_push()
A little help here?

但是一个普通的 Car 对象不能:

>>> give_me_a_car.need_a_push()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'Car' object has no attribute 'need_a_push'

到这一点,一个 Yugo 能做一些 Car 不能的事情,Yugo 的独特个性可以显现出来。

通过 super() 从父类获得帮助

我们看到子类如何添加或覆盖父类的方法。如果它想调用那个父类方法呢?“我很高兴你问”,super() 这里,我们定义了一个名为 EmailPerson 的新类,代表一个带有电子邮件地址的 Person。首先是我们熟悉的 Person 定义:

>>> class Person():
...     def __init__(self, name):
...         self.name = name
...

请注意,以下子类中的__init__()调用具有额外的email参数:

>>> class EmailPerson(Person):
...     def __init__(self, name, email):
...         super().__init__(name)
...         self.email = email

当为类定义__init__()方法时,您正在替换其父类的__init__()方法,后者不再自动调用。因此,我们需要显式调用它。以下是发生的情况:

  • super()获取父类Person的定义。

  • __init__()方法调用了Person.__init__()方法。它负责将self参数传递给超类,因此您只需提供任何可选参数。在我们的情况下,Person()接受的唯一其他参数是name

  • self.email = email行是使这个EmailPersonPerson不同的新代码。

继续,让我们制作其中的一个生物:

>>> bob = EmailPerson('Bob Frapples', 'bob@frapples.com')

我们应该能够访问nameemail属性:

>>> bob.name
'Bob Frapples'
>>> bob.email
'bob@frapples.com'

为什么我们不直接定义我们的新类如下?

>>> class EmailPerson(Person):
...     def __init__(self, name, email):
...         self.name = name
...         self.email = email

我们本可以这样做,但那样会破坏我们对继承的使用。我们使用super()使Person执行其工作,就像一个普通的Person对象一样。还有另一个好处:如果将来Person的定义发生变化,使用super()将确保EmailPersonPerson继承的属性和方法将反映这些变化。

当子类以自己的方式执行某些操作但仍需要来自父类的东西(就像现实生活中一样)时,请使用super()

多重继承

你刚刚看到一些没有父类的经典示例,还有一些有一个父类的示例。实际上,对象可以从多个父类继承。

如果您的类引用其没有的方法或属性,Python 将在所有父类中查找。如果其中有多个类具有相同名称的东西,谁会胜出?

与人类的遗传不同,在那里无论来自谁的优势基因都会获胜,Python 中的继承取决于方法解析顺序。每个 Python 类都有一个特殊的方法称为mro(),返回一个访问该类对象的方法或属性时要访问的类的列表。类似的属性称为__mro__,是这些类的元组。就像突然死亡的季后赛一样,第一个赢家胜出。

在这里,我们定义了一个顶级Animal类,两个子类(HorseDonkey),然后从这些类派生出两个:⁴

>>> class Animal:
...     def says(self):
 return 'I speak!'
...
>>> class Horse(Animal):
...     def says(self):
...         return 'Neigh!'
...
>>> class Donkey(Animal):
...     def says(self):
...         return 'Hee-haw!'
...
>>> class Mule(Donkey, Horse):
...     pass
...
>>> class Hinny(Horse, Donkey):
...     pass
...

如果我们查找Mule的方法或属性,Python 将按照以下顺序查找:

  1. 对象本身(类型为Mule

  2. 对象的类(Mule

  3. 类的第一个父类(Donkey

  4. 类的第二个父类(Horse

  5. 祖父类(Animal)类

对于Hinny来说,情况大致相同,但是HorseDonkey之前:

>>> Mule.mro()
[<class '__main__.Mule'>, <class '__main__.Donkey'>,
<class '__main__.Horse'>, <class '__main__.Animal'>,
<class 'object'>]
>>> Hinny.mro()
[<class '__main__.Hinny'>, <class '__main__.Horse'>,
<class '__main__.Donkey'>, <class '__main__.Animal'>,
class 'object'>]

那么这些优雅的动物怎么说呢?

>>> mule = Mule()
>>> hinny = Hinny()
>>> mule.says()
'hee-haw'
>>> hinny.says()
'neigh'

我们按(父亲,母亲)顺序列出了父类,所以它们说话像他们的爸爸一样。

如果HorseDonkey没有says()方法,那么骡或骡马将使用祖父类Animal类的says()方法,并返回'I speak!'

Mixins

您可以在类定义中包含一个额外的父类,但仅作为助手使用。也就是说,它不与其他父类共享任何方法,并避免了我在上一节中提到的方法解析歧义。

这样的父类有时被称为mixin类。用途可能包括像日志记录这样的“副”任务。这里是一个漂亮打印对象属性的 mixin:

>>> class PrettyMixin():
...     def dump(self):
...         import pprint
...         pprint.pprint(vars(self))
...
>>> class Thing(PrettyMixin):
...     pass
...
>>> t = Thing()
>>> t.name = "Nyarlathotep"
>>> t.feature = "ichor"
>>> t.age = "eldritch"
>>> t.dump()
{'age': 'eldritch', 'feature': 'ichor', 'name': 'Nyarlathotep'}

在自卫中

除了使用空格之外,Python 的一个批评是需要将self作为实例方法的第一个参数(您在前面的示例中看到的方法类型)。Python 使用self参数来查找正确的对象属性和方法。例如,我将展示如何调用对象的方法,以及 Python 在幕后实际做了什么。

还记得之前例子中的Car类吗?让我们再次调用它的exclaim()方法:

>>> a_car = Car()
>>> a_car.exclaim()
I'm a Car!

下面是 Python 在幕后实际做的事情:

  • 查找对象a_car的类(Car)。

  • 将对象a_car作为self参数传递给Car类的exclaim()方法。

只是为了好玩,您甚至可以以这种方式自行运行它,它将与正常的(a_car.exclaim())语法相同运行:

>>> Car.exclaim(a_car)
I'm a Car!

然而,永远没有理由使用那种更冗长的风格。

属性访问

在 Python 中,对象的属性和方法通常是公开的,您被期望自律行事(这有时被称为“成年人同意”政策)。让我们比较直接方法与一些替代方法。

直接访问

正如您所见,您可以直接获取和设置属性值:

>>> class Duck:
...     def __init__(self, input_name):
...         self.name = input_name
...
>>> fowl = Duck('Daffy')
>>> fowl.name
'Daffy'

但是如果有人行为不端呢?

>>> fowl.name = 'Daphne'
>>> fowl.name
'Daphne'

接下来的两个部分展示了如何为不希望被意外覆盖的属性获取一些隐私。

Getter 和 Setter

一些面向对象的语言支持私有对象属性,这些属性无法从外部直接访问。程序员可能需要编写gettersetter方法来读取和写入这些私有属性的值。

Python 没有私有属性,但是您可以使用名称混淆的 getter 和 setter 来获得一些隐私。(最佳解决方案是使用下一节描述的属性。)

在下面的示例中,我们定义了一个名为Duck的类,具有一个名为hidden_name的单个实例属性。我们不希望直接访问这个属性,因此我们定义了两个方法:一个 getter(get_name())和一个 setter(set_name())。每个方法都通过名为name的属性访问。我在每个方法中添加了一个print()语句,以显示它何时被调用:

>>> class Duck():
...     def __init__(self, input_name):
...         self.hidden_name = input_name
...     def get_name(self):
...         print('inside the getter')
...         return self.hidden_name
...     def set_name(self, input_name):
...         print('inside the setter')
...         self.hidden_name = input_name
>>> don = Duck('Donald')
>>> don.get_name()
inside the getter
'Donald'
>>> don.set_name('Donna')
inside the setter
>>> don.get_name()
inside the getter
'Donna'

用于属性访问的属性

对于属性隐私的 Python 解决方案是使用属性

有两种方法可以做到这一点。第一种方法是将name = property(get_name, set_name)添加为我们之前Duck类定义的最后一行:

>>> class Duck():
>>>     def __init__(self, input_name):
>>>         self.hidden_name = input_name
>>>     def get_name(self):
>>>         print('inside the getter')
>>>         return self.hidden_name
>>>     def set_name(self, input_name):
>>>         print('inside the setter')
>>>         self.hidden_name = input_name
>>>     name = property(get_name, set_name)

旧的 getter 和 setter 仍然有效:

>>> don = Duck('Donald')
>>> don.get_name()
inside the getter
'Donald'
>>> don.set_name('Donna')
inside the setter
>>> don.get_name()
inside the getter
'Donna'

现在您还可以使用属性name来获取和设置隐藏的名称:

>>> don = Duck('Donald')
>>> don.name
inside the getter
'Donald'
>>> don.name = 'Donna'
inside the setter
>>> don.name
inside the getter
'Donna'

在第二种方法中,您添加了一些装饰器,并用name替换了方法名get_nameset_name

  • @property,放在 getter 方法之前

  • @*name*.setter,放在 setter 方法之前

这是它们在代码中的实际表现:

>>> class Duck():
...     def __init__(self, input_name):
...         self.hidden_name = input_name
...     @property
...     def name(self):
...         print('inside the getter')
...         return self.hidden_name
...     @name.setter
...     def name(self, input_name):
...         print('inside the setter')
...         self.hidden_name = input_name

你仍然可以像访问属性一样访问name

>>> fowl = Duck('Howard')
>>> fowl.name
inside the getter
'Howard'
>>> fowl.name = 'Donald'
inside the setter
>>> fowl.name
inside the getter
'Donald'
注意

如果有人猜到我们称呼我们的属性为hidden_name,他们仍然可以直接作为fowl.hidden_name读取和写入它。在“隐私的名称混淆”中,你将看到 Python 提供了一种特殊的方式来隐藏属性名称。

用于计算值的属性

在之前的例子中,我们使用name属性来引用存储在对象内部的单个属性(hidden_name)。

属性还可以返回一个计算值。让我们定义一个Circle类,它有一个radius属性和一个计算出的diameter属性:

>>> class Circle():
...     def __init__(self, radius):
...         self.radius = radius
...     @property
...     def diameter(self):
...         return 2 * self.radius
...

创建一个带有其radius初始值的Circle对象:

>>> c = Circle(5)
>>> c.radius
5

我们可以将diameter称为像radius这样的属性:

>>> c.diameter
10

这是有趣的一部分:我们可以随时改变radius属性,并且diameter属性将从当前的radius值计算出来:

>>> c.radius = 7
>>> c.diameter
14

如果你没有为属性指定一个 setter 属性,你不能从外部设置它。这对于只读属性很方便:

>>> c.diameter = 20
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: can't set attribute

使用属性而不是直接访问属性有另一个优点:如果你改变属性的定义,你只需要修复类定义内的代码,而不是所有的调用者。

用于隐私的名称混淆

在稍早的Duck类示例中,我们称呼我们的(不完全)隐藏属性为hidden_name。Python 有一个属性命名约定,这些属性不应该在其类定义之外可见:以双下划线(__)开头。

让我们将hidden_name重命名为__name,如此所示:

>>> class Duck():
...     def __init__(self, input_name):
...         self.__name = input_name
...     @property
...     def name(self):
...         print('inside the getter')
...         return self.__name
...     @name.setter
...     def name(self, input_name):
...         print('inside the setter')
...         self.__name = input_name
...

看一看是否一切仍在正常工作:

>>> fowl = Duck('Howard')
>>> fowl.name
inside the getter
'Howard'
>>> fowl.name = 'Donald'
inside the setter
>>> fowl.name
inside the getter
'Donald'

看起来不错。而且你不能访问__name属性:

>>> fowl.__name
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'Duck' object has no attribute '__name'

这种命名约定并不完全使其私有,但 Python 确实mangle属性名称以使外部代码不太可能偶然发现它。如果你好奇并且承诺不告诉每个人,⁵ 这里是它变成什么样子:

>>> fowl._Duck__name
'Donald'

请注意,它没有打印inside the getter。虽然这并不是完美的保护,但名称混淆阻止了对属性的意外或故意的直接访问。

类和对象属性

您可以将属性分配给类,并且它们将被其子对象继承:

>>> class Fruit:
...     color = 'red'
...
>>> blueberry = Fruit()
>>> Fruit.color
'red'
>>> blueberry.color
'red'

但是如果您更改子对象中属性的值,则不会影响类属性:

>>> blueberry.color = 'blue'
>>> blueberry.color
'blue'
>>> Fruit.color
'red'

如果您稍后更改类属性,它不会影响现有的子对象:

>>> Fruit.color = 'orange'
>>> Fruit.color
'orange'
>>> blueberry.color
'blue'

但这将影响到新的:

>>> new_fruit = Fruit()
>>> new_fruit.color
'orange'

方法类型

一些方法是类本身的一部分,一些是从该类创建的对象的一部分,还有一些都不是:

  • 如果没有前置装饰器,那么它是一个实例方法,其第一个参数应该是self,用于引用对象本身。

  • 如果有一个前置的@classmethod装饰器,那么它是一个类方法,其第一个参数应该是cls(或任何其他,只要不是保留字class),用于指代类本身。

  • 如果有一个前置的@staticmethod装饰器,那么它是一个静态方法,它的第一个参数不是对象或类。

以下各节有一些详细信息。

实例方法

当你在类定义中的方法中看到初始的self参数时,它是一个实例方法。这些是您通常在创建自己的类时编写的方法类型。实例方法的第一个参数是self,当您调用它时,Python 会将对象传递给方法。到目前为止,您已经看到了这些。

类方法

相反,类方法影响整个类。对类进行的任何更改都会影响其所有对象。在类定义中,前置的@classmethod装饰器表示随后的函数是一个类方法。此方法的第一个参数也是类本身。Python 的传统是将参数称为cls,因为class是一个保留字,不能在这里使用。让我们为A定义一个类方法来统计有多少个对象实例已经创建了:

>>> class A():
...     count = 0
...     def __init__(self):
...         A.count += 1
...     def exclaim(self):
...         print("I'm an A!")
...     @classmethod
...     def kids(cls):
...         print("A has", cls.count, "little objects.")
...
>>>
>>> easy_a = A()
>>> breezy_a = A()
>>> wheezy_a = A()
>>> A.kids()
A has 3 little objects.

注意在__init__()中我们引用了A.count(类属性),而不是self.count(这将是一个对象实例属性)。在kids()方法中,我们使用了cls.count,但我们也可以使用A.count

静态方法

类定义中的第三种方法既不影响类也不影响其对象;它只是为了方便而存在,而不是漂浮在自己周围。它是一个静态方法,前面有一个@staticmethod装饰器,没有初始的selfcls参数。以下是作为CoyoteWeapon类的商业广告的示例:

>>> class CoyoteWeapon():
...     @staticmethod
...     def commercial():
...         print('This CoyoteWeapon has been brought to you by Acme')
...
>>>
>>> CoyoteWeapon.commercial()
This CoyoteWeapon has been brought to you by Acme

注意,我们不需要从CoyoteWeapon类创建对象来访问此方法。非常“class-y”。

鸭子类型

Python 对多态性有着宽松的实现;它根据方法的名称和参数,无论它们的类如何,都将相同的操作应用于不同的对象。

现在让我们为所有三个Quote类使用相同的__init__()初始化程序,但添加两个新函数:

  • who()只返回保存的person字符串的值

  • says()返回具有特定标点符号的保存的words字符串

现在让我们看看它们的运作方式:

>>> class Quote():
...     def __init__(self, person, words):
...         self.person = person
...         self.words = words
...     def who(self):
...         return self.person
...     def says(self):
...         return self.words + '.'
...
>>> class QuestionQuote(Quote):
...      def says(self):
...          return self.words + '?'
...
>>> class ExclamationQuote(Quote):
...      def says(self):
...          return self.words + '!'
...
>>>

我们没有改变QuestionQuoteExclamationQuote的初始化方式,因此我们没有重写它们的__init__()方法。然后 Python 会自动调用父类Quote__init__()方法来存储实例变量personwords。这就是为什么我们可以在从子类QuestionQuoteExclamationQuote创建的对象中访问self.words的原因。

接下来,让我们创建一些对象:

>>> hunter = Quote('Elmer Fudd', "I'm hunting wabbits")
>>> print(hunter.who(), 'says:', hunter.says())
Elmer Fudd says: I'm hunting wabbits.
>>> hunted1 = QuestionQuote('Bugs Bunny', "What's up, doc")
>>> print(hunted1.who(), 'says:', hunted1.says())
Bugs Bunny says: What's up, doc?
>>> hunted2 = ExclamationQuote('Daffy Duck', "It's rabbit season")
>>> print(hunted2.who(), 'says:', hunted2.says())
Daffy Duck says: It's rabbit season!

says() 方法的三个不同版本为三个类提供了不同的行为。这是面向对象语言中的传统多态性。Python 更进一步,让你运行具有这些方法的 任何 对象的 who()says() 方法。让我们定义一个名为 BabblingBrook 的类,它与我们之前的树林猎人和被猎物(Quote 类的后代)没有关系:

>>> class BabblingBrook():
...     def who(self):
...         return 'Brook'
...     def says(self):
...         return 'Babble'
...
>>> brook = BabblingBrook()

现在运行各种对象的 who()says() 方法,其中一个(brook)与其他对象完全无关:

>>> def who_says(obj):
...     print(obj.who(), 'says', obj.says())
...
>>> who_says(hunter)
Elmer Fudd says I'm hunting wabbits.
>>> who_says(hunted1)
Bugs Bunny says What's up, doc?
>>> who_says(hunted2)
Daffy Duck says It's rabbit season!
>>> who_says(brook)
Brook says Babble

有时这种行为被称为 鸭子类型,来自一句古老的谚语:

如果它走起来像鸭子,叫起来像鸭子,那它就是鸭子。

一个聪明人

我们能否对鸭子的一句智慧的话提出异议?

inp2 1001

图 10-1. 鸭子类型并非按字母顺序寻找

魔术方法

现在你可以创建并使用基本对象了。你将在本节中学到的内容可能会让你感到惊讶——是一种好的方式。

当你键入诸如 a = 3 + 8 这样的内容时,整数对象如何知道如何实现 + 呢?或者,如果你键入 name = "Daffy" + " " + "Duck",Python 如何知道 + 现在意味着将这些字符串连接起来?aname 如何知道如何使用 = 来得到结果?你可以通过使用 Python 的 特殊方法(或者更戏剧化地说,魔术方法)来解决这些运算符。

这些方法的名称以双下划线 (__) 开头和结尾。为什么呢?它们极不可能被程序员选为变量名。你已经见过一个:__init__() 从其类定义和传入的任何参数中初始化一个新创建的对象。你还见过(“用于隐私的名称混淆”)“dunder”命名如何帮助混淆类属性名称以及方法。

假设你有一个简单的 Word 类,并且你想要一个 equals() 方法来比较两个单词但忽略大小写。也就是说,包含值 'ha'Word 将被视为等于包含 'HA' 的一个。

接下来的示例是一个首次尝试,使用一个我们称之为 equals() 的普通方法。self.text 是这个 Word 对象包含的文本字符串,equals() 方法将其与 word2 的文本字符串(另一个 Word 对象)进行比较:

>>> class Word():
...    def __init__(self, text):
...        self.text = text
...
...    def equals(self, word2):
...        return self.text.lower() == word2.text.lower()
...

然后,从三个不同的文本字符串创建三个 Word 对象:

>>> first = Word('ha')
>>> second = Word('HA')
>>> third = Word('eh')

当字符串 'ha''HA' 与小写比较时,它们应该相等:

>>> first.equals(second)
True

但字符串 'eh' 将不匹配 'ha'

>>> first.equals(third)
False

我们定义了方法 equals() 来进行这个小写转换和比较。只需说 if first == second 就好了,就像 Python 的内置类型一样。那么,我们就这样做吧。我们将 equals() 方法更改为特殊名称 __eq__()(一会儿你就会明白为什么):

>>> class Word():
...     def __init__(self, text):
...         self.text = text
...     def __eq__(self, word2):
...         return self.text.lower() == word2.text.lower()
...

让我们看看它是否奏效:

>>> first = Word('ha')
>>> second = Word('HA')
>>> third = Word('eh')
>>> first == second
True
>>> first == third
False

太神奇了!我们只需要 Python 的特殊方法名称来测试相等性,__eq__()。表 10-1 和 10-2 列出了最有用的魔术方法的名称。

Table 10-1. 比较的魔术方法

方法描述
__eq__( self, other )self == other
__ne__( self, other )self != other
__lt__( self, other )self < other
__gt__( self, other )self > other
__le__( self, other )self <= other
__ge__( self, other )self >= other

Table 10-2. 数学运算的魔术方法

方法描述
__add__( self, other )self + other
__sub__( self, other )self other
__mul__( self, other )self * other
__floordiv__( self, other )self // other
__truediv__( self, other )self / other
__mod__( self, other )self % other
__pow__( self, other )self ** other

您并不受限于使用数学运算符,如+(魔术方法__add__())和(魔术方法__sub__())与数字。例如,Python 字符串对象使用+进行连接和*进行复制。还有许多其他方法,在特殊方法名称中有详细记录。其中最常见的方法在表 10-3 中呈现。

Table 10-3. 其他杂项魔术方法

方法描述
__str__( self )str( self )
__repr__( self )repr( self )
__len__( self )len( self )

除了__init__()之外,您可能会发现自己在自己的方法中最常使用__str__()。这是您打印对象的方式。它被print()str()和字符串格式化器使用,您可以在第五章中了解更多。交互式解释器使用__repr__()函数将变量回显到输出。如果您未定义__str__()__repr__()中的任何一个,您将获得 Python 对象的默认字符串版本:

>>> first = Word('ha')
>>> first
<__main__.Word object at 0x1006ba3d0>
>>> print(first)
<__main__.Word object at 0x1006ba3d0>

让我们为Word类添加__str__()__repr__()方法,使其更加美观:

>>> class Word():
...     def __init__(self, text):
...         self.text = text
...     def __eq__(self, word2):
...         return self.text.lower() == word2.text.lower()
...     def __str__(self):
...         return self.text
...     def __repr__(self):
...         return 'Word("'  + self.text  + '")'
...
>>> first = Word('ha')
>>> first          # uses __repr__
Word("ha")
>>> print(first)   # uses __str__
ha

要深入了解更多特殊方法,请查看Python 文档

聚合与组合

继承是一种很好的技术,当你希望子类在大多数情况下像其父类一样时使用(当子类 是一个 父类)。在构建精细的继承层次时很诱人,但有时组合聚合更有意义。它们的区别是什么?在组合中,一个东西是另一个东西的一部分。一只鸭子 是一个 鸟(继承),但 有一个 尾巴(组合)。尾巴不是鸭子的一种,而是鸭子的一部分。在下面的例子中,让我们创建billtail对象,并将它们提供给一个新的duck对象:

>>> class Bill():
...     def __init__(self, description):
...         self.description = description
...
>>> class Tail():
...     def __init__(self, length):
...         self.length = length
...
>>> class Duck():
...     def __init__(self, bill, tail):
...         self.bill = bill
...         self.tail = tail
...     def about(self):
...         print('This duck has a', self.bill.description,
...             'bill and a', self.tail.length, 'tail')
...
>>> a_tail = Tail('long')
>>> a_bill = Bill('wide orange')
>>> duck = Duck(a_bill, a_tail)
>>> duck.about()
This duck has a wide orange bill and a long tail

聚合表达了关系,但要松散一些:一个东西 使用 另一个东西,但两者都是独立存在的。一只鸭子 使用 一片湖水,但它们并非彼此的一部分。

对象或其他东西何时使用

这里有一些指南,帮助你决定是否将代码和数据放入类、模块(在第十一章中讨论)或其他某些地方:

  • 当你需要许多具有相似行为(方法)但在内部状态(属性)上不同的个体实例时,对象是最有用的。

  • 类支持继承,而模块不支持。

  • 如果只需要一个东西,模块可能是最好的选择。无论在程序中引用多少次 Python 模块,只加载一个副本。(Java 和 C++程序员:你可以将 Python 模块用作单例。)

  • 如果你有多个变量,其中包含多个值并且可以作为多个函数的参数传递,那么将它们定义为类可能更好。例如,你可以使用一个带有sizecolor等键的字典来表示彩色图像。你可以为程序中的每个图像创建一个不同的字典,并将它们作为参数传递给scale()transform()等函数。随着键和函数的增加,这可能会变得混乱。定义一个Image类更加一致,包含sizecolor属性以及scale()transform()方法会更好。这样,所有与彩色图像相关的数据和方法都定义在同一个地方。

  • 使用问题的最简解决方案。字典、列表或元组比模块更简单、更小、更快,通常比类更简单。

    Guido 的建议:

    避免过度工程化的数据结构。元组比对象更好(尝试使用命名元组也是如此)。优先选择简单字段而不是 getter/setter 函数……内置数据类型是你的朋友。使用更多的数字、字符串、元组、列表、集合、字典。还要查看 collections 库,特别是 deque。

    Guido van Rossum

  • 一个更新的替代方案是数据类,在“数据类”中。

命名元组

因为 Guido 刚提到它们,而我还没有,这是谈论命名元组的好地方。命名元组是元组的一个子类,可以通过名称(使用*.name)以及位置(使用[偏移]*)访问值。

让我们从上一节的示例中获取示例,并将Duck类转换为一个命名元组,其中billtail是简单的字符串属性。我们将调用namedtuple函数并传入两个参数:

  • 名称

  • 字段名称的字符串,用空格分隔

Python 不会自动提供命名元组,因此在使用它们之前需要加载一个模块。我们在以下示例的第一行中执行了这样的操作:

>>> from collections import namedtuple
>>> Duck = namedtuple('Duck', 'bill tail')
>>> duck = Duck('wide orange', 'long')
>>> duck
Duck(bill='wide orange', tail='long')
>>> duck.bill
'wide orange'
>>> duck.tail
'long'

你也可以从一个字典中创建一个命名元组:

>>> parts = {'bill': 'wide orange', 'tail': 'long'}
>>> duck2 = Duck(**parts)
>>> duck2
Duck(bill='wide orange', tail='long')

在上面的代码中,看看**parts。这是一个关键字参数。它从parts字典中提取键和值,并将它们作为参数提供给Duck()。它与以下代码具有相同的效果:

>>> duck2 = Duck(bill = 'wide orange', tail = 'long')

命名元组是不可变的,但可以替换一个或多个字段并返回另一个命名元组:

>>> duck3 = duck2._replace(tail='magnificent', bill='crushing')
>>> duck3
Duck(bill='crushing', tail='magnificent')

我们本可以将duck定义为一个字典:

>>> duck_dict = {'bill': 'wide orange', 'tail': 'long'}
>>> duck_dict
{'tail': 'long', 'bill': 'wide orange'}

你可以向字典添加字段:

>>> duck_dict['color'] = 'green'
>>> duck_dict
{'color': 'green', 'tail': 'long', 'bill': 'wide orange'}

但不是一个命名元组:

>>> duck.color = 'green'
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'Duck' object has no attribute 'color'

总结一下,以下是命名元组的一些优点:

  • 它看起来和表现得像一个不可变对象。

  • 它比对象更省空间和时间。

  • 你可以使用点符号而不是类似字典风格的方括号来访问属性。

  • 你可以将其用作字典键。

数据类

许多人喜欢创建对象主要用于存储数据(作为对象属性),而不是行为(方法)。刚才你看到命名元组可以作为替代数据存储的一种选择。Python 3.7 引入了数据类

这是一个普通的对象,只有一个名为name的属性:

>> class TeenyClass():
...     def __init__(self, name):
...         self.name = name
...
>>> teeny = TeenyClass('itsy')
>>> teeny.name
'itsy'

使用数据类做同样的事情看起来有些不同:

>>> from dataclasses import dataclass
>>> @dataclass
... class TeenyDataClass:
...     name: str
...
>>> teeny = TeenyDataClass('bitsy')
>>> teeny.name
'bitsy'

除了需要一个@dataclass装饰器外,你还需要使用变量注释的形式定义类的属性,如*name*: *type**name*: *type* = *val*,比如color: strcolor: str = "red"*type*可以是任何 Python 对象类型,包括你创建的类,而不仅限于像strint这样的内置类型。

当你创建数据类对象时,可以按照类中指定的顺序提供参数,或者以任意顺序使用命名参数:

>>> from dataclasses import dataclass
>>> @dataclass
... class AnimalClass:
...     name: str
...     habitat: str
...     teeth: int = 0
...
>>> snowman = AnimalClass('yeti', 'Himalayas', 46)
>>> duck = AnimalClass(habitat='lake', name='duck')
>>> snowman
AnimalClass(name='yeti', habitat='Himalayas', teeth=46)
>>> duck
AnimalClass(name='duck', habitat='lake', teeth=0)

AnimalClass为其teeth属性定义了默认值,因此在创建duck时无需提供它。

你可以像访问任何其他对象一样引用对象属性:

>>> duck.habitat
'lake'
>>> snowman.teeth
46

数据类还有很多功能。参见这篇指南或官方(详尽的)文档

属性

你已经看到如何创建类并添加属性,以及它们可能涉及大量打字的事情,比如定义__init__(),将其参数分配给self的对应项,并创建所有这些 dunder 方法,如__str__()。命名元组和数据类是标准库中的替代品,当你主要想创建数据集时可能更容易。

每个人都需要的一个 Python 库比较了普通类、命名元组和数据类。它推荐第三方包attrs,原因很多——少打字、数据验证等等。看一看,看看你是否喜欢它胜过内置解决方案。

即将到来

在下一章中,你将在代码结构中进一步提升到 Python 的模块

要做的事情

10.1 创建一个名为Thing的类,不包含内容并打印它。然后,从这个类创建一个名为example的对象并打印它。打印的值是相同的还是不同的?

10.2 创建一个名为Thing2的新类,并将值'abc'赋给名为letters的类属性。打印letters

10.3 再次创建一个名为Thing3的类,这次将值'xyz'赋给名为letters的实例(对象)属性。打印letters。你需要从该类创建一个对象吗?

10.4 创建一个名为Element的类,具有实例属性namesymbolnumber。使用值'Hydrogen''H'1创建该类的一个对象。

10.5 使用这些键和值创建一个字典:'name': 'Hydrogen', 'symbol': 'H', 'number': 1。然后,使用这个字典从Element类创建一个名为hydrogen的对象。

10.6 对于Element类,定义一个名为dump()的方法,打印对象属性(namesymbolnumber)的值。使用这个新定义创建hydrogen对象,并使用dump()打印其属性。

10.7 调用print(hydrogen)。在Element的定义中,将方法dump的名称更改为__str__,创建一个新的hydrogen对象,并再次调用print(hydrogen)

10.8 修改Element使属性namesymbolnumber变为私有。为每个属性定义一个 getter 属性来返回其值。

10.9 定义三个类:BearRabbitOctothorpe。对于每个类,只定义一个方法:eats()。这应该返回'berries'Bear)、'clover'Rabbit)或'campers'Octothorpe)。分别创建一个对象并打印它们吃的是什么。

10.10 定义这些类:LaserClawSmartPhone。每个类只有一个方法:does()。它返回'disintegrate'Laser)、'crush'Claw)或'ring'SmartPhone)。然后,定义一个名为Robot的类,其中包含每个组件对象的一个实例。为Robot定义一个does()方法,打印其组件对象的功能。

¹ 即使你不想。

² Python 中的命名经常会看到双下划线的例子;为了节省音节,有些人将其发音为dunder

³ 一个 80 年代的便宜但不怎么样的汽车。

⁴ 骡马的父亲是驴,母亲是马;小母驴的父亲是马,母亲是驴。

⁵ 你能保守秘密吗?显然,我不能。

第十一章:模块、包和好东西

在您从底层向上攀登的过程中,您已经从内置数据类型进展到构建越来越大的数据和代码结构。在本章中,您终于学会如何在 Python 中编写现实的完整程序。您将编写自己的模块,并学习如何使用来自 Python 标准库和其他来源的模块。

本书的文本是按层次结构组织的:单词、句子、段落和章节。否则,它会很快变得难以阅读。¹ 代码的组织原则大致相同:数据类型类似于单词;表达式和语句类似于句子;函数类似于段落;模块类似于章节。继续这个类比,在本书中,当我说某些内容将在第八章中解释时,在编程中这就像是引用另一个模块中的代码。

模块和 import 语句

我们将在多个文件中创建和使用 Python 代码。模块只是包含任何 Python 代码的文件。您不需要做任何特殊处理——任何 Python 代码都可以被其他人用作模块。

我们通过使用 Python 的import语句引用其他模块的代码。这使得导入模块中的代码和变量对您的程序可用。

导入一个模块

import语句的最简单用法是import module,其中*module是另一个 Python 文件的名称,不带.py*扩展名。

假设你和几个人想要快速解决午餐问题,但又不想进行长时间的讨论,最后总是由最吵闹的那个人决定。让电脑来决定吧!让我们编写一个单一函数的模块,返回一个随机的快餐选择,以及调用该函数并打印选择的主程序。

模块(fast.py)显示在示例 11-1 中。

示例 11-1。fast.py
from random import choice

places = ['McDonalds", "KFC", "Burger King", "Taco Bell",
     "Wendys", "Arbys", "Pizza Hut"]

def pick():  # see the docstring below?
    """Return random fast food place"""
    return choice(places)

并且示例 11-2 展示了导入它的主程序(称为lunch.py)。

示例 11-2。lunch.py
import fast

place = fast.pick()
print("Let's go to", place)

如果您将这两个文件放在同一个目录中,并指示 Python 将lunch.py作为主程序运行,它将访问fast模块并运行其pick()函数。我们编写了pick()的这个版本,以从字符串列表中返回一个随机结果,因此主程序将获得并打印出这个结果:

$ python lunch.py
Let's go to Burger King
$ python lunch.py
Let's go to Pizza Hut
$ python lunch.py
Let's go to Arbys

我们在两个不同的地方使用了导入:

  • 主程序lunch.py导入了我们的新模块fast

  • 模块文件fast.py从 Python 的标准库模块random中导入了choice函数。

我们在主程序和模块中以两种不同的方式使用了导入:

  • 在第一种情况下,我们导入了整个 fast 模块,但需要使用 fast 作为 pick() 的前缀。在这个 import 语句之后,只要我们在名称前加上 fast.fast.py 中的所有内容对主程序都是可用的。通过用模块的名称限定模块的内容,我们避免了任何糟糕的命名冲突。其他模块可能有一个 pick() 函数,我们不会误调用它。

  • 在第二种情况下,我们在一个模块内部,并且知道这里没有其他名为 choice 的内容,因此直接从 random 模块中导入了 choice() 函数。

我们本可以像示例 11-3 中所示那样编写 fast.py,在 pick() 函数内部导入 random 而不是在文件顶部。

示例 11-3. fast2.py
places = ['McDonalds", "KFC", "Burger King", "Taco Bell",
     "Wendys", "Arbys", "Pizza Hut"]

def pick():
    import random
    return random.choice(places)

像编程的许多方面一样,使用最清晰的风格。使用模块限定的名称(random.choice)更安全,但需要稍微多输入一些字。

如果导入的代码可能在多个地方使用,请考虑从函数外部导入;如果知道其使用将受限制,请从函数内部导入。有些人喜欢将所有导入都放在文件顶部,以明确代码的所有依赖关系。无论哪种方式都可以。

使用另一个名称导入模块

在我们的主 lunch.py 程序中,我们调用了 import fast。但如果你:

  • 还有另一个名为 fast 的模块吗?

  • 想使用一个更易记的名称吗?

  • 门夹到手指想减少打字?

在这些情况下,你可以使用一个别名导入,如示例 11-4 所示。让我们使用别名 f

示例 11-4. fast3.py
import fast as f
place = f.pick()
print("Let's go to", place)

从一个模块中仅导入你想要的内容

你可以导入整个模块,也可以只导入部分。你刚刚看到了后者:我们只想要 random 模块中的 choice() 函数。

像模块本身一样,你可以为每个导入的东西使用别名。

让我们再重新做几次 lunch.py。首先,从 fast 模块中以其原始名称导入 pick()(示例 11-5)。

示例 11-5. fast4.py
from fast import pick
place = pick()
print("Let's go to", place)

现在将其导入为 who_cares(示例 11-6)。

示例 11-6. fast5.py
from fast import pick as who_cares
place = who_cares()
print("Let's go to", place)

我们从单行代码、到多行函数、到独立程序、再到同一目录中的多个模块。如果你没有很多模块,同一目录也可以正常工作。

为了使 Python 应用程序能够更好地扩展,你可以将模块组织成称为的文件和模块层次结构。一个包只是包含 .py 文件的子目录。而且你可以进行多层次的组织,有目录在其中。

我们刚刚写了一个选择快餐的模块。让我们再添加一个类似的模块来提供人生建议。我们将在当前目录中创建一个名为 questions.py 的新主程序。现在在其中创建一个名为 choices 的子目录,并将两个模块放入其中——fast.pyadvice.py。每个模块都有一个返回字符串的函数。

主程序(questions.py)有额外的导入和行(示例 11-7)。

示例 11-7. questions.py
from sources import fast, advice

print("Let's go to", fast.pick())
print("Should we take out?", advice.give())

那个from sources让 Python 在当前目录下查找名为sources的目录。在sources内部,它查找fast.pyadvice.py文件。

第一个模块(choices/fast.py)与以前相同的代码,只是移动到了choices目录中(示例 11-8)。

示例 11-8. choices/fast.py
from random import choice

places = ["McDonalds", "KFC", "Burger King", "Taco Bell",
     "Wendys", "Arbys", "Pizza Hut"]

def pick():
    """Return random fast food place"""
    return choice(places)

第二个模块(choices/advice.py)是新的,但功能与快餐相似(示例 11-9)。

示例 11-9. choices/advice.py
from random import choice

answers = ["Yes!", "No!", "Reply hazy", "Sorry, what?"]

def give():
    """Return random advice"""
    return choice(answers)
注意

如果你的 Python 版本早于 3.3,那么在sources子目录中还需要一件事才能使其成为 Python 包:一个名为*__init__.py*的文件。这可以是一个空文件,但是在 3.3 之前的 Python 中,需要这样做才能将包含它的目录视为包。(这是另一个常见的 Python 面试问题。)

运行主程序questions.py(从当前目录,而不是sources中)来看看会发生什么:

$ python questions.py
Let's go to KFC
Should we take out? Yes!
$ python questions.py
Let's go to Wendys
Should we take out? Reply hazy
$ python questions.py
Let's go to McDonalds
Should we take out? Reply hazy

模块搜索路径

我刚才说过,Python 会在当前目录下查找子目录choices及其模块。实际上,它还会在其他地方查找,并且你可以控制这一过程。

早些时候,我们从标准库的random模块导入了函数choice()。这不在你的当前目录中,因此 Python 还需要在其他地方查找。

要查看 Python 解释器查找的所有位置,导入标准的sys模块并使用它的path列表。这是一个目录名称和 ZIP 存档文件列表,Python 按顺序搜索以找到要导入的模块。

你可以访问并修改这个列表。这是我 Mac 上 Python 3.7 的sys.path值:

>>> import sys
>>> for place in sys.path:
...     print(place)
...

/Library/Frameworks/Python.framework/Versions/3.7/lib/python37.zip
/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7
/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/lib-dynload

那个初始的空输出行是空字符串'',它表示当前目录。如果''sys.path的第一位,当你尝试导入某些东西时,Python 首先查找当前目录:import fast会寻找fast.py。这是 Python 的常规设置。此外,当我们创建名为sources的子目录并在其中放置 Python 文件时,它们可以通过import sourcesfrom sources import fast来导入。

将使用第一个匹配项。这意味着如果你定义了一个名为random的模块,并且它在标准库之前的搜索路径中,那么现在就无法访问标准库的random了。

你可以在代码中修改搜索路径。假设你希望 Python 在其他任何位置之前查找*/my/modules*目录:

>>> import sys
>>> sys.path.insert(0, "/my/modules")

相对和绝对导入

到目前为止,在我们的示例中,我们从以下位置导入了我们自己的模块:

  • 当前目录

  • 子目录choices

  • Python 标准库

这在你有与标准模块同名的本地模块时效果很好。你想要哪一个?

Python 支持绝对相对导入。到目前为止你看到的例子都是绝对导入。如果你键入import rougarou,对于搜索路径中的每个目录,Python 会查找名为rougarou.py(一个模块)或名为rougarou(一个包)的文件。

  • 如果rougarou.py与你调用问题的同一目录中,你可以用from . import rougarou来相对于你所在位置导入它。

  • 如果它位于你的上一级目录中:from .. import rougarou

  • 如果它位于名为creatures的同级目录下:from ..creatures import rougarou

...的符号借鉴于 Unix 对当前目录父目录的简写。

对于 Python 导入系统中可能遇到的问题的深入讨论,请参阅Python 导入系统的陷阱

命名空间包

你已经看到可以将 Python 模块打包为:

  • 一个单一的模块.py 文件)

  • (包含模块及可能其他包的目录)

你也可以通过命名空间包在多个目录中分割一个包。假设你想要一个名为critters的包,其中包含每种危险生物(真实或想象中,据说具有背景信息和防护提示)的 Python 模块。随着时间的推移,这可能会变得很庞大,你可能希望按地理位置细分。一种选择是在critters下添加位置子包,并将现有的*.py模块文件移到它们下面,但这会打破其他导入它们的模块。相反,我们可以向上*进行如下操作:

  • critters上创建新的位置目录

  • 在这些新父目录下创建表兄弟目录critters

  • 将现有模块移动到它们各自的目录中。

这需要一些说明。假设我们从这样的文件布局开始:

critters
 ⌞ rougarou.py
 ⌞ wendigo.py

这些模块的正常导入看起来像这样:

from critters import wendigo, rougarou

现在如果我们决定在美国的地点,文件和目录看起来会像这样:

north
 ⌞ critters
   ⌞ wendigo.py
south
 ⌞ critters
   ⌞ rougarou.py

如果northsouth都在你的模块搜索路径中,你可以像它们仍然共存于单目录包一样导入这些模块:

from critters import wendigo, rougarou

模块与对象

你应该把你的代码放入一个模块中,还是放入一个对象中?什么时候适合?

它们在许多方面看起来很相似。一个名为thing的对象或模块,具有称为stuff的内部数据值,让你可以像thing.stuff那样访问该值。stuff可能在创建模块或类时已经定义,也可能是后来分配的。

模块中的所有类、函数和全局变量对外部都是可用的。对象可以使用属性和“dunder”(__ …)命名来隐藏或控制对它们数据属性的访问。

这意味着你可以这样做:

>>> import math
>>> math.pi
3.141592653589793
>>> math.pi = 3.0
>>> math.pi
3.0

你刚刚搞砸了这台计算机上每个人的计算吗?是的!不,开玩笑的。² 这并未影响 Python 的 math 模块。你只是改变了你调用程序导入的 math 模块代码的副本中 pi 的值,并且所有关于你罪行的证据将在程序结束时消失。

你的程序导入的任何模块只有一个副本,即使你多次导入它。你可以用它保存对任何导入代码感兴趣的全局事物。这与类似,尽管你可以从中创建许多对象,但类也只有一个副本。

Python 标准库中的好东西

Python 的一个显著特点是其“即插即用”——一个包含许多有用任务的大型标准库模块。它们被保持分开,以避免膨胀核心语言。当你打算写一些 Python 代码时,经常值得先检查是否已经有标准模块实现了你想要的功能。令人惊讶的是,你会经常遇到标准库中的一些小宝石。Python 还为这些模块提供了权威的 文档,以及一个 教程。Doug Hellmann 的网站 Python Module of the Week 和书籍 The Python Standard Library by Example(Addison-Wesley Professional)也是非常有用的指南。

本书的即将到来的章节涵盖了许多特定于网络、系统、数据库等的标准模块。在本节中,我讨论一些具有通用用途的标准模块。

使用 setdefault()defaultdict() 处理缺失键

你已经看到尝试访问字典的不存在键会引发异常。使用字典的 get() 函数返回一个默认值可以避免异常。setdefault() 函数类似于 get(),但也会在键缺失时向字典中分配一个项目:

>>> periodic_table = {'Hydrogen': 1, 'Helium': 2}
>>> periodic_table
{'Hydrogen': 1, 'Helium': 2}

如果键原先 在字典中,新值就会被使用:

>>> carbon = periodic_table.setdefault('Carbon', 12)
>>> carbon
12
>>> periodic_table
{'Hydrogen': 1, 'Helium': 2, 'Carbon': 12}

如果我们尝试为 现有 键分配不同的默认值,将返回原始值且不会发生任何更改:

>>> helium = periodic_table.setdefault('Helium', 947)
>>> helium
2
>>> periodic_table
{'Hydrogen': 1, 'Helium': 2, 'Carbon': 12}

defaultdict() 类似,但在创建字典时就指定了任何新键的默认值。它的参数是一个函数。在这个例子中,我们传递了函数 int,它将被调用为 int() 并返回整数 0

>>> from collections import defaultdict
>>> periodic_table = defaultdict(int)

现在任何缺失的值将是整数 (int), 其值为 0:

>>> periodic_table['Hydrogen'] = 1
>>> periodic_table['Lead']
0
>>> periodic_table
defaultdict(<class 'int'>, {'Hydrogen': 1, 'Lead': 0})

defaultdict() 的参数是一个返回要分配给缺失键的值的函数。在下面的例子中,当需要时执行 no_idea() 来返回一个值:

>>> from collections import defaultdict
>>>
>>> def no_idea():
...     return 'Huh?'
...
>>> bestiary = defaultdict(no_idea)
>>> bestiary['A'] = 'Abominable Snowman'
>>> bestiary['B'] = 'Basilisk'
>>> bestiary['A']
'Abominable Snowman'
>>> bestiary['B']
'Basilisk'
>>> bestiary['C']
'Huh?'

你可以使用 int()list()dict() 函数来返回这些类型的默认空值:int() 返回 0list() 返回一个空列表 ([]),dict() 返回一个空字典 ({})。如果省略参数,新键的初始值将设置为 None

顺便说一下,您可以使用lambda在调用内部定义您的默认生成函数:

>>> bestiary = defaultdict(lambda: 'Huh?')
>>> bestiary['E']
'Huh?'

使用int是制作自己的计数器的一种方法:

>>> from collections import defaultdict
>>> food_counter = defaultdict(int)
>>> for food in ['spam', 'spam', 'eggs', 'spam']:
...     food_counter[food] += 1
...
>>> for food, count in food_counter.items():
...     print(food, count)
...
eggs 1
spam 3

在上面的示例中,如果food_counter是一个普通的字典而不是defaultdict,每次尝试增加字典元素food_counter[food]时,Python 都会引发一个异常,因为它不会被初始化。我们需要做一些额外的工作,如下所示:

>>> dict_counter = {}
>>> for food in ['spam', 'spam', 'eggs', 'spam']:
...     if not food in dict_counter:
...         dict_counter[food] = 0
...     dict_counter[food] += 1
...
>>> for food, count in dict_counter.items():
...     print(food, count)
...
spam 3
eggs 1

使用 Counter()计算项数

说到计数器,标准库中有一个可以执行前面示例工作以及更多工作的计数器:

>>> from collections import Counter
>>> breakfast = ['spam', 'spam', 'eggs', 'spam']
>>> breakfast_counter = Counter(breakfast)
>>> breakfast_counter
Counter({'spam': 3, 'eggs': 1})

函数most_common()以降序返回所有元素,或者如果给定了计数,则仅返回前count个元素:

>>> breakfast_counter.most_common()
[('spam', 3), ('eggs', 1)]
>>> breakfast_counter.most_common(1)
[('spam', 3)]

您可以组合计数器。首先,让我们再次看看breakfast_counter中有什么:

>>> breakfast_counter
>>> Counter({'spam': 3, 'eggs': 1})

这一次,我们创建了一个名为lunch的新列表,以及一个名为lunch_counter的计数器:

>>> lunch = ['eggs', 'eggs', 'bacon']
>>> lunch_counter = Counter(lunch)
>>> lunch_counter
Counter({'eggs': 2, 'bacon': 1})

我们组合两个计数器的第一种方法是通过加法,使用+

>>> breakfast_counter + lunch_counter
Counter({'spam': 3, 'eggs': 3, 'bacon': 1})

正如您所预期的,您可以使用-从另一个计数器中减去一个计数器。早餐吃什么而午餐不吃呢?

>>> breakfast_counter - lunch_counter
Counter({'spam': 3})

好的,现在我们可以吃午餐了,但是我们早餐不能吃什么呢?

>>> lunch_counter - breakfast_counter
Counter({'bacon': 1, 'eggs': 1})

类似于第八章中的集合,您可以使用交集运算符&获取共同的项:

>>> breakfast_counter & lunch_counter
Counter({'eggs': 1})

交集选择了具有较低计数的共同元素('eggs')。这是有道理的:早餐只提供了一个鸡蛋,所以这是共同的计数。

最后,您可以使用并集运算符|获取所有项:

>>> breakfast_counter | lunch_counter
Counter({'spam': 3, 'eggs': 2, 'bacon': 1})

项目'eggs'再次是两者共同的。与加法不同,联合操作并未将它们的计数相加,而是选择计数较大的那个。

使用 OrderedDict()按键排序

这是使用 Python 2 解释器运行的示例:

>>> quotes = {
...     'Moe': 'A wise guy, huh?',
...     'Larry': 'Ow!',
...     'Curly': 'Nyuk nyuk!',
...     }
>>> for stooge in quotes:
...  print(stooge)
...
Larry
Curly
Moe
注意

从 Python 3.7 开始,字典会按照它们被添加的顺序保留键。OrderedDict对于早期版本非常有用,因为它们具有不可预测的顺序。本节中的示例仅在您使用的 Python 版本早于 3.7 时才相关。

OrderedDict()记住键添加的顺序,并从迭代器中以相同的顺序返回它们。尝试从一个(, )元组序列创建一个OrderedDict

>>> from collections import OrderedDict
>>> quotes = OrderedDict([
...     ('Moe', 'A wise guy, huh?'),
...     ('Larry', 'Ow!'),
...     ('Curly', 'Nyuk nyuk!'),
...     ])
>>>
>>> for stooge in quotes:
...     print(stooge)
...
Moe
Larry
Curly

栈+队列==deque

一个deque(发音为deck)是一个双端队列,具有栈和队列的特性。当你想要从序列的任一端添加或删除项时,它非常有用。在这里,我们从单词的两端向中间工作,以查看它是否是回文。函数popleft()从 deque 中删除最左边的项并返回它;pop()则删除最右边的项并返回它。它们一起从两端向中间工作。只要末尾字符匹配,它就会持续弹出,直到达到中间位置:

>>> def palindrome(word):
...     from collections import deque
...     dq = deque(word)
...     while len(dq) > 1:
...        if dq.popleft() != dq.pop():
...            return False
...     return True
...
...
>>> palindrome('a')
True
>>> palindrome('racecar')
True
>>> palindrome('')
True
>>> palindrome('radar')
True
>>> palindrome('halibut')
False

我将其用作双端队列的简单说明。如果你真的想要一个快速的回文检查器,只需将字符串与其反转比较就简单得多了。Python 没有字符串的 reverse() 函数,但它确实有一种通过切片来反转字符串的方法,如下例所示:

>>> def another_palindrome(word):
...     return word == word[::-1]
...
>>> another_palindrome('radar')
True
>>> another_palindrome('halibut')
False

用 itertools 遍历代码结构

itertools包含特殊用途的迭代器函数。每次在forin循环中调用时,它返回一个项目,并在调用之间记住其状态。

chain() 将其参数视为单个可迭代对象运行:

>>> import itertools
>>> for item in itertools.chain([1, 2], ['a', 'b']):
...     print(item)
...
1
2
a
b

cycle()是一个无限迭代器,循环遍历其参数:

>>> import itertools
>>> for item in itertools.cycle([1, 2]):
...     print(item)
...
1
2
1
2
.
.
.

等等。

accumulate() 计算累积值。默认情况下,它计算总和:

>>> import itertools
>>> for item in itertools.accumulate([1, 2, 3, 4]):
...     print(item)
...
1
3
6
10

您可以将一个函数作为accumulate()的第二个参数提供,它将被用于代替加法。该函数应该接受两个参数并返回一个单一的结果。这个例子计算一个累积乘积:

>>> import itertools
>>> def multiply(a, b):
...     return a * b
...
>>> for item in itertools.accumulate([1, 2, 3, 4], multiply):
...     print(item)
...
1
2
6
24

itertools 模块还有许多其他函数,尤其是一些组合和排列函数,在需要时可以节省时间。

使用pprint()进行漂亮打印

我们所有的例子都使用print()(或者在交互式解释器中仅使用变量名)来打印东西。有时,结果很难读取。我们需要一个漂亮打印机,比如pprint()

>>> from pprint import pprint
>>> quotes = OrderedDict([
...     ('Moe', 'A wise guy, huh?'),
...     ('Larry', 'Ow!'),
...     ('Curly', 'Nyuk nyuk!'),
...     ])
>>>

简单的print()只是将东西倒出来:

>>> print(quotes)
OrderedDict([('Moe', 'A wise guy, huh?'), ('Larry', 'Ow!'),
 ('Curly', 'Nyuk nyuk!')])

但是,pprint()试图对齐元素以提高可读性:

>>> pprint(quotes)
{'Moe': 'A wise guy, huh?',
 'Larry': 'Ow!',
 'Curly': 'Nyuk nyuk!'}

获取随机数

我们在本章的开头玩了random.choice()。它从给定的序列(列表、元组、字典、字符串)中返回一个值:

>>> from random import choice
>>> choice([23, 9, 46, 'bacon', 0x123abc])
1194684
>>> choice( ('a', 'one', 'and-a', 'two') )
'one'
>>> choice(range(100))
68
>>> choice('alphabet')
'l'

使用sample()函数一次获取多个值:

>>> from random import sample
>>> sample([23, 9, 46, 'bacon', 0x123abc], 3)
[1194684, 23, 9]
>>> sample(('a', 'one', 'and-a', 'two'), 2)
['two', 'and-a']
>>> sample(range(100), 4)
[54, 82, 10, 78]
>>> sample('alphabet', 7)
['l', 'e', 'a', 't', 'p', 'a', 'b']

要从任意范围获取一个随机整数,您可以使用choice()sample()range(),或者使用randint()randrange()

>>> from random import randint
>>> randint(38, 74)
71
>>> randint(38, 74)
60
>>> randint(38, 74)
61

randrange()range()一样,有起始(包含)和结束(不包含)整数的参数,还有一个可选的整数步长:

>>> from random import randrange
>>> randrange(38, 74)
65
>>> randrange(38, 74, 10)
68
>>> randrange(38, 74, 10)
48

最后,获取一个在 0.0 到 1.0 之间的随机实数(浮点数):

>>> from random import random
>>> random()
0.07193393312692198
>>> random()
0.7403243673826271
>>> random()
0.9716517846775018

更多电池:获取其他 Python 代码

有时,标准库没有您需要的功能,或者没有以正确的方式执行。有一个完整的开源、第三方 Python 软件世界。良好的资源包括以下内容:

你可以在activestate找到许多较小的代码示例。

本书几乎所有的 Python 代码都使用您计算机上的标准 Python 安装,其中包括所有内置函数和标准库。某些地方特别提到了requests在第一章中;更多细节请参见第十八章。附录 B 展示了如何安装第三方 Python 软件,以及许多其他开发细节。

即将发生的事情

下一章是一个实用章节,涵盖 Python 中许多数据操作的方面。您将遇到二进制bytesbytearray数据类型,在文本字符串中处理 Unicode 字符,并使用正则表达式搜索文本字符串。

要做的事情

11.1 创建一个名为zoo.py的文件。在其中,定义一个名为hours()的函数,打印字符串'Open 9-5 daily'。然后,使用交互解释器导入zoo模块并调用其hours()函数。

11.2 在交互解释器中,将zoo模块作为menagerie导入,并调用其hours()函数。

11.3 仍然在解释器中,直接从zoo中导入hours()函数并调用它。

11.4 将hours()函数作为info导入并调用它。

11.5 创建一个名为plain的字典,其键值对为'a': 1'b': 2'c': 3,然后打印它。

11.6 从上一个问题中列出的相同对创建一个名为fancyOrderedDict并打印它。它是否按照plain的顺序打印?

11.7 创建一个名为dict_of_listsdefaultdict,并传递list作为参数。用一次赋值操作将列表dict_of_lists['a']并附加值'something for a'。打印dict_of_lists['a']

¹ 至少,比它现在的阅读性少一点。

² 还是会?布娃哈哈。