SICP Python 描述 2.5 面向对象编程

2.5 面向对象编程

来源:2.5 Object-Oriented Programminghtml

译者:飞龙python

协议:CC BY-NC-SA 4.0git

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

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

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

2.5.1 对象和类

类能够用做全部类型为该类的对象的模板。每一个对象都是某个特定类的实例。咱们目前使用的对象都拥有内建类型,可是咱们能够定义新的类,就像定义函数那样。类的定义规定了在该类的对象之间共享的属性和方法。咱们会经过从新观察银行帐户的例子,来介绍类的语句。express

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

Account类容许咱们建立银行帐户的多个实例。建立新对象实例的动做被称为实例化该类。Python 中实例化某个类的语法相似于函数的调用语句。这里,咱们使用参数'Jim'(帐户持有者的名称)来调用Account数组

>>> a = Account('Jim')

对象的属性是和对象关联的名值对,它能够经过点运算符来访问。属性特定于具体的对象,而不是类的全部对象,也叫作实例属性。每一个Account对象都拥有本身的余额和帐户持有者名称,它们是实例属性的一个例子。在更宽泛的编程社群中,实例属性可能也叫作字段、属性或者实例变量。网络

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

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

>>> a.deposit(15)
15

在 OOP 中,咱们说方法能够在特定对象上调用。做为调用withdraw方法的结果,要么取钱成功,余额减小并返回,要么请求被拒绝,帐户打印出错误信息。

>>> 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语句建立,它只包含单个子句。类的语句定义了类的名称和基类(会在继承那一节讨论),以后包含了定义类属性的语句组:

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

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

类一般围绕实例属性来组织,实例属性是名值对,不和类自己关联但和类的每一个对象关联。经过为实例化新对象定义方法,类规定了它的对象的实例属性。

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

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

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

构造器将实例属性名称balance0绑定。它也将属性名称holder绑定到account_holder上。形参account_holder__init__方法的局部名称。另外一方面,经过最后一个赋值语句绑定的名称holder是一直存在的,由于它使用点运算符被存储为self的属性。

定义了Account类以后,咱们就能够实例化它:

>>> a = Account('Jim')

这个对Account类的“调用”建立了新的对象,它是Account的实例,以后以两个参数调用了构造函数__init__:新建立的对象和字符串'Jim'。按照惯例,咱们使用名称self来命名构造器的第一个参数,由于它绑定到了被实例化的对象上。这个惯例在几乎全部 Python 代码中都适用。

如今,咱们可使用点运算符来访问对象的balanceholder

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

身份。每一个新的帐户实例都有本身的余额属性,它的值独立于相同类的其它对象。

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

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

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

虽然由同一个调用来构造,绑定到ab的对象并不相同。一般,使用赋值将对象绑定到新名称并不会建立新的对象。

>>> c = a
>>> c is a
True

用户定义类的新对象只在类(好比Account)使用调用表达式被实例化的时候建立。

方法。对象方法也由class语句组中的def语句定义。下面,depositwithdraw都被定义为Account类的对象上的方法:

>>> 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参数来访问对象,因此它们能够访问并操做对象的状态。

为了调用这些方法,咱们再次使用点运算符,就像下面这样:

>>> 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,咱们就能使用字符串来查找某个属性,就像分发字典那样:

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

咱们也可使用hasattr测试对象是否拥有某个具名属性:

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

对象的属性包含全部实例属性,以及全部定义在类中的属性(包括方法)。方法是须要特别处理的类的属性。

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

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

经过在点运算符的返回值上调用type,咱们能够在交互式解释器中看到它们的差别。做为类的属性,方法只是个函数,可是做为实例属性,它是绑定方法:

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

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

咱们能够以两种方式调用deposit:做为函数或做为绑定方法。在前者的例子中,咱们必须为self参数显式提供实参。而对于后者,self参数已经自动绑定了。

>>> 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语句组中的赋值语句建立,位于任何方法定义以外。在更宽泛的开发者社群中,类属性也被叫作类变量或静态变量。下面的类语句以名称interestAccount建立了类属性。

>>> 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

这个属性仍旧能够经过类的任何实例来访问。

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

可是,对类属性的单一赋值语句会改变全部该类实例上的属性值。

>>> 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赋值,咱们会建立属性的新实例,它和现有的类属性具备相同名称。

>>> jim_account.interest = 0.08

这个属性值会经过点运算符返回:

>>> jim_account.interest
0.08

可是,类属性interest会保持为原始值,它能够经过全部其余帐户返回。

>>> tom_account.interest
0.04

类属性interest的改动会影响tom_account,可是jim_account的实例属性不受影响。

>>> 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,而且具备较低的利率。这里,咱们演示上述行为:

>>> 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

CheckingAccountAccount的特化。在 OOP 的术语中,通用的帐户会做为CheckingAccount的基类,而CheckingAccountAccount的子类(术语“父类”和“超类”一般等同于“基类”,而“派生类”一般等同于“子类”)。

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

继承也在对象隐喻中有重要做用,不只仅是一种实用的组织方式。继承意味着在类之间表达“is-a”关系,它和“has-a”关系相反。活期帐户是(is-a)一种特殊类型的帐户,因此让CheckingAccount继承Account是继承的合理使用。另外一方面,银行拥有(has-a)所管理的银行帐户的列表,因此两者都不该继承另外一个。反之,帐户对象的列表应该天然地表现为银行帐户的实例属性。

2.5.6 使用继承

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

>>> 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的完整实如今下面:

>>> 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中继承。

>>> 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和绑定到10amount调用deposit方法。

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

译者注:CheckingAccount的实例也是Account的实例,这个说法是有问题的。

调用祖先。被覆盖的属性仍然能够经过类对象来访问。例如,咱们能够经过以包含withdraw_charge的参数调用Accountwithdraw方法,来实现CheckingAccountwithdraw方法。

要注意咱们调用self.withdraw_charge而不是等价的CheckingAccount.withdraw_charge。前者的好处就是继承自CheckingAccount的类可能会覆盖支取费用。若是是这样的话,咱们但愿咱们的withdraw实现使用新的值而不是旧的值。

2.5.7 多重继承

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

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

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

以后,一个聪明的总经理设想了AsSeenOnTVAccount,它拥有CheckingAccountSavingsAccount的最佳特性:支取和存入的费用,以及较低的利率。它将储蓄帐户和活期存款帐户合二为一!“若是咱们构建了它”,总经理解释道,“一些人会注册并支付全部这些费用。甚至咱们会给他们一美圆。”

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

实际上,这个实现就完整了。存款和取款都须要费用,使用了定义在CheckingAccountSavingsAccount中的相应函数。

>>> 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

可是若是引用有歧义呢,好比withdraw方法的引用,它定义在AccountCheckingAccount中?下面的图展现了AsSeenOnTVAccount类的继承图。每一个箭头都从子类指向基类。

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

AsSeenOnTVAccount, CheckingAccount, SavingsAccount, Account, object

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

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

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

这个用于查询方法解析顺序的算法并非这门课的主题,可是 Python 的原做者使用一篇原文章的引用来描述它。

2.5.8 对象的做用

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

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

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

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

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

相关文章
相关标签/搜索