Python in nutshell 2nd 简明翻译 (7)

Chapter 5. 面向对象的Python

Python是面向对象的编程语言。跟其它一些面向对象的语言不同,Python并不强制要求你必须按面向对象的原则来编程,可以依据程序不同部分的不同特点与要求,混合面向过程的编程与面向对象的编程。

一般来说,面向对象的优势是封装数据与行卫,以及继承等面向对象的普遍优势。但是面向过程的编程也具有简单易懂的优势,所以在Python中可以按需进行选择。

现在,Python正处在两个不同的对象模型的迁移过程中。本章主要讲述推荐使用的新型对象模型,它更简单,更正规,更强大。当我说到类或实例时,若没有特别说明,均指新型类或实例。

然而,在Python2.x版本中,传统对象模型仍会是默认使用的模型,可能在几年后,当Python版本3发布时,新型对象模型才会成为默认使用的对象模型。所以在本章中,相关的主题在按新型对象模型讲述完后,都会再指出传统模型与新模型间的微小差异,以及如何在Python2.x中混合使用两种对象模型。

在本章的最后,还会讲到专用方法(Special Method),以及两个高级概念:装饰器(decorator)与元类(metaclass)。

 

5.1. 类与实例

类是用户自定义类型,你可以通过实例化类来得到实例,也就是此自定义类型的对象。Python通过类对象开实例对象来支持这些概念。

5.1.1.

类是具有以下特征的Python对象:

你可以像一个函数一样调用一个类对象,调用将会返回另一个对象,就是此类的实例,类就是此实例对象的类型。

类可以拥有任意数目的,可以绑定或引用的命名属性。

普通的数据对象,或描述符(descriptor)(包括函数)都可以成为类属性的值。

绑定到函数的类属性也被称为类方法。

方法具有特定的命名约定,若方法名以两个下划线开始,并以两个下划线结束,它就是一个专有方法。若类提供这样的方法,当针对类实例进行很多操作时, Python会隐式地调用它们。

类支持继承,当类中没有包括指定的属性或方法时,继承支持将这些属性或方法委托到其它类。

类的实例也是Python对象,它可以拥有任意数目的,你可以绑定或引用的命名属性。当实例对象本身不拥有某个属性时,它会隐式地将查找操作委托到它的类上,紧跟着,类也可以把这些查找操作委托到它们的超类上。

Python中,类是可以与其它对象完全一样进行对待的对象。因此,你可以将一个类作为参数传递给一个函数调用。一个函数也可以将类作为返回值返回。与其它对象一样,类也可以被绑定到变量(局部或全局变量)上,也可以被当作字典的键值。

5.1.2. class 语句

class是最主要用来创建类的方式。它是一个单子句组合语句,语法如下:

class classname(base-classes):

    statement(s)

其中,classname是一个标识符。在class语句执行结束后,它就是一个绑定到此类对象的变量。

base-classes是一组用逗号分隔的,且运算值为类对象的表达式。你可以称它们为被创建类的基类、超类,或父类。被创建的类可以被称为基类的继承类、子类、扩展类、或派生类。同样,这个类也是它基类的直接子类或派生类。

在语法上,base-classes是可选的,你可以创建一个没有基类的类,只要省略base-classes与括号就行了(在Python2.5中,也可以写上一对空括号)。但是,出于向后兼容的需要,一个没有基类的类是一个传统类型的类(除非你定义了__metaclass__属性,后面会有详细描述)。要创建一个没有明确基类的新型类C,类定义必须是 class C(object): ,指定基类为object对象,就指定了这个类是一个新型类。若你定义的类的所有基类都是传统类,且没有定义__metaclass__属性,则此类为传统类,否则,它就是一个新型类(即使它的基类有一些是新型类,有一些是传统类,它也是新型类)。

