史上最全Python学习笔记(基于《Python学习手册(第4版)》)——Part6 类和OOP(中)

Chap28 类代码编写细节

class语句

class语句是Python主要的OOP工具,但与C++不同的是,Python的class并不是声明式的。迹象def一样,class语句是对象的创建者并且是一个隐含的赋值运算——执行时,它会产生类对象,并把其引用值存储在前面所使用的变量名。此外,像def一样,class语句也是真正的可执行代码。知道Python抵达并运算定义的class语句前,这个类都不存在(一般都是在其所在的模块被导入时,在这之前都不会存在)。

一般形式

class是复合语句,其缩进语句的主体一般都出现在头一行的下边。在头一行中,超类列在类名称之后的括号内,由逗号相隔。列出一个以上的超类会引起多重继承。以下为class语句的一般形式:

class<name>(superclass,...):  # assign to name
    data=value  # shared class data
    def method(self,...):
        self.member=value

在class语句内,任何赋值语句都会产生类属性,而且还有特殊名称重载运算符。例如,名为__init__的函数会在实例对象构造时调用(如果定义过的话)。

例子

就像之前见过的那样,类几乎就是命名空间,也就是定义变量名(属性)的工具,把数据和逻辑导出给客户端。就像模块文件,位于class语句主体中的语句会建立其属性。当Pyhon执行class语句时(不是调用类),会从头至尾执行其主体内的所有语句。在这个过程中,进行的赋值运算会在这个类作用域中创建变量名,从而称为对应的类对象内的属性。因此,类就像函数和模块:

  • 就像函数一样,class语句时本地作用域,由内嵌的赋值语句建立的变量名,就存在这个本地作用域内。
  • 就像模块内的变量名,在class语句内赋值的变量名会变成类对象中的属性。

类的主要的不同之处在于其命名空间也是Python继承的基础。在类或者实例对象中找不到的所引用的属性,就会在其他类中获取。

因为class是复合语句,所以任何种类的语句都可位于其主体内:pinrt、=、if、def等。当class语句自身运行时(不是稍后调用类来创建实例的时候),class语句内的所有语句都会执行。在class语句内赋值的变量名,会创建类属性,而内嵌的def则会创建类方法,但是,其他赋值语句也可制作属性。

class SharedData:
    spam=42  # Generates a class data attribute
    
x=SharedData()  # Make two instances
y=SharedData()
x.spam,y.spam  # They inherit and share 'spam'
(42, 42)

如上,因为变量名是在class语句的顶层进行赋值的,因此会附加到类中,从而为所有的实例共享。可以通过类名称来修改它,也可以通过实例或列引用来修改。

SharedData.spam=99
x.spam,y.spam,SharedData.spam
(99, 99, 99)

这种类属性可以用于管理贯穿所有实例的信息。例如,所产生的实例的数目的计数器(31章会扩展这一概念)。现在,如果通过实例而不是类来给变量名spam赋值,看看会发生什么:

x.spam=88
x.spam,y.spam,SharedData.spam
(88, 99, 99)

对实例的属性进行赋值运算会在该实例内创建或修改变量名,而不是在共享的类中。通常情况下,继承搜索只会在属性引用时发生,而不是在赋值运算时发生:对对象属性进行赋值总是会修改该对象,除此之外没有其他影响。例如,y.spam会通过继承而在类中查找,但是,对x.spam进行赋值运算则会把该变量名附加在x本身上。

下面的这个例子,可以更容易地理解这种行为,把相同的变量名储存在两个位置。假设执行下列类:

class MixedNames:  # Define class
    data='spam'  # Assign class attr
    def __init__(self,value):  # Assign method name
        self.data=value  # Assign instance attr
    def display(self):
        print(self.data,MixedNames.data)  # Instance attr,class arrt

这个类有两个def,把类属性和方法函数绑定在一起。此外,也包含一个=赋值语句。因为赋值语句是在类中赋值变量名data,该变量名会在这个类的作用域内存在,变成类对象的属性。就像所有类属性,这个data会被继承,从而被所有没有自己的data属性的类的实例所共享。

当创建这个类的实例的时候,变量名data会在构造函数方法内对self.data进行赋值运算,从而把data附加在这些实例上。

当创建这个类的实例的时候,变量名data会在构造函数方法内对self.data进行赋值运算,从而把data附加在这些实例上。

x=MixedNames(1)
y=MixedNames(2)
x.display();y.display()
1 spam
2 spam

结果就是,data存在于两个地方:在实例对象内(由__init__中的slef.data赋值运算所创建)以及在实例继承变量名的类中(由类中的data赋值运算所创建)。类的display方法打印了这两个版本,先以点号运算得到self实例的属性,然后才是类。

利用这些技术把属性储存在不同对象内,可以决定其可见范围。附加在类上时,变量名是共享的;附加在实例上时,变量名是属于每个实例的数据而不是共享的行为或数据。虽然继承搜索会查找变量名,但总是可以通过直接读取所需要的对象,而获得树中任何地方的属性。

方法

如果了解了函数,就了解了类中的方法。方法位于class语句的主体内,是由def语句建立的函数对象。从抽象的视角来看,方法替实例对象提供了要继承的行为。从程序设计的角度来看,方法的工作方式与简单函数完全一致,只是有个重要差异:方法的第一个参数总是接受方法调用的隐性主体,也就是实例对象。

换句话说,Python自动把实例方法的调用对应到类方法函数,如下所示。方法调用需要通过实例,就像这样:

instance.method(args...)

这会自动翻译成以下形式的类方法函数调用:

class.method(instance,args...)

class通过Python继承搜索流程找出方法名称所在之处。事实上,两种调用形式在Python中都有效。

除了方法属性名称是正常的继承外,第一个参数就是方法调用背后唯一的神奇之处。在类方法中,按惯例第一个参数通常称为self(严格地说,只有其位置重要,而不是其名称)。这个参数给方法提供了一个钩子,从而返回调用的主体,也就是实例对象:因为类可以产生许多实例对象,所以需要这个参数来惯例每个实例彼此各不相同的数据。

C++(JAVA也一样)程序员会发现,Pthon的self参数与C++的this指针很像。不过,Python中,self一定要在程序代码中明确地写出:方法一定要通过self来取出或修改由当前方法调用或正在处理的实例的属性。这种让self明确和的本质是有意设计的:这个变量名存在,会让你明确脚本中使用的实例属性名称,而不是本地作用域或全局作用域中的变量名。

例子

假设定义了下面的类:

class NextClass:  # Define class
    def printer(self,text):
        self.message=text
        print(self.message)

变量名printer引用了一个函数对象。因为这是在class语句的作用域中赋值的,就会变成类对象的属性,被由这个类创建的每个实例所继承。通常,因为像printer这类方法都是设计成处理实例的,所以得通过实例予以调用。

x=NextClass()

x.printer('instance call')
instance call
x.message
'instance call'

当通过对实例进行点号运算调用它时,printer会先通过继承将其定为,然后它的self参数会自动赋值为实例对象(x)。text参数会获得在调用时传入的字符串(‘instance call’)。注意:i那位Python会自动传递第一个参数给self,实际上只需传递一个参数。在printer中,变量名self是用于读取或设置每个实例的数据的,因为self引用的是当前正在处理的实例。

方法能通过实例或类本身两种方法其中的任意一种进行调用。例如,可以通过类名称调用printer,之傲明确地传递了一个实例给self参数。

NextClass.printer(x,'class call')  # direct class call
class call
x.message  # and instance changed again
'class call'

通过实例和类的调用具有相同的效果,只要在类实例中传递了相同的实例对象。实际上,在默认的情况下,如果尝试不带任何实例调用的方法时,就会得出错信息。

NextClass.printer('bad call')
---------------------------------------------------------------------------

TypeError                                 Traceback (most recent call last)

<ipython-input-11-628a808881c2> in <module>()
----> 1 NextClass.printer('bad call')


TypeError: printer() missing 1 required positional argument: 'text'

调用超类构造方法

方法一般是通过实例调用的。不过,通过类调用方法也扮演了一些特殊的角色。常见的场景涉及了构造方法。就像所有属性__init__方法是由继承进行查找的。也就是说,在构造时,Python会找出并且只调用一个__init__。如果要保证子类的构造方法也会执行超类构造时的逻辑,一般必须通过类明确地调用超类的__init__方法。

class Super:
    def __init__(self,x):
        ...default code...
class Sub(Super):
    def __init__(self,x,y):
        Super.__init__(self,x)
        ...default code...
        
I=Sub(1,2)

这是代码有可能直接调用运算符重载方法的环境之一。如果真的想运行超类的构造方法,自然只能用这种方法进行调用:没有这样的调用,子类会完全取代超类的构造方法。这门技术在实际中更显示的介绍,可以参加本章最后的例子。

其他方法调用的可能性

这种通过类调用方法的模式,是扩展继承方法行为(而不是完全取代)的一般基础。在第31章中,将会遇到Python2.2时新增的选项:静态方法,可以让程序员编写不预期第一参数为实例对象的方法。这类方法可像简单的无实例的函数那样运作,其变量名属于其所在类的作用域,并且可以用来管理类数据。一个相关的概念,类方法,当调用的时候接受一个类而不是一个实例,并且它可以用来管理基于每个类的数据。不过,这是高级的选用扩展功能。通常来说,一定要为方法传入实例,无论通过实例还是类调用都行。

继承

像class语句这样的命名空间工具的重点就是支持变量名继承。本节扩展了Python中关于属性继承的一些机制和角色。

在Python中,当对象进行点号运算时,就发生了继承,而且设计了搜索属性定义树(一个或多个命名空间)。每次使用object.attr形式的表达式时(object是实例或类对象),Python会从头至尾搜索命名空间,先从对象开始,寻找所能找到的第一个attr。这包括在方法中对self属性的引用。因为树种较低的定义会覆盖较高的定义,继承构成了专有化的基础。

属性树的构造

通常来说:

  • 实例属性是由对方法内self属性进行赋值预算而生成的。
  • 类属性是通过class语句内的语句(赋值语句)而生成的。
  • 超类的连接是通过class语句首行的括号内列出类而生成的。

继承方法的专有化

刚才谈到了继承树的搜索模式,变成了将系统专有化的最好方式。因为继承会现在子类寻找变量名,然后才查找超类,子类就可以对超类的属性重新定义来取代默认的行为。实际上,可以把整个系统做成类的层次,再新增外部的子类来对其进行扩展,而不是再原处修改已经存在的逻辑。

重新定义继承变量名的该奶奶引出了各种专有化技术。例如,子类可以完全取代继承的属性,提供超类可以找到的属性,并且通过已覆盖的方法回调超类来扩展超类的方法。再之前已经看到过实际中取代的做法。下面是如何进行扩展的例子:

class Super:
    def method(self):
        print('in Super.method')
        
class Sub(Super):
    def method(self):  # Override method
        print('starting Sub.method')  # Add actions here
        Super.method(self)  # Run default action
        print('ending Sub.method')
        
x=Sub()
x.method()
starting Sub.method
in Super.method
ending Sub.method

直接调用超类方法是这里的重点。Sub类以其专有化的版本取代了Super的方法函数。但是,取代时,Sub又回调了Super所导出的版本,从而实现了其默认的行为。换句话说,Sub.method只是扩展了Super.method的行为,而不是完全取代它。这种扩展编码模式常常用于构造函数。例如,参考本章之前的“方法”一节。

类接口技术

扩展知识一种与超类接口的方式。下面展示的specialize.py文件定义了多个类,示范了一些常用的技巧。

  • Super:定义一个method函数以及在子类中期待一个动作的delegate。
  • Inheritor:没有提供任何新的变量名,因此会获得Super中定义的一切内容。
  • Replacer:用自己的版本覆盖Super的method。
  • Extender:覆盖并回调默认的method,从而定制Super的method。
  • Provider:实现Super的delegate方法预期的action方法。
class Super:
    def method(self):
        print('in Super.method')
    def delegate(self):
        self.action()  # Expected to be defined
        
class Inheritor(Super):
    pass

class Replacer(Super):
    def method(self):
        print('in Replacer.method')
        
class Extender(Super):
    def method(self):
        print('starting Extender.method')
        Super.method(self)
        print('ending Extender.method')
        
class Provider(Super):
    def action(self):
        print('in Provider.action')
        
if __name__=='__main__':
    for klass in (Inheritor,Replacer,Extender):
        print('\n'+klass.__name__+'...')
        klass().method()
        print('\nProvider...')
        x=Provider()
        x.action()
Inheritor...
in Super.method

Provider...
in Provider.action

Replacer...
in Replacer.method

Provider...
in Provider.action

Extender...
starting Extender.method
in Super.method
ending Extender.method

Provider...
in Provider.action

抽象超类

注意上一个例子中的Provider类是如何工作的。当通过Provider实例调用delegate方法时,有两个独立的继承搜索会发生:

  1. 在最初x.delegate的调用中,Python会搜索Provider实例和它上层的对象,知道在Super中找到delegate的方法。实例x会像往常一样传递给这个方法的self参数。
  2. 在Super.delegate方法中,selef.acion()会对self以及它上层的对象启动新的独立继承搜索。因为self指的是Provider实例,在Provider子类中就会找到action方法。

