Classed 类

来源:互联网 发布:藏剑二少捏脸数据下载 编辑:程序博客网 时间:2024/05/21 19:45

与其他编程语言相比较,Python的类机制用尽可能少的新语法和语义定义了类。它是C++Modual-3类机制的混合。Python的类提供了面向对象编程(Object Oriented Programming)所有标准特性:

  • 类继承机制允许多个父类
  • 子类可以覆盖父类的任何方法
  • 方法可以以相同的名称调用父类的方法
  • 对象可以包含任意数量和种类的数据
  • 和模块一样,类具有Python的动态性
    • 在运行时创建
    • 在创建后可以被修改

C++术语中,大多数的类成员(包括数据成员)是 公开的,所有的成员是虚拟的。像Modula-3,在成员方法中没有简便的方式引用对象的成员:成员方法在声明时用一个确切的参数代表对象,在调用时隐式的传入。像Smalltalk,类本身就是对象。这为类的导入和重命名提供了合理性。与C++Modula-3, 内置类型可以作为父类用来拓展。像C++,类实例可以重定义大多数带有特殊语法的内置操作符(算数运算,下标)
(由于缺少公认可接受的术语来讨论类,我将偶尔的使用SmalltalkC++术语。我喜欢使用Modular-3的术语,因此它的面对对象的语义与Python的比较相似,但不想很少有读者听过他)

关于全名和对象的一些话

对象可以有独立和多个名字可以绑定到同一个对象上。这在别的语言中也叫别名。在首次浏览Python的时候通常不会意识到, 在处理不可变的基本类型时可能完全忽略掉(数值,字符串,元组)。然而,别名在Python代码涉及到可变对象例如列表,字典,和大多数其它类型时表现出惊人的效果。这通常用于优化程序,因为别名的行为像指针在某些表现上。例如,例如,传递一个对象的开销是很小的,因为实现上只是传递了一个指针;如果一个函数修改了作为参数传递的一个对象,调用者将会看见改变–这消除在Pascal中需要传递两个不同的参数的机制

初识类

类带来了一小点新的语法,三个新的对象类型,和一些新语义

类定义语法

类最简单的定义格式

class ClassName:    <statment-1>    .    .    .    <statement-N>

定义类,像定义函数一样要先执行才能有影响。(你当然可以把定义类的语句放在if语句的一个分支,或者函数里)
实际上,在类中定义的语句通常是函数定义,但是别的语句也是允许的,然后有时候非常有用–我们稍后回来介绍。在类中定义的函数通常有一个特殊形式的参数列表,这是由调用方法的习惯决定的–再次,我们会稍后在解释。
当类进入定义后,一个新的全名空间创建了,作为局部作用域–然后,所有的赋值成为这个新命名空间局部变量。特别是这里的函数定义会绑定新函数的名字
当一个类被定义,一个类对象也被创建了。基本上它是对类定义创建的命名空间进行了一个包装;我们在下一章进一步学习。原始的局部作用域(类定义引入之前生效的那个)得到恢复,类对象在这里绑定的类定义头部的类名(例子中是className)

类定义

类对象提供两种操作:属性引用和初始化

属性引用使用Python所有的属性引用标准语法obj.name。有效属性名字是当类对象被创建后类命名空间中所有的名字。因此,如果class像下面这样定义

class MyClass:    """A simple example class"""    i = 12345    def f(self):        return 'hello world'

然后MyClass.iMyClass.f 是有效的属性引用,分别返回一个整数和函数对象,Class 属性也可以被重新赋值,因此你可以通过赋值来改变MyClass.i的值。_doc_也是一个有效属性,返回一个属于类的文档字符串:A simple example class

类实例化使用函数符号。紧紧可以假装类对象是一个可以返回一个新的类实例但缺少参数的函数。例如(假定上面定义的类存在)

x = MyClass()

创建一个新的类实例,并把这个对象赋值给本地变量x
初始化操作创建一个空的对象。很多类希望创建的对象可以自定义一个初始状态。因此一个类可以定义一个叫__init__()的特殊方法,像下面这样:

def __init__(self):    self.data = []

当一个类定义一个叫__init__()的方法,类初始化的时候自定调用__init__()为新创建的类实例。所以在下面的示例中,可以获得一个新的,已初始化的实例:

x = MyClass()

当然,为了更好的灵活性,__init__()方法可以带有参数,所以在下面的示例中,在类初始化的操作时,参数传递给了__init__()。例如,

