9. 类
9.1. 名称和对象
9.2. Python 作用域和命名空间
如果一个名称被声明为全局变量,则所有引用和赋值将直接指向包含该模块的全局名称的中间作用域。要重新绑定在最内层作用域以外找到的变量,可以使用 nonlocal
语句声明为非本地变量。如果没有被声明为非本地变量,这些变量将是只读的(尝试写入这样的变量只会在最内层作用域中创建一个 新的 局部变量,而同名的外部变量保持不变)。
通常,当前局部作用域将(按字面文本)引用当前函数的局部名称。在函数以外,局部作用域将引用与全局作用域相一致的命名空间:模块的命名空间。类定义将在局部命名空间内再放置另一个命名空间。
9.2.1. 作用域和命名空间示例
您还可以在 global
赋值之前看到之前没有 spam 的绑定。
9.3. 初探类
类引入了一些新语法,三种新对象类型和一些新语义。
9.3.1. 类定义语法
9.3.2. 类对象
类对象支持两种操作:属性引用和实例化。
属性引用 使用 Python 中所有属性引用所使用的标准语法: obj.name
。有效的属性名称是类对象被创建时存在于类命名空间中的所有名称。因此,如果类定义是这样的:
当然,__init__()
方法还可以有额外参数以实现更高灵活性。在这种情况下,提供给类实例化运算符的参数将被传递给 __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. 实例对象
9.3.4. 方法对象
9.3.5. 类和实例变量
一般来说,实例变量用于每个实例的唯一数据,而类变量用于类的所有实例共享的属性和方法:
class Dog:
kind = 'canine' # class variable shared by all instancesdef __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'
正如 名称和对象 中已讨论过的,共享数据可能在涉及 mutable 对象例如列表和字典的时候导致令人惊讶的结果。例如以下代码中的 tricks 列表不应该被用作类变量,因为所有的 Dog 实例将只共享一个单独的列表:
class Dog:
tricks = [] # mistaken use of a class variabledef __init__(self, name):self.name = namedef 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 = nameself.tricks = [] # creates a new empty list for each dogdef 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. 补充说明
方法可以通过使用 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()
来检查一个实例的类型:isinstance(obj, int)
仅会在obj.__class__
为int
或某个派生自int
的类时为True
。使用
issubclass()
来检查类的继承关系: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>
https://www.python.org/download/releases/2.3/mro/
9.6. 私有变量
那种仅限从一个对象内部访问的“私有”实例变量在 Python 中并不存在。但是,大多数 Python 代码都遵循这样一个约定:带有一个下划线的名称 (例如 _spam
) 应该被当作是 API 的非公有部分 (无论它是函数、方法或是数据成员)。这应当被视为一个实现细节,可能不经通知即加以改变。
由于存在对于类私有成员的有效使用场景(例如避免名称与子类所定义的名称相冲突),因此存在对此种机制的有限支持,称为 名称改写。任何形式为 __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() methodclass 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
类中被替换为 _Mapping__update
而在 MappingSubclass
类中被替换为 _MappingSubclass__update
。
请注意,改写规则的设计主要是为了避免意外冲突;访问或修改被视为私有的变量仍然是可能的。这在特殊情况下甚至会很有用,例如在调试器中。
请注意传递给 exec()
或 eval()
的代码不会将发起调用类的类名视作当前类;这类似于 global
语句的效果,因此这种效果仅限于同时经过字节码编译的代码。同样的限制也适用于 getattr()
, setattr()
和 delattr()
,以及对于 __dict__
的直接引用。
9.7. 杂项说明
有时会需要使用类似于 Pascal 的“record”或 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()
方法的实例对象,而 m.__func__
则是该方法所对应的函数对象。
9.8. 迭代器
到目前为止,您可能已经注意到大多数容器对象都可以使用 for
语句:
看过迭代器协议的幕后机制,给你的类添加迭代器行为就很容易了。定义一个 __iter__()
方法来返回一个带有 __next__()
方法的对象。如果类已定义了 __next__()
,则 __iter__()
可以简单地返回 self
:
class Reverse:"""Iterator for looping over a sequence backwards."""def __init__(self, data):self.data = dataself.index = len(data)def __iter__(self):return selfdef __next__(self):if self.index == 0:raise StopIterationself.index = self.index - 1return self.data[self.index]
>>> rev = Reverse('spam')>>> iter(rev)<__main__.reverse object at>>>> for char in rev:... print(char)...
m
a
p
s
9.9. 生成器
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>>> 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']
【1】存在一个例外。模块对象有一个秘密的只读属性 __dict__
,它返回用于实现模块命名空间的字典;__dict__
是属性但不是全局名称。显然,使用这个将违反命名空间实现的抽象,应当仅被用于事后调试器之类的场合。
阅读笔记
类,对象,继承,实例,记录,结构,作用域,可见度,私有变量……如果对这些知识表示零基础,那么Python入门肯定不是天方夜谭,而是百分之百的噩梦一场。