这种“填空”的代码结构一般就是OOP的软件框架。至少,从delegate方法的角度来看,这个例子中的超类有时也称作是抽象超类——也就是类的部分行为默认是由其子类所提供的。如果预期的方法没有在子类中定义,当继承搜索失败时,Python会引发未定义变量名的异常。

类的编写者偶尔会使用assert语句,使这种子类需求更为明显,或者引发内置的异常NotImplementError(将在下一部分深入学习可能触发异常的语句时深入讲解)。作为提前介绍,下面是assert方法的实际应用示例:

class Super:
    def delegate(self):
        self.action()
    def action(self):
        assert False,'action must be defined!'  # if this version is called

X=Super()
X.delegate()
---------------------------------------------------------------------------

AssertionError                            Traceback (most recent call last)

<ipython-input-20-c3402a1669b5> in <module>()
      6 
      7 X=Super()
----> 8 X.delegate()


<ipython-input-20-c3402a1669b5> in delegate(self)
      1 class Super:
      2     def delegate(self):
----> 3         self.action()
      4     def action(self):
      5         assert False,'action must be defined!'  # if this version is called


<ipython-input-20-c3402a1669b5> in action(self)
      3         self.action()
      4     def action(self):
----> 5         assert False,'action must be defined!'  # if this version is called
      6 
      7 X=Super()


AssertionError: action must be defined!

将在32和33章介绍assert。简而言之,如果其表达式运算结构为假,就会引发带有出错信息的异常。在这里,表达式总是为假(0).因此,如果没有方法重新定义,继承就会找到这个版本,触发出错信息。此外,有些类只在该类的不完整方法中直接产生NotImplemented异常。

class Super:
    def delegate(self):
        self.action()
    def action(self):
        raise NotImplementedError('action must be defined!')
        
X=Super()
X.delegate()
---------------------------------------------------------------------------

NotImplementedError                       Traceback (most recent call last)

<ipython-input-23-59f783ee492e> in <module>()
      6 
      7 X=Super()
----> 8 X.delegate()


<ipython-input-23-59f783ee492e> in delegate(self)
      1 class Super:
      2     def delegate(self):
----> 3         self.action()
      4     def action(self):
      5         raise NotImplementedError('action must be defined!')


<ipython-input-23-59f783ee492e> in action(self)
      3         self.action()
      4     def action(self):
----> 5         raise NotImplementedError('action must be defined!')
      6 
      7 X=Super()


NotImplementedError: action must be defined!

对于子类,除非提供了期待的方法来代替超类中默认的方法,否则将会得到异常。

class SubWrong(Super):pass

Y=SubWrong()
Y.delegate()
---------------------------------------------------------------------------

NotImplementedError                       Traceback (most recent call last)

<ipython-input-25-845759ebdd3e> in <module>()
      2 
      3 Y=SubWrong()
----> 4 Y.delegate()


<ipython-input-23-59f783ee492e> in delegate(self)
      1 class Super:
      2     def delegate(self):
----> 3         self.action()
      4     def action(self):
      5         raise NotImplementedError('action must be defined!')


<ipython-input-23-59f783ee492e> in action(self)
      3         self.action()
      4     def action(self):
----> 5         raise NotImplementedError('action must be defined!')
      6 
      7 X=Super()


NotImplementedError: action must be defined!
class SubRight(Super):
    def action(self):
        print('spam')
        
Y=SubRight()
Y.delegate()
spam

如果要参考这一节中概念的更实际的例子,可以参考第31章结尾的习题8以及第六部分中的解答(在附录B)

Python2.6和Python3.0中的抽象超类

在Python2.6和Python3.0中,前一小节的抽象超类(即“抽象基类”),需要由子类填充的方法,它们也可以以特殊的类语法来实现。我们编写代码的这种方法根据版本不同而有所变化。在Python3.0中,可以在class头部使用一个关键字参数,以及特殊的@装饰器语法,这二者都将在稍后更详细地进行学习。

from abc import ABCMeta,abstractmethod

class Super(metaclass=ABCMeta):
    @abstractmethod
    def method(self,...):
        pass

但是在Python2.6中,则使用一个类属性:

class Super:
    __metaclass__=ABCMeta
    @abstractmethod
    def met
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值