类之间的派生关系可以传递:若C1C2的子类,C2C3的子类,则C1也是C3的子类。内置函数 issubclass(C1,C2)来用检测继承关系,若C1C2的子类,则返回true,否则返回false。同时,所有类都是它自身的子类,所以对于任意类Cissubclass(C,C)总是为true

跟在class语句后的非空语句序列就是类的主体部分。在class语句被执行时,类的主体部分代码也会立即被执行。在主体部分代码运行结束前,新的类对象不会被创建出来,即由类名所表示的变量也处于非绑定状态。

最后,还要注意,class语句并没有立即创建任何类的实例,而只是定义了一组可以被所有类实例(可以在稍后,通过调用类来创建类实例)共享使用的属性。

5.1.3. 类定义的主体部分

类主体部分是你用来定义类属性的地方。这些属性可以是描述符对象(包括函数对象),也可以是任意类型的变通数据对象(类的属性也可以是另一个类,比如说,你可以在类中定义一个嵌套类)。

5.1.3.1. 类(类对象)的属性

通过在类定义的主体代码部分为一个标识符赋值就可以指定一个属性。例如:

class C1(object):

    x = 23

print C1.x                               # prints: 23

现在类C1有一个名字为x的属性,此属性被绑定到值23。使用C1.x可以引用此属性。

当然,在类定义主体之外,也可以绑定属性或移除属性绑定。例如:

class C2(object): pass

C2.x = 23

print C2.x                               # prints: 23

当然,将所有创建类属性的工作放在类定义主体代码内,可以有助于提高代码的可读性。类属性可以被此类的所有实例共享。

class语句还隐式地设置了一些类属性。属性__name__用来表示在class语句中使用的类名标识符。属性__bases__是一个包含类对象的tuple,其中包含类所有的基类。如:

print C1._ _name_ _, C1._ _bases_ _         # prints: C1, (<type 'object'>,)

属性__dict__是一个字典,用来保持对所有其它类属性的引用。对于任意类C,对象x,标识符S(属性__name____bases____dict__除外),C.S = x等同于C.__dict__[‘S’] = x。如:

C1.y = 45

C1._ _dict_ _['z'] = 67

print C1.x, C1.y, C1.z                   # prints: 23, 45, 67

对于在类定义的代码体内创建的属性,还是在类定义代码体外,还是通过C.__dict__创建的属性,它们之间完全相同。

唯一要注意的是,在类定义代码体内创建属性,不需要使用 . 操作符,如:

class C3(object):

    x = 23

    y = x + 22                         # must use just x, not C3.x

但是,在类中定义的方法内引用类属性,就必须要使用 . 操作符,否则,这个属性就成了函数的局部变量了。如:

class C4(object):

    x = 23

    def amethod(self):

        print C4.x                     # must use C4.x, not just x

 

 

 

对于属性引用(如表达式C.S),拥有比属性绑定更多的语义,后面部分会有详细描述。

5.1.3.2. 类内部的函数定义

因为对于大部分类来说,函数(也被称为类的方法)是很重要的类属性,所以在类定义中需要使用到def语句。类定义中的def语句与前面提到的函数部分中def语句的用法完全一致。只是,对于类方法,强制它们必须个参数,通常使用的参数名为self,用来引用到调用此方法的类实例。self参数在方法调用中扮演一个特殊的角色。示例如下:

class C5(object):

    def hello(self):

        print "Hello"

类还可以定义各种专有方法(方法名以双下划线开始并以双下划线结束)。

5.1.3.3. 类的私有变量

当在类中(或方法中)使用一个由双下划线开始的标识符(注意,此标识符的结尾没有使用双下划线),如__identPython编译器会隐式地将此标识符转换为_classname__ident,其中classname为类的名字。这样类就可以为属性、方法、全局变量等使用一个“私有”标识符,极大地降低出现同名的风险。

按照约定,所有以单下划线开始的标识符在绑定它的作用域内是私有的,不管此作用域是类作用域还是其它作用域。Python并不强制使用这个约定,但编程员还是应该尊重它。

