当前位置:   article > 正文

《数据结构与算法——Python语言描述》笔记(5)_數據結構和算法 python 和 c++ 語言描述

數據結構和算法 python 和 c++ 語言描述

第2章 抽象数据类型和python类

2.3 类的定义和使用

对于类定义和方法定义等机制,有以下几点说明:

  • 执行完一个类定义,从而创建了相应的类对象后,还可以通过属性赋值的放啊为这个类(对象)增加新属性。不仅可以增加数据属性,也可以增加函数属性。
  • 如果在一个方法函数里调用同一个类的其他实例方法,则要明确地通过函数的第一个参数self以属性描述的方式写方法调用。如对实例方法f的调用使用self.g(...)。
  • 方法函数中如果要定义一个全局变量或者函数,可以使用global声明。比如在某个函数内部使用了global sss ,接着写了一行sss=5,然后在其他函数中可以直接使用而且可以修改。(经过试验,发现分两次写可以,即先用global声明,再赋值,但如果直接用global sss=5就会报错。也是神奇
  • python提供了一个内置函数isinstance,专门用于检查类和对象的关系。表达式isinstance(obj, cls)检查对象obj是否是cls类的实例,如果是,则返回True,否则是False。

除了实例方法外,类里面还可以定义两种特别的函数:

  • 第一类是静态方法。在def行上方加上修饰符@staticmethod。静态方法就是普通的函数,只是由于一些需要将其定义在类里面,静态方法不需要self参数,可以通过类名或者实例名进行调用。比如可以使用Rational_numbers._gcd(...)或者self._gcd(...)来调用这个静态方法。
  • 第二类是类方法。在def行上方加上修饰符@classmethod,这种方法必须有一个表示其调用类的参数,习惯用cls作为参数名,还可以有任意多个其他参数。类方法也是类对象的属性,可以以属性访问的形式调用。在类方法执行时,调用它的类将自动约束到方法的cls参数,可以通过这个参数访问该类的其他属性。人们通常用类方法实现与本类所有对象有关的操作

举一个例子,假设定义的类要维护一个计数器,记录运行程序中创建该类的实例对象的个数,如下:

  1. class Countable():
  2. counter=0
  3. def __init__(self):
  4. Countable.counter+=1
  5. @classmethod
  6. def get_count(cls):
  7. return Countable.counter
  8. x=Countable()
  9. b=Countable()
  10. z=Countable()
  11. # 输出3,表示有3个实例
  12. print(Countable.get_count())

        这个类里面有一个属性是counter,这是类的属性,而非实例的属性,因此在类创建新的实例对象时,由于初始化函数,该属性会自增,但不会重新赋值。使用的时候多加注意。注意,get_count函数如果写成实例函数是行不通的,因为找不到可以调用这个的实例对象,而写成静态函数是可以的(试验过)。这里写成了类方法,应该说最为合适。

        类定义作为python语言里面的一种重要的定义结构,也是一种作用域单位。在类定义的名字(标识符)具有局部作用域。如果需要在类外使用,一种是使用实例属性,利用类生成一个实例对象,再用对象.属性调用,一种是使用类属性,用类名.属性进行调用。下面的定义是合法的:

  1. class C():
  2. a=0
  3. b=a+1
  4. def __init__(self):
  5. pass
  6. x=C.b
  7. print(x)

        可以看出,使用类自身属性的时候,甚至无需先生成类的实例,即无需对类初始化。

        注意到一个情况,即类属性在类内调用和在类外调用的时候,均需要采用类名.属性的方式(无论是数据属性还是方法属性),这不同于一般的方法定义。一般的函数,如果在其中定义了一个变量,那么在这个函数所有内部(包括嵌套包含的函数)中都可以访问,但是类属性则不同,并不会被延伸到类的全部作用域,必须要加上类名.来进行引用

---------------------------------------------------------------------------------------------------------------------------------  

        类继承的主要作用有两个:一个是基于已有的类定义新类,通过继承的方式复用已有类的功能,重用代码;另一个作用更为重要,建立一组类之间的继承关系,从而更好地组织代码,构建复杂的程序(如接口类)。派生类可以原封不动地使用基类的所有方法和属性,也可以修改之,还可以扩展新功能和属性。

        假设类C是类B的派生类,则C类的对象也可以看成是B的对象,人们经常希望在类B的实例对象的上下文中可以使用类C的实例对象,这就是面向对象编程的最重要的一条规则:替换原理。一个类可能是其他类的派生类,它有可能被用作基类去定义新的派生类,从而在程序中形成了一种层次结构。Python有一个最基本的内置类object,其中定义了一些所有的类都需要的功能,如果一个类没有说明基类,则该类就自动以object为基类。也就是说,object是用户自定义基类的直接或者派生类的基类。

        基于已有类BaseClass定义派生类的语法形式是:class <类名> (BaseClass,...):

列在类名后面括号中的参数就是制定的基类。Python内置函数issubclass检测两个类是否具有继承关系,如果cls2是cls1的直接或者间接基类,则表达式issubclass(cls1, cls2)返回True,否则返回False。

举一个简单的例子,我们定义一个自己的字符串类:

  1. class mystr(str):
  2. pass
  3. s=mystr(1234)
  4. print(issubclass(mystr,str))
  5. print(isinstance(s,mystr))
  6. print(isinstance(s,str))

结果均为True。

        派生类一般要重新定义__init__函数,完成该类实例的初始化,常见的情况是要求派生类的对象可以作为基类的对象,用在要求基类对象的环境中,在使用这种对象时,可能调用派生类自己的方法,也可以调用基类继承的方法,因此这种实例对象就应该包含基类的所有属性,完成这一初始化工作就是直接调用基类的__init__()方法,也就是说,在派生类的__init__()函数中,一般要这样写:

  1. class DerivedClass(BaseClass):
  2. def __init__(self,...):
  3. BaseClass.__init__(self,...)
  4. ....# 初始化函数的其他操作

在调用基类的初始化方法时,必须明确写出基类的名字,不能从self出发去调用。在调用基类的__init__时,必须把表示本对象的self作为调用的第一个实参。可能还需要传一些其他参数,这个调用完成了派生类实例中属于基类那部分属性的初始化工作。

        在派生类中覆盖基类的方法时,也经常希望新函数是基类同名函数的扩展,也就是说要让新函数包含覆盖函数的已有功能,处理方法和__init__类似,也是在新函数的定义中,用BaseClass.methodName(...)形式调用基类的方法。在这种调用中,注意要把本对象的self作为函数调用的第一个实参。(实际上,可以用这种形式调用基类的任何函数)

        如果从一个派生类的实例对象出发去调用方法,python解释器需要确定应该调用哪个函数(基类还是派生类),查找过程从实例对象所属的类开始,如果在这个类里面找到,则采用相应的函数定义,没有找到则去基类找。这样的话,意味着派生类可以覆盖基类的同名方法。

        看下面的代码,假设B是C的基类,显然创建B类的实例对象后,调用x.f()会打印出B.g called。但是如果创建了一个C类的实例对象并且调用了y.f()怎样?

  1. # code showing dynamic binding
  2. class B:
  3. def f(self):
  4. self.g()
  5. def g(self):
  6. print("B.g called")
  7. class C(B):
  8. def g(self):
  9. print('C.g called')
  10. x=B()
  11. x.f()
  12. y=C()
  13. y.f()

        由于C类没有定义f函数,因此y.f()实际调用的是类B里的f,由于在f的定义中出现了self.g(),那么如何确定应该调用的函数g呢?由于正文中,f的定义出现在B里,类B的self类型是B,从这个类型去查找g,则执行的是B中的g函数,这就是静态绑定(静态约束),但python和大多数面向对象的语言一样,基于方法调用时self表示的那个实例对象的类型去寻找g,也就是说,实例对象是C类的,在函数执行过程中出现self,一律视为C类,自然要去C类中寻找g,这就是动态约束,也就是动态绑定,输出C.g called。在程序设计领域,这种通过动态约束确定调用关系的函数称为虚函数

        python提供了一个内置函数super,把它用在派生类的定义里,就是要求从这个类的直接基类开始做属性检索。采用super函数而不直接写具体基类的名字,产生的查找过程更加灵活,如果直接写基类的名字,无论在什么情况下执行,总是调用该基类的方法,如果写super(),Python解释器会根据当前类的情况去寻找相应的基类,自动确定究竟该使用哪个基类的属性

        函数super有几种使用方法,简单的是不带参数的调用形式,如super().m(....)。如果在一个方法函数的定义中出现这个调用语句,则执行到这里,python就会从这个对象所属的基类开始,按照上面的属性规则去查找函数m,下面一段代码:

  1. class C1:
  2. def __init__(self,x,y):
  3. self.x=x
  4. self.y=y
  5. def m1(self):
  6. print(self.x,self.y)
  7. class C2(C1):
  8. # 下面的两行代码可以省略,省略的话,自动调用C1的初始化函数
  9. def __init__(self,a,b):
  10. C1.__init__(self,a,b)
  11. def m1(self):
  12. super().m1()
  13. print("Some special service.")
  14. x=C2(5,2)
  15. x.m1()

如果执行C2里的m1,python会从C2的基类中开始找m1,也就是从C1中查找。由于C1里面有m1的定义,因此最终调用的是C1的函数m1,显然这种形式的super函数调用(并进而调用基类的某方法)只能出现方法函数的定义里面,在实际调用时,当前实例将作为被调用函数的self实参

        函数super的第二种使用是super(C, obj).m(...),这种写法要求从指定的类C的基类开始查找函数属性m,调用里出现的obj必须是类C的一个实例。Python解释器找到函数m后将用obj作为函数的self实参,这种写法可以出现在程序的任何地方,不限于函数内。

----------------------------------------------------------------------------------------------------------------------------------

2.4 Python异常

        编程中有时需要自己定义异常类型,如果需要这样做,那么就应该选一个系统异常类,从它派生。异常是Python语言的一套特殊控制机制,用于支持错误的检查和处理,也可以用于特殊的控制转移。如果程序执行中发生异常,无论是解释器发现的异常情况,还是raise语句引发的异常,正常执行控制流立刻终止。

        此刻,解释器转入异常处理模式,查找能处理此异常的处理器,如果没有发现,则在交互环境下输出错误信息。程序运行中发生的异常都有特定的名字,如ValueError,TypeError,ZeroDivisionError等等,解释器根据发生的异常寻找处理器。Python中处理异常的结构是try语句,每个try语句可以处理任意多except语句,这种子语句就是异常处理器。














声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/Gausst松鼠会/article/detail/169077
推荐阅读
相关标签
  

闽ICP备14008679号