>>> class Complex:    def __init__(self, realpart, imagpart):        self.r = realpart        self.i = imagpart>>> x = Complex(3.0,  -4.5)>>> x.r3.0>>> x.i-4.5>>> 

实例化对象

现在我们参用实例对象作什么?实例对象仅理解的操作是属性引用。这有两种有效属性名字。数据属性和方法

数据属性 相当于Smalltakl的实例变量 C++中的成员属性。数据属性不用声明;像本地变量一样,它们会在第一次给它们赋值时生成。例如,如果x是上面创建的MyClass实例,下面的代码将会打印16,而不会产生错误

x.counter = 1while x.counter < 10:    x.counter = x.counter * 2print x.counterdel x.counter

另一种实例属性引用叫方法,方法是一个属性对象的函数(在Python中,方法这个术语不只针对类实例;其他对象类型也可以拥有方法。例如,例如,列表对象拥有叫做append, insert, remove sort 等方法。然而,在下面的讨论中,我们将使用方法这个术语仅仅代表类实例的方法,除非在特定的状态下)
实例对象的有效的方法名依赖于它的类。通过定义,类中所有函数的属性定义了其实例中相应的方法。所以在我们的例子中,x.f是一个有效的方法引用,虽然,MyClass.f是一个函数,但x.i并不是,因为MyClass.i也不是方法。但x.fMyClass.f关不是一样的东西–它是方法对象,不是函数对象

函数对象

通常,一个方法在其绑定之后可以立即被调用

x.f()

MyClass例子中,它会返回一个hello world字符串。然而,并不需要直接调用方法:x.f是一个方法对象,它可以被存储起来,过一会在调用。例如:

xf = x.fwhile True:    print xf()

将会不断打印hello world直到程序结束
当一个方法被调用时确切的发生了什么?你可能注意到x.f()被调用的时候没有参数,甚至为f()定义的特殊参数也没有,参数发生了什么?当然 当一个需要参数的函数被调用时没有传入参数会引发异常–即使这个参数没有使用。

当然你可能猜到了答案:这个方法的特殊之处在于这个对象被当作参数传入了这个函数。在我们的例子中,调用x.f()和调用MyClass.f(x)是完全一样的。通常,调用一个带有一系列参数的方法和调用类里的同名方法一是样的,只要在参数列表的前面插入这个对象
如果你还是不理解方法是怎样工作的,看一看具体的执行结果可能帮助你解决问题。当一个实例属性被引用,但这个属性并不是数据属性,它们去类里寻找。如果名字点号后面的是一个有效的类函数属性对象,就会将实例对象和函数对象封装进一个抽象对象:这就是方法。以一个参数列表调用方法对象时,它被重新拆封,用实例对象和原始的参数列表构造一个新的参数列表,然后以这个新的参数列表调用对应的函数对象
一般来讲,实例变量是每个实例的特别的数据。类变量用于所有实例的属性和方法共享

class Dog:    kind = 'canine'         # class variable shared by all instances    def __init__(self, name):        self.name = name    # instance variable unique to each instance>>> d = Dog('Fido')>>> e = Dog('Buddy')>>> d.kind                  # shared by all dogs'canine'>>> e.kind                  # shared by all dogs'canine'>>> d.name                  # unique to d'Fido'>>> e.name                  # unique to e'Buddy'

中上面的讨论中,使用可变对象的共享数据(例如列表,字典)可能带来惊讶的影响。例如,在下面的代码中这个诡异的列表可能不适合用于类变量因为这个列表被所有的Dog实例共享

class Dog:    tricks = []             # mistaken use of a class variable    def __init__(self, name):        self.name = name    def add_trick(self, trick):        self.tricks.append(trick)>>> d = Dog('Fido')>>> e = Dog('Buddy')>>> d.add_trick('roll over')>>> e.add_trick('play dead')>>> d.tricks                # unexpectedly shared by all dogs['roll over', 'play dead']

这个类正确的设计应该用实例变量来代替

class Dog:    def __init__(self, name):        self.name = name        self.tricks = []    # creates a new empty list for each dog    def add_trick(self, trick):        self.tricks.append(trick)>>> d = Dog('Fido')>>> e = Dog('Buddy')>>> d.add_trick('roll over')>>> e.add_trick('play dead')>>> d.tricks['roll over']>>> e.tricks['play dead']