5.1.3.4. 类注释字符串

类定义代码体的第一句语句可以是一个字符串,编译器会将此字符串绑定到类的__doc__属性。

5.1.4. 描述符

一个描述符就是实例对象的类提供了__get__方法的新型对象。描述符是用来控制存取类实例属性的类属性。概略地说,就是当你存取一个实例的属性时,Python通过调用相应描述符的__get__方法来获取属性值。

5.1.4.1. 覆盖与非覆盖描述符

若一个描述符的类还提供了专有方法__set__,则此描述符为覆盖描述符;若描述符的类只提供了__get__方法而没有__set__方法,则此描述符为非覆盖描述符。

例如,函数对象的类提供了__get__方法,而没有提供__set__方法,因此,函数对象是非覆盖描述符。简单地说,当你通过相应的覆盖描述符来为一个实例属性指派一个值,Python将通过调用描述符的__set__方法来处理。

传统类也可以拥有描述符,但是这些描述符只能作为非覆盖描述符,即使它提供了__set__方法。

5.1.5. 实例

要创建类的实例,只要像调用函数一样调用类对象就行了。每次调用都会返回一个新的实例:

anInstance = C5( )

调用内置函数isinstance(I,C)可以检测一个实例是否是某个类类型,若IC的一个实例,则返回true,否则,返回false

5.1.5.1. __init__

如果类继承或自己定义了名字为__init__的方法,则在调用类对象是创建新实例时,会在新实例上隐式地执行__init__方法,用来进行实例级的初始化工作。对类对象的调用必须提供与__init__方法声明相一致的调用参数(当然,self参数除外)。例如:

class C6(object):

    def _ _init_ _(self, n):

        self.x = n

下面是调用类对象创建类实例的语句:

anotherInstance = C6(42)

C6类定义的__init__方法中,初始化绑定了实例属性x。注意__init__不能有返回值,否则会引发TypeError异常。

使用__init__方法的主要目的是为新创建的类实例绑定、创建实例属性。当然,在__init__方法之外,也可以绑定属性或解除绑定属性。如果,只使用__init__方法来初始化绑定所有的实例属性可以大大提高代码的可读性。

若没有提供__init__方法,则在调用类对象创建类实例时不能带任何参数,且被创建出来的实例对象也不会拥有任何实例属性。

5.1.5.2. 实例对象的属性

一旦创建了一个实例,就可以通过 . 操作符来存它的属性(包括数据与方法)。

通过绑定一个值到一个属性引用上,就可以为实例对象添加一个属性。如:

class C7: pass

z = C7( )

z.x = 23

print z.x                                # prints: 23

实例对象z现在有了一个名字为x的绑定到数值23的属性。使用z.x可以访问此属性。

请注意专有方法__setattr__,若类提供了此方法,则它会拦截所有的绑定操作(稍后章节会有详细描述),语句z.x=23的隐式处理就是type(z).__setattr__(z,23)。此外,针对新型类的实例对象,若要绑定一个存在相应覆盖描述符的属性,则相应描述符的__set__方法会拦截绑定操作。在这种情况下,语句x.z = 23实际上将会执行type(z).x.__set__(z,23)。(请注意,对于传统类的实例对象,由于不支持覆盖描述符,所以不会有上述的隐式处理)

在创建实例的过程中,隐式地设置了两个实例属性。对于实例zz.__class__表示实例的类对象,z.__dict__z用来保持所有属性引用的字典属性。

你可以重绑定(但是不能解除绑定)这两个属性,虽然这很少用到。一个新型类实例对象的__class__属性只能重绑定新型类对象,同样,传统实例对象的__class__属性只能重绑定到传统类对象上。

