类的定义、参数声明、数据成员使用详解(9) – python入门教程
类提供了一种将数据和功能捆绑在一起的方法。创建一个新类会创建一种新类型的对象,允许创建该类型的新实例。每个类实例都可以附加属性以维护其状态。类实例也可以具有用于修改其状态的方法(由其类定义)。
与其他编程语言相比,Python 的类机制添加的类具有最少的新语法和语义。它是 C++ 和 Modula-3 中的类机制的混合体。Python 类提供了面向对象编程的所有标准特性:类继承机制允许多个基类,派生类可以覆盖其基类或类的任何方法,方法可以调用同名基类的方法. 对象可以包含任意数量和种类的数据。与模块一样,类具有 Python 的动态特性:它们是在运行时创建的,并且可以在创建后进一步修改。
在 C++ 术语中,通常类成员(包括数据成员)是 公共的(见下文Private Variables除外),并且所有成员函数都是 虚拟的。与在 Modula-3 中一样,没有用于从其方法中引用对象成员的简写:方法函数使用表示对象的显式第一个参数声明,该参数由调用隐式提供。在 Smalltalk 中,类本身就是对象。这为导入和重命名提供了语义。与 C++ 和 Modula-3 不同,内置类型可以用作用户扩展的基类。此外,就像在 C++ 中一样,大多数具有特殊语法的内置运算符(算术运算符、下标等)都可以为类实例重新定义。
(由于缺乏普遍接受的术语来谈论类,我会偶尔使用 Smalltalk 和 C++ 术语。我会使用 Modula-3 术语,因为它的面向对象语义比 C++ 更接近 Python,但我希望很少有读者听说过。)
9.1 关于名称和对象的一句话
对象具有个体性,多个名称(在多个范围内)可以绑定到同一个对象。这在其他语言中称为别名。乍一看 Python 通常不会意识到这一点,在处理不可变的基本类型(数字、字符串、元组)时可以安全地忽略它。然而,别名对涉及可变对象(如列表、字典和大多数其他类型)的 Python 代码的语义具有可能令人惊讶的影响。这通常用于程序的好处,因为别名在某些方面表现得像指针。例如,传递一个对象很便宜,因为实现只传递了一个指针;如果一个函数修改了作为参数传递的对象,调用者将看到变化——这消除了对 Pascal 中两种不同的参数传递机制的需要。
9.2 Python 作用域和命名空间
在介绍类之前,我首先要告诉你一些关于 Python 的作用域规则的事情。类定义在命名空间中发挥了一些巧妙的技巧,您需要了解作用域和命名空间如何工作才能完全理解发生了什么。顺便说一句,关于这个主题的知识对任何高级 Python 程序员都很有用。
让我们从一些定义开始。
命名空间是从名称到对象的映射。大多数命名空间目前都是作为 Python 字典实现的,但这通常不会以任何方式引起注意(性能除外),并且将来可能会发生变化。命名空间的示例有: 内置名称集(包含函数,如abs()
, 和内置异常名称);模块中的全局名称;以及函数调用中的本地名称。从某种意义上说,对象的属性集也形成了命名空间。关于命名空间,重要的是要了解不同命名空间中的名称之间绝对没有关系。例如,两个不同的模块可以同时定义一个函数maximize
而不会混淆——模块的用户必须在其前面加上模块名称。
顺便说一句,我对点后面的任何名称都使用属性一词——例如,在表达式z.real
中,real
是对象的属性 z
。严格来说,模块中对名称的引用是属性引用:在表达式modname.funcname
中,modname
是一个模块对象并且funcname
是它的一个属性。在这种情况下,模块的属性和模块中定义的全局名称之间恰好有一个直接的映射:它们共享相同的命名空间!
属性可以是只读的或可写的。 在后一种情况下,可以分配给属性。 模块属性是可写的:您可以写 modname.the_answer = 42。可写属性也可以用 del 语句删除。 例如,del modname.the_answer 将从 modname 命名的对象中删除属性 the_answer。
命名空间是在不同的时刻创建的,并且具有不同的生命周期。包含内置名称的命名空间是在 Python 解释器启动时创建的,并且永远不会被删除。模块的全局命名空间是在读入模块定义时创建的;通常,模块名称空间也会持续到解释器退出。解释器的顶级调用执行的语句,无论是从脚本文件中读取还是以交互方式读取,都被视为名为 的模块的一部分__main__
,因此它们具有自己的全局命名空间。(内置名称实际上也存在于一个模块中;这被称为builtins
。)
函数的本地命名空间在函数被调用时创建,并在函数返回或引发未在函数内处理的异常时删除。(实际上,忘记将是描述实际发生的事情的更好方式。)当然,递归调用每个都有自己的本地名称空间。
范围是 Python 程序的文本区域,其中命名空间可直接访问。这里的“可直接访问”意味着对名称的非限定引用尝试在命名空间中查找该名称。
尽管范围是静态确定的,但它们是动态使用的。在执行期间的任何时候,至少有三个嵌套作用域的命名空间可以直接访问:
- 首先搜索的最内层范围包含本地名称
- 从最近的封闭范围开始搜索的任何封闭函数的范围包含非本地名称,但也包含非全局名称
- 倒数第二个范围包含当前模块的全局名称
- 最外层范围(最后搜索)是包含内置名称的命名空间
如果名称被声明为全局名称,则所有引用和分配都直接进入包含模块全局名称的中间范围。要重新绑定在最内层范围之外找到的变量,nonlocal
可以使用该语句;如果未声明为非局部变量,则这些变量是只读的(尝试写入此类变量只会在最内层范围内创建一个新的局部变量,而保持相同名称的外部变量不变)。
通常,本地范围引用(文本上)当前函数的本地名称。在函数之外,本地范围引用与全局范围相同的命名空间:模块的命名空间。类定义在本地范围内放置了另一个命名空间。
重要的是要认识到范围是由文本确定的:在模块中定义的函数的全局范围是该模块的命名空间,无论从何处调用该函数或通过什么别名调用该函数。另一方面,名称的实际搜索是在运行时动态完成的——然而,语言定义正在向静态名称解析发展,在“编译”时,所以不要依赖动态名称解析!(实际上,局部变量已经是静态确定的。)
Python 的一个特殊之处在于——如果没有global
ornonlocal
语句生效——对名称的赋值总是进入最内层范围。赋值不会复制数据——它们只是将名称绑定到对象。删除也是如此:该语句从本地范围引用的命名空间中删除绑定。事实上,所有引入新名称的操作都使用本地范围:特别是,语句和函数定义将模块或函数名称绑定在本地范围内。del x
x
import
该global
语句可用于指示特定变量存在于全局范围内并且应该在那里反弹;该 nonlocal
声明表明特定变量存在于封闭范围内,应该在那里反弹。
9.2.1 范围和命名空间示例
这是一个示例,演示如何引用不同的范围和命名空间,以及如何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
您还可以看到在全局分配之前没有先前的垃圾邮件绑定。
9.3 类
类引入了一些新语法、三种新对象类型和一些新语义。
9.3.1 类定义语法
最简单的类定义形式如下所示:
class ClassName:
<statement-1>
.
.
.
<statement-N>
def
语句),必须在它们生效之前执行。(可以想象,您可以将类定义放在if
语句的分支或函数内。)
在实践中,类定义中的语句通常是函数定义,但其他语句是允许的,有时是有用的——我们稍后会回到这个。类中的函数定义通常具有一种特殊形式的参数列表,由方法的调用约定决定——同样,这将在后面解释。
当输入一个类定义时,会创建一个新的命名空间,并将其用作局部范围——因此,对局部变量的所有赋值都会进入这个新的命名空间。特别是,函数定义在这里绑定了新函数的名称。
当一个类定义正常离开时(通过结束),一个类对象被创建。这基本上是对类定义创建的命名空间内容的包装;我们将在下一节中了解更多关于类对象的信息。原始的本地范围(在输入类定义之前生效的那个)被恢复,并且类对象在此处绑定到类定义标题中给出的类名(ClassName
在示例中)。
9.3.2 类对象
类对象支持两种操作:属性引用和实例化。
属性引用使用用于 Python 中所有属性引用的标准语法:obj.name
. 有效的属性名称是创建类对象时类名称空间中的所有名称。因此,如果类定义如下所示:
class MyClass:
"""A simple example class"""
i = 12345
def f(self):
return 'hello world'
那么 MyClass.i 和 MyClass.f 是有效的属性引用,分别返回一个整数和一个函数对象。 类属性也可以赋值,所以你可以通过赋值来改变 MyClass.i 的值。 __doc__ 也是一个有效属性,返回属于该类的文档字符串:“A simple example class”。
类实例化使用函数表示法。 假设类对象是一个返回类的新实例的无参数函数。 例如(假设上面的类):
x = MyClass()
创建该类的新实例并将该对象分配给局部变量x
。
实例化操作(“调用”类对象)创建一个空对象。许多类喜欢创建具有针对特定初始状态定制的实例的对象。因此,一个类可以定义一个名为 的特殊方法 __init__()
,如下所示:
>>>
>>> class Complex:
... def __init__(self, realpart, imagpart):
... self.r = realpart
... self.i = imagpart
...
>>> x = Complex(3.0, -4.5)
>>> x.r, x.i
(3.0, -4.5)
9.3.3 实例对象
现在我们可以用实例对象做什么?实例对象理解的唯一操作是属性引用。有两种有效的属性名称:数据属性和方法。
数据属性对应于 Smalltalk 中的“实例变量”和 C++ 中的“数据成员”。数据属性不需要声明;像局部变量一样,它们在第一次被分配时就出现了。例如,如果 x
是MyClass
上面 created 的实例,下面的代码将打印 value 16
,不留痕迹:
x.counter = 1
while x.counter < 10:
x.counter = x.counter * 2
print(x.counter)
del x.counter
实例对象的有效方法名称取决于其类。根据定义,作为函数对象的类的所有属性都定义了其实例的相应方法。所以在我们的例子中,x.f
is 是一个有效的方法引用,sinceMyClass.f
是一个函数,但x.i
不是,since MyClass.i
不是。但与它x.f
不一样MyClass.f
——它是一个方法对象,而不是一个函数对象。
9.3.4 方法对象
通常,绑定后立即调用方法:
x.f()
在MyClass
示例中,这将返回字符串。但是,没有必要立即调用方法:是一个方法对象,可以存储起来并在以后调用。例如:'hello world'
x.f
xf = x.f
while True:
print(xf())
hello world
调用方法时究竟会发生什么?你可能已经注意到 x.f()
上面没有参数的调用,即使函数定义f()
指定了一个参数。争论是怎么回事?当一个需要参数的函数在没有任何参数的情况下被调用时,Python 肯定会引发异常——即使该参数实际上并没有被使用……
其实,你可能已经猜到答案了:方法的特殊之处在于实例对象作为函数的第一个参数传递。在我们的示例中,调用x.f()
完全等同于MyClass.f(x)
. 通常,使用包含n 个参数的列表调用方法等效于使用参数列表调用相应的函数,该参数列表是通过在第一个参数之前插入方法的实例对象而创建的。
如果您仍然不了解方法的工作原理,那么查看实现也许可以澄清问题。当引用实例的非数据属性时,将搜索实例的类。如果名称表示作为函数对象的有效类属性,则通过将实例对象和刚刚在抽象对象中找到的函数对象打包(指向)来创建方法对象:这就是方法对象。当使用参数列表调用方法对象时,从实例对象和参数列表构造一个新的参数列表,并使用这个新的参数列表调用函数对象。
9.3.5 类和实例变量
一般来说,实例变量用于每个实例唯一的数据,类变量用于类的所有实例共享的属性和方法:
class Dog:
kind = 'canine' # class variable shared by all instances
def __init__(self, name):
self.name = name # instance variable unique to each instance
>>> d = Dog('Fido')
>>> e = Dog('Buddy')
>>> d.kind # shared by all dogs
'canine'
>>> e.kind # shared by all dogs
'canine'
>>> d.name # unique to d
'Fido'
>>> e.name # unique to e
'Buddy'
正如A Word About Names and Objects中所讨论的,共享数据在涉及可变对象(例如列表和字典)时可能会产生令人惊讶的效果。例如,以下代码中的技巧列表不应用作类变量,因为所有Dog 实例仅共享一个列表:
class Dog:
tricks = [] # mistaken use of a class variable
def __init__(self, name):
self.name = name
def add_trick(self, trick):
self.tricks.append(trick)
>>> d = Dog('Fido')
>>> e = Dog('Buddy')
>>> d.add_trick('roll over')
>>> e.add_trick('play dead')
>>> d.tricks # unexpectedly shared by all dogs
['roll over', 'play dead']
正确的类设计应该使用实例变量:
class Dog:
def __init__(self, name):
self.name = name
self.tricks = [] # creates a new empty list for each dog
def add_trick(self, trick):
self.tricks.append(trick)
>>> d = Dog('Fido')
>>> e = Dog('Buddy')
>>> d.add_trick('roll over')
>>> e.add_trick('play dead')
>>> d.tricks
['roll over']
>>> e.tricks
['play dead']
9.4 随机备注
数据属性覆盖同名方法属性;为了避免意外的名称冲突,这可能会导致大型程序中难以发现的错误,明智的做法是使用某种约定,以最大限度地减少冲突的可能性。可能的约定包括将方法名称大写,在数据属性名称前加上一个小的唯一字符串(可能只是一个下划线),或者对方法使用动词,对数据属性使用名词。
数据属性可以被方法以及对象的普通用户(“客户”)引用。换句话说,类不能用于实现纯抽象数据类型。事实上,Python 中没有任何东西可以强制执行数据隐藏——这一切都基于约定。(另一方面,用 C 编写的 Python 实现可以完全隐藏实现细节并在必要时控制对对象的访问;这可以被用 C 编写的 Python 扩展使用。)
客户端应该小心使用数据属性——客户端可能会通过在其数据属性上加盖戳来弄乱由方法维护的不变量。请注意,只要避免名称冲突,客户端可以将自己的数据属性添加到实例对象而不影响方法的有效性——同样,命名约定在这里可以省去很多麻烦。
从方法中引用数据属性(或其他方法!)没有简写。我发现这实际上增加了方法的可读性:浏览方法时不会混淆局部变量和实例变量。
通常,方法的第一个参数被调用self
。这只不过是一个约定:这个名字self
对 Python 绝对没有特殊意义。但是请注意,如果不遵循约定,您的代码对于其他 Python 程序员的可读性可能会降低,并且还可以想象 编写依赖于这种约定的类浏览器程序。
任何作为类属性的函数对象都为该类的实例定义了一个方法。函数定义不必以文本形式包含在类定义中:将函数对象分配给类中的局部变量也是可以的。例如:
# 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
现在f
,g
和h
是类的所有属性都C
引用函数对象,因此它们都是实例的方法 C
—h
完全等同于g
. 请注意,这种做法通常只会使程序的读者感到困惑。
方法可以通过使用参数的方法属性来调用其他方法self
:
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)
每个值都是一个对象,因此有一个类(也称为它的类型)。它存储为object.__class__
.
9.5 继承
class DerivedClassName(BaseClassName):
<statement-1>
.
.
.
<statement-N>
该名称BaseClassName
必须在包含派生类定义的范围内定义。代替基类名称,也允许使用其他任意表达式。这可能很有用,例如,当基类在另一个模块中定义时:
class DerivedClassName(modname.BaseClassName):
派生类定义的执行过程与基类相同。构造类对象时,会记住基类。这用于解析属性引用:如果在类中找不到请求的属性,则搜索继续查找基类。如果基类本身是从某个其他类派生的,则此规则将递归应用。
派生类的实例化没有什么特别之处: DerivedClassName()
创建类的新实例。方法引用的解析如下:搜索相应的类属性,必要时沿着基类链向下搜索,如果产生函数对象,则方法引用有效。
派生类可以覆盖其基类的方法。因为方法在调用同一对象的其他方法时没有特殊权限,所以基类的方法调用同一个基类中定义的另一个方法最终可能会调用覆盖它的派生类的方法。(对于 C++ 程序员:Python 中的所有方法都是有效的virtual
。)
派生类中的重写方法实际上可能想要扩展而不是简单地替换同名的基类方法。有一个简单的方法可以直接调用基类方法:只需调用. 这有时对客户也很有用。(请注意,这仅在基类可在全局范围内访问时才有效。)BaseClassName.methodname(self, arguments)
BaseClassName
Python 有两个使用继承的内置函数:
- 用于
isinstance()
检查实例的类型: 仅当is或某个类派生自.isinstance(obj, int)
True
obj.__class__
int
int
- 用于
issubclass()
检查类继承:因为 是. 但是, is因为不是 的子类。issubclass(bool, int)
True
bool
int
issubclass(float, int)
False
float
int
9.5.1 多重继承
Python 也支持一种多重继承形式。具有多个基类的类定义如下所示:
class DerivedClassName(Base1, Base2, Base3):
<statement-1>
.
.
.
<statement-N>
DerivedClassName
,则在 中搜索它Base1
,然后(递归地)在 的基类中Base1
搜索,如果在那里找不到,则在 中搜索Base2
,依此类推。
事实上,它比这稍微复杂一些。方法解析顺序动态变化以支持对super()
. 这种方法在其他一些多继承语言中称为 call-next-method,并且比单继承语言中的超级调用更强大。
动态排序是必要的,因为所有多重继承的情况都表现出一种或多种菱形关系(其中至少一个父类可以从最底层的类通过多条路径访问)。例如,所有的类都继承自object
,所以任何多重继承的情况都提供了不止一个到达的路径object
。为了防止基类被多次访问,动态算法以保留每个类中指定的从左到右的顺序的方式线性化搜索顺序,只调用每个父类一次,并且是单调的(意味着一个类可以在不影响其父类的优先顺序的情况下进行子类化)。总之,这些属性使得设计具有多重继承的可靠且可扩展的类成为可能。
9.6 私有变量
在 Python 中不存在只能从对象内部访问的“私有”实例变量。然而,大多数 Python 代码都遵循一个约定:前缀为下划线(例如_spam
)的名称应被视为 API 的非公共部分(无论是函数、方法还是数据成员)。它应被视为实施细节,如有更改,恕不另行通知。
由于类私有成员有一个有效的用例(即避免名称与子类定义的名称发生名称冲突),因此对这种机制的支持有限,称为name mangling。表单的任何标识符 __spam
(至少两个前导下划线,最多一个尾随下划线)在文本上替换为_classname__spam
,其中classname
是去掉前导下划线的当前类名。只要它出现在类的定义中,就无需考虑标识符的句法位置,就可以完成这种修饰。
名称修饰有助于让子类覆盖方法而不破坏类内方法调用。例如:
class Mapping:
def __init__(self, iterable):
self.items_list = []
self.__update(iterable)
def update(self, iterable):
for item in iterable:
self.items_list.append(item)
__update = update # private copy of original update() method
class MappingSubclass(Mapping):
def update(self, keys, values):
# provides new signature for update()
# but does not break __init__()
for item in zip(keys, values):
self.items_list.append(item)
MappingSubclass
要引入 标识符,上面的示例也可以工作,因为它分别在 类和类中__update
替换 为。_Mapping__update
Mapping
_MappingSubclass__update
MappingSubclass
请注意,修改规则主要是为了避免事故。仍然可以访问或修改被认为是私有的变量。这甚至在特殊情况下很有用,例如在调试器中。
请注意,传递给exec()
或eval()
不将调用类的类名视为当前类的代码;这类似于global
语句的效果,其效果同样仅限于字节编译在一起的代码。相同的限制适用于 getattr()
,setattr()
和delattr()
, 以及 __dict__
直接引用时。
9.7 其他
有时,使用类似于 Pascal“记录”或 C“结构”的数据类型,将一些命名数据项捆绑在一起是很有用的。一个空的类定义会很好:
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
read()
并readline()
从字符串缓冲区获取数据,并将其作为参数传递。
实例方法对象也有属性:m.__self__
是带有方法的实例对象,是方法m()
对应m.__func__
的函数对象。
9.8 迭代器
到目前为止,您可能已经注意到大多数容器对象都可以使用以下for
语句进行循环:
for element in [1, 2, 3]:
print(element)
for element in (1, 2, 3):
print(element)
for key in {'one':1, 'two':2}:
print(key)
for char in "123":
print(char)
for line in open("myfile.txt"):
print(line, end='')
这种访问方式清晰、简洁、方便。迭代器的使用遍及并统一了 Python。在幕后,该for
语句调用iter()
容器对象。该函数返回一个迭代器对象,该对象定义了__next__()
一次访问容器中元素的方法。当没有更多元素时, __next__()
引发一个StopIteration
异常,告诉 for
循环终止。您可以使用内置函数调用该__next__()
方法;next()
这个例子展示了它是如何工作的:
>>>
>>> s = 'abc'
>>> it = iter(s)
>>> it
<iterator object at 0x00A1DB50>
>>> next(it)
'a'
>>> next(it)
'b'
>>> next(it)
'c'
>>> next(it)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
next(it)
StopIteration
了解了迭代器协议背后的机制后,很容易将迭代器行为添加到您的类中。定义一个__iter__()
方法,该方法返回一个带有__next__()
方法的对象。如果类定义了__next__()
,那么__iter__()
就可以返回self
:
class Reverse:
"""Iterator for looping over a sequence backwards."""
def __init__(self, data):
self.data = data
self.index = len(data)
def __iter__(self):
return self
def __next__(self):
if self.index == 0:
raise StopIteration
self.index = self.index - 1
return self.data[self.index]
>>>
>>> rev = Reverse('spam')
>>> iter(rev)
<__main__.Reverse object at 0x00A1DB50>
>>> for char in rev:
... print(char)
...
m
a
p
s
9.9 生成器
Generator是一个简单而强大的创建迭代器的工具。它们像常规函数一样编写,但yield
只要它们想要返回数据就使用该语句。每次next()
调用它时,生成器都会从它停止的地方恢复(它会记住所有数据值以及最后执行的语句)。一个例子表明生成器可以很容易地创建:
def reverse(data):
for index in range(len(data)-1, -1, -1):
yield data[index]
>>>
>>> for char in reverse('golf'):
... print(char)
...
f
l
o
g
__iter__()
和__next__()
方法是自动创建的。
另一个关键特性是局部变量和执行状态在调用之间自动保存。self.index
与使用和之类的实例变量的方法相比,这使得函数更容易编写,也更清晰self.data
。
除了自动创建方法和保存程序状态之外,当生成器终止时,它们会自动引发StopIteration
. 结合起来,这些特性使得创建迭代器变得很容易,而无需花费更多的精力来编写常规函数。
9.10 生成器表达式
一些简单的生成器可以使用类似于列表推导的语法简洁地编码为表达式,但使用括号而不是方括号。这些表达式是为生成器立即被封闭函数使用的情况而设计的。生成器表达式比完整的生成器定义更紧凑但通用性更低,并且比等效的列表推导更易于记忆。
例子:
>>>
>>> sum(i*i for i in range(10)) # sum of squares
285
>>> xvec = [10, 20, 30]
>>> yvec = [7, 5, 3]
>>> sum(x*y for x,y in zip(xvec, yvec)) # dot product
260
>>> from math import pi, sin
>>> sine_table = {x: sin(x*pi/180) for x in range(0, 91)}
>>> unique_words = set(word for line in page for word in line.split())
>>> valedictorian = max((student.gpa, student.name) for student in graduates)
>>> data = 'golf'
>>> list(data[i] for i in range(len(data)-1, -1, -1))
['f', 'l', 'o', 'g']