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
注意,局部 赋值(这是默认状态)不会改变 scope_test 对 spam 的绑定。 nonlocal
赋值会改变 scope_test 对 spam 的绑定,而 global
赋值会改变模块层级的绑定。
而且,global
赋值前没有 spam 的绑定。
9.3. 初探类
类引入了一点新语法,三种新的对象类型和一些新语义。
9.3.1. 类定义语法
最简单的类定义形式如下:
class ClassName:
<语句-1>
<语句-N>
与函数定义 (def
语句) 一样,类定义必须先执行才能生效。把类定义放在 if
语句的分支里或函数内部试试。
在实践中,类定义内的语句通常都是函数定义,但也可以是其他语句。这部分内容稍后再讨论。类里的函数定义一般是特殊的参数列表,这是由方法调用的约定规范所指明的 --- 同样,稍后再解释。
当进入类定义时,将创建一个新的命名空间,并将其用作局部作用域 --- 因此,所有对局部变量的赋值都是在这个新命名空间之内。 特别的,函数定义会绑定到这里的新函数名称。
当 (从结尾处) 正常离开类定义时,将创建一个 类对象。 这基本上是一个围绕类定义所创建的命名空间的包装器;我们将在下一节中了解有关类对象的更多信息。 原始的 (在进入类定义之前有效的) 作用域将重新生效,类对象将在这里与类定义头所给出的类名称进行绑定 (在这个示例中为 ClassName
)。
9.3.2. Class 对象
类对象支持两种操作:属性引用和实例化。
属性引用 使用 Python 中所有属性引用所使用的标准语法: obj.name
。 有效的属性名称是类对象被创建时存在于类命名空间中的所有名称。 因此,如果类定义是这样的:
class MyClass:
"""一个简单的示例类"""
i = 12345
def f(self):
return 'hello world'
那么 MyClass.i
和 MyClass.f
就是有效的属性引用,将分别返回一个整数和一个函数对象。 类属性也可以被赋值,因此可以通过赋值来改变 MyClass.i
的值。 __doc__
也是一个有效的属性,将返回所属类的文档字符串: "A simple example class"
。
类的 实例化 使用函数表示法。 可以把类对象视为是返回该类的一个新实例的不带参数的函数。 举例来说(假设使用上述的类):
x = MyClass()
创建类的新 实例 并将此对象分配给局部变量 x
。
实例化操作 (“调用”类对象) 会创建一个空对象。 许多类都希望创建的对象实例是根据特定初始状态定制的。 因此一个类可能会定义名为 __init__()
的特殊方法,就像这样:
def __init__(self):
self.data = []
当一个类定义了 __init__()
方法时,类的实例化会自动为新创建的类实例唤起 __init__()
。 因此在这个例子中,可以通过以下语句获得一个已初始化的新实例:
x = MyClass
()
当然,__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. 实例对象
现在我们能用实例对象做什么? 实例对象所能理解的唯一操作是属性引用。 有两种有效的属性名称:数据属性和方法。
数据属性 对应于 Smalltalk 中的“实例变量”,以及 C++ 中的“数据成员”。 数据属性不需要声明;就像局部变量一样,它们将在首次被赋值时产生。 举例来说,如果 x
是上面创建的 MyClass
的实例,则以下代码将打印数值 16
,且不保留任何追踪信息:
x.counter = 1
while x.counter < 10:
x.counter = x.counter * 2
print(x.counter)
del x.counter
另一种实例属性引用称为 方法。 方法是“从属于”对象的函数。
实例对象的有效方法名称依赖于其所属的类。 根据定义,一个类中所有是函数对象的属性都是定义了其实例的相应方法。 因此在我们的示例中,x.f
是有效的方法引用,因为 MyClass.f
是一个函数,而 x.i
不是方法,因为 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
,直到结束。
当一个方法被调用时究竟会发生什么? 你可能已经注意到尽管 f()
的函数定义指定了一个参数,但上面调用 x.f()
时却没有带参数。 这个参数发生了什么事? 当一个需要参数的函数在不附带任何参数的情况下被调用时 Python 肯定会引发异常 --- 即使参数实际上没有被使用...
实际上,你可能已经猜到了答案:方法的特殊之处就在于实例对象会作为函数的第一个参数被传入。 在我们的示例中,调用 x.f()
其实就相当于 MyClass.f(x)
。 总之,调用一个具有 n 个参数的方法就相当于调用再多一个参数的对应函数,这个参数值为方法所属实例对象,位置在其他参数之前。
总而言之,方法的运作方式如下。 当一个实例的非数据属性被引用时,将搜索该实例所属的类。 如果名称表示一个属于函数对象的有效类属性,则指向实例对象和函数对象的引用将被打包为一个方法对象。 当传入一个参数列表调用该方法对象时,将基于实例对象和参数列表构造一个新的参数列表,并传入这个新参数列表调用相应的函数对象。
9.3.5. 类和实例变量
一般来说,实例变量用于每个实例的唯一数据,而类变量用于类的所有实例共享的属性和方法:
class Dog:
kind = 'canine' # 类变量被所有实例所共享
def __init__(self, name):
self.name = name # 实例变量为每个实例所独有
>>> d = Dog('Fido')
>>> e = Dog('Buddy')
>>> d.kind # 被所有的 Dog 实例所共享
'canine'
>>> e.kind # 被所有的 Dog 实例所共享
'canine'
>>> d.name # 为 d 所独有
'Fido'
>>> e.name # 为 e 所独有
'Buddy'
正如 名称和对象 中已讨论过的,共享数据可能在涉及 mutable 对象例如列表和字典的时候导致令人惊讶的结果。 例如以下代码中的 tricks 列表不应该被用作类变量,因为所有的 Dog 实例将只共享一个单独的列表:
class Dog:
tricks = [] # 类变量的错误用法
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 # 非预期地被所有的 Dog 实例所共享
['roll over', 'play dead']
正确的类设计应该使用实例变量:
class Dog:
def __init__(self, name):
self.name = name
self.tricks = [] # 为每个 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']
数据属性可以被方法以及一个对象的普通用户(“客户端”)所引用。 换句话说,类不能用于实现纯抽象数据类型。 实际上,在 Python 中没有任何东西能强制隐藏数据 --- 它是完全基于约定的。 (而在另一方面,用 C 语言编写的 Python 实现则可以完全隐藏实现细节,并在必要时控制对象的访问;此特性可以通过用 C 编写 Python 扩展来使用。)
客户端应当谨慎地使用数据属性 --- 客户端可能通过直接操作数据属性的方式破坏由方法所维护的固定变量。 请注意客户端可以向一个实例对象添加他们自己的数据属性而不会影响方法的可用性,只要保证避免名称冲突 --- 再次提醒,在此使用命名约定可以省去许多令人头痛的麻烦。
在方法内部引用数据属性(或其他方法!)并没有简便方式。 我发现这实际上提升了方法的可读性:当浏览一个方法代码时,不会存在混淆局部变量和实例变量的机会。
方法的第一个参数常常被命名为 self
。 这也不过就是一个约定: self
这一名称在 Python 中绝对没有特殊含义。 但是要注意,不遵循此约定会使得你的代码对其他 Python 程序员来说缺乏可读性,而且也可以想像一个 类浏览器 程序的编写可能会依赖于这样的约定。
任何一个作为类属性的函数都为该类的实例定义了一个相应方法。 函数定义的文本并非必须包含于类定义之内:将一个函数对象赋值给一个局部变量也是可以的。 例如:
# 在类之外定义的函数
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):
<语句-1>
<语句-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):
<语句-1>
<语句-N>
对于多数目的来说,在最简单的情况下,你可以认为搜索从父类所继承属性的操作是深度优先、从左到右的,当层次结构存在重叠时不会在同一个类中搜索两次。 因此,如果某个属性在 DerivedClassName
中找不到,就会在 Base1
中搜索它,然后(递归地)在 Base1
的基类中搜索,如果在那里也找不到,就将在 Base2
中搜索,依此类推。
真实情况比这个更复杂一些;方法解析顺序会动态改变以支持对 super()
的协同调用。 这种方式在某些其他多重继承型语言中被称为后续方法调用,它比单继承型语言中的 super 调用更强大。
动态调整顺序是有必要的,因为所有多重继承的情况都会显示出一个或更多的菱形关联(即至少有一个上级类可通过多条路径被最底层的类所访问)。 例如,所有类都是继承自 object
,因此任何多重继承的情况都提供了一条以上的路径可以通向 object
。 为了确保基类不会被访问一次以上,动态算法会用一种特殊方式将搜索顺序线性化,保留每个类所指定的从左至右的顺序,只调用每个上级类一次,并且保持单调性(即一个类可以被子类化而不影响其父类的优先顺序)。 总而言之,这些特性使得设计具有多重继承的可靠且可扩展的类成为可能。 要了解更多细节,请参阅 Python 2.3 方法解析顺序。
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 # 原始 update() 方法的私有副本
class MappingSubclass(Mapping):
def update(self, keys, values):
# 为 update() 提供了新的签名
# 但不会破坏 __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" 的数据类型是很有用的,将一些带名称的数据项捆绑在一起。 实现这一目标的理想方式是使用 dataclasses
:
from dataclasses import dataclass
@dataclass
class Employee:
name: str
dept: str
salary: int
>>> john = Employee('john', 'computer lab', 1000)
>>> john.dept
'computer lab'
>>> john.salary
一段期望使用特定抽象数据类型的 Python 代码通常可以通过传入一个模拟了该数据类型的方法的类作为替代。 例如,如果你有一个基于文件对象来格式化某些数据的函数,你可以定义一个带有 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)
<str_iterator object at 0x10c90e650>
>>> next(it)
>>> next(it)
>>> next(it)
>>> next(it)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
next(it)
StopIteration
了解了迭代器协议背后的机制后,就可以轻松地为你的类添加迭代器行为了。 定义 __iter__()
方法用于返回一个带有 __next__()
方法的对象。 如果类已定义了 __next__()
,那么 __iter__()
可以简单地返回 self
:
class Reverse:
"""对一个序列执行反向循环的迭代器。"""
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)
9.9. 生成器
生成器 是一个用于创建迭代器的简单而强大的工具。 它们的写法类似于标准的函数,但当它们要返回数据时会使用 yield
语句。 每次在生成器上调用 next()
时,它会从上次离开的位置恢复执行(它会记住上次执行语句时的所有数据值)。 一个显示如何非常容易地创建生成器的示例如下:
def reverse(data):
for index in range(len(data)-1, -1, -1):
yield data[index]
>>> for char in reverse('golf'):
... print(char)
可以用生成器来完成的任何功能同样可以通用前一节所描述的基于类的迭代器来完成。 但生成器的写法更为紧凑,因为它会自动创建 __iter__()
和 __next__()
方法。
另一个关键特性在于局部变量和执行状态会在每次调用之间自动保存。 这使得该函数相比使用 self.index
和 self.data
这种实例变量的方式更易编写且更为清晰。
除了会自动创建方法和保存程序状态,当生成器终结时,它们还会自动引发 StopIteration
。 这些特性结合在一起,使得创建迭代器能与编写常规函数一样容易。
9.10. 生成器表达式
某些简单的生成器可以写成简洁的表达式代码,所用语法类似列表推导式,但外层为圆括号而非方括号。 这种表达式被设计用于生成器将立即被外层函数所使用的情况。 生成器表达式相比完整的生成器更紧凑但较不灵活,相比等效的列表推导式则更为节省内存。
>>> sum(i*i for i in range(10)) # 平方和
>>> xvec = [10, 20, 30]
>>> yvec = [7, 5, 3]
>>> sum(x*y for x,y in zip(xvec, yvec)) # 点乘
>>> 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__
是一个属性但不是全局名称。 显然,使用个将违反命名空间实现的抽象,应当仅限用于事后调试器之类的情况。
2001-2025, Python Software Foundation.
This page is licensed under the Python Software Foundation License Version 2.
Examples, recipes, and other code in the documentation are additionally licensed under the Zero Clause BSD License.
See History and License for more information.