Bootstrap

《Python基础教程》学习笔记——魔法方法、特性和迭代器

魔法方法、特性和迭代器

如果你使用的不是Python 3

在Python 2 中,要让你的类是新式的,要么在模块
开头包含赋值语句 metaclass = type ,要么直接或间接地继承内置类 object 或其他新式类。

看下面两个类:

class NewStyle(object):
	more_code_here
class OldStyle:
	more_code_here

在这两个类中, NewStyle 是一个新式类,而 OldStyle 是一个旧式类。
如果文件开头包含赋值语句 metaclass = type ,这两个类都将是新式类。

如果你的程序无需与旧版Python兼容,建议将所有类都定义为新式类,并使用函数 super 等功能。

在Python 3 中没有旧式类,因此无需显式地继承 object 或将 metaclass 设置为 type 。
所有的类都将隐式地继承 object 。如果没有指定超类,将直接继承它,否则将间接地继承它。

构造函数

魔法方法:构造函数(constructor)。
其实就是初始化方法,只是命名为 init

构造函数不同于普通方法的地方在于,将在对象创建后自动调用它们。

如:

>>> f = FooBar()
>>> f.init()

构造函数只需像下面这样做:

>>> f = FooBar()

在Python中,创建构造函数,只需将方法 init 的名称从普通的 init 改为魔法版 __init__即可。

class FooBar:
	def __init__(self):
		self.somevar = 42
>>> f = FooBar()
>>> f.somevar
42

给构造函数添加几个参数:

class FooBar:
	def __init__(self, value=42):
		self.somevar = value

如果要指定这个参数(或者说如果这个参数不是可选的),使用这个构造函数:

>>> f = FooBar('This is a constructor argument')
>>> f.somevar
'This is a constructor argument'
注意 
Python提供了魔法方法 __del__ ,也称作 析构函数(destructor)。
这个方法在对象被销毁(作为垃圾被收集)前被调用,但鉴于你无法知道准确的调用时间,建议尽可能不要使用 __del__ 。

重写普通方法和特殊的构造函数

每个类都有一个或多个超类,并从它们那里继承行为。

对类 B 的实例调用方法(或访问其属性)时,如果找不到该方法(或属性),将在其超类 A 中查找。

看下面两个类:

class A:
	def hello(self):
		print("Hello, I'm A.")
class B(A):
	pass

类 A 定义了一个名为 hello 的方法,并被类 B 继承。

>>> a = A()
>>> b = B()
>>> a.hello()
Hello, I'm A.
>>> b.hello()
Hello, I'm A.

由于类 B 自己没有定义方法 hello ,因此对其调用方法 hello 时,打印的是消息 “Hello, I’m A.” 。

要在子类中添加功能,一种基本方式是添加方法。

例如, B 可以重写方法 hello ,如下述修改后的类 B 定义所示:

class B(A):
	def hello(self):
		print("Hello, I'm B.")

修改后, b.hello() 的结果如下:

>>> b = B()
>>> b.hello()
Hello, I'm B.

重写 是继承机制的一个重要方面,对构造函数来说尤其重要。

重写构造函数时更有可能遇到一个特别的问题:
重写构造函数时,必须调用超类(继承的类)的构造函数,否则可能无法正确地初始化对象。

看下面的 Bird 类:

class Bird:
	def __init__(self):
		self.hungry = True
	def eat(self):
	if self.hungry:
		print('Aaaah ...')
		self.hungry = False
	else:
		print('No, thanks!')

这个类定义了所有鸟都具备的一种基本能力:进食。
使用这个类:

>>> b = Bird()
>>> b.eat()
Aaaah ...
>>> b.eat()
No, thanks!

看子类 SongBird ,它新增了鸣叫功能。

class SongBird(Bird):
	def __init__(self):
		self.sound = 'Squawk!'
	def sing(self):
		print(self.sound)

使用:

>>> sb = SongBird()
>>> sb.sing()
Squawk!

SongBird 是 Bird 的子类,继承了方法 eat ,但如果你尝试调用它,将发现一个问题。

>>> sb.eat()
Traceback (most recent call last):
	File "<stdin>", line 1, in ?
	File "birds.py", line 6, in eat
		if self.hungry:
AttributeError: SongBird instance has no attribute 'hungry'

出现这种错误的原因在SongBird 中重写了构造函数,但新的构造函数没有包含任何初始化属性 hungry 的代码。

要消除这种错误, SongBird 的构造函数必须调用其超类( Bird )的构造函数,以确保基本的初始化得以执行。

有两种方法:调用未关联的超类构造函数,以及使用函数 super 。

调用未关联的超类构造函数

前面问题的解决方案:

class SongBird(Bird):
	def __init__(self):
		Bird.__init__(self)
		self.sound = 'Squawk!'
	def sing(self):
		print(self.sound)

在 SongBird 类中,只添加了一行,其中包含代码 Bird.init(self) 。

>>> sb = SongBird()
>>> sb.sing()
Squawk!
>>> sb.eat()
Aaaah ...
>>> sb.eat()
No, thanks!

对实例调用方法时,方法的参数 self 将自动关联到实例(称为关联的方法)。

如果通过类调用方法(如 Bird.init ),就没有实例与其相关联。

在这种情况下,你可随便设置参数 self 。这样的方法称为未关联的

使用函数 super

函数 super,这个函数只适用于新式类。

调用这个函数时,将当前类和当前实例作为参数。对其返回的对象调用方法时,调用的将是超类(而不是当前类)的方法。

下面是前述示例的修订版本:

class Bird:
	def __init__(self):
		self.hungry = True
	def eat(self):
		if self.hungry:
			print('Aaaah ...')
			self.hungry = False
		else:
			print('No, thanks!')
			
class SongBird(Bird):
	def __init__(self):
		super().__init__()
		self.sound = 'Squawk!'
	def sing(self):
		print(self.sound)

这个新式版本与旧式版本等效:

>>> sb = SongBird()
>>> sb.sing()
Squawk!
>>> sb.eat()
Aaaah ...
>>> sb.eat()
No, thanks!

元素访问

基本的序列和映射协议

序列和映射基本上是元素(item)的集合,要实现它们的基本行为(协议),不可变对象需要实现2个方法,而可变对象需要实现4个。

  • _len_(self) : 这个方法应返回集合包含的项数,对序列来说为元素个数,对映射来说为键-值对数。
    如果 _len_ 返回零(且没有实现覆盖这种行为的_nonzero_ ),对象在布尔上下文中将被视为假(就像空的列表、元组、字符串和字典一样)。

  • _getitem_(self, key) : 这个方法应返回与指定键相关联的值。
    对序列来说,键应该是0~n -1的整数(也可以是负数,这将在后面说明),其中n为序列的长度。对映射来说,键可以是任何类型。

  • _setitem_(self, key, value) : 这个方法应以与键相关联的方式存储值,以便以后能够使用 _getitem_ 来获取。
    当然,仅当对象可变时才需要实现这个方法。

  • _delitem_(self, key) : 这个方法在对对象的组成部分使用 _del_ 语句时被调用,应删除与 key 相关联的值。
    同样,仅当对象可变(且允许其项被删除)时,才需要实现这个
    方法。

对于这些方法,还有一些额外的要求:

  • 对于序列,如果键为负整数,应从末尾往前数。换而言之, x[-n] 应与 x[len(x)-n] 等效。
  • 如果键的类型不合适(如对序列使用字符串键),可能引发 TypeError 异常。
  • 对于序列,如果索引的类型是正确的,但不在允许的范围内,应引发 IndexError 异常。

算术序列,其中任何两个相邻数字的差都相同。

从 list、dict 和 str 派生

在标准库中,模块 collections 提供了抽象和具体的基类,但你也可以继承内置类型。

如果要实现一种行为类似于内置列表的序列类型,可直接继承 list 。

一个简单的示例——一个带访问计数器的列表。

class CounterList(list):
	def __init__(self, *args):
		super().__init__(*args)
		self.counter = 0
	def __getitem__(self, index):
		self.counter += 1
		return super(CounterList, self).__getitem__(index)

CounterList 类深深地依赖于其超类( list )的行为。 CounterList 没有重写的方法(如 append 、 extend 、 index 等)都可直接使用。

在两个被重写的方法中,使用 super 来调用超类的相应方法,并添加了必要的行为:初始化属性 counter (在__init__ 中)和更新属性 counter (在__getitem__ 中)。

下面的示例演示了 CounterList 的可能用法:

>>> cl = CounterList(range(10))
>>> cl
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> cl.reverse()
>>> cl
[9, 8, 7, 6, 5, 4, 3, 2, 1, 0]
>>> del cl[3:6]
>>> cl
[9, 8, 7, 3, 2, 1, 0]
>>> cl.counter
0
>>> cl[4] + cl[2]
9
>>> cl.counter
2

它有一个 counter 属性(其初始值为0)。每当你访问列表元素时,这个属性的值都加1。

其他魔法方法

特性

存取方法,它们是名称类似于 getHeight 和 setHeight 的方法,用于获取或设置属性(这些属性可能是私有的)。

例如,看下面的 Rectangle 类:

class Rectangle:
	def __init__(self):
		self.width = 0
		self.height = 0
	def set_size(self, size):
		self.width, self.height = size
	def get_size(self):
		return self.width, self.height

如何使用这个类:

>>> r = Rectangle()
>>> r.width = 10
>>> r.height = 5
>>> r.get_size()
(10, 5)
>>> r.set_size((150, 100))
>>> r.width
150

get_size 和 set_size 是假想属性 size 的存取方法,这个属性是一个由 width 和 height 组成的元组。

Python能够替你隐藏存取方法,让所有的属性看起来都一样。
通过存取方法定义的属性通常称为特性(property)。

函数 property

函数 property 使用起来很简单。

如果你编写了一个类,如前面的 Rectangle 类,只需再添加一行代码。

class Rectangle:
	def __init__ (self):
		self.width = 0
		self.height = 0
	def set_size(self, size):
		self.width, self.height = size
	def get_size(self):
		return self.width, self.height
	size = property(get_size, set_size)

通过调用函数 property 并将存取方法作为参数(获取方法在前,设置方法在后)创建了一个特性,然后将名称 size 关联到这个特性。
这样,就能以同样的方式对待 width 、 height 和 size ,而无需关心它们是如何实现的。

实际上,调用函数 property 时,还可不指定参数、指定一个参数、指定三个参数或指定四个参数。
如果没有指定任何参数,创建的特性将既不可读也不可写。
如果只指定一个参数(获取方法),创建的特性将是只读的。
第三个参数是可选的,指定用于删除属性的方法(这个方法不接受任何参数)。
第四个参数也是可选的,指定一个文档字符串。这些参数分别名为 fget 、fset 、 fdel 和 doc 。
如果你要创建一个只可写且带文档字符串的特性,可使用它们作为关键字参数来实现。

静态方法和类方法

静态方法和类方法是这样创建的:将它们分别包装 staticmethod 和 classmethod 类的对象中。

静态方法的定义中没有参数 self ,可直接通过类来调用。
类方法的定义中包含类似于 self 的参数,通常被命名为 cls 。
对于类方法,也可通过对象直接调用,但参数 cls 将自动关联到类。

简单的示例:

class MyClass:

	def smeth():
		print('This is a static method')
	smeth = staticmethod(smeth)

	def cmeth(cls):
		print('This is a class method of', cls)
	cmeth = classmethod(cmeth)

在Python 2.4中,引入了一种名为装饰器的新语法,可用于像这样包装方法。(实际上,装饰器可用于包装任何可调用的对象,并且可用于方法和函数。)可指定一个或多个装饰器,为此可在方法(或函数)前面使用运算符 @ 列出这些装饰器(指定了多个装饰器时,应用的顺序与列出的顺序相反)。

class MyClass:

	@staticmethod
	def smeth():
		print('This is a static method')
	
	@classmethod
	def cmeth(cls):
		print('This is a class method of', cls)

定义这些方法后,就可像下面这样使用它们(无需实例化类):

>>> MyClass.smeth()
This is a static method
>>> MyClass.cmeth()
This is a class method of <class '__main__.MyClass'>

_getattr_、_setsttr_ 等方法

可以拦截对对象属性的所有访问企图,其用途之一是在旧式类中实现特性(在旧式类中,函数 property 的行为可能不符合预期)。
要在属性被访问时执行一段代码,必须使用一些魔法方法。

下面的四个魔法方法提供了你需要的所有功能(在旧式类中,只需使用后面三个)。

  • _getattribute_(self, name) : 在属性被访问时自动调用(只适用于新式类)。
  • _getattr_(self, name) : 在属性被访问而对象没有这样的属性时自动调用。
  • _setattr_(self, name, value) : 试图给属性赋值时自动调用。
  • _delattr_(self, name) : 试图删除属性时自动调用。

前面的Rectangle 示例,但这里使用的是魔法方法:

class Rectangle:
	def __init__ (self):
		self.width = 0
		self.height = 0
	def __setattr__(self, name, value):
		if name == 'size':
			self.width, self.height = value
		else:
			self. __dict__[name] = value
	def __getattr__(self, name):
		if name == 'size':
			return self.width, self.height
		else:
			raise AttributeError()

对于这个代码示例,需要注意如下两点。

  • 即便涉及的属性不是 size ,也将调用方法__setattr__ 。
  • 仅当没有找到指定的属性时,才会调用方法__getattr__ 。

迭代器

_iter_ ,它是迭代器协议的基础。

迭代器协议

迭代(iterate)意味着重复多次,就像循环那样。

使用 for 循环迭代过序列和字典,但实际上也可迭代其他对象:实现了方法__iter__ 的对象。

方法__iter__ 返回一个迭代器,它是包含方法__next__ 的对象,而调用这个方法时可不提供任何参数。

当你调用方法__next__ 时,迭代器应返回其下一个值。如果迭代器没有可供返回的值,应引发 StopIteration 异常。
你还可使用内置的便利函数 next ,在这种情况下, next(it) 与it._next_() 等效。

使用迭代器更通用、更简单、更优雅。

注意 
更正规的定义是,实现了方法 __iter__ 的对象是 可迭代的,
而实现了方法 __next__ 的对象是 迭代器。

从迭代器创建序列

除了对迭代器和可迭代对象进行迭代(通常这样做)之外,还可将它们转换为序列。
在可以使用序列的情况下,大多也可使用迭代器或可迭代对象(诸如索引和切片等操作除外)。

例子:使用构造函数 list 显式地将迭代器转换为列表。

>>> class TestIterator:
... value = 0
... def __next__(self):
... self.value += 1
... if self.value > 10: raise StopIteration
... return self.value
... def __iter__(self):
... return self
...
>>> ti = TestIterator()
>>> list(ti)
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

生成器

生成器也被称为简单生成器(simple generator)。

无论编写什么程序,都完全可以不使用生成器。

生成器是一种使用普通函数语法定义的迭代器。

创建生成器

生成器创建起来与函数一样简单。

创建一个将嵌套列表展开的函数。
这个函数将一个类似于下面的列表作为参数:

nested = [[1, 2], [3, 4], [5]]

这是一个列表的列表。
函数应按顺序提供这些数字,下面是一种解决方案:

def flatten(nested):
	for sublist in nested:
		for element in sublist:
			yield element

它首先迭代所提供嵌套列表中的所有子列表,然后按顺序迭代每个子列表的元素。

包含 yield 语句的函数都被称为生成器

生成器不是使用 return 返回一个值,而是可以生成多个值,每次一个。每次使用 yield 生成一个值后,函数都将冻结,即在此停止执行,等待被重新唤醒。被重新唤醒后,函数将从停止的地方开始继续执行。

为使用所有的值,可对生成器进行迭代。

>>> nested = [[1, 2], [3, 4], [5]]
>>> for num in flatten(nested):
... print(num)
...
1
2
3
4
5

>>> list(flatten(nested))
[1, 2, 3, 4, 5]

递归式生成器

处理任意层嵌套的列表,可使用递归。

def flatten(nested):
	try:
		for sublist in nested:
			for element in flatten(sublist):
				yield element
	except TypeError:
		yield nested

调用 flatten 时,有两种可能性(处理递归时都如此):基线条件和递归条件。
在基线条件下,要求这个函数展开单个元素(如一个数)。
在这种情况下, for 循环将引发 TypeError 异常(因为你试图迭代一个数),而这个生成器只生成一个元素。

如果要展开的是一个列表(或其他任何可迭代对象),就需要做些工作:
遍历所有的子列表(其中有些可能并不是列表)并对它们调用 flatten ,然后使用另一个 for 循环生成展开后的子列表中的所有元素。

>>> list(flatten([[[1], 2], 3, 4, [5, [6, 7]], 8]))
[1, 2, 3, 4, 5, 6, 7, 8]

然而,这个解决方案存在一个问题。如果 nested 是字符串或类似于字符串的对象,它就属于序列,因此不会引发 TypeError 异常,可你并不想对其进行迭代。

要处理这种问题,必须在生成器开头进行检查

def flatten(nested):
	try:
		# 不迭代类似于字符串的对象:
		try: nested + ''
		except TypeError: pass
		else: raise TypeError
		for sublist in nested:
		for element in flatten(sublist):
			yield element
	except TypeError:
		yield nested

这个版本也可用于字符串:

>>> list(flatten(['foo', ['bar', ['baz']]]))
['foo', 'bar', 'baz']

注意,这里没有执行类型检查:没有检查 nested 是否是字符串,而只是检查其行为是否类似于字符串,即能否与字符串拼接。
一种更自然的替代方案是,使用 isinstance 以及字符串和类似于字符串的对象的一些抽象超类,但遗憾的是没有这样的标准类。

通用生成器

生成器是包含关键字 yield 的函数,但被调用时不会执行函数体内的代码,而是返回一个迭代器。

每次请求值时,都将执行生成器的代码,直到遇到 yield 或 return 。
yield 意味着应生成一个值,而 return 意味着生成器应停止执行(即不再生成值;仅当在生成器调用 return 时,才能不提供任何参数)。

生成器由两个单独的部分组成:生成器的函数生成器的迭代器

生成器的函数是由 def 语句定义的,其中包含 yield 。生成器的迭代器是这个函数返回的结果。
这两个实体通常被视为一个,通称为生成器

生成器的方法

在生成器开始运行后,可使用生成器和外部之间的通信渠道向它提供值。
这个通信渠道包含如下两个端点。

  • 外部世界: 外部世界可访问生成器的方法 send ,这个方法类似于 next ,但接受一个参数(要发送的“消息”,可以是任何对象)。
  • 生成器: 在挂起的生成器内部, yield 可能用作表达式而不是语句

注意,仅当生成器被挂起(即遇到第一个 yield )后,使用 send (而不是 next )才有意义。
要在此之前向生成器提供信息,可使用生成器的函数的参数。

生成器还包含另外两个方法。

  • 方法 throw : 用于在生成器中( yield 表达式处)引发异常,调用时可提供一个异常类型、一个可选值和一个 traceback 对象。
  • 方法 close : 用于停止生成器,调用时无需提供任何参数。

模拟生成器

如果使用的是较老的Python版本,就无法使用生成器。

下面是一个简单的解决方案,让你能够使用普通函数模拟生成器。

首先,在函数体开头插入如下一行代码:

result = []

接下来,将类似于 yield some_expression 的代码行替换为如下代码行:

yield some_expression with this:
result.append(some_expression)

最后,在函数末尾添加如下代码行:

return result

使用这种方法并不能模拟所有的生成器,但可模拟大部分生成器。

这无法模拟无穷生成器,因为显然不能将这种生成器的值都存储到一个列表中。

下面使用普通函数重写了生成器 flatten :

def flatten(nested):
	result = []
	try:
		# 不迭代类似于字符串的对象:
		try: nested + ''
		except TypeError: pass
		else: raise TypeError
		for sublist in nested:
			for element in flatten(sublist):
				result.append(element)
	except TypeError:
		result.append(nested)
	return result

八皇后问题

生成器的回溯

对于需要尝试所有组合直到找到答案的问题,这种回溯策略对其解决很有帮助。
这种问题的解决方案类似于下面这样:

# 伪代码
for each possibility at level 1:
	for each possibility at level 2:
		...
			for each possibility at level n:
				is it viable?

要直接使用 for 循环来实现,必须知道有多少层。如果无法知道,可使用递归。

问题

计算机科学谜题:你需要将8个皇后放在棋盘上,条件是任何一个皇后都不能威胁其他皇后,即任何两个皇后都不能吃掉对方。怎样才能做到这一点呢?应将这些皇后放在什么地方呢?

这是一个典型的回溯问题:在棋盘的第一行尝试为第一个皇后选择一个位置,再在第二行尝试为第二个皇后选择一个位置,依次类推。在发现无法为一个皇后选择合适的位置后,回溯到前一个皇后,并尝试为它选择另一个位置。最后,要么尝试完所有的可能性,要么找到了答案。

状态表示