补充建议

数据属性与方法属性同名时,数据属性会覆盖方法属性,为了避免命名冲突(可能会造成在大型程序中难以定位bug),用一些习惯来减少冲突的机会,可以用的习惯包括方法名称首首字母大写,使用一个独特的小写字母(也许只是一个下划线)作为数据属性的前缀,或者方法使用动词而数据属性使用名词。
数据属性可以被方法引用,也可能被一个普通对象引用。另一种说法,类是不能用来实现纯抽象数据类型。在事实上,Python不能强制隐藏数据–这完全基于约定。(另一方面,如果必要的话用C实现Python,可以完全隐藏一个对象的实现细节控制访问
客户端应该小心的使用数据属性–客户可能通过弄乱他们的数据属性而使那些由方法维护的常是不是变得混乱。注意,只要避免冲突,客户端可以为实例对象添加自己的数据属性而不影响方法的正确性,只要避免命名冲突–再一次强调,一个良好的命名习惯可以省掉不少令头痛的事
这里没有简洁的语法在一个对象的方法中引用这个对象的数据属性。我发现这实际上增加了方法的可读性情:在浏览一个方法的时候, 可以方便的区分出实例变量或本地变量
经常的,一个方法的每一个参数被称为self.这仅仅是一个约定:self在Python中没有绝对的特殊意义。注意,然而,如果你不遵守约定的话,你的代码对于其他Python程序员来说可读性会变得很差,可以想像的是一个类浏览程序可以也依赖这样的约定。
任何函数对象都是一个类的属性为这个类的实例定义的方法。在类中定义函数不是必要的,把一个函数对象赋值给一个类的本地变量也是可以的。例如:

# Function defined outside the classdef f1(self, x, y):    return min(x, x+y)class C:    f = f1    def g(self):        return 'hello world'    h = g

注意f,gh都是类C的属性(函数对象),因此这也是所有C类实例对象的属性–hg相等。注意实际上这个程序会使读者疑惑
方法可以调用其他方法通过使用函数属性(self参数)

class Bag:    def __init__(self):        self.data = []    def add(self, x):        self.data.append(x)    def addtwice(self, x):        self.add(x)        self.add(x)

方法可以像引用普通函数那样引用全局名字。与方法关联的全局作用域是包含类定义的模块(一个类永远不能作为全局作用域)。尽管很少有好的理由在方法中使用全局数据,全局作用域确有很多合法的用途:其一是方法可以调用导入全局作用域的函数和模块,也可以调用在其中定义的类和函数。通常,包含此方法的类也会定义在这个全局作用域,在下一节我们会了解为何一个方法要引用自己的类
每一个值都是一个对象,因此每一值都从属于一个类(也叫作类型)。他以object.__class__存储

继承

当然,一个程序语言不支持类继承将变得没有意义。子类定义的语法看上去像下面:

class DerivedClassName(BaseClassName):    <statement-1>    .    .    .    <statement-N>

BaseClassName与其子类一定要在同一个作用域中。用共他任意表达式代替基类的名称也是允许的。这会很有用,当一个父类定义在其他模块的时候

class DerivedClassName(modname.BaseClassName):

子类定义的执行过程和基类是相同的。类对象创建后,父类会被保存。这用于解析属性的引用:如果在一个类中没有找到引用的属性,则会继续在他的父类中寻找。如果父类本身是由别的类派生出而来,这个规则会递归应用。
子类的初始化没有什么特别:DerivedClassName()创建一个新的类实例。方法引用解析遵守下面的规则:在相关类属性中寻找,必要时没父类链逐级搜索,如果找到了这个函数对象,那么这个方法的引用变是合法的。
子类可以覆盖他们的父类方法。因为方法在调用这个对象的其他方法时没有特权,父类的方法调用本类的方法时,可参实际上最终调用了子类的覆盖方法。(对于C++程序员:Python中的所有方法实际上都是虚的。)
子类中的覆盖方法可能是想要扩充而不是简单的替代基类中的重名方法。有一个简单的方式来直接调用父类的方法:BaseClassName.methodname(self,arguments)。有时这对于客户端也很有用。(要注意只有BaseClassName在同一个全局作用域定义或导入时才能这样用)
Python有两个继承相关的内置函数

  • 使用isinstance()来检测实例类型:isinstance(obj, int)会是True如果obj.__class__int或其子类
  • 使用issubclass()来检测类继承:issubclass(bool, int)Ture因为boolint的子类。然而issubclass(unicode, str)False因为unicode不是str的子类(他们仅仅共享同一个原型,basestring).

多重继承

Python也在一定限度上支持多重继承。具有多个父类的类定义如下:

class DerivedClassName(Base1,Base2,Base3):<statement-1>...<statement-N>

对于老式的类来说,唯一的规则是深度优先,从左至右。因此如果一个属性在DerivedClassName中没有被找到,会在Base1中寻找,然后递归的在Base1的继承链中寻找,如果还没有被找到,则会在Base2中寻找
(对于某些人,广度优先–在搜索Base1的父类之前先搜索Base2Base3 –看起来更加自然。然而,在你能弄明白Base1的某个特定属性与Base2中的一个属性名称冲突的后果之前,你需要知道该特定属性实际上是定义在Base1中还是在其某一个基类中。深度优先规则合Base1的直接属性和继承的属性之间没有差别
对于新式类,方法的解析顺序动态变化地支持合作地对super()进行调用。这种方式在其他多继承语言中也叫作call-next-method。它双单继承中的super更强大。
对于新式类,动态调整顺序是必要的。因为所有的多继承都会有一个或多个菱形关系(从最底部的类向上,至少会有一个父类可以通过多条路径访问到)例如,所有新式类都继承自object,所以任何多继承都会有多条路径到达object。为了防止基类被重复访问,动态算法线性化搜索顺序,每个类都按从左到右的顺序特别指定了顺序,每个父类只调用一次,这是单调的(也就昌说一个类被继承时不会影响它祖先的次序)。所有这些特性使得设计可靠并且扩展的多继承类成为可能

私有变量和类本地引用

在Python中不存在只能从一个实例对象里访问的私有变量。然而,有一个通常的约定被大多数Python代码遵守:一个带有_前缀的名字会被视为非公开的API(无论它是一个函数,方法或者成员属性)。它应该被视为实现细节,如果将来更改,不会另行通知
因为有一个合理的类私有成员的使用场景(即为了避免名称与子类定义的名称冲突),Python对这种机制有简单的支持,称为name mangling.任何以__spam的标识符(前面至少两个下划线,后面至少一个下划线)在文本上会被替换成为_classname__spam,classname是当前类名。此mangling会生效而不考虑该标识符的句法位轩,只要它出现在类的定义范围内。
name mangling对于子类重写父类方法而不破坏父类内部方法调用例如:

class Mapping:    def __init__(self, iterable):        self.items_list = []        self.__update(iterable)    def update(self, iterable):        for item in iterable:            self.items_list.append(item)    __update = update   # private copy of original update() methodclass MappingSubclass(Mapping):    def update(self, keys, values):        # provides new signature for update()        # but does not break __init__()        for item in zip(keys, values):            self.items_list.append(item)

请注意mangling规则的目的主要是避免发生意外;它仍然有可能被访问或修改一个被认为是私有的变量。这在特殊情况下,例如调试的时候,还是有用的。
请注意传递给exec,eval()或execfile()的代码没有考虑要将调用类的类名当作当前类;这类似于global语句的效果,影响只限于一起进行字节编译的代码。相同的限制适用于getattr(),setattr()delattr(),以及直接引用__dict__时。

零碎的说明

一些时候一种类似于Pascalrecord 或者C struct的数据类型是很有用的,把几个已命名的数据项目绑定在一起。一个空的类定义可以很好地做到

class Employee:    passjohn = Employee() # Create an empty employee record# Fill the fields of the recordjohn.name = 'John Doe'john.dept = 'computer lab'john.salary = 1000

某一段 Python 代码需要一个特殊的抽象数据结构的话,通常可以传入一个类来模拟该数据类型的方法。例如,如果你有一个用于从文件对象中格式化数据的函数,你可以定义一个带有read ()和readline () 方法的类,以此从字符串缓冲读取数据,然后将该类的对象作为参数传入前述的函数。

实例的方法对象也有属性: m.im_self是具有方法m()的实例对象,m.im_func是方法的函数对象。

异常也是类

用户自定义的异常也被标识为类。使用这种机制可以创建异常的层级拓展
对于raise语句有两种有效的形式

raise Class, instanceraise instance

在第一种形式,instance必须是一个Class的实例,或者是一个子类的实例。第二个形式是一个下面的一个简写:

raise instance.__class__, instance

except子句中的类如果与异常是同一个类或者是其基类,那么它们就是相容的。(但是反过来是不行的–except子名列出的子类与基类是不相容的)。例如,下面的代友将按该顺序打印B、C、D:

class B:    passclass C(B):    passclass D(C):    passfor c in [B, C, D]:    try:        raise c()    except D:        print "D"    except C:        print "C"    except B:        print "B"

请注意,如果except子句的顺序倒过来(except B B在最前面,它就会打印B,B,B–第一个匹配的异常被触发。
打印一个异常类的错误信息时,先打印类名,然后是一个空格、一个冒号,然后是内置函数str()将类转换得到的完整字符串

我的理解:也就是说,引发一个异常时,这个异常类本身或者其父类可以捕获
except声明捕获异常顺序也很重要,由上至下,优先级依次递减
异常不会被同级的多个excep捕获

Itertaors

到目前为止,你可能注意到大多数的容器对象可以用for语句循环:

for element in [1, 2, 3]:    print elementfor element in (1, 2, 3):    print elementfor key in {'one':1, 'two':2}:    print keyfor char in "123":    print charfor line in open("myfile.txt"):    print line,

这种风格的访问干净,简洁,方便。迭代器的用法在Python普遍而且统一。在后台,for语句在容器对象上调用iter(),这个函数返回一个定义了next()方法的迭代器–一次只访问一个元素。当容器没有可以访问的元素是,next()引发一个StopIteration的异常告诉for循环终止。下面的例子展示了他怎么工作

>>> s = 'abc'>>> it = iter(s)>>> it<iterator object at 0x00A1DB50>>>> it.next()'a'>>> it.next()'b'>>> it.next()'c'>>> it.next()Traceback (most recent call last):  File "<stdin>", line 1, in ?    it.next()StopIteration

看过iterator机制后,可以很容易的为你的类加上迭代行为。定义一个__iter__()方法,返回一个带有next()方法的对象。如果类定义了next(),__iter__()可以只返回self:

class Reverse:    """Iterator for looping over a sequence backwards."""    def __init__(self, data):        self.data = data        self.index = len(data)    def __iter__(self):        return self    def next(self):        if self.index == 0:            raise StopIteration        self.index = self.index - 1        return self.data[self.index]
>>> rev = Reverse('spam')>>> iter(rev)<__main__.Reverse object at 0x00A1DB50>>>> for char in rev:...     print char...maps

生成器

生成器是一个简洁的功能强大的工具用来创建迭代器。他可以像正常函数定义但是可以无论什么时候返回数据通过使用yield语句。每次next()被调用,生成器从他停止的地方重新开始(他会记住所有的数据,还有哪条语句是最后一次执行的)。
这个例子展示了生成器可以被平凡的创建

def reverse(data):    for index in range(len(data) - 1, -1, -1):        yield data[index]
>>> for char in reverse('golf'):...     print char...flog

生成器作的任何事用在上一章介绍的基于类的迭代器也可以作。自动创建的_iter_()next()方法使生成器非常的简洁
另外的主要特性是本地变量和执行状态在调用的时候自动保存。这使这个函数容易被创建,非常干净,与使用实例变量相比像self.indexself.data
除了自动创建的方法和保存程序状态,当一个生成器终止,它自动的引发一个StopIteration。这些特性结合起来很容易创建一个迭代器,而不用特殊的写一个正常的函数

生成器表达式

一些简单的生成器可以简洁的被输写作为一个使用表达式使用像列表表达式的语法,但是用圆括号代替中括号。这些表达式被设计用来在这些场景使用–生成器在闭合的函数中立即被使用。生成器表达式比完全的生成器更紧凑但是功能性要略逊于生成器函数,生成器表达式比等价的列表表达式对内在更友好

例如:

>>> sum(i*i for i in range(10))                 # sum of squares285>>> xvec = [10, 20, 30]>>> yvec = [7, 5, 3]>>> sum(x*y for x,y in zip(xvec, yvec))         # dot product260>>> from math import pi, sin>>> sine_table = dict((x, sin(x*pi/180)) for x in range(0, 91))>>> unique_words = set(word  for line in page  for word in line.split())>>> valedictorian = max((student.gpa, student.name) for student in graduates)>>> data = 'golf'>>> list(data[i] for i in range(len(data)-1,-1,-1))['f', 'l', 'o', 'g']
1 0
原创粉丝点击