# 2.5 面向对象编程

> 来源：[2.5   Object-Oriented Programming](http://www-inst.eecs.berkeley.edu/~cs61a/sp12/book/objects.html#object-oriented-programming)

> 译者：[飞龙](https://github.com/wizardforcel)

> 协议：[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/)

面向对象编程（OOP）是一种用于组织程序的方法，它组合了这一章引入的许多概念。就像抽象数据类型那样，对象创建了数据使用和实现之间的抽象界限。类似消息传递中的分发字典，对象响应行为请求。就像可变的数据结构，对象拥有局部状态，并且不能直接从全局环境访问。Python 对象系统提供了新的语法，更易于为组织程序实现所有这些实用的技巧。

但是对象系统不仅仅提供了便利；它也为程序设计添加了新的隐喻，其中程序中的几个部分彼此交互。每个对象将局部状态和行为绑定，以一种方式在数据抽象背后隐藏二者的复杂性。我们的约束程序的例子通过在约束和连接器之前传递消息，产生了这种隐喻。Python 对象系统使用新的途径扩展了这种隐喻，来表达程序的不同部分如何互相关联，以及互相通信。对象不仅仅会传递消息，还会和其它相同类型的对象共享行为，以及从相关的类型那里继承特性。

面向对象编程的范式使用自己的词汇来强化对象隐喻。我们已经看到了，对象是拥有方法和属性的数据值，可以通过点运算符来访问。每个对象都拥有一个类型，叫做类。Python 中可以定义新的类，就像定义函数那样。

## 2.5.1 对象和类

类可以用作所有类型为该类的对象的模板。每个对象都是某个特定类的实例。我们目前使用的对象都拥有内建类型，但是我们可以定义新的类，就像定义函数那样。类的定义规定了在该类的对象之间共享的属性和方法。我们会通过重新观察银行账户的例子，来介绍类的语句。

在介绍局部状态时，我们看到，银行账户可以自然地建模为拥有`balance`的可变值。银行账户对象应该拥有`withdraw`方法，在可用的情况下，它会更新账户余额，并返回所请求的金额。我们希望添加一些额外的行为来完善账户抽象：银行账户应该能够返回它的当前余额，返回账户持有者的名称，以及接受存款。

`Account`类允许我们创建银行账户的多个实例。创建新对象实例的动作被称为实例化该类。Python 中实例化某个类的语法类似于函数的调用语句。这里，我们使用参数`'Jim'`（账户持有者的名称）来调用`Account`。

```py
>>> a = Account('Jim')
```

对象的属性是和对象关联的名值对，它可以通过点运算符来访问。属性特定于具体的对象，而不是类的所有对象，也叫做实例属性。每个`Account`对象都拥有自己的余额和账户持有者名称，它们是实例属性的一个例子。在更宽泛的编程社群中，实例属性可能也叫做字段、属性或者实例变量。

```py
>>> a.holder
'Jim'
>>> a.balance
0
```

操作对象或执行对象特定计算的函数叫做方法。方法的副作用和返回值可能依赖或改变对象的其它属性。例如，`deposit`是`Account`对象`a`上的方法。它接受一个参数，即需要存入的金额，修改对象的`balance`属性，并返回产生的余额。

```py
>>> a.deposit(15)
15
```

在 OOP 中，我们说方法可以在特定对象上调用。作为调用`withdraw`方法的结果，要么取钱成功，余额减少并返回，要么请求被拒绝，账户打印出错误信息。

```py
>>> a.withdraw(10)  # The withdraw method returns the balance after withdrawal
5
>>> a.balance       # The balance attribute has changed
5
>>> a.withdraw(10)
'Insufficient funds'
```

像上面展示的那样，方法的行为取决于对象属性的改变。两次以相同参数对`withdraw`的调用返回了不同的结果。

## 2.5.2 类的定义

用户定义的类由`class`语句创建，它只包含单个子句。类的语句定义了类的名称和基类（会在继承那一节讨论），之后包含了定义类属性的语句组：


```py
class <name>(<base class>):
    <suite>
```

当类的语句被执行时，新的类会被创建，并且在当前环境第一帧绑定到`<name>`上。之后会执行语句组。任何名称都会在`class`语句的`<suite>`中绑定，通过`def`或赋值语句，创建或修改类的属性。

类通常围绕实例属性来组织，实例属性是名值对，不和类本身关联但和类的每个对象关联。通过为实例化新对象定义方法，类规定了它的对象的实例属性。

`class`语句的`<suite>`部分包含`def`语句，它们为该类的对象定义了新的方法。用于实例化对象的方法在 Python 中拥有特殊的名称，`__init__`（`init`两边分别有两个下划线），它叫做类的构造器。

```py
>>> class Account(object):
        def __init__(self, account_holder):
            self.balance = 0
            self.holder = account_holder
```

`Account`的`__init__`方法有两个形参。第一个是`self`，绑定到新创建的`Account`对象上。第二个参数，`account_holder`，在被调用来实例化的时候，绑定到传给该类的参数上。

构造器将实例属性名称`balance`与`0`绑定。它也将属性名称`holder`绑定到`account_holder`上。形参`account_holder`是`__init__`方法的局部名称。另一方面，通过最后一个赋值语句绑定的名称`holder`是一直存在的，因为它使用点运算符被存储为`self`的属性。

定义了`Account`类之后，我们就可以实例化它：

```py
>>> a = Account('Jim')
```

这个对`Account`类的“调用”创建了新的对象，它是`Account`的实例，之后以两个参数调用了构造函数`__init__`：新创建的对象和字符串`'Jim'`。按照惯例，我们使用名称`self`来命名构造器的第一个参数，因为它绑定到了被实例化的对象上。这个惯例在几乎所有 Python 代码中都适用。

现在，我们可以使用点运算符来访问对象的`balance`和`holder`。

```py
>>> a.balance
0
>>> a.holder
'Jim'
```

**身份。**每个新的账户实例都有自己的余额属性，它的值独立于相同类的其它对象。

```py
>>> b = Account('Jack')
>>> b.balance = 200
>>> [acc.balance for acc in (a, b)]
[0, 200]
```

为了强化这种隔离，每个用户定义类的实例对象都有个独特的身份。对象身份使用`is`和`is not`运算符来比较。

```py
>>> a is a
True
>>> a is not b
True
```

虽然由同一个调用来构造，绑定到`a`和`b`的对象并不相同。通常，使用赋值将对象绑定到新名称并不会创建新的对象。

```py
>>> c = a
>>> c is a
True
```

用户定义类的新对象只在类（比如`Account`）使用调用表达式被实例化的时候创建。

**方法。**对象方法也由`class`语句组中的`def`语句定义。下面，`deposit`和`withdraw`都被定义为`Account`类的对象上的方法：

```py
>>> class Account(object):
        def __init__(self, account_holder):
            self.balance = 0
            self.holder = account_holder
        def deposit(self, amount):
            self.balance = self.balance + amount
            return self.balance
        def withdraw(self, amount):
            if amount > self.balance:
                return 'Insufficient funds'
            self.balance = self.balance - amount
            return self.balance
```

虽然方法定义和函数定义在声明方式上并没有区别，方法定义有不同的效果。由`class`语句中的`def`语句创建的函数值绑定到了声明的名称上，但是只在类的局部绑定为一个属性。这个值可以使用点运算符在类的实例上作为方法来调用。

每个方法定义同样包含特殊的首个参数`self`，它绑定到方法所调用的对象上。例如，让我们假设`deposit`在特定的`Account`对象上调用，并且传递了一个对象值：要存入的金额。对象本身绑定到了`self`上，而参数绑定到了`amount`上。所有被调用的方法能够通过`self`参数来访问对象，所以它们可以访问并操作对象的状态。

为了调用这些方法，我们再次使用点运算符，就像下面这样：

```py
>>> tom_account = Account('Tom')
>>> tom_account.deposit(100)
100
>>> tom_account.withdraw(90)
10
>>> tom_account.withdraw(90)
'Insufficient funds'
>>> tom_account.holder
'Tom'
```

当一个方法通过点运算符调用时，对象本身（这个例子中绑定到了`tom_account`）起到了双重作用。首先，它决定了`withdraw`意味着哪个名称；`withdraw`并不是环境中的名称，而是`Account`类局部的名称。其次，当`withdraw`方法调用时，它绑定到了第一个参数`self`上。求解点运算符的详细过程会在下一节中展示。

## 2.5.3 消息传递和点表达式

方法定义在类中，而实例属性通常在构造器中赋值，二者都是面向对象编程的基本元素。这两个概念很大程度上类似于数据值的消息传递实现中的分发字典。对象使用点运算符接受消息，但是消息并不是任意的、值为字符串的键，而是类的局部名称。对象也拥有具名的局部状态值（实例属性），但是这个状态可以使用点运算符访问和操作，并不需要在实现中使用`nonlocal`语句。

消息传递的核心概念，就是数据值应该通过响应消息而拥有行为，这些消息和它们所表示的抽象类型相关。点运算符是 Python 的语法特征，它形成了消息传递的隐喻。使用带有内建对象系统语言的优点是，消息传递能够和其它语言特性，例如赋值语句无缝对接。我们并不需要不同的消息来“获取”和“设置”关联到局部属性名称的值；语言的语法允许我们直接使用消息名称。

**点表达式。**类似`tom_account.deposit`的代码片段叫做点表达式。点表达式包含一个表达式，一个点和一个名称：

```
<expression> . <name>
```

`<expression>`可为任意的 Python 有效表达式，但是`<name>`必须是个简单的名称（而不是求值为`name`的表达式）。点表达式会使用提供的`<name>`，对值为`<expression>`的对象求出属性的值。

内建的函数`getattr`也会按名称返回对象的属性。它是等价于点运算符的函数。使用`getattr`，我们就能使用字符串来查找某个属性，就像分发字典那样：

```py
>>> getattr(tom_account, 'balance')
10
```

我们也可以使用`hasattr`测试对象是否拥有某个具名属性：

```py
>>> hasattr(tom_account, 'deposit')
True
```

对象的属性包含所有实例属性，以及所有定义在类中的属性（包括方法）。方法是需要特别处理的类的属性。

**方法和函数。**当一个方法在对象上调用时，对象隐式地作为第一个参数传递给方法。也就是说，点运算符左边值为`<expression>`的对象，会自动传给点运算符右边的方法，作为第一个参数。所以，对象绑定到了参数`self`上。

为了自动实现`self`的绑定，Python 区分函数和绑定方法。我们已经在这门课的开始创建了前者，而后者在方法调用时将对象和函数组合到一起。绑定方法的值已经将第一个函数关联到所调用的实例，当方法调用时实例会被命名为`self`。

通过在点运算符的返回值上调用`type`，我们可以在交互式解释器中看到它们的差异。作为类的属性，方法只是个函数，但是作为实例属性，它是绑定方法：

```py
>>> type(Account.deposit)
<class 'function'>
>>> type(tom_account.deposit)
<class 'method'>
```

这两个结果的唯一不同点是，前者是个标准的二元函数，带有参数`self`和`amount`。后者是一元方法，当方法被调用时，名称`self`自动绑定到了名为`tom_account`的对象上，而名称`amount`会被绑定到传递给方法的参数上。这两个值，无论函数值或绑定方法的值，都和相同的`deposit`函数体所关联。

我们可以以两种方式调用`deposit`：作为函数或作为绑定方法。在前者的例子中，我们必须为`self`参数显式提供实参。而对于后者，`self`参数已经自动绑定了。

```py
>>> Account.deposit(tom_account, 1001)  # The deposit function requires 2 arguments
1011
>>> tom_account.deposit(1000)           # The deposit method takes 1 argument
2011
```

函数`getattr`的表现就像运算符那样：它的第一个参数是对象，而第二个参数（名称）是定义在类中的方法。之后，`getattr`返回绑定方法的值。另一方面，如果第一个参数是个类，`getattr`会直接返回属性值，它仅仅是个函数。

**实践指南：命名惯例。**类名称通常以首字母大写来编写（也叫作驼峰拼写法，因为名称中间的大写字母像驼峰）。方法名称遵循函数命名的惯例，使用以下划线分隔的小写字母。

有的时候，有些实例变量和方法的维护和对象的一致性相关，我们不想让用户看到或使用它们。它们并不是由类定义的一部分抽象，而是一部分实现。Python 的惯例规定，如果属性名称以下划线开始，它只能在方法或类中访问，而不能被类的用户访问。

## 2.5.4 类属性

有些属性值在特定类的所有对象之间共享。这样的属性关联到类本身，而不是类的任何独立实例。例如，让我们假设银行以固定的利率对余额支付利息。这个利率可能会改变，但是它是在所有账户中共享的单一值。

类属性由`class`语句组中的赋值语句创建，位于任何方法定义之外。在更宽泛的开发者社群中，类属性也被叫做类变量或静态变量。下面的类语句以名称`interest`为`Account`创建了类属性。

```py
>>> class Account(object):
        interest = 0.02            # A class attribute
        def __init__(self, account_holder):
            self.balance = 0
            self.holder = account_holder
        # Additional methods would be defined here
```

这个属性仍旧可以通过类的任何实例来访问。

```py
>>> tom_account = Account('Tom')
>>> jim_account = Account('Jim')
>>> tom_account.interest
0.02
>>> jim_account.interest
0.02
```

但是，对类属性的单一赋值语句会改变所有该类实例上的属性值。

```py
>>> Account.interest = 0.04
>>> tom_account.interest
0.04
>>> jim_account.interest
0.04
```

**属性名称。**我们已经在我们的对象系统中引入了足够的复杂性，我们需要规定名称如何解析为特定的属性。毕竟，我们可以轻易拥有同名的类属性和实例属性。

像我们看到的那样，点运算符由表达式、点和名称组成：

```
<expression> . <name>
```

为了求解点表达式：

1.  求出点左边的`<expression>`，会产生点运算符的对象。
2.  `<name>`会和对象的实例属性匹配；如果该名称的属性存在，会返回它的值。
3.  如果`<name>`不存在于实例属性，那么会在类中查找`<name>`，这会产生类的属性值。
4.  这个值会被返回，如果它是个函数，则会返回绑定方法。

在这个求值过程中，实例属性在类的属性之前查找，就像局部名称具有高于全局的优先级。定义在类中的方法，在求值过程的第三步绑定到了点运算符的对象上。在类中查找名称的过程有额外的差异，在我们引入类继承的时候就会出现。

**赋值。**所有包含点运算符的赋值语句都会作用于右边的对象。如果对象是个实例，那么赋值就会设置实例属性。如果对象是个类，那么赋值会设置类属性。作为这条规则的结果，对对象属性的赋值不能影响类的属性。下面的例子展示了这个区别。

如果我们向账户实例的具名属性`interest`赋值，我们会创建属性的新实例，它和现有的类属性具有相同名称。

```py
>>> jim_account.interest = 0.08
```

这个属性值会通过点运算符返回：

```py
>>> jim_account.interest
0.08
```

但是，类属性`interest`会保持为原始值，它可以通过所有其他账户返回。

```py
>>> tom_account.interest
0.04
```

类属性`interest`的改动会影响`tom_account`，但是`jim_account`的实例属性不受影响。

```py
>>> Account.interest = 0.05  # changing the class attribute
>>> tom_account.interest     # changes instances without like-named instance attributes
0.05
>>> jim_account.interest     # but the existing instance attribute is unaffected
0.08
```

## 2.5.5 继承

在使用 OOP 范式时，我们通常会发现，不同的抽象数据结构是相关的。特别是，我们发现相似的类在特化的程度上有区别。两个类可能拥有相似的属性，但是一个表示另一个的特殊情况。

例如，我们可能希望实现一个活期账户，它不同于标准的账户。活期账户对每笔取款都收取额外的 $1，并且具有较低的利率。这里，我们演示上述行为：

```py
>>> ch = CheckingAccount('Tom')
>>> ch.interest     # Lower interest rate for checking accounts
0.01
>>> ch.deposit(20)  # Deposits are the same
20
>>> ch.withdraw(5)  # withdrawals decrease balance by an extra charge
14
```

`CheckingAccount`是`Account`的特化。在 OOP 的术语中，通用的账户会作为`CheckingAccount`的基类，而`CheckingAccount`是`Account`的子类（术语“父类”和“超类”通常等同于“基类”，而“派生类”通常等同于“子类”）。

子类继承了基类的属性，但是可能覆盖特定属性，包括特定的方法。使用继承，我们只需要关注基类和子类之间有什么不同。任何我们在子类未指定的东西会自动假设和基类中相同。

继承也在对象隐喻中有重要作用，不仅仅是一种实用的组织方式。继承意味着在类之间表达“is-a”关系，它和“has-a”关系相反。活期账户是（is-a）一种特殊类型的账户，所以让`CheckingAccount`继承`Account`是继承的合理使用。另一方面，银行拥有（has-a）所管理的银行账户的列表，所以二者都不应继承另一个。反之，账户对象的列表应该自然地表现为银行账户的实例属性。

## 2.5.6 使用继承

我们通过将基类放置到类名称后面的圆括号内来指定继承。首先，我们提供`Account`类的完整实现，也包含类和方法的文档字符串。

```py
>>> class Account(object):
        """A bank account that has a non-negative balance."""
        interest = 0.02
        def __init__(self, account_holder):
            self.balance = 0
            self.holder = account_holder
        def deposit(self, amount):
            """Increase the account balance by amount and return the new balance."""
            self.balance = self.balance + amount
            return self.balance
        def withdraw(self, amount):
            """Decrease the account balance by amount and return the new balance."""
            if amount > self.balance:
                return 'Insufficient funds'
            self.balance = self.balance - amount
            return self.balance
```

`CheckingAccount`的完整实现在下面：

```py
>>> class CheckingAccount(Account):
        """A bank account that charges for withdrawals."""
        withdraw_charge = 1
        interest = 0.01
        def withdraw(self, amount):
            return Account.withdraw(self, amount + self.withdraw_charge)
```

这里，我们引入了类属性`withdraw_charge`，它特定于`CheckingAccount`类。我们将一个更低的值赋给`interest`属性。我们也定义了新的`withdraw`方法来覆盖定义在`Account`对象中的行为。类语句组中没有更多的语句，所有其它行为都从基类`Account`中继承。

```py
>>> checking = CheckingAccount('Sam')
>>> checking.deposit(10)
10
>>> checking.withdraw(5)
4
>>> checking.interest
0.01
```

`checking.deposit`表达式是用于存款的绑定方法，它定义在`Account`类中，当 Python 解析点表达式中的名称时，实例上并没有这个属性，它会在类中查找该名称。实际上，在类中“查找名称”的行为会在原始对象的类的继承链中的每个基类中查找。我们可以递归定义这个过程，为了在类中查找名称：

1.  如果类中有带有这个名称的属性，返回属性值。
2.  否则，如果有基类的话，在基类中查找该名称。

在`deposit`中，Python 会首先在实例中查找名称，之后在`CheckingAccount`类中。最后，它会在`Account`中查找，这里是`deposit`定义的地方。根据我们对点运算符的求值规则，由于`deposit`是在`checking`实例的类中查找到的函数，点运算符求值为绑定方法。这个方法以参数`10`调用，这会以绑定到`checking`对象的`self`和绑定到`10`的`amount`调用`deposit`方法。

对象的类会始终保持不变。即使`deposit`方法在`Account`类中找到，`deposit`以绑定到`CheckingAccount`实例的`self`调用，而不是`Account`的实例。

> 译者注：`CheckingAccount`的实例也是`Account`的实例，这个说法是有问题的。

**调用祖先。**被覆盖的属性仍然可以通过类对象来访问。例如，我们可以通过以包含`withdraw_charge`的参数调用`Account`的`withdraw`方法，来实现`CheckingAccount`的`withdraw`方法。

要注意我们调用`self.withdraw_charge`而不是等价的`CheckingAccount.withdraw_charge`。前者的好处就是继承自`CheckingAccount`的类可能会覆盖支取费用。如果是这样的话，我们希望我们的`withdraw`实现使用新的值而不是旧的值。

## 2.5.7 多重继承

Python 支持子类从多个基类继承属性的概念，这是一种叫做多重继承的语言特性。

假设我们从`Account`继承了`SavingsAccount`，每次存钱的时候向客户收取一笔小费用。

```py
>>> class SavingsAccount(Account):
        deposit_charge = 2
        def deposit(self, amount):
            return Account.deposit(self, amount - self.deposit_charge)
```

之后，一个聪明的总经理设想了`AsSeenOnTVAccount`，它拥有`CheckingAccount`和`SavingsAccount`的最佳特性：支取和存入的费用，以及较低的利率。它将储蓄账户和活期存款账户合二为一！“如果我们构建了它”，总经理解释道，“一些人会注册并支付所有这些费用。甚至我们会给他们一美元。”

```py
>>> class AsSeenOnTVAccount(CheckingAccount, SavingsAccount):
        def __init__(self, account_holder):
            self.holder = account_holder
            self.balance = 1           # A free dollar!
```

实际上，这个实现就完整了。存款和取款都需要费用，使用了定义在`CheckingAccount`和`SavingsAccount`中的相应函数。

```py
>>> such_a_deal = AsSeenOnTVAccount("John")
>>> such_a_deal.balance
1
>>> such_a_deal.deposit(20)            # $2 fee from SavingsAccount.deposit
19
>>> such_a_deal.withdraw(5)            # $1 fee from CheckingAccount.withdraw
13
```

就像预期那样，没有歧义的引用会正确解析：

```py
>>> such_a_deal.deposit_charge
2
>>> such_a_deal.withdraw_charge
1
```

但是如果引用有歧义呢，比如`withdraw`方法的引用，它定义在`Account`和`CheckingAccount`中？下面的图展示了`AsSeenOnTVAccount`类的继承图。每个箭头都从子类指向基类。

![](img/multiple_inheritance.png)

对于像这样的简单“菱形”，Python 从左到右解析名称，之后向上。这个例子中，Python 按下列顺序检查名称，直到找到了具有该名称的属性：

```
AsSeenOnTVAccount, CheckingAccount, SavingsAccount, Account, object
```

继承顺序的问题没有正确的解法，因为我们可能会给某个派生类高于其他类的优先级。但是，任何支持多重继承的编程语言必须始终选择同一个顺序，便于语言的用户预测程序的行为。

**扩展阅读。**Python 使用一种叫做 C3 Method Resolution Ordering 的递归算法来解析名称。任何类的方法解析顺序都使用所有类上的`mro`方法来查询。

```py
>>> [c.__name__ for c in AsSeenOnTVAccount.mro()]
['AsSeenOnTVAccount', 'CheckingAccount', 'SavingsAccount', 'Account', 'object']
```

这个用于查询方法解析顺序的算法并不是这门课的主题，但是 Python 的原作者使用一篇[原文章的引用](http://python-history.blogspot.com/2010/06/method-resolution-order.html)来描述它。

## 2.5.8 对象的作用

Python 对象系统为使数据抽象和消息传递更加便捷和灵活而设计。类、方法、继承和点运算符的特化语法都可以让我们在程序中形成对象隐喻，它能够提升我们组织大型程序的能力。

特别是，我们希望我们的对象系统在不同层面上促进关注分离。每个程序中的对象都封装和管理程序状态的一部分，每个类语句都定义了一些函数，它们实现了程序总体逻辑的一部分。抽象界限强制了大型程序不同层面之间的边界。

面向对象编程适合于对系统建模，这些系统拥有相互分离并交互的部分。例如，不同用户在社交网络中互动，不同角色在游戏中互动，以及不同图形在物理模拟中互动。在表现这种系统的时候，程序中的对象通常自然地映射为被建模系统中的对象，类用于表现它们的类型和关系。

另一方面，类可能不会提供用于实现特定的抽象的最佳机制。函数式抽象提供了更加自然的隐喻，用于表现输入和输出的关系。一个人不应该强迫自己把程序中的每个细微的逻辑都塞到类里面，尤其是当定义独立函数来操作数据变得十分自然的时候。函数也强制了关注分离。

类似 Python 的多范式语言允许程序员为合适的问题匹配合适的范式。为了简化程序，或使程序模块化，确定何时引入新的类，而不是新的函数，是软件工程中的重要设计技巧，这需要仔细关注。