对于任意实例对象z,任意对象x,以及一个标识符S(此标识符不能为__class____dict__,除非被__setattr__或覆盖描述符的__set__方法拦截,否则z.S = x 等效于z.__dict__[‘S’] = x。例如:

z.y = 45

z._ _dict_ _['z'] = 67

print z.x, z.y, z.z                         # prints: 23, 45, 67

__init__方法中设置属性与通过__dict__字典设置属性是效果完全等同的处理。

5.1.5.3. 工厂函数

依据一些条件来创建类的实例,或基于重用的目的避免创建类的多个实例,是我们总能碰到的任务。一个错误的概念是通过__init__方法来返回特定所需的对象,但这在Python中不能实现,因为__init__只能返回None,否则会引必异常。要实现灵活创建对象的最佳办法是使用一个普通的函数而不是直接调用类对象。担当这个职责的函数就被称为工厂函数。

工厂函数提供了灵活的处理方式:它可以返回一个已存在的可重用实例,或者依据条件创建某个类的新实例。假设你有两个类(SpecialCaseNormalCase),并且希望能基于一个参数来有选择地创建某个类的实例,下面的函数appropriateCase就可以完成此任务:

class SpecialCase(object):

    def amethod(self): print "special"

class NormalCase(object):

    def amethod(self): print "normal"

 

def appropriateCase(isnormal=True):

    if isnormal: return NormalCase( )

    else: return SpecialCase( )

 

aninstance = appropriateCase(isnormal=False)

aninstance.amethod( )                     # prints "special", as desired

 

5.1.5.4. __new__

每个新型类都拥有(或继承有)一个静态方法__new__(稍后章节详细描述静态方法)。当你通过调用C(*args, **kwds)来创建类C的新实例时,Python会首先调用C.__new__(C, *args, **kwds)方法来处理。而__new__方法返回的对象x就是创建得到的实例对象。之后,在确认x确实是C或其子类的实例对象后,Python会调用C.__init__(x, *args, **kwds)来初始化实例x的属性。(若x不是C或其子类的实例对象,则x的内部状态将保持在__new__方法执行完后的状态上,即Python不会调用__init__方法)。例如,x = C(23)等同于:

x = C.__new__(C, 23)

if isinstance(x, C): type(x).__init__(x, 23)

 

object.__new__方法以类对象为第一个参数(注意,所有新型类的基类都包含内置的object类),它会创建一个此类的新的、未初始化实例。若类定义了__init__方法,则__new__方法会忽略所有其它的参数,但是若类没有提供__init__方法,且__new__方法除接收到类本身作为第一个参数外,还存在其它参数,将会抛出异常。

在覆盖__new__方法时,不需要通过添加 __new__ = staticmethod(__new__) 语句来特别声明其为静态方法,Python会自动将名字为__new__的方法按静态方法对待。

还有一种很少出现的情况是,你在其它程序中重绑定了C.__new__方法,则在类C的外部,需要使用C.__new__ = staticmethod(whatever) 来声明静态方法。

__new__方法拥有像上面提到的工厂函数一样的在创建实例方面的灵活性。__new__可以按条件有选择地返回已存在的实例,或创建一个新的实例。当__new__方法确实需要创建一个新实例时,它可以委托创建操作给object.__new__方法(即调用object.__new__方法),或类的其它超类的__new__方法。以下的例子实现了单实例模式:

class Singleton(object):

    _singletons = {}

    def _ _new_ _(cls, *args, **kwds):

        if cls not in cls._singletons:

            cls._singletons[cls] = super(Singleton, cls)._ _new_ _(cls)

        return cls._singletons[cls]

(内置的super方法在稍后章节中详细描述)

任何Singleton类的没有覆盖掉__new__方法的子类,都将只会有一个实例存在。此处要特别注意,若子类定义了__init__方法,则你要自己保证针对同一个实例对象进行重复调用的安全性。(在此方法中,保证了实例的唯一性,但在你重复调用类实例时,__new__方法会返回同一个实例对象,按照上面的描述,针对__new__方法返回的实例,每一次都会运行__init__方法)

传统类不支持__new__方法。

 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值