• 第十课时之类


    9.类

    Python的类机制通过最小的新语法和语义在语言中实现了类

    9.1术语相关

    对象具有特性,并且有多个名称(在多个作用域中)可以绑定同一个对象上。在其他语言中被称为别名。在对python的第一印象中这通常会被忽略,并且当处理不可变基础类型(数字,字符串,元组)时可以被放心的忽略。但是在调用列表,字典这类可变对象,或者大多数程序外部类型(文件,窗体等)描述实体时,别名对python代码的语义便具有(有意而为)影响。这通常有助于程序的优化,因为在某些方面别名表现的就像是指针,例如,你可以轻易的传递一个对象,因为通过继承只是传递一个指针。并且如果一个方法修改了一个作为参数传递的对象,调用者可以接收这一变化---这消除了两种不同的参数传递机制的需要。

    9.2python作用域和命名空间

    命名空间是从命名到对象的映射当前命名空间主要是通过python字典实现的,不过通常不关心具体的实现方式(除非处于性能考虑),以后也有可能改变其实现方式。一下有一些命名空间的例子:内置命名(abs()这样的函数,以及内置异常名)集,模块中的全局命名,函数调用中的局部命名。某种意义上讲对象的属性集也是一个命名空间。关于命名空间需要了解的一件很重要的事就是不同命名空间中的命名没有任何联系,例如两个不同的模块可能都会定义一个名为maxsize的函数而不会发生混淆--用户必须以模块名为前缀来引用它们。

    严格来讲从模块中引用命名是引用属性:表达式modname.funcname中,modname是一个模块对象,funcname是它的一个属性。因此模块的属性和模块中的全局命名有直接的映射关系:它们共享一命名空间。

    作用域就是一个python程序可以直接访问命名空间的正文区域。这里的直接访问意思是一个对名称的错误引用会尝试在命名空间内查找。尽管作用域是静态定义,在使用时它们都是动态的。每次执行时,至少有三个命名空间可以直接访问的作用域嵌套在一起:

    •  包含局部命名的使用域在最里面,首先被搜索:其次搜索的是中层的作用域,这里包含了同级的函数;最后搜索最外面的作用域,它包含内置命名。
    •  首先搜索最内层的作用域,它包含局部命名任意函数包含的作用域,是内层嵌套作用域搜索起点,包含非局部,但是也非全局的命名
    •  接下来的作用域包含当前模块的全局命名
    •  最外层的作用域(最后搜索)是包含内置命名的命名空间 

    如果一个命名声明为全局的,那么对它的所有引用和赋值会直接搜索包含这个模块全局命名的作用域。如果要重新绑定最里层作用域之外的变量,可以使用nonlocal语句;如果不声明nonlocal,这些变量将是只读的(对这样的变量赋值会在最里面的作用域创建一个新的局部变量,外部具有相同命名的那个变量不会改变)

    Python的一个特别之处在于:如果没有使用global语法,其赋值操作总是在最里层的作用域。赋值不会复制数据,只是将命名绑定到对象。删除也是如此:del x只是从局部作用域的命名空间中删除命名x。事实上所有引入新命名的操作都作用与局部作用域。特别是import语句和函数定义将模块名或函数绑定于局部作用域(可以使用global语句将变量引入到全局作用域)

    global语句用以指明某个特定的变量为全局作用域,并重新绑定它。nonlocal语句用以指明某个特定的变量为封闭作用域,并重新绑定它。

    9.2.1作用域和命名空间示例

    以下是一个示例,演示了如何引用不同作用域和命名空间,以及globalnonlocal如何影响变量绑定:

    以上示例代码的输出为:

     

    注意local赋值语句是无法改变testx绑定。nonlocal赋值语句改变了testx绑定,并且global赋值语句从模块级改变了x绑定。

    9.3初识类

    类引入了一些新语法:三种新的对象型和一些新的语义

    9.3.1类定义语法

    类定义最简单的形式如下:

    类的定义就像函数定义(def语句),要先执行才能生效。

      进入类定义部分后,会创建一个新的命名空间,作为局部作用域。因此所有的赋值成为这个新的命名空间的局部变量。特别是函数定义在此绑定了新的命名。

    9.3.2类对象

    类对象支持两种操作:属性引用和实例化

         属性引用使用和python中所有的属性引用一样的标准语法:obj.name。类对象创建后,类命名空间中所有的命名都是有效属性名。所以如果类定义是这样:

    那么MyClassMyClass.f是有效的属性引用,分别返回一个整数和一个方法对象。也可以对类属性赋值,你可以通过给MyClass.x赋值来修改它。__doc__也是一个有效的属性,返回类的文档字符串:’A simple example class’.

         类的实例化使用函数符号。只要将类对象看作是一个返回新的类1无参数函数即可。例如(假设沿用前面的类)

    以上创建了一个新的类实例并将对象赋给局部变量f

    这个实例化操作(‘调用’一个类对象)来创建一个空的对象。很多类都倾向于将对象创建为有初始状态的。因此类可能会定义一个名为__init__的特殊方法,像下面这样:

    类定义了__init__方法的话,类的实例化操作会自动为创建的类实例调用__init__()方法。所以在下列中可以这样创建一个新的实例:

    当然出于弹性的需要,__init__()方法可以有参数。事实上参数通过__init__()传递到类的实例化操作上。例如:

    9.3.3实例对象

    现在我们可以用实例对象做什么?实例对象唯一可用的操作就是属性引用。有两种有效的属性名。

     数据属性相当于Smalltalk中的’实例变量’C++中的‘数据成员’。和局部变量一样,数据属性不需要声明,第一次使用时它们就会生成。例如如果f是前面创建的MyClass实例,下面的这段代码会打印出16而在堆栈中留下多余的东西:

    另一种为实例对象所接受的引用属性是方法。方法是’属于’一个对象的函数。(python中,方法不止是类实例所独有:其它类型的对象也可有方法。例如,链表对象有append,insert,remove,sort等等方法。然而在后面的介绍中,除非特别说明我们提到的方法特值类方法)

    实例对象的有效名称依赖与他的类。按照定义,类中所有(用户定义)的函数对象对应它的实例中的方法。所以在我们的例子中,f.test是一个有效的方法引用,因为MyClass.test是一个函数。不过f.testMyClass不同,它是一个方法对象,不是一个函数对象。

    9.3.4方法对象

    通常方法通过右绑定方式调用

    MyClass示例中这会返回字符串’hello world’,然而也不是一定要直接调用方法。f.test是一个方法对象它可以存储起来以后调用。例如:

     

    会不断打印hello world

    调用方法时发生了什么?你可能注意到调用f.test()时没有引用前面标出的变量,尽管在test()的函数定义中指明了一个参数。这个参数怎么了?事实上如果函数调用中缺少参数,python会抛出异常--甚至这个参数实际上没什么用...

    9.3.5类和实例变量

    一般来说,实例变量用于对每一个实例都是唯一的数据,类变量用于类的所有实例共享的属性和方法:

    正如在术语相关讨论的,可变对象,例如列表和字典的共享数据可能带来意外的效果。例如下面代码中的tickets列表不应该用作类变量,因为所有的Cat实例将共享同一个列表:

    这个类的正确设计应该使用一个实例变量:

    9.4一些说明

    数据属性会覆盖同名的方法属性。为了避免意外的名称冲突,这在大型程序中是极难发现的Bug.使用一些约定来减少冲突的机会是明智的。使用这些约定包括:大写方法名称的首字母,使用一个唯一的小字符串(也许只是一个下划线)作为数据属性名称的前缀,或者方法使用动词而数据属性使用名词。

    数据属性可以被方法引用,也可以有一个对象的普通用户(客户)使用。换句话说类不能用来实现纯净的数据类型。

    从方法内部引用数据属性(或其他方法)并没有快捷方式。我觉得这实际上增加了方法的可读性:当浏览一个方法时在局部变量和实例变量之间不会出现令人费解的情况。

    一般方法的第一个参数被命名为self。这仅仅是一个约定:对python而言,名称self绝对没有任何特殊含义。(但是请注意:如果不遵循这个约定,对其他的python程序员而言你的代码可读性就会变差,而且有些类查看器程序也可能是遵循此约定编写的)

    类属性的任何函数对象都为那个类的实例定义了一个方法。函数定义代码不一定非得定义在类中:也可以将一个函数对象赋值给类中的一个局部变量。例如:

    现在g,hx都是类A的属性,引用的都是函数对象,因此他们都是A实例的方法--x严格等于h

    通过self参数的方法属性,方法可以调用其它的方法:

    方法可以像引用普通函数那样引用全局命名。与方法关联的全局作用域是包含类定义的模块。全局作用域有很多合法的用途:其一是方法可以调用导入全局作用域的函数和方法,也可以调用定义在其中的类和函数。通常包含此方法的类也会定义在这个全局作用域,在下一节我们会了解为何一个方法要引用自己的类。

    每个值都是一个对象,因此每个值都有一个类(class)也称为它的类型(type),它存储为object.__class__.

    9.5继承

    派生类的定义如下所示:

    命名BaseClassName(实例中的基类名)必须与派生类定义在一个作用域内。除了类,还可以使用表达式,基类定义在另一个模块中时这一点非常有用:

     

    派生类定义的执行过程和基类是一样的构造派生类对象时,就记住基类。这在解析属性引用时尤其有用:如果在类中找不到请求调用的属性,就搜索基类。如果基类是由别的类派生而来,这个规则会递归的应用上去。

         派生类中的覆盖方法可能是想要扩充而不是简单的方法可以直接调用基类方法,只要调用:BaseClassName.metnodname(self,arguments)。有时这对于客户也很有用。(要注意只有BaseClassName在同一全局作用域定义或导入时才能这样用)

         Python有两个用于继承的函数:

    *函数isinstance()用于检查实例类型:isinstance(obj,int)只有在obj.__class__int或其它从int继承的类型

    *函数issubclass()用于检查继承:issubclass(bool,int)True,因为boolint的子类

         然而issubclass(flooat,int)False,因为float不是int的子类

    9.5.1多继承

    Python同样有限的支持多继承形式。多继承的类定义形如下列:

     在大多数情况下,在最简单的情况下你能想到的搜索属性是父类继承的深度优先,左到右而不是搜索两次在同一个类层次结构中,其中有一个重叠。因此如果在DerivedClassName(实例中的派生类)中没有找到某个属性,就会搜索Base1,然后(递归的)搜索其基类,如果最终没有找到就搜索Base2以此类推。

         实际上super()可以动态的改变解析顺序。这个方式可见于其它的一些多继承语言。

         为了防止重复访问基类,通过动态的线性化算法,每个类都按从左到右的顺序特别指定了顺序,每个祖先类只调用一次,这是单调的(意外着一个类被继承时不会影响它祖先的次序)

    9.6私有变量

    只能从对象内部访问的’私有’实例变量,在python中不存在。然而也有一个变通的访问用于大多数python代码:以一个下划线开头的命名(_spam)会被处理为API的非公开部分(无论它是一个函数,方法或数据成员)。它会被视为一个实现细节,无需公开

    因为有一个正当的类私有成员用途(即避免子类里定义的命名与之冲突),在python提供了这种结构的有限支持,称为name mangling(命名编码)。任何形如__spam的标识(前面至少两个下划线,后面至多一个),被替代为_classname__spam,去掉前导下划线的classname即当前的类名。此语法不关注标的位置,只要求在类定义内。

     名称重整是有助于子类重写方法,而不会打破组内的方法调用。

    9.7补充

    有时类似于C 中“结构(struct)”的数据类型很有用,它将一组已命名的数据项绑定在一起。一个空的类定义可以很好的实现它:

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

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

    实例方法对象也有属性:m.__self__ 是一个实例方法所属的对象,而 m.__func__ 是这个方法对应的函数对象。

    9.8异常也是类

    用户自定义异常也可以是类。利用这个机制可以创建可扩展的异常体系。

      一下是两种新的,有效的(语义上的)异常抛出形式,使用raise语句:

    第一种形式中,Class必须是type或其派生类的一个实例。第二种形式是一下形式的简写:

    发生的异常且类型如果是except子句中列出的类,或者是其派生类,那么他们就是相符的(反过来说--发生的异常其类型如果是异常子句中列出的类的基类,它们就不相符)。例如:以下代码会按顺序打印BCD

    要注意的是如果异常子句的顺序颠倒过来(except B在最前),它就会打印BBB--第一个匹配的异常被触发。

       打印一个异常类的错误信息时,先打印类名,然后是一个空格,一个冒号,然后是用内置函数str()将类转换得到的完整字符串。

    9.9迭代器

    现在你可能注意到大多数容器对象都可以用for遍历:

    这种形式的访问清晰,简洁,方便。迭代器的用法在python中普遍而且统一。在后台,for语句在容器对象中调用iter()。该函数返回一个定义了__next__()方法的迭代器对象,它在容器中逐一访问元素。没有后续的元素时,__next__()抛出一个StopIteration异常通知for语句循环结束。你可以是用内建的next()函数调用__next__()方法:一下是其工作原理的示例:

    了解了迭代器协议的后台机制,就可以很容易的给自己的类添加迭代器行为。定义一个__iter__()方法,使其返回一个带有__next__()方法对象。如果这个类已经定义了__next__(),那么__iter__()只需要返回self:

    9.10生成器

     Generator是创建迭代器的简单而强大的工具。它们写起来就像是正规的函数,需要返回数据的时候使用yield语句。每次next()被调用时,生成器回复它脱离的位置(它记忆语句最后一次执行的位置和所有的数据值)。一下示例演示了生成器可以很简单的创建出来:

    前一节中描述了基于类的迭代器,它能作的每一件事生成器也能做到。因为自动创建了__iter__()__next__()方法,生成器显得如此简洁。

    9.11生成器表达式

    有时简单的生成器可以用简洁的方式调用,就像不带中括号的链表推导式。这些表达式是为函数调用生成器而设计的。生成器表达式比完整的生成器定义更简洁,但是没有那么多变,而且通常比等价的链表推导式更容易记。例如:

    补充

    有一个例外。模块对象有一个隐秘的只读对象,名为__dict__,它返回用于实现模块命名空间的字典,命名__dict__是一个属性而非全局命名。显然使用它违反了命名空间实现的抽象原则,应该被严格限制于调试中。

     

  • 相关阅读:
    附上我的Github源码
    Java Automic包下的AtomicInteger
    思维题:四个小伙伴过桥问题
    TCP的三次握手过程?为什么会采用三次握手,若采用二次握手可以吗
    输入www.baidu.com会发生什么
    外网IP和内网IP区别
    Queue接口分析:add和offer区别,remove和poll方法到底啥区别
    手工创建一个线程池
    volatile关键字
    牛客网 买手串
  • 原文地址:https://www.cnblogs.com/LQ6H/p/manuals-10.html
Copyright © 2020-2023  润新知