本系列文章是我学习Python3.9的官方tutorial的笔记,大部分来源于官网的中文翻译,但由于该翻译有些部分实在太差和啰嗦,我做了很多删除和修改,还有部分原文讲不明白的,我参考其他资料增加了进一步阐述说明。
类提供了一种组合数据和功能的方法。 创建一个新类意味着创建一个新的对象类型,从而允许创建一个该类型的新实例。 每个类的实例可以拥有保存自己状态的属性和改变自己状态的方法(定义在类中的)。
8.1 Python的作用域
学习类之前,先来了解一下Python的作用域,Python的作用域分四层,由内到外分别是:
- local:当前函数局部作用域,函数范围内定义的局部变量仅在函数范围内可见
- nonlocal:非本地作用域,即当前函数的所有外层函数(可以是很多层嵌套)的局部变量
- global:当前模块的全局作用域,执行脚本或交互式执行的当前模块是_main_
- built-in:内置作用域,即Python解释器的内置变量
内层作用域的变量外场不可见,外层作用域的变量在内层默认是readonly的,如果需要修改外层作用域的变量,需要对变量进行global或者nonlocal的声明,如果没有声明,赋值效果仅仅是创建一个内层局部变量而已并不会修改外层变量。
用例子来说明这几个作用域的区别以及global和nolocal关键字的用法会更加清晰。
>>> gcount = 0
>>> def global_test():
... gcount+=1
... print (gcount)
>>> global_test()
Traceback (most recent call last):
File "<pyshell#6>", line 1, in <module>
global_test()
File "<pyshell#5>", line 2, in global_test
gcount+=1
UnboundLocalError: local variable 'gcount' referenced before assignment
上面程序报错,是因为第一行的gcount是全局变量 (此处可以省略global关键字),在global_test()函数内部对它只能读,程序中显然对gcount进行了赋值修改,所以解释器函数内的gcount是local变量,因为+=是一个先读后加的操作,因此会报local变量在赋值前先被引用的错误。
在函数内加上global声明即可正常运行:
>>> gcount = 0
>>> def global_test():
... global gcount
... gcount +=1
... print (gcount)
>>> global_test()
1
在如果只是读全局变量,则不需要global声明也可以正常使用:
>>> gcount = 0
>>> def global_test():
... print (gcount)
>>> global_test()
0
nonlocal关键字用来在函数中使用外层但非全局的变量:
def scope_test():
spam = "test spam" # 此处声明的spam对下面几个函数来说属于nonlocal变量
def do_local():
spam = "local spam"
def do_nonlocal():
nonlocal spam
spam = "nonlocal spam"
def do_global():
global spam
spam = "global spam"
do_local()
print("After local assignmane:", spam)
do_nonlocal()
print("After nonlocal assignment:",spam)
do_global()
print("After global assignment:",spam)
scope_test()
print("In global scope:",spam)
outputs:
After local assignmane: test spam # local改不了nonlocal变量
After nonlocal assignment: nonlocal spam # nonlocal声明可以改nonlocal变量
After global assignment: nonlocal spam # global声明只能改global变量改不了nonlocal变量
In global scope: global spam # global变量赋值成功
8.2 初探类
8.2.1 类定义语法
最简单的类定义看起来像这样:
class ClassName:
<statement-1>
.
.
.
<statement-N>
当进入类定义时,将创建一个新的命名空间,并将其用作局部作用域 --- 因此,所有对局部变量的赋值都是在这个作用域之内。 包括函数定义也在这个这个作用域内绑定函数名称。
类定义被执行后,会创建一个类对象,即这个类定义的命名空间或者作用域的一个包装。
8.2.2 类对象
类对象支持两种操作:属性引用和实例化。
属性引用 使用的语法为: 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__()
的特殊方法,就像这样:
def __init__(self):
self.data = []
当一个类定义了 __init__()
方法时,类的实例化操作会自动为新创建的类实例发起调用 __init__()
。
当然,__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)
8.2.3 函数对象和方法对象
继续用这个例子来说明:
class MyClass:
"""A simple example class"""
i = 12345
def f(self):
return 'hello world'
MyClass是一个类对象,而x = MyClass()
即利用类对象创建了一个MyClass的实例对象并赋值给x。对于类对象MyClass来说,里面定义了属性f是一个函数对象,可以通过MyClass.f(x)
的方式来调用;对于x这个实例对象,我们定义里面的属性f为方法对象,可以直接通过x.f()
来调用。注意区别,x.f()
调用时并没有传递第一个参数self,实际上对于每个实例方法的调用,Python都会自动将调用者(即.前面的x)作为第一个参数自动传入,底层调用的依然是类对象MyClass.f(x)
这个函数。有点绕,简单的说就是x.f()
等价与MyClass.f(x)
,其中x是MyClass的实例。
8.2.4 类变量和实例变量
一般来说,实例变量是每个实例独有的数据,而类变量则是该类所有实例共享的,看例子:
class Dog:
kind = 'canine' # 类变量
def __init__(self, name):
self.name = name # 实例变量
>>> 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 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']
如果同样的属性名称同时出现在实例和类中,则属性查找会优先选择实例:
>>> class Warehouse:
purpose = 'storage'
region = 'west'
>>> w1 = Warehouse()
>>> print(w1.purpose, w1.region)
storage west
>>> w2 = Warehouse()
>>> w2.region = 'east'
>>> print(w2.purpose, w2.region)
storage east
另外要注意,每个值都是一个对象,因此具有相应的类或者说类型,存储在object._class_中 。
8.3 继承
派生类或者叫子类的定义语法如下:
class DerivedClassName(BaseClassName):
<statement-1>
.
.
.
<statement-N>
派生类定义的执行过程与基类相同。 当构造类对象时,基类会被记住。 此信息将被用来解析属性引用:如果请求的属性在派生类中找不到,搜索将转往基类中进行查找。 如果基类本身也派生自其他某个类,则此规则将被递归地应用。
派生类的实例化没有任何特殊之处: DerivedClassName() 会创建该类的一个新实例。
派生类可能会重写其基类的方法,将覆盖基类的方法,尽管是基类的方法在调用该对象的其他基类方法时,最终也可能会调用覆盖它的派生类的方法。
def Animal:
def name(self):
return 'animal'
def talk(self):
print('I am ', self.name())
def Cat(Animal):
def name(self):
return 'cat'
c = Cat()
c.talk()
output:
I am cat
在派生类中的重写基类方法可能仍需要调用基类的方法时,可以通过BaseClassName.methodname(self, arguments)类对象的方式调用。
Python有两个内置函数可被用于继承机制:
- 使用
isinstance()
来检查一个实例的类型:isinstance(obj, int)
仅会在obj.__class__
为int
或某个派生自int
的类时为True
。 - 使用
issubclass()
来检查类的继承关系:issubclass(bool, int)
为True
,因为bool
是int
的子类。 但是,issubclass(float, int)
为False
,因为float
不是int
的子类。
8.3.1 多重继承
Python 也支持多重继承。 带有多个基类的类定义语句如下所示:
class DerivedClassName(Base1, Base2, Base3):
<statement-1>
.
.
.
<statement-N>
大多数情况下,你可以认为搜索从父类所继承属性的操作是深度优先、从左至右的。因此,如果某一属性在 DerivedClassName 中未找到,则会到 Base1 中搜索它,然后(递归地)到 Base1 的基类中搜索,如果在那里未找到,再到 Base2 中搜索,依此类推。
而实际情况比这个更复杂一些,方法解析顺序会动态改变以支持对 super()
的协同调用。
动态改变顺序是有必要的,因为所有多重继承的情况都会显示出一个或更多的菱形关联(即至少有一个父类可通过多条路径被最底层类所访问)。 例如,所有类都是继承自 object
,因此任何多重继承的情况都提供了一条以上的路径可以通向 object
。 为了确保基类不会被访问一次以上,动态算法会用一种特殊方式将搜索顺序线性化, 保留每个类所指定的从左至右的顺序,只调用每个父类一次,并且保持单调(即一个类可以被子类化而不影响其父类的优先顺序)。
8.4 私有变量
原则上仅限实例内部访问的‘私有变量’在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() 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 类中被替换为 _Mapping__update 而在 MappingSubclass 类中被替换为 _MappingSubclass__update。
请注意,改写规则的设计主要是为了避免意外冲突;访问或修改私有变量仍然是可能的。这在特殊情况下甚至会很有用,例如在调试器中。
8.5 迭代器
您可能已经注意到大多数容器对象都可以使用 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='')
在背后,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
8.6 生成器
生成器 是一个用于创建迭代器的简单而强大的工具。 它们的写法类似于标准的函数,但当它们要返回数据时会使用 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
。
这些特性结合在一起,使得创建迭代器能与编写常规函数一样容易。
8.7 生成器表达式
某些简单的生成器可以写成简洁的表达式代码,所用语法类似列表推导式,但外层为圆括号而非方括号。 这种表达式被设计用于生成器将立即被外层函数所使用的情况。 生成器表达式相比完整的生成器更紧凑但较不灵活,相比等效的列表推导式则更为节省内存。
>>> 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']