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
01
215) a.deposit(
15
3.类的声明
\(a.\)初始化声明
一个声明类的语句有以下的结构: 1
2class<name>:
<suite>
一个类为了具体声明实例对象的属性,会内置一个初始化方法__init__
,这称为类的构建(\(constructor\)): 1
2
3
4class 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
'Kirk') a = Account(
这个对类的调用创建了一个新的对象,这个对象是类的一个实例(\(instance\))。__init__
的第一个参数self
与这个对象绑定了,传入的是第二个参数的值。这样之后我们就可以访问实例的属性了:
1
2
3
4 a.balance
0
a.holder
'Kirk'
每个独立声明的类的实例都有自己的独特身份(\(identity\)),例如以下语句所展示的:
1
2
3
4
5
6
7
8
9'Spock') b = Account(
200 b.balance =
for acc in (a, b)] [acc.balance
[0, 200]
is a a
True
is not b a
True
\(b.\)方法的声明
在class
语句内部使用def
语句即可声明类的方法:
1
2
3
4
5
6
7
8
9
10
11
12class 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') spock_account = Account(
100) spock_account.deposit(
100
90) spock_account.withdraw(
10
90) spock_account.withdraw(
'Insufficient funds'
spock_account.holder
'Spock'
每个方法的第一个参数都是self
,表示调用该方法的对象。当一个方法通过点标记被调用后,对象执行了两个功能:
- 决定对应的方法具体是什么。例如示例中的
withdraw
,这里的withdraw
是类中的局部名称。 - 作为方法的特殊参数。
三、信息传递与点表达式
1.类中的信息传递模式
在类中,对象通过点标记接受信息,但这些信息不再是分派字典中的字符串key
了,而是类局部下的名称。同时类也有局部状态的值,我们可以运用点标记来访问与操作它们、而不需要nonlocal
语句。
2.类的属性的实现
类的属性可以通过简单的赋值语句实现,例如下面对\(Account\)类的interest
属性的赋值:
1
2
3
4
5
6class 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') spock_account = Account(
'Kirk') kirk_account = Account(
spock_account.interest
0.02
kirk_account.interest
0.02
同样地,当类的属性被改变了,所有该类的实例的这一属性都会改变:
1
2
3
4
50.04 Account.interest =
spock_account.interest
0.04
kirk_account.interest
0.04
2.点表达式
点表达式是形如以下的语句: 1
<expression>.<name>
<expression>
表达式得出)来访问对象的属性、并返回该属性的值。
同时,通过内置函数getattr
也可用字符串访问对象的属性,像分派字典里的那样:
1
2getattr(spock_account, 'balance')
10
getattr
函数还可以测试对象中有无某种属性:
1
2hasattr(spock_account, 'deposit')
True
3.方法与函数的差异
当某个对象调用了一个方法时,这个对象会直接作为该方法第一个特殊参数。这样方法的特殊参数self
与对象就得到了绑定。
为了实现这种自动绑定,\(python\)对方法与函数进行了区分。利用type
函数运行如下程序:
1
2
3
4type(Account.deposit)
<class 'function'>
type(spock_account.deposit)
<class 'method'>
四、类的属性
1.类的属性的概念
有的类中的属性会被所有属于该类的对象共享,这种属性是与类直接连接的、并不是与某个单独的类的实例相连。
3.类的属性的访问
类的属性的访问也可通过点表达式实现: 1
<expression>.<name>
访问类的属性遵循以下过程:
- 计算
<expression>
,然后得到对应的对象。 <name>
随后对应到该对象的属性,如果该对象对应属性存在,就返回该属性。- 如果不存在,
<name>
就会去类中寻找对应属性,并读取对应属性。 - 如果这个属性是方法,就返回对应函数;否则返回对应值。
在这一流程中,实例自己的属性先于类的属性被查找,这与局部环境对全局环境具有优先级相类似
4.类的属性的赋值规则
在类的赋值语句中,如果对象是类的话,赋值语句就会设置一个类的属性;如果对象是实例的话,赋值语句就会设置一个实例的属性。对实例属性的改变不会影响类的属性。
下面的例子中,我们对实例的属性重新赋值: 1
2
30.08 kirk_account.interest =
kirk_account.interest
0.081
2 spock_account.interest
0.04
改变类的属性后,该实例的属性也不会被影响:
1
2
3
4
50.05 # changing the class attribute Account.interest =
# changes instances without like-named instance attributes spock_account.interest
0.05
# but the existing instance attribute is unaffected kirk_account.interest
0.08
五、继承
1.继承的概念
在面向对象的编程中,两个类可能有相似的属性,但一个是另一个的特殊例子。例如,我们想实现一个跟先前的\(Account\)不同的\(CheckingAccount\),该类的特殊如下:
1
2
3
4
5
6
7'Spock') ch = CheckingAccount(
# Lower interest rate for checking accounts ch.interest
0.01
20) # Deposits are the same ch.deposit(
20
5) # withdrawals decrease balance by an extra charge ch.withdraw(
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
16class 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
6class 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'Sam') checking = CheckingAccount(
10) checking.deposit(
10
5) checking.withdraw(
4
checking.interest
0.01
这里,我们还可以得出在子类中查找<name>
的流程: *
如果这个名称在子类中,就输出对应属性的值。 *
否则,在基准类中找这个名称。
例如该例中的deposit
属性,由于只在基准类中得到定义,因此子类属性的执行与基准类属性的执行相一致。
即使子类覆盖了基准类的属性,子类依然可以访问基准类的属性。例如实现子类的withdraw
函数的过程中调用了基准类的withdraw
。
注意到我们调用的是self.withdraw_charge
而非CheckingAccount.withdraw_charge
,因为我们无法确定此时的self
一定是CheckingAccount
,这样确保了万无一失。
3.接口
一个对象接口(\(object\;interface\))是一些被共享的属性的集合。例如,对于所有的account
,都会有deposit
与withdraw
方法和balance
属性。\(Account\)类和\(CheckingAccount\)类都实现了这一接口。而继承通过这一方式促进了名称的共享。
六、多重继承
1.多重继承的概念与实现
\(python\)支持一个继承不同基准类的子类的实现。
假设我们已知一个继承\(Account\)的\(SavingAccount\),它会在每次deposit
时收取一定费用:
1
2
3
4class SavingsAccount(Account):
deposit_charge = 2
def deposit(self, amount):
return Account.deposit(self, amount - self.deposit_charge)account
\(AsSeenOnTVAccount\),它同时实现check
与save
功能,那么我们可以通过继承两个类来实现这一个类:
1
2
3
4class 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"John") such_a_deal = AsSeenOnTVAccount(
such_a_deal.balance
1
20) # $2 fee from SavingsAccount.deposit such_a_deal.deposit(
19
5) # $1 fee from CheckingAccount.withdraw such_a_deal.withdraw(
13
such_a_deal.deposit_charge
2
such_a_deal.withdraw_charge
1
2.多重继承的顺序结构
在多重继承中,假如某个名为<name>
的属性在两个继承类中都有,程序抉择的顺序结构如下:
对于这种“钻石型”的继承结构,\(python\)会采用“从下往上、从左往右”的查找顺序,在各个类中查找该属性,直到找到为止。
七、对对象的总结
像这节介绍的类、方法、继承、点表达式等特殊语法,让我们能够更好地形象化我们对程序的隐喻。
在编写程序时,我们希望我们的对象系统推动程序的不同方面关注不同的问题。每个对象封装、解决问题的一个状态,并且每个类的声明实现程序总体逻辑的一部分。而抽象屏障让问题的不同方面的界限更加的鲜明。
面向对象编程很好地契合了分为独立的模块化系统、每个系统互相交互的程序设计需求。例如我们对社交网络等生活中事物的模拟,当我们想在程序中实现这些模型时,我们可以把系统中的对象模块化为程序中的对象,同时用类表明它们的类别与联系。
同时,对象并不是唯一的实现抽象的方式。对于输入输出式的抽象,函数抽象显得更为自然。认识到什么时候该用对象抽象的方式、什么时候该用函数抽象的方式,是编程中关键的问题。