类就是一组数据和函数的集合。创建了一个新的类意味着创建了一个全新的对象 类型,也就允许我们创建新的此类型的 实例。每个类实例都带有属性以维护其状态。同样也有方法(在它自己的类中定义)来修改这些状态。
相比其他编程语言,使用 Python 的类机制创建类是最方便简洁的。Python 的类机制结合了 C++ 和 Modua-3 的机制,同时也符合所有面向对象编程的标准:类继承机制允许有多个基类,一个派生的类可以覆盖基类的任意方法,每个方法也可以调用基类的同名方法。每个对象都可以包含任意数量和类型的数据。与模块一样,类也具有 Python 动态的特性:即时创建,创建后仍可修改。
在 C++ 的概念中,常规的类成员都是 公共(public) 的(例外的情况请看 Private Variables 这一节)成员函数都是 虚(virtual) 的。 到了 Modula-3 中,我们没有从其自身的方法中引用对象成员的快速写法:方法函数声明时要在第一参数中标明对象,这样就会在调用时隐式的调用。在 Smalltalk 中,类本身就是对象。同时还有导入和重命名语法。但不同于 C++ 和 Modula-3,它的内置类型可以直接作为基类由用户进行扩展。同样,它也有类似 C++ 的地方,大部分有特殊语法的内置操作符(算术运算符,下标等等)都可以被类实例重定义。
面向对象技术简介
- 类(Class): 用来描述具有相同的属性和方法的对象的集合。它定义了该集合中每个对象所共有的属性和方法。对象是类的实例。
- 类变量:类变量在整个实例化的对象中是公用的。类变量定义在类中且在函数体之外。类变量通常不作为实例变量使用。
- 数据成员:类变量或者实例变量用于处理类及其实例对象的相关的数据。
- 方法重载:如果从父类继承的方法不能满足子类的需求,可以对其进行改写,这个过程叫方法的覆盖(override),也称为方法的重载。
- 实例变量:定义在方法中的变量,只作用于当前实例的类。
- 继承:即一个派生类(derived class)继承基类(base class)的字段和方法。继承也允许把一个派生类的对象作为一个基类对象对待。例如,有这样一个设计:一个Dog类型的对象派生自Animal类,这是模拟”是一个(is-a)”关系(例图,Dog是一个Animal)。
- 实例化:创建一个类的实例,类的具体对象。
- 方法:类中定义的函数。
- 对象:通过类定义的数据结构实例。对象包括两个数据成员(类变量和实例变量)和方法。
Python 作用域与命名空间
介绍类之前,我必须先向你介绍一下 Python 的作用域规则。类定义在命名空间中有一些非常聪明的技巧,而且你需要知道作用域和命名空间是如何工作的这样才能完全理解它做了什么。顺便一提,本节的知识对任何高级的 Python 编程都很!有!用!
让我们先从几个定义开始。
namespace(命名空间) 是一个从名字到对象的映射。大部分命名空间当前都由 Python 字典实现,但一般情况下基本不会去关注它们(除了要面对性能问题时),而且也有可能在将来更改。下面是几个命名空间的例子:存放内置函数的集合(里面含有 abs()
这样的函数,和其他的内置名称);模块中的全局名称;函数调用中的本地名称。从某种意义上说,某对象的的属性集合也是一种命名空间的形式;比如,两个不同的模块都可能定义了一个 maximize
的函数,为了不引起混乱用户必须用模块名作为前缀修饰一下。
顺便一提, 接下来所有跟在 .
后面的单词我都称其为 attribute(属性),比如, z.real
中的 real
就是对象 z
的一个属性。严格来说,引用模块中的名称就是属性引用: modname.funcname
中的 modname
是一个模块对象,funcname
自然就是它的一个属性了。还有一种在模块的属性与本模块的全局名称之间恰好发生了一个直接的映射的情况:它们共享了同一个命名空间![1]。
属性可以是只读的,也可以是可写的。在后一种情况中,可以指定某个属性某些内容。如果模块属性可写:你可以用 modname.the_answer = 42
来指定。可写的属性也同样可以被 del
语句删除。 例如, del modname.the_answer
将会删除掉 modname
的 the_answer
属性。
命名空间会在不同时刻被创建,也会拥有不同的命名空间。命名空间中包含着在 Python 解释器启动之初创建的内置名称,并且永远不会被删除。模块中的全局命名空间也会在模块被读入时创建,一般情况下也会持续到解释器退出。声明的执行由上层解释器调用,不管是从文件中读入还是交互式的,模块中包含最多的是一个叫 __main__
的东西,每个模块都有自己的全局命名空间(实际上内置名称也在模块中存在,它们被称为 builtins
.) )。
函数的本地命名空间在函数被调用时创建,函数返回或抛出异常时但没在函数内处理时删除。(实际上,忘记处理可能是描述实际所发生的了什么的更好的方式..)当然,递归调用每一次都有自己的本地命名空间。
scope (作用域) 是一段 Python 程序的文本区域,处于其中的命名空间是可直接访问的。「可直接访问」在这里的意思是非限定性引用的某名称会尝试在此命名空间中查找。
尽管作用域一般都是静态的,不过也常常被动态的用。在任何执行的时候,每段代码都至少有3个嵌套的作用域可直接访问。
- 最内层的作用域,会被首先搜索,里面放的是本地名称。
- 任何处于函数内的作用域,会从在最接近它的作用域中开始寻找,这层的命名空间中放的是非本地但也非全局的名称。
- 倒数第二层作用域是包含着当前模块的全局名称。
- 最外层作用域(最后搜索的一层)是包含内置名称的命名空间。
如果某名称是在全局进行的声明,那么所有的引用和分配都会直接导向中间的这层包含模块的全局名称的作用域中。要想让最内层的作用域重新绑定一个在外层出现过的变量,我们可以用 nonlocal
声明来完成;如果不声明 nonlocal (非本地),这些变量则都是只读的(任何尝试写入这种变量的行为都将会创建一个 全新 的本地变量,不会对最外层的那个有丝毫影响。)
通常情况下,本地作用域引用着当前函数的本地名称。外层的函数引用的是和全局作用域一样的命名空间:模块的命名空间。类定义放置在本地作用域的另一个命名空间中。
意识到作用域取决于文本是很重要的:某个模块中所定义的函数的全局作用域是它所在的模块的命名空间,不管这函数来自什么地方或以什么别名被调用。换句话说,实际的名称搜索是在动态的情况下完成的,也就是运行时 — 但,语言定义的发展是朝着静态命名去的,在 「编译」阶段完成,所以不要试图依赖任何动态的命名!(实际上,本地变量已经是静态定义的了。)
Python 中也有皮一下的地方 – 如果不用 global
声明,那么所分配的变量总是在它所处位置的最内层。分配不会复制数据 – 它们只是把名字绑定到对象上。对删除来讲也是一样: del x
声明会把 x
从本地作用域所引用的命名空间中移除绑定。实际上,所有引入新名称的操作都会使用本地作用域:尤其是 import
声明和绑定在模块中的函数定义或者在本地作用域的函数名称。
global
声明被用在要指定某个特殊的变量要在全局作用域中存活且应该在这重新被绑定的情况下;nonlocal
声明则是用在指示某变量存在于某封闭的作用域且应该在这被重新绑定的情况下。
作用域和命名空间例子
用个小例子来演示下如何引用不同的作用域和命名空间,以及 global
和 nonlocal
是如何影响变量绑定的:
1 | def scope_test(): |
输出如下:
1 | After local assignment: test spam |
注意 本地 的分配并未改变 scope_test 中绑定的 spam,而 nonlocal
标明过的分配则改变了 scope_test 绑定的 spam,global
则更改的是模块层面的绑定。
不知道你有没有注意到,我们在 global
之前是没有绑定 spam 的。
类对象
类定义语法
类定义的形式很简单像这样既可:
1 | class ClassName: |
类的定义与函数定义(def
statements)差不多,在它们生效前我们需要预先执行这些定义(你也可以在 if
分支或函数内部声明类)。
在实践中,类定义内的声明通常是函数定义,不过也有其他的声明,而且还挺有用 – 我们之后再谈这个。在类中定义的函数通常有一个特有的参数列表,指代是作为方法调用的 — 同样我们稍后再解释。
进入到类定义后,会创建一个新的命名空间作为本地作用域 — 也因此,所有的本地变量的指定都会进到这个新的作用域里。尤其是定义函数所绑定的是此函数的名字。
类定义正常结束时,一个新的 类对象 就被创建出来了。这是类定义在命名空间中最基本的一层包装;我们在下一节中详细讨论这个。原始的本地作用域(在进入类定义前生效的那个)会被重新安装,然后将类名字(就是上例中的 ClassName
)绑定到这个类对象上。
类编码风格
类名应采用驼峰命名法,即类名中的每个单词首字母都大写,而不使用下划线。实例名和模块名都采用小写的格式,并且在单词之间加上下划线。
类对象
类对象支持两种操作:属性引用和实例化。
Attribute references (属性引用) 使用的是 Python 中标准的属性引用语法: obj.name
。有效的属性名都会在此类创建时被塞入的命名空间中。所以,如果一个类定义看起来像这样:
1 | class MyClass: |
MyClass.i
和 MyClass.f
都是有效的属性引用,分别返回的是一个整数和一个函数对象。类属性同样是可分配的,所以你可以更改 MyClass.i
的值。__doc__
同样也是一个有效属性,返回的是此类的文档字符串: "简单的例子"
。
类的 实例化 类似函数的形式。把它假装成一个无参数且返回的是此类实例的函数就行。看代码(用的上面那个类):
1 | x = MyClass() |
这样就创建一个新的类 实例 并把它分配给了本地变量 x
。
实例化操作(「调用」类对象)创建的是一个空对象。大多数类都想在创建时自定义初始化状态。所以类通常也会定义一个名为 __init__()
的方法:
1 | def __init__(self): |
当某类定义了 __init__()
方法,类实例化时就会为新的类实例自动调用 __init__()
方法。所以,我们不需要做任何改变:
1 | x = MyClass() |
当然,__init__()
方法也可以有参数变得更加易用。需要参数时,在参数实例化时给定的参数会传递到 __init__()
上:
1 | class Complex: |
实例对象
那么..我们要用这个实例对象干什么呢?最基本的操作时属性引用。我们现在有两种有效的属性名:数据属性和方法。
data attributes(数据属性) 等同于 Smalltalk 中的「实例变量」,以及 C++ 中的 「数据成员」。数据属性不需要提前声明;就像本地变量一样,它们会在第一次分配时传播到已有的命名空间中。举个例子,假设我们已经创建了 MyClass
的实例 x
,下面的代码会打印出 16
且不留下痕迹:
1 | x.counter = 1 |
另一种实例属性引用是 method (方法)。一个方法也就是一个 「属于」某个对象的函数。(在 Python 中,方法一词并不被类实例独占:其他对象属性也同样具有方法。比如,列表对象也有如 append
, insert
, remove
, sort
的方法。不过,接下来的讨论中我们所说的方法只指代类实例对象中的方法,除非特别指明。)
实例对象的有效方法名依赖于它的类。基于定义,所有是函数对象的类属性定义都会等同于它所实例化后的方法。所以在我们的例子中,x.f
是一个有效的方法引用,因为 MyClass.f
就是一个函数,但 x.i
则不是,因为 MyClass.i
就不是。但 x.f
并不是 MyClass.f
– 在这里它变成了 方法对象 而不是函数对象。
方法对象
通常,绑定后可以立即调用方法:
1 | x.f() |
在 MyClass
例子中,会返回一个字符串 "hello world"
。不过,我们并不需要立即调用:x.f
是一个方法对象,可以被存储下来并且在任何其他时间调用:
1 | xf = x.f |
将会一直打印 hello world
。
调用方法时到底发生了什么?你可能注意到了,上面的 x.f()
调用并没有写参数,即使我们定义 f()
时指定了一个参数。那个函数呢?没错,如果没有足够的函数所需的参数 Python 会抛出一个异常 – 即使这参数可能实际并没有用到。
聪明的你,可能已经猜到了答案:关于方法最特殊的一件事就是实例对象会传递第一个参数到函数中。在我们的例子中, x.f()
实际上等同于 MyClass.f(x)
。通俗点讲,调用一个有 n 个参数的方法等同于调用在这些参数前插入了一个方法的实例作为第一个参数的函数。
如果仍然不明白方法是怎么工作的,我们了解下实现过程可能会有些帮助。当一个引用一个实例的非数据属性时,实例对象类会首先被搜索。如果这个名字指代的是一个有些的类属性而且还是一个函数对象,那方法对象就会被创建用于包装(指针指向)实例对象和函数对象在同一个抽象对象中:这就是方法对象的形成。当带着参数调用方法对象时,会结合实例对象和参数列表创建一个新的参数列表,方法对象所调用的就是这个新的参数列表。
类和实例变量
通俗来讲,实例变量是每个实例独有的数据,而类变量则是会让所有此类的实例所共享的方法和属性:
1 | class Dog: |
在 A Word About Names and Objects 的讨论中,共享的数据可能在调用 mutable 可变对象(比如列表和字典)时有意料之外的效果。举个例子, 下面写的 tricks 列表就不应该作为一个类变量存在,因为同一个列表会在所有的 Dog实例中共享:
1 | class Dog: |
正确的设计应该是用实例变量代替:
1 | class Dog: |
补充说明
数据属性会覆盖同名的方法属性;为了避免命名冲突(冲突的话在大型程序中往往会引起很难查找的 bug),用一些大家都遵守的约定来最小化冲突的机会是非常明智的。一般有大写方法名字,使用独特的短字符串来给数据属性加上前缀(也可以是仅仅一个下划线),或者使用动词命名方法,而使用名词命名数据属性。
数据属性不光可以被此对象的用户(「客户」)一方使用,我们在方法内同样可以使用。换句话说,类不能用于实现纯粹的抽象数据类型。实际上,在 Python 中强制数据隐藏起来也是不可能的 — 它们只是在约定。(换..换句话说, Python 中实现于 C 的部分可以做到完全的隐藏实现细节,也可以控制一个对象的访问;这一点可以用在用 C 写 Python 的扩展上)。
客户一方(就是创建了实例后再用)也应该小心地使用数据属性 — 因为有可能弄乱方法们维护的数据属性一致性。不过客户一方也可以添加自己的数据属性进去,只要避免影响到方法的有效性就行,也就是说避免命名冲突 — 再说一遍!避免命名冲突很!重!要!
Python 中并无在方法内快捷访问数据熟悉的途径(方法的也没有!)。因为这样可以提高可读性:这样就可以快速弄清楚本地变量和实例变量。
通常我们把方法的第一个参数命名为 self
. 这只是一个约定: self
这个名字对 Python 来讲并无特殊含义。不过要注意,如果不遵守的话~,其他的 Python 程序猿可能会不知道你写的啥呦~,而且还可依据此来写一个 class browser 的程序
任何作为类属性定义的函数对象都会作为实例或类的方法。把相关的函数定义在类的文本域内并不是必须的:指定一个函数对象到类的本地变量中同样是 Ok 的:
1 | # 定义在类外了... |
f
,g
, h
都是类 C
的属性,全都是属性对象,同时也都是 C
实例的方法 — h
等同于 g
。要注意,这里这种写法一般是为了让程序变得混乱。
使用 self
参数可以在方法内调用其他的方法:
1 | class Bag: |
方法也可以像引用普通函数一样引用全局名称。与方法关联的全局作用域是包含着它的模块。(类永远不能作为全局作用域使用)若是有一个在方法中必须要用到全局数据的理由,那要遵守以下几点:其实只有一件事,全局作用域中引入的函数和模块可以使用,在全局作用域中的函数和类也可以使用。通常,全局作用域中的类所包含的方法都是它自己定义的,我们将在下一节找到几个合理的理由来解释为什么一个方法需要引用它自己的类。
每个值都是一个对象,也因此都有 class (也被称为 type)。这些东西都被放在了 object.__class__
中。
使用 @property
在使用 @property
之前,让我们先来看一个简单的例子:
1 | class Exam(object): |
在上面,我们定义了一个 Exam 类,为了避免直接对 _score
属性操作,我们提供了 get_score 和 set_score 方法,这样起到了封装的作用,把一些不想对外公开的属性隐蔽起来,而只是提供方法给用户操作,在方法里面,我们可以检查参数的合理性等。
这样做没什么问题,但是我们有更简单的方式来做这件事,Python 提供了 property
装饰器,被装饰的方法,我们可以将其『当作』属性来用,看下面的例子:
1 | class Exam(object): |
在上面,我们给方法 score 加上了 @property
,于是我们可以把 score 当成一个属性来用,此时,又会创建一个新的装饰器 score.setter
,它可以把被装饰的方法变成属性来赋值。
另外,我们也不一定要使用 score.setter
这个装饰器,这时 score 就变成一个只读属性了:
1 | class Exam(object): |
@property
把方法『变成』了属性。
继承
在面向对象编程中,当我们已经创建了一个类,而又想再创建一个与之相似的类,比如添加几个方法,或者修改原来的方法,这时我们不必从头开始,可以从原来的类派生出一个新的类,我们把原来的类称为父类或基类,而派生出的类称为子类,子类继承了父类的所有数据和方法。
- 继承可以拿到父类的所有数据和方法,子类可以重写父类的方法,也可以新增自己特有的方法。
- 有了继承,才有了多态,不同类的对象对同一消息会作出不同的相应。
当然,一个不支持继承的「类」不足以被称为类。在类的定义中,继承的语法是这样的:
1 | class DerivedClassName(BaseClassName): |
类名 BaseClassName
必须被定义在一个包含派生类 DerivedClassName
定义的作用域下。相较于直接使用基类名,任何其它表达式也是可以被填入的。这个特性经常被用到,比如,当基类被定义在其它模块中时:
1 | class DerivedClassName(modname.BaseClassName): |
派生类定义时的执行流程和基类相同。当一个类对象被创建,它会记录它的基类。这将被用于解析对象的属性:如果一个需要的属性不存在于当前类中,紧接着就会去基类中寻找。如果该基类也是从其他类派生出来的,那么相同的过程也会递归地被应用到这些类中。
实例化派生类也没有什么特别的: DerivedClassName()
就会创建类的一个新的实例。方法引用则按如下的方式被解析:首先在当前类中搜索对应的属性,然后沿着继承链往下搜索,如果找到了一个函数对象,那么这个方法引用就是可用的。
派生类可以重写基类的方法。因为方法在调用同一对象其它方法的时候没有什么特权,所以当派生类的实例调用某个基类的方法后,该基类的方法可能会再次调用派生类覆写的另一个基类方法。(对于 C++ 程序员而言, Python 中所有的方法都是 virtual
函数。)
派生类中重写的方法一般用于扩展同名的基类方法,而非简单的替换。 Python 中有一种简单的直接调用基类方法的方案:调用 BaseClassName.methodname(self, arguments)
即可。这在某些情景下也是有用的。(注意这个方法只有在基类 BaseClassName
在全局作用域下可以访问才能使用。)
Python 提供了两个判断继承关系的内建函数:
- 使用
isinstance()
检查一个实例的类型:当且仅当obj.__class__
是int
或其它从int
派生的类时,isinstance(obj, int)
才会返回True
。 - 使用
issubclass()
检查类之间的继承关系:因为bool
是int
的一个子类,所以issubclass(bool, int)
返回True
。然而,因为float
不是int
的派生类,所以issubclass(float, int)
返回False
。
多重继承
Python 也支持多重继承。一个具有多个基类的类定义如下所示:
1 | class DerivedClassName(Base1, Base2, Base3): |
对于多数目的,在最简单的情况下,你可以认为搜索父类中继承的属性是深度优先,从左到右,而不是在继承结构中重叠的同一个类中搜索两次。因此,如果一个属性在 DerivedClassName
中没有找到,则在 Base1
中查找,再在 Base1
的基类中(递归地)查找,如果未能找到,则在 Base2
中查找,以此类推。
事实上,这个过程要稍稍更复杂一些;方法解析顺序是动态变化的,以支持合作调用 super()
。这种方法在其他多继承语言中被称为调用下一方法,比单继承语言中的 super 调用更加强大。
动态排序是必要的,因为所有多重继承的情况都表现出一个或多个菱形关系(其中至少有一个父类可以通过最底层的多个路径访问)。例如,所有的类都继承自 object
,所以任何情况的多重继承都提供了不止一条的路径到达 object
。 为了避免基类被多次访问,动态算法保证在每个类中进行从左到右特定顺序的线性搜索,因此每个父类只被调用一次,并且这个方法是单调的(意味着类可以被子类化而不影响其的优先顺序)。 总之,这些特性使得设计具有多重继承的可靠的且可扩展的类成为可能。 更多细节请参考 https://www.python.org/download/releases/2.3/mro/.
你不知道的 super
在类的继承中,如果重定义某个方法,该方法会覆盖父类的同名方法,但有时,我们希望能同时实现父类的功能,这时,我们就需要调用父类的方法了,可通过使用 super
来实现,比如:
1 | class Animal(object): |
在上面,Animal 是父类,Dog 是子类,我们在 Dog 类重定义了 greet
方法,为了能同时实现父类的功能,我们又调用了父类的方法,看下面的使用:
1 | 'dog') dog = Dog( |
super
的一个最常见用法可以说是在子类中调用父类的初始化方法了,比如:
1 | class Base(object): |
深入 super()
看了上面的使用,你可能会觉得 super
的使用很简单,无非就是获取了父类,并调用父类的方法。其实,在上面的情况下,super 获得的类刚好是父类,但在其他情况就不一定了,super 其实和父类没有实质性的关联。
让我们看一个稍微复杂的例子,涉及到多重继承,代码如下:
1 | class Base(object): |
其中,Base 是父类,A, B 继承自 Base, C 继承自 A, B,它们的继承关系是一个典型的『菱形继承』,如下:
1 | Base |
现在,让我们看一下使用:
1 | c = C() |
如果你认为 super
代表『调用父类的方法』,那你很可能会疑惑为什么 enter A 的下一句不是 enter Base 而是 enter B。原因是,super 和父类没有实质性的关联,现在让我们搞清 super
是怎么运作的。
MRO 列表
事实上,对于你定义的每一个类,Python 会计算出一个方法解析顺序(Method Resolution Order, MRO)列表,它代表了类继承的顺序,我们可以使用下面的方式获得某个类的 MRO 列表:
1 | # or C.__mro__ or C().__class__.mro() C.mro() |
那这个 MRO 列表的顺序是怎么定的呢,它是通过一个 C3 线性化算法来实现的,这里我们就不去深究这个算法了,感兴趣的读者可以自己去了解一下,总的来说,一个类的 MRO 列表就是合并所有父类的 MRO 列表,并遵循以下三条原则:
- 子类永远在父类前面
- 如果有多个父类,会根据它们在列表中的顺序被检查
- 如果对下一个类存在两个合法的选择,选择第一个父类
super 原理
1 | def super(cls, inst): |
其中,cls 代表类,inst 代表实例,上面的代码做了两件事:
- 获取 inst 的 MRO 列表
- 查找 cls 在当前 MRO 列表中的 index, 并返回它的下一个类,即 mro[index + 1]
当你使用 super(cls, inst)
时,Python 会在 inst 的 MRO 列表上搜索 cls 的下一个类。
现在,让我们回到前面的例子。
首先看类 C 的 __init__
方法:
1 | super(C, self).__init__() |
这里的 self 是当前 C 的实例,self.class.mro() 结果是:
1 | [__main__.C, __main__.A, __main__.B, __main__.Base, object] |
可以看到,C 的下一个类是 A,于是,跳到了 A 的 __init__
,这时会打印出 enter A,并执行下面一行代码:
1 | super(A, self).__init__() |
注意,这里的 self 也是当前 C 的实例,MRO 列表跟上面是一样的,搜索 A 在 MRO 中的下一个类,发现是 B,于是,跳到了 B 的 __init__
,这时会打印出 enter B,而不是 enter Base。
整个过程还是比较清晰的,关键是要理解 super 的工作方式,而不是想当然地认为 super 调用了父类的方法。
小结
- 事实上,
super
和父类没有实质性的关联。 super(cls, inst)
获得的是 cls 在 inst 的 MRO 列表中的下一个类。
私有变量
只能从对像内部访问的『私有』实例变量,在 Python 中不存在。然而,在大多数 Python 代码中存在一个这样的约定:以一个下划线开头的命名(例如 _spam
)会被处理为 API 的非公开部分(无论它是一个函数、方法或数据成员)。它会被视为一个实现细节,无需公开。
因为有一个正当的类私有成员用途(即避免子类里定义的命名与之冲突),Python 提供了对这种结构的有限支持,称为 name mangling (命名编码) 。任何形如 __spam
的标识(前面至少两个下划线,后面至多一个下划线),被替代为 _classname__spam
,去掉前导下划线的 classname
即当前的类名。此语法不关注标识的位置,只要求在类定义内。
名称重整是有助于子类重写方法,而不会打破组内的方法调用。例如:
1 | class Mapping: |
需要注意的是编码规则设计为尽可能的避免冲突,被认作为私有的变量仍然有可能被访问或修改。在特定的场合它也是有用的,比如调试的时候。
要注意的是代码传入 exec()
, eval()
时不考虑所调用的类的类名,视其为当前类,这类似于 global
语句的效应,已经按字节编译的部分也有同样的限制。这也同样作用于 getattr()
, setattr()
和 delattr()
,像直接引用 __dict__
一样。
零碎知识点
有时候,有一个类似于 Pascal 「记录」或者 C 「结构体」的数据类型是非常有用的,它能够将一些命名数据项捆绑在一起:
1 | class Employee: |
一块 Python 代码通常希望能够传递特定抽象数据类型 ,而一个类则会模拟该数据类型的方法。例如,如果你有一个函数,可以格式化文件对象当中的一些数据;那么,你就可以定义一个带有 read()
方法和 readline()
方法的类,这两个方法可以从数据缓冲区中读取数据没并且将其作为参数传递出去。
实例方法对象也有属性: m.__self__
是带有方法 m()
的实例对象,并且 m.__func__
是和方法相对应的函数对象。
- 类是具有相同属性和方法的一组对象的集合,实例是一个个具体的对象。
- 方法是与实例绑定的函数。
获取对象信息可使用下面方法:
type(obj)
:来获取对象的相应类型;isinstance(obj, type)
:判断对象是否为指定的 type 类型的实例;hasattr(obj, attr)
:判断对象是否具有指定属性/方法;getattr(obj, attr[, default])
获取属性/方法的值, 要是没有对应的属性则返回 default 值(前提是设置了 default),否则会抛出 AttributeError 异常;setattr(obj, attr, value)
:设定该属性/方法的值,类似于 obj.attr=value;dir(obj)
:可以获取相应对象的所有属性和方法名的列表:
__new__
在__init__
之前被调用,用来创建实例。__str__
是用 print 和 str 显示的结果,__repr__
是直接显示的结果。__getitem__
用类似obj[key]
的方式对对象进行取值__getattr__
用于获取不存在的属性 obj.attr__call__
使得可以对实例进行调用
迭代器
目前为止,你可能发现了,大部分容器对象都能被 for
所循环:
1 | for element in [1, 2, 3]: |
这种形式的访问很清晰,简洁,方便。其背后是迭代器在起作用,for
声明会调用容器对象的 iter()
函数,这个函数则返回一个迭代器对象,迭代器对象有 __next__()
方法,它会让容器中的元素一次返回一个。 __next__()
会抛出 StopIteration
异常来让 for
结束。你也可以用 next()
函数来调用它的 __next__()
方法;下面的例子显示了迭代器是如何工作的:
1 | 'abc' s = |
了解了迭代器协议背后的机制,我们就可以很容易得在我们自己的类中添加迭代器行为。__iter__()
方法需要返回一个带有 __next__()
方法的对象。如果类仅仅定义了__next__()
, __iter__()
那么返回的对象就是它自己 self
。
1 | class Reverse: |
1 | 'spam') rev = Reverse( |
生成器
Generator 是一个简单又强大的创建迭代器的工具。写它们只要像常规函数一样就可以,只不过用的是 yield
代替 return
返回数据。 每次 next()
调用生成器时,生成器就会从它断开的地方恢复(它会记录所有的数据和最后执行的声明)。下面写个例子来展示下生成器并不神秘难写。
1 | def reverse(data): |
1 | for char in reverse('golf'): |
所以生成器能做的事情,我们之前介绍过的以类为基础的迭代器也可以做。生成器之所以显得更加紧凑,是因为 __iter__()
和 __next__()
方法都被自动隐式的创建了。
生成器的另一个特色是本地变量和执行条件都会被自动保存。这就让我们很容易写出生成器函数,同时也比使用实例属性像是 self.index
,self.data
来的简洁。
除了自动创建的方法和保存的程序状态,当生成器结束时,还会自动抛出 StopIteration
. 这些东西组合起来,就变成了一个让我们非常容易书写的迭代器形式。
生成器表达式
一些简单的生成器我们可以用类似列表表达式的代码做出来,只要把方括号换成圆括号就行了。生成器表达式用来一般用在在函数内需要写即用即删的数据的时候。生成器表达式比起完整的生成器要更加紧凑但并不如它功能强大,不过比起列表表达式来内存占用更少。
例子:
1 | for i in range(10)) # 平方之和 sum(i*i |