2.5.面向对象编程

\(2.5\)面向对象编程

一、面向对象编程概述

  在前文的讲述中,对象具有如下的特点:

  • 对象间可以建立抽象屏障
  • 对象具有无法被全局环境访问的局部状态
  • \(\cdots\)

  \(Python\)的对象系统可以利用面向对象编程(\(object-oriented\;programme\))语言来表述。

  对象系统为多个独立代理(\(agent\))在计算机内部的互动提供了新的隐喻方法。每个对象都将局部状态和行为通过抽象的方式进行绑定,从而将两者的复杂性进行抽象。对象间互相交流,而它们交流的结果得出了有用的结论。对象间不仅传递信息,而且和其它相同类型的对象共享行为、继承相同类型对象的特性

二、类(\(class\))

1.类的概念

  类是作为所有属于该类的对象的模版(\(template\))。每个对象都是类的一个实例(\(instance\))。

  类的声明具体说明了类的对象间共享的属性与方法。以之前的银行账户\(account\)为例,可以如下声明一个类:

1
a=Account('Kirk')

2.类的子概念

  类的属性(\(attribute\))是一个与对象相关联的“名字-值”对,通过“点标记”(\(dot-notation\))能访问类的属性。

1
2
3
4
>>> a.holder
'Kirk'
>>> a.balance
0
  类的方法(\(method\))是作用于对象及其计算的函数。方法的返回值与副作用取决于对象的属性、并且能改变对象的一些属性。
1
2
>>> a.deposit(15)
15

3.类的声明

  \(a.\)初始化声明

  一个声明类的语句有以下的结构:

1
2
class<name>:
<suite>

  一个类为了具体声明实例对象的属性,会内置一个初始化方法__init__,这称为类的构建(\(constructor\))

1
2
3
4
>>> class Account:
def __init__(self, account_holder):
self.balance = 0
self.holder = account_holder

  \(p.s.\)\(Python\)中,方法名以双下划线 __ 开头和结尾的形式被称为特殊方法(\(special\;methods)\)__init__是其中一种,它用于初始化对象的状态。当你创建一个新的实例时,\(Python\)会自动调用这个方法来初始化对象的属性。

  这里的参数account_holder是局部名称。但通过点标记,account_holder被绑定到了赋值语句上,因为此时account_holder作为self的属性被存储了。

  具体化\(Account\)类后,我们就可以创建一个具体的类的实例:

1
>>> a = Account('Kirk')

  这个对类的调用创建了一个新的对象,这个对象是类的一个实例(\(instance\))__init__的第一个参数self与这个对象绑定了,传入的是第二个参数的值。这样之后我们就可以访问实例的属性了:

1
2
3
4
>>> a.balance
0
>>> a.holder
'Kirk'

  每个独立声明的类的实例都有自己的独特身份(\(identity\)),例如以下语句所展示的:

1
2
3
4
5
6
7
8
9
>>> b = Account('Spock')
>>> b.balance = 200
>>> [acc.balance for acc in (a, b)]
[0, 200]

>>> a is a
True
>>> a is not b
True

  \(b.\)方法的声明

  在class语句内部使用def语句即可声明类的方法:

1
2
3
4
5
6
7
8
9
10
11
12
>>> class Account:
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语句作为属性在类中局部绑定。例如下面对\(Account\)方法的调用:

1
2
3
4
5
6
7
8
9
>>> spock_account = Account('Spock')
>>> spock_account.deposit(100)
100
>>> spock_account.withdraw(90)
10
>>> spock_account.withdraw(90)
'Insufficient funds'
>>> spock_account.holder
'Spock'

  每个方法的第一个参数都是self,表示调用该方法的对象。当一个方法通过点标记被调用后,对象执行了两个功能:

  • 决定对应的方法具体是什么。例如示例中的withdraw,这里的withdraw是类中的局部名称。
  • 作为方法的特殊参数

三、信息传递与点表达式

