Python学习入门教程(28)—类(之三)

(本号正在连续推出以Python官网文档为主线的系统学习Python的系列文章或视频,感兴趣的朋友们欢迎搜索关注。在这里学习Python事半功倍!本文及后续文章如无特别声明均以Windows平台作为演示平台,Python版本为:3.8.1)


【本篇继上篇继续讲解关于"类"的内容】

相关的一些讨论

如果相同的属性名同时出现在实例和它的类中,那么属性查找将优先考虑该实例:

<code>>>> class Warehouse:
purpose = 'storage'
region = 'west'

>>> w1 = Warehouse()
>>> print(w1.purpose, w1.region)
storage west
>>> w2 = Warehouse()
>>> w2.region = 'east'
>>> print(w2.purpose, w2.region)
storage east/<code>

数据属性既可以由方法引用,也可以由对象的普通用户引用。换句话说,Python的类不能用于实现纯抽象数据类型。实际上,Python没有保证数据隐藏的任何措施,所谓的数据隐藏都是基于惯例的,也就是说大家约定把某种命名形式的数据作为私有数据对待,但任何用户仍旧可以只直接引用它。(需要说明的是,用C语言编写的Python实现可以完全隐藏实现细节,并在必要时控制对对象的访问,这可以用于用C编写的Python扩展)

用户应该谨慎地使用数据属性,因为稍有不慎用户可能会因为随意修改数据属性而破坏了本来该由方法维护的数据的一致性。注意,只要不发生名称冲突,用户可以在不影响方法有效性的情况下将自己的数据属性添加到实例对象中,但是遵守命名约定才能省去很多不必要的麻烦。

从方法中引用数据属性(或其他方法)没有简便方式。这样实际上可以增加方法的可读性,当阅读一个方法时,不可能混淆局部变量和实例变量。

通常,方法的第一个参数名为self。这只不过是一个约定,名称self对Python绝对没有特殊的含义。但是注意,如果不遵循约定,您的代码对其他Python程序员的可读性可能会降低,而且有的类浏览器程序可能是依赖于这种约定编写的。

类的任何函数对象属性都为该类的实例定义了一个方法。函数定义不需要封装在类定义中,完全可以将类外定义的函数对象赋值给类中的局部变量。例如:

<code># Function defined outside the class
def f1(self, x, y):
return min(x, x+y)

class C:
f = f1

def g(self):
return 'hello world'

h = g/<code>

现在f、g和h都是C类的属性,用来引用函数对象,因此它们都是类C的实例的方法。h与g是完全等价的。注意,这种类外定义函数的做法不是一种好做法,通常只会使程序的阅读者感到困惑。

实例方法可以通过使用self参数的方法属性来调用其他方法:

<code>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)/<code>

方法可以以与普通函数相同的方式引用全局名称。与方法关联的全局作用域是包含其定义的模块。类永远不用作全局作用域。尽管很少有在方法中使用全局数据的必要,但是全局作用域有许多合理的用法。例如,方法可以使用导入到全局作用域的函数和模块,以及其中中定义的函数和类。通常,包含该方法的类本身是在这个全局作用域中定义的。

在Python中每个值都是一个对象,因此也就有一个类(也称为其类型)与其对应,并且被存储在object._ class__中。

继承

Python支持类的继承,派生类定义的语法如下:

<code>class DerivedClassName(BaseClassName):
<statement-1>
.
.
.
<statement-n>/<statement-1>/<code>

名称BaseClassName必须定义在包含派生类定义的范围中。基类名还允许使用其他表达形式。这可能是很有用的,例如,当基类在另一个模块中定义时:

<code>class DerivedClassName(modname.BaseClassName):
<statement-1>
.
.
.
<statement-n>/<statement-1>/<code>

派生类定义的执行过程与基类相同。当类对象被构造时,基类会被记住用于解析属性引用,如果在类中没有找到请求的属性,则会继续在基类中查找。如果基类本身派生自其他类,则递归应用此规则。

派生类的实例化没有什么特别之处:DerivedClassName()创建一个类的新实例。方法引用的解析如下:首先搜索相应的类属性,如果在当前类中没有找到该属性,则将沿基类链继续搜索。如果在此过程中找到了函数对象,则方法引用就是有效的;如果没找到,则会报错。

派生类可以重写其基类的方法。由于方法在调用其对象的其他方法时没有特别的优先权,所以基类的方法如果调用同一基类中定义的另一个方法时,则可能最终调用覆盖该类的派生类的方法。(对于C++程序员来说,Python中的所有方法实际上都是虚函数。)

派生类中的重写了的方法实际上可能希望扩展而不是简单地替换同名的基类方法。有一种直接调用基类方法的简单方法:调用BaseClassName.methodname(self,arguments)。这有时对用户也是有用的。(注意,只有在基类作为BaseClassName在全局作用域内可访问时才有效。)

Python有两个与继承相关的内置函数:

  • isinstance() 用于检查一个实例的类型。例如,isinstance(obj, int)只有在obj为int型或者是派生自int型的类时才为真。
  • issubclass() 用于检查类继承关系。例如issubclass(bool, int)为真,因为bool是int的子类,而issubclass(float, int)为假,因为float不是int的子类。

多继承

Python还支持多重继承,也就是可以定义包含多个基类的子类。定义是这样的:

<code>class DerivedClassName(Base1, Base2, Base3):
<statement-1>
.
.
.
<statement-n>/<statement-1>/<code>

在大多数最简单的情况下,可以将对继承自父类的属性的搜索视为从左到右的深度优先搜索,而不是在继承结构中有重叠的同一个类中搜索两次。如果在DerivedClassName中没有找到属性,则在Base1中搜索它,然后(递归地)在Base1的基类中搜索它,如果在基类中没有找到,则在Base2中搜索它,依此类推。

事实上,实际的搜索过程要稍微复杂一些,为了支持对super()的调用,解析顺序是动态变化的。这种方法在其他一些多继承语言中称为call-next-method,它比单继承语言中的super调用更强大。

多继承中动态排序是必要的,因为多继承关系中很有可能出现一个或多个菱形关系(其中至少有一个父类可以从最下面的类通过多条路径访问)。例如,所有类都继承自object,因此任何情况下的多重继承都提供了到达object的多个路径。为了防止不止一次地访问此类基类,动态算法按照每个类中指定的父类以从左到右的顺序线性化了搜索顺序,从而保证每个父类只被单调地调用一次。这样一来,一个类可以派生子类而不影响对其父类搜索时的优先顺序。总之,这些属性使设计具有多重继承的可靠的可扩展类成为可能。


[关于"类"部分的内容本篇未完,下篇将继续讲解]

【结束】


分享到:


相關文章: