7. 类#
类提供了把数据和功能绑定在一起的方法。Python中支持面向对象编程(OOP)的所有标准特性:类的继承机制支持多个基类、派生的类能覆盖基类的方法、类的方法能调用基类中的同名方法。对象可包含任意数量和类型的数据。和模块一样,类也支持 Python 动态特性:在运行时创建,创建后还可以修改。
7.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
7.2. 类定义#
最简单的类定义形式如下:
class ClassName:
<语句-1>
.
.
.
<语句-N>
7.3. 类对象(class对象)#
类对象(这里指的是类似其他面向对象语言中的类名)支持两种操作:属性引用(这里的属性是广义上的属性,包含一般意义的属性和方法)和实例化。
属性引用 使用 Python 中所有属性引用所使用的标准语法: obj.name。 有效的属性名称是类对象被创建时存在于类命名空间中的所有名称。
class MyClass:
"""一个简单的示例类"""
i = 12345
def f(self):
return 'hello world'
x = MyClass() # 类的实例化
print(x, type(x))
<__main__.MyClass object at 0x7fde44b03850> <class '__main__.MyClass'>
MyClass.i
print(MyClass.f, type(MyClass.f))
<function MyClass.f at 0x7fde44ad3a30> <class 'function'>
支持初始化操作的类对象:
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)
7.4. 实例对象#
实例对象所能理解的唯一操作是属性引用。 有两种有效的属性名称:数据属性和方法。
x = MyClass() # 类的实例化
x.i = 100
x.i, MyClass.i
(100, 12345)
x.j = 100
x.j
100
del x.j # 新增的属性可以删除
try:
del x.i
except:
print("x的i属性是类定义时候就存在的,不能删除掉")
#del MyClass.i
#MyClass.i
type(MyClass.f), type(x.f) # 一个是函数,一个是方法
(function, method)
xf = x.f # xf是方法对象
print(xf())
hello world
方法的特殊之处就在于实例对象会作为函数的第一个参数被传入。 调用 x.f() 其实就相当于 MyClass.f(x)。 调用xf()也相当于MyClass.f(x)。
7.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'
e.kind = "test"
print(d.kind, e.kind) # 由于kind是imutable对象,每个对象都有一个副本,
# 修改其中一个之后不会影响另外一个
canine test
d.name # d所独有
'Fido'
e.name # e所独有
'Buddy'
7.6. 类变量是mutable对象时候#
当类变量是 mutable 对象例如列表和字典的时候,需要特别小心。
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']
如果同样的属性名称同时出现在实例和类中,则属性查找会优先选择实例:
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
7.7. 继承#
派生类定义的语法如下所示:
class DerivedClassName(BaseClassName):
<语句-1>
.
.
.
<语句-N>
当基类定义在另一个模块中时:
class DerivedClassName(modname.BaseClassName):
Python有两个内置函数可被用于继承机制:
使用 isinstance() 来检查一个实例的类型: isinstance(obj, int) 仅会在 obj.class 为 int 或某个派生自 int 的类时为 True。
使用 issubclass() 来检查类的继承关系: issubclass(bool, int) 为 True,因为 bool 是 int 的子类。 但是,issubclass(float, int) 为 False,因为 float 不是 int 的子类。
7.7.1. 多重继承#
Python 也支持一种多重继承。 带有多个基类的类定义语句如下所示:
class DerivedClassName(Base1, Base2, Base3):
<语句-1>
.
.
.
<语句-N>
7.8. 私有变量#
那种仅限从一个对象内部访问的“私有”实例变量在 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。
请注意,改写规则的设计主要是为了避免意外冲突;访问或修改被视为私有的变量仍然是可能的。这在特殊情况下甚至会很有用,例如在调试器中。
7.9. 数据类#
通过 dataclasses 可以实现类型C “struct”效果的类:
from dataclasses import dataclass
@dataclass
class Employee:
name: str
dept: str
salary: int
john = Employee('john', 'computer lab', 1000)
john.dept, john.name, john.salary
('computer lab', 'john', 1000)
7.10. 迭代器#
大多数容器对象都可以使用 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='')
1
2
3
1
2
3
one
two
1
2
3
---------------------------------------------------------------------------
FileNotFoundError Traceback (most recent call last)
Cell In[25], line 9
7 for char in "123":
8 print(char)
----> 9 for line in open("myfile.txt"):
10 print(line, end='')
File ~/myenv/lib/python3.10/site-packages/IPython/core/interactiveshell.py:324, in _modified_open(file, *args, **kwargs)
317 if file in {0, 1, 2}:
318 raise ValueError(
319 f"IPython won't let you open fd={file} by default "
320 "as it is likely to crash IPython. If you know what you are doing, "
321 "you can use builtins' open."
322 )
--> 324 return io_open(file, *args, **kwargs)
FileNotFoundError: [Errno 2] No such file or directory: 'myfile.txt'
在幕后,for 语句会在容器对象上调用 iter()。 该函数返回一个定义了 next() 方法的迭代器对象,此方法将逐一访问容器中的元素。 当元素用尽时,next() 将引发 StopIteration 异常来通知终止 for 循环。 你可以使用 next() 内置函数来调用 next() 方法,下面例子显示它的运作方式:
s = 'abc'
it = iter(s)
next(it)
'a'
next(it)
'b'
next(it)
'c'
next(it)
---------------------------------------------------------------------------
StopIteration Traceback (most recent call last)
Cell In[22], line 1
----> 1 next(it)
StopIteration:
## 自定义一个迭代器
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 at 0x7f43b1d44c40>
for char in rev:
print(char)
m
a
p
s
7.11. 生成器#
生成器 是一个用于创建迭代器的简单而强大的工具。 它们的写法类似于标准的函数,但当它们要返回数据时会使用 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。 这些特性结合在一起,使得创建迭代器能与编写常规函数一样容易。
7.11.1. 生成器表达式#
某些简单的生成器可以写成简洁的表达式代码,所用语法类似列表推导式,但外层为圆括号而非方括号。 这种表达式被设计用于生成器将立即被外层函数所使用的情况。 生成器表达式相比完整的生成器更紧凑但较不灵活,相比等效的列表推导式则更为节省内存。
sum(i*i for i in range(10)) # 平方和
285
xvec = [10, 20, 30]
yvec = [7, 5, 3]
sum(x*y for x,y in zip(xvec, yvec)) # 点乘
260
data = 'golf'
list(data[i] for i in range(len(data)-1, -1, -1))
['f', 'l', 'o', 'g']
nique_words = set(word for line in page for word in line.split())
---------------------------------------------------------------------------
NameError Traceback (most recent call last)
Cell In[30], line 1
----> 1 nique_words = set(word for line in page for word in line.split())
NameError: name 'page' is not defined
valedictorian = max((student.gpa, student.name) for student in graduates)
---------------------------------------------------------------------------
NameError Traceback (most recent call last)
Cell In[32], line 1
----> 1 valedictorian = max((student.gpa, student.name) for student in graduates)
NameError: name 'graduates' is not defined