【5.0】类
前言
类是一种对象的抽象化描述,它是基于面向对象语言而产生的概念。类提供了一种组合数据和功能的方法。 创建一个新类意味着创建一个新的对象 类型,从而允许创建一个该类型的新 实例 。 每个类的实例可以拥有保存自己状态的属性。 一个类的实例也可以有改变自己状态的(定义在类中的)方法。
Python 的类提供了面向对象编程的所有标准特性:类继承机制允许多个基类,派生类可以覆盖它基类的任何方法,一个方法可以调用基类中相同名称的的方法。对象可以包含任意数量和类型的数据。和模块一样,类也拥有 Python 天然的动态特性:它们在运行时创建,可以在创建后修改。
初识类
定义类
类的定义语法格式类似于如下所示:
1 | class ClassName: |
当进入类定义时,将创建一个新的命名空间,并将其用作局部作用域 — 因此,所有对局部变量的赋值都是在这个新命名空间之内。 特别的,函数定义会绑定到这里的新函数名称。
类对象
类对象支持两种操作:属性引用和实例化。
属性引用使用 Python 中所有属性引用所使用的标准语法: obj.name
。 因此,如果类定义是这样的:
1 | class ClassName: |
那么ClassName.a
和ClassName.Test()
就是合法有效的属性引用,执行结果将会返回一个字符串和一个函数对象。
你可以通过
__doc__
属性来查看对应类的描述,即上述代码中的这是这个类的描述
类的实例化,代码如下所示:
1 | a = ClassName() |
其中ClassName()
表示执行这个类的实例化,它会生成并返回这个类实例化的对象,我们使用变量a
来“承接”这个实例化的对象,本质上变量a
是一个指向对象的指针,不过我们并不需要了解这么多,只需要了解如何实例化对象即可。
在实例化对象的时候,会触发构造函数,听起来可能有点不明所以,通俗来说,就是你在执行ClassName()
实例化对象后,这个对象被实例化后执行的第一个函数(方法)被称为构造函数,它在Python
中的定义是__init__(self)
这个方法,这个构造函数并不是默认存在的,你如果定义了它,那么它就会在对象被实例化后第一个执行,代码示例:
1 | class ClassName: |
继承
继承是所有面向对象语言所又具有的,派生类的语法格式如下所示:
1 | class SubClass(BaseClassName): |
名称 BaseClassName
必须定义于包含派生类定义的作用域中。 也允许用其他任意表达式代替基类名称所在的位置。 这有时也可能会用得上,例如,当基类定义在另一个模块中的时候:
1 | class SubClassName(modname.BaseClassName): |
派生类定义的执行过程与基类相同。 当构造类对象时,基类会被记住。 此信息将被用来解析属性引用:如果请求的属性在类中找不到,搜索将转往基类中进行查找。 如果基类本身也派生自其他某个类,则此规则将被递归地应用。
派生类可能会重载其基类的方法。 因为方法在调用同一对象的其他方法时没有特殊权限,调用同一基类中定义的另一方法的基类方法最终可能会调用覆盖它的派生类的方法。
多重继承
Python 也支持一种多重继承。其语法格式如下所示:
1 | class SubClassName(Base1, Base2, Base3): |
对于多数应用来说,在最简单的情况下,你可以认为搜索从父类所继承属性的操作是深度优先、从左至右的,当层次结构中存在重叠时不会在同一个类中搜索两次。 因此,如果某一属性在 DerivedClassName
中未找到,则会到 Base1
中搜索它,然后(递归地)到 Base1
的基类中搜索,如果在那里未找到,再到 Base2
中搜索,依此类推。
动态改变顺序是有必要的,因为所有多重继承的情况都会显示出一个或更多的菱形关联(即至少有一个父类可通过多条路径被最底层类所访问)。 例如,所有类都是继承自 object
,因此任何多重继承的情况都提供了一条以上的路径可以通向 object
。 为了确保基类不会被访问一次以上,动态算法会用一种特殊方式将搜索顺序线性化, 保留每个类所指定的从左至右的顺序,只调用每个父类一次,并且保持单调(即一个类可以被子类化而不影响其父类的优先顺序)。 总而言之,这些特性使得设计具有多重继承的可靠且可扩展的类成为可能。 要了解更多细节,请参阅 httpss://www.python.org/download/releases/2.3/mro/。
私有变量
那种仅限从一个对象内部访问的“私有”实例变量在 Python 中并不存在。 但是,大多数 Python 代码都遵循这样一个约定:带有一个下划线的名称 (例如 _spam
) 应该被当作是 API 的非公有部分 (无论它是函数、方法或是数据成员)。 这应当被视为一个实现细节,可能不经通知即加以改变。
由于存在对于类私有成员的有效使用场景(例如避免名称与子类所定义的名称相冲突),因此存在对此种机制的有限支持,称为 名称改写。 **任何形式为 __spam
的标识符(至少带有两个前缀下划线,至多一个后缀下划线)的文本将被替换为 _classname__spam
**,其中 classname
为去除了前缀下划线的当前类名称。 这种改写不考虑标识符的句法位置,只要它出现在类定义内部就会进行。
名称改写有助于让子类重载方法而不破坏类内方法调用。例如:
1 | class Mapping: |
上面的示例即使在 MappingSubclass
引入了一个 __update
标识符的情况下也不会出错,因为它会在 Mapping
类中被替换为 _Mapping__update
而在 MappingSubclass
类中被替换为 _MappingSubclass__update
。
杂项说明
有时会需要使用类似于 Pascal 的“record”或 C 的“struct”这样的数据类型,将一些命名数据项捆绑在一起。 这种情况适合定义一个空类:
1 | class Employee: |
pass
是一个空的占位符,其并不执行任何操作,仅仅占位保持代码的完整性或者语法的正确性
迭代器
在平时编写代码中,我们常常使用for
来直接循环访问列表等容器对象中的“元素”,例如:
1 | for element in [1, 2, 3]: |
这种访问风格清晰、简洁又方便。 迭代器的使用非常普遍并使得 Python 成为一个统一的整体。 在幕后,for
语句会在容器对象上调用 iter()
。 该函数返回一个定义了 __next__()
方法的迭代器对象,此方法将逐一访问容器中的元素。 当元素用尽时,__next__()
将引发 StopIteration
异常来通知终止 for
循环。你可以使用 next()
内置函数来调用 __next__()
方法;这个例子显示了它的运作方式:
1 | 'abc' s = |
看过迭代器协议的幕后机制,给你的类添加迭代器行为就很容易了。 定义一个 __iter__()
方法来返回一个带有 __next__()
方法的对象。 如果类已定义了 __next__()
,则 __iter__()
可以简单地返回 self
:
1 | class Reverse: |
1 | 'spam') rev = Reverse( |
生成器
生成器 是一个用于创建迭代器的简单而强大的工具。 它们的写法类似于标准的函数,但当它们要返回数据时会使用 yield
语句。 每次在生成器上调用 next()
时,它会从上次离开的位置恢复执行(它会记住上次执行语句时的所有数据值)。 一个显示如何非常容易地创建生成器的示例如下:
1 | def reverse(data): |
1 | for char in reverse('golf'): |
可以用生成器来完成的操作同样可以用前一节所描述的基于类的迭代器来完成。 但生成器的写法更为紧凑,因为它会自动创建 __iter__()
和 __next__()
方法。
End
类的部分是重点,迭代器了解知道需要使用的时候能会用查文档就好,生成器是为了迭代器方便使用的另一种封装方式,了解即可,需要用的时候再查相关详细文档。