1.类中的信息传递模式

  在类中,对象通过点标记接受信息,但这些信息不再是分派字典中的字符串key了,而是类局部下的名称。同时类也有局部状态的值,我们可以运用点标记来访问与操作它们、而不需要nonlocal语句。

2.类的属性的实现

  类的属性可以通过简单的赋值语句实现,例如下面对\(Account\)类的interest属性的赋值:

1
2
3
4
5
6
>>> class Account:
interest = 0.02 # A class attribute
def __init__(self, account_holder):
self.balance = 0
self.holder = account_holder
# Additional methods would be defined here

  这个属性可以被所有该类的实例获取

1
2
3
4
5
6
>>> spock_account = Account('Spock')
>>> kirk_account = Account('Kirk')
>>> spock_account.interest
0.02
>>> kirk_account.interest
0.02

  同样地,当类的属性被改变了,所有该类的实例的这一属性都会改变

1
2
3
4
5
>>> Account.interest = 0.04
>>> spock_account.interest
0.04
>>> kirk_account.interest
0.04

2.点表达式

  点表达式是形如以下的语句:

1
<expression>.<name>
  一个点表达式通过一个对象和一个属性的名字(通过<expression>表达式得出)来访问对象的属性、并返回该属性的值。

  同时,通过内置函数getattr也可用字符串访问对象的属性,像分派字典里的那样:

1
2
>>> getattr(spock_account, 'balance')
10

  getattr函数还可以测试对象中有无某种属性:

1
2
>>> hasattr(spock_account, 'deposit')
True

3.方法与函数的差异

  当某个对象调用了一个方法时,这个对象会直接作为该方法第一个特殊参数。这样方法的特殊参数self与对象就得到了绑定。

  为了实现这种自动绑定,\(python\)对方法与函数进行了区分。利用type函数运行如下程序:

1
2
3
4
>>> type(Account.deposit)
<class 'function'>
>>> type(spock_account.deposit)
<class 'method'>

四、类的属性

1.类的属性的概念

  有的类中的属性会被所有属于该类的对象共享,这种属性是与类直接连接的、并不是与某个单独的类的实例相连。

3.类的属性的访问

  类的属性的访问也可通过点表达式实现:

1
<expression>.<name>

  访问类的属性遵循以下过程:

  • 计算<expression>,然后得到对应的对象。
  • <name>随后对应到该对象的属性,如果该对象对应属性存在,就返回该属性。
  • 如果不存在,<name>就会去类中寻找对应属性,并读取对应属性。
  • 如果这个属性是方法,就返回对应函数;否则返回对应值。

  在这一流程中,实例自己的属性先于类的属性被查找,这与局部环境对全局环境具有优先级相类似

4.类的属性的赋值规则

  在类的赋值语句中,如果对象是类的话,赋值语句就会设置一个类的属性;如果对象是实例的话,赋值语句就会设置一个实例的属性。对实例属性的改变不会影响类的属性

  下面的例子中,我们对实例的属性重新赋值:

1
2
3
>>> kirk_account.interest = 0.08
>>> kirk_account.interest
0.08
  但是,类的属性依然不变:
1
2
>>> spock_account.interest
0.04

  改变类的属性后,该实例的属性也不会被影响

1
2
3
4
5
>>> Account.interest = 0.05  # changing the class attribute
>>> spock_account.interest # changes instances without like-named instance attributes
0.05
>>> kirk_account.interest # but the existing instance attribute is unaffected
0.08

五、继承

1.继承的概念

  在面向对象的编程中,两个类可能有相似的属性,但一个是另一个的特殊例子。例如,我们想实现一个跟先前的\(Account\)不同的\(CheckingAccount\),该类的特殊如下:

1
2
3
4
5
6
7
>>> ch = CheckingAccount('Spock')
>>> 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\)术语中,普遍的类\(Account\)\(CheckingAccount\)的基准类,而\(CheckingAccount\)\(Account\)的子类

  子类会继承基准类的属性,但会覆盖原有的一些属性,包括一些方法。在继承中,我们只具体说明子类与基准类的差别,剩余的就默认与基准类一致。

  继承在我们的对象隐喻中也有着重要作用,它揭示了对象间的is-a关系。例如\(CheckingAccount\)is-a\(Account\)的一种特殊形式,那么继承的操作就顺理成章了。而如果两者只是单纯的has-a关系,那么两者不应有继承关系。

