四时宝库

程序员的知识宝库

Python学习入门教程(26)—类(之一)

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


类提供了一种将数据和功能捆绑在一起的方法。创建新类将创建新的对象类型,从而允许创建该类型的新实例。每个类实例都可以添加属性来维护其状态。类实例还可以拥有用于修改其状态的方法(由类定义)。

与其他编程语言相比,Python的类机制添加的新语法和语义是最少的,它混合了C++和Modula-3中的类机制。Python类提供了面向对象编程的所有标准特性:类继承机制允许多个基类,派生类可以覆盖其基类的任何方法,而方法可以调用具有相同名称的基类的方法。对象可以包含任意数量和类型的数据。与模块一样,类也具有Python的动态特性:它们在运行时创建,在创建之后可以进一步修改。

按C++术语的说法,通常类成员(包括数据成员)是公共的(除了下面的私有变量),所有成员函数都是虚函数。与在Modula-3中一样,没有用于从对象的方法中引用对象成员的简化方法:方法函数声明时使用显式的第一个参数来表示其所属对象,此参数在调用该方法时隐式提供。与Smalltalk一样,类本身也是对象。这为导入和重命名提供了语义支持。与C++和Modula-3不同,内置类型可以用作用户扩展的基类。与C++中一样,大多数具有特殊语法的内置操作符(算术操作符、下标等)都可以在类中被重新定义。

对象和对象的名称

Python的对象也是具有个体性的。可以将多个名称(在多个范围内)绑定到同一个对象,这些名称在其他语言中称为别名。在初次接触Python时,这一点往往会被忽视。在处理不可变的基本类型(数字、字符串、元组)时,忽略了这一点不会有问题。然而对于可变对象,如列表、字典和大多数其他类型,忽视了这一点可能会造成问题。在程序中使用别名是有好处的,由于别名表现得像C++中的指针,这样传递对象的操作是廉价的,因为本质上只传递一个指针。还有个好处是:如果函数修改了作为参数传递的对象,调用者将也能看到这一修改。

Python的作用域和命名空间

在介绍类之前,首先介绍一些关于Python作用域规则的内容。类定义对命名空间进行了一些巧妙的处理,完全理解这些需要了解作用域和命名空间是如何工作的。顺便说一下,这部分知识对任何高级Python程序员都是有用的。先介绍几个定义:

命名空间 是从名称到对象的映射。目前,大多数命名空间都是以Python字典的方式实现的,怎么实现并不重要(除了性能之外),实现方法将来是可能发生变化的。对下面名字的映射就是一些命名空间的例子:内置的名称(包含abs()等函数和内置的异常名称);模块中的全局名;以及函数调用中的本地名称。在某种意义上,对象的属性集也形成命名空间。关于命名空间,需要注意的重要一点是,不同命名空间中的名称之间没有任何关系。例如,两个不同的模块都可以定义一个名为maximize的函数而不会混淆,但使用该函数的用户必须在它前面加上模块名,形如:module.maximize。

属性 对点后面的任何名称都称作属性。例如,表达式z.real是对象z的一个属性。严格来说,对模块中名称的引用是属性引用:在表达式modname.funcname中,modname是一个模块对象,而funcname是它的一个属性。在这种情况下,模块的属性和模块中定义的全局名称之间正好有一个简单的映射:它们共享相同的命名空间。属性可以是只读的,也可以是可写的。在后一种情况下,可以对属性赋值,例如modname.the_answer = 42。也可以用del语句删除模块属性,例如del modname.the_answer,执行之后属性the_answer将从modname对象中删除。

命名空间可以在不同的时刻创建,并且具有不同的生存期。包含内置名称的命名空间是在Python解释器启动时创建的,并且不会被删除。模块的全局命名空间是在模块定义被读入时创建的,通常模块命名空间也会持续到解释器退出为止。解释器的顶层调用所执行的语句,无论是从脚本文件中读取的,还是交互执行的,都被认为是一个名为__main__的模块的一部分,因此它们有自己的全局命名空间。(内置的名称实际上存在于一个名为builtins的模块中。)

函数的本地命名空间在调用该函数时创建,在函数返回或引发未在函数中处理的异常时删除。当然,每个递归调用都有自己的本地命名空间。

作用域 是Python程序中可以直接访问一个命名空间的文本区域。这里的“直接访问”意味着对名称的非限定引用会尝试在该命名空间中查找。虽然作用域是静态确定的,但它们是动态使用的。在执行过程中的任何时候,至少有三个嵌套的作用域的命名空间是可以被直接访问的:

  • 最内层的作用域(首先搜索它)包含本地名称
  • 任何封闭函数的作用域(从最近的封闭作用域开始搜索)包含非本地的名称,但也包含非全局的名称
  • 从外数第二个作用域包含当前模块的全局名称
  • 最外层的作用域(最后搜索)是包含内置名称

如果一个名称被声明为全局的,那么所有引用和赋值都直接指向包含其所在模块的全局名称的中间作用域。要更新在最内层作用域之外的变量,可以使用nonlocal语句。如果没有声明为nonlocal 变量,那么这些变量是只读的(对这样一个变量进行写操作,只会在最内部的作用域中创建一个新的局部变量,而相同名称的外部变量则保持不变)。

通常,位于函数内部的局部作用域引用当前函数的局部名称。在函数外部,局部作用域引用与全局作用域相同的命名空间,即所属模块的命名空间。类的定义会在局部作用域中另外引入一个命名空间。

一定要注意作用域是根据程序源文本确定的:在模块中定义的函数的全局作用域引用的是该模块的命名空间,不管从何处调用该函数,也不管调用的别名是什么。另一方面,虽然实际的名称搜索是在运行时动态完成的,但是语言定义却正朝着静态名称解析的方向发展(在“编译”时),所以不要依赖于动态名称解析。(事实上,局部变量已经被静态地确定了。)

Python的一个特殊之处是,如果没有全局或非局部语句在起作用,则对名称的赋值总是进入最内层的作用域。赋值不复制数据,它们只是将名称绑定到对象。删除也是如此,del x语句从局部作用域引用的命名空间中移除x的绑定。实际上,所有引入新名称的操作都使用局部作用域,特别是import语句和函数定义将模块或函数名绑定到局部作用域中。

global语句可以用来指示某个变量是位于全局范围内的,应该在全局范围内进行绑定。nonlocal语句表示某个变量位于在最内层作用域之外的一个封闭的作用域内,应该在那里进行绑定。

作用域和命名空间的示例如下:

def scope_test():
    def do_local():
        spam = "local spam"

    def do_nonlocal():
        nonlocal spam
        spam = "nonlocal spam"

    def do_global():
        global spam
        spam = "global spam"

    spam = "test spam"
    do_local()
    print("After local assignment:", spam)
    do_nonlocal()
    print("After nonlocal assignment:", spam)
    do_global()
    print("After global assignment:", spam)

scope_test()
print("In global scope:", spam)

输出结果为:

After local assignment: test spam
After nonlocal assignment: nonlocal spam
After global assignment: nonlocal spam
In global scope: global spam

注意:在示例中可以看到,本地赋值(默认值)没有改变scope_test对spam的绑定,nonlocal赋值更改了scope_test对spam的绑定,global赋值更改了模块级绑定。还可以看到,在global赋值之前没有针对spam的绑定。


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

【结束】

篇尾寄语:万丈高楼平地起,是否具有扎实的基础决定一个人能否走远以及能走多远。Python的学习也是同样的道理!

发表评论:

控制面板
您好,欢迎到访网站!
  查看权限
网站分类
最新留言
    友情链接