可简单地使用元组(或列表)来表示可能的解(或其一部分),其中每个元素表示相应行中皇后所在的位置(即列)。
因此,如果 state[0] == 3 ,就说明第1行的皇后放在第4列(还记得吧,我们从0开始计数)。在特定的递归层级(特定的行),你只知道上面各皇后的位置,因此状态元组的长度小于8(即皇后总数)。

检测冲突

先来做些简单的抽象。

要找出没有冲突(即任何一个皇后都吃不到其他皇后)的位置组合,首先必须定义冲突是什么。

函数 conflict 接受(用状态元组表示的)既有皇后的位置,并确定下一个皇后的位置是否会导致冲突。

def conflict(state, nextX):
	nextY = len(state)
	for i in range(nextY):
		if abs(state[i] - nextX) in (0, nextY - i):
			return True
	return False

参数 nextX 表示下一个皇后的水平位置(x坐标,即列),而 nextY 为下一个皇后的垂直位置(y坐标,即行)。

这个函数对既有的每个皇后执行简单的检查:如果下一个皇后与当前皇后的x坐标相同或在同一条对角线上,将发生冲突,因此返回 True ;如果没有发生冲突,就返回 False 。

表达式:

abs(state[i] - nextX) in (0, nextY - i)

如果下一个皇后和当前皇后的水平距离为0(在同一列)或与它们的垂直距离相等(位于一条对角线上),这个表达式就为真;否则为假。

基线条件

下面先来看基线条件:最后一个皇后。
对于这个皇后,你想如何处理呢?假设你想找出所有可能的解——给定其他皇后的位置,可将这个皇后放在什么位置(可能什么位置都不行)?

def queens(num, state):
	if len(state) == num-1:
		for pos in range(num):
			if not conflict(state, pos):
				yield pos

如果只剩下最后一个皇后没有放好,就遍历所有可能的位置,并返回那些不会引发冲突的位置。参数 num 为皇后总数,而参数 state 是一个元组,包含已放好的皇后的位置。

递归条件

处理好基线条件后,可在递归条件中假设来自更低层级(编号更大的皇后)的结果都是正确的。
因此,只需在函数 queens 的前述实现中给 if 语句添加一个 else 子句。

对于递归调用,向它提供的是由当前行上面的皇后位置组成的元组。
对于当前皇后的每个合法位置,递归调用返回的是由下面的皇后位置组成的元组。
为了让这个过程不断进行下去,只需将当前皇后的位置插入返回的结果开头,如下所示:

...
else:
	for pos in range(num):
		if not conflict(state, pos):
			for result in queens(num, state + (pos,)):
				yield (pos,) + result

另外,还可给参数指定默认值。

def queens(num=8, state=()):
	for pos in range(num):
		if not conflict(state, pos):
			if len(state) == num-1:
				yield (pos,)
			else:
				for result in queens(num, state + (pos,)):
					yield (pos,) + result

(pos,) 中的逗号必不可少(不能仅用圆括号将 pos 括起),这样得到的才是元组。

生成器 queens 提供了所有的解(即所有合法的皇后位置组合)。

>>> list(queens(3))
[]
>>> list(queens(4))
[(1, 3, 0, 2), (2, 0, 3, 1)]
>>> for solution in queens(8):
... print solution
...
(0, 4, 7, 5, 2, 6, 1, 3)
(0, 5, 7, 2, 6, 3, 1, 4)
...
(7, 2, 0, 5, 1, 4, 6, 3)
(7, 3, 0, 2, 5, 1, 6, 4)
>>>

如果运行 queens 时将参数 num 设置为8,将快速显示大量的解。

>>> len(list(queens(8)))
92

扫尾工作

在任何情况下,清晰的输出都是好事,因为这让查找bug等工作更容易。

def prettyprint(solution):
	def line(pos, length=len(solution)):
		return '. ' * (pos) + 'X ' + '. ' * (length-pos-1)
	for pos in solution:
		print(line(pos))

在 prettyprint 中创建了一个简单的辅助函数。

下面随机地选择一个解,并将其打印出来,以确定它是正
确的。

>>> import random
>>> prettyprint(random.choice(list(queens(8))))
. . . . . X . .
. X . . . . . .
. . . . . . X .
X . . . . . . .
. . . X . . . .
. . . . . . . X
. . . . X . . .
. . X . . . . .

在这里插入图片描述

小结

本章介绍的新函数
在这里插入图片描述
学习参考资料:

《Python基础教程》 第3版
;