2.继承操作的实现

  首先我们给出基准类的实现作为基础:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
>>> class Account:
"""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\)的实现如下,我们通过<name> + <value>/def的形式定义\(CheckingAccount\)独有的属性:

1
2
3
4
5
6
>>> 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)

  下面是该子类对应属性的输出结果:

1
2
3
4
5
6
7
>>> checking = CheckingAccount('Sam')
>>> checking.deposit(10)
10
>>> checking.withdraw(5)
4
>>> checking.interest
0.01

  这里,我们还可以得出在子类中查找<name>的流程: * 如果这个名称在子类中,就输出对应属性的值。 * 否则,在基准类中找这个名称

  例如该例中的deposit属性,由于只在基准类中得到定义,因此子类属性的执行与基准类属性的执行相一致。

  即使子类覆盖了基准类的属性,子类依然可以访问基准类的属性。例如实现子类的withdraw函数的过程中调用了基准类的withdraw

  注意到我们调用的是self.withdraw_charge而非CheckingAccount.withdraw_charge,因为我们无法确定此时的self一定是CheckingAccount,这样确保了万无一失。

3.接口

  一个对象接口(\(object\;interface\))是一些被共享的属性的集合。例如,对于所有的account,都会有depositwithdraw方法和balance属性。\(Account\)类和\(CheckingAccount\)类都实现了这一接口。而继承通过这一方式促进了名称的共享。

六、多重继承

1.多重继承的概念与实现

  \(python\)支持一个继承不同基准类的子类的实现。

  假设我们已知一个继承\(Account\)\(SavingAccount\),它会在每次deposit时收取一定费用:

1
2
3
4
>>> class SavingsAccount(Account):
deposit_charge = 2
def deposit(self, amount):
return Account.deposit(self, amount - self.deposit_charge)
  这是,如果我们要实现一个基于\(SavingAccount\)\(CheckingAccount\)的新account\(AsSeenOnTVAccount\),它同时实现checksave功能,那么我们可以通过继承两个类来实现这一个类:
1
2
3
4
>>> class AsSeenOnTVAccount(CheckingAccount, SavingsAccount):
def __init__(self, account_holder):
self.holder = account_holder
self.balance = 1 # A free dollar!
  这样,只需要初始化,该子类的其它部分可借助继承的类来实现:
1
2
3
4
5
6
7
8
9
10
11
>>> 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
>>> such_a_deal.deposit_charge
2
>>> such_a_deal.withdraw_charge
1

2.多重继承的顺序结构

  在多重继承中,假如某个名为<name>的属性在两个继承类中都有,程序抉择的顺序结构如下:

  对于这种“钻石型”的继承结构,\(python\)会采用“从下往上、从左往右”的查找顺序,在各个类中查找该属性,直到找到为止。

七、对对象的总结

  像这节介绍的类、方法、继承、点表达式等特殊语法,让我们能够更好地形象化我们对程序的隐喻。

  在编写程序时,我们希望我们的对象系统推动程序的不同方面关注不同的问题。每个对象封装、解决问题的一个状态,并且每个类的声明实现程序总体逻辑的一部分。而抽象屏障让问题的不同方面的界限更加的鲜明。

  面向对象编程很好地契合了分为独立的模块化系统、每个系统互相交互的程序设计需求。例如我们对社交网络等生活中事物的模拟,当我们想在程序中实现这些模型时,我们可以把系统中的对象模块化为程序中的对象,同时用类表明它们的类别与联系

  同时,对象并不是唯一的实现抽象的方式。对于输入输出式的抽象,函数抽象显得更为自然。认识到什么时候该用对象抽象的方式、什么时候该用函数抽象的方式,是编程中关键的问题。