Python基础复习---重点知识笔记(二)
文章目录
-
七、再谈抽象
-
-
7.1 对象魔法 ⭐🌙🔺
-
- 7.1.1 多态
- 7.1.2 封装
- 7.1.3 继承
-
7.2 类
-
- 7.2.1 类的创建
- 7.2.2 属性、函数、方法
- 7.2.3 隐藏--不能从外部访问⭐
- 7.2.4 类的命名空间
- 7.2.5 指定超类⭐
- 7.2.6 深入探讨继承
- 7.2.7 多个超类
- 7.2.8 抽象基类
-
-
八、魔法方法、特性和迭代器
-
-
8.1 构造函数⭐
-
- 8.1.1 重写特殊的构造函数
-
8.2 元素访问⭐🌙
-
8.3 特性
-
8.4 迭代器
-
8.5 生成器
-
- 8.5.1 递归式生成器
- 8.5.2 通用生成器
-
8.6 八皇后问题⭐
-
-
总结
七、再谈抽象
多态 :可对不同类型的对象执行相同的操作,而这些操作就像“被施了魔法”一样能够
正常运行。
封装 :对外部隐藏有关对象工作原理的细节。
继承 :可基于通用类创建出专用类。
7.1 对象魔法 ⭐🌙🔺
7.1.1 多态
“有多种形态” 。这大致意味着即便你不知道变量指向的是哪种对象,也能够对其执行操作,且操作的行为将随对象所属的类型(类)而异。
>>> object.get_price()
2.5
-
- 案例展示
标准库模块random包含一个名为
choice的函数,它从序列中随机选择一个元素 。
# 案例一 count()方法
>>> from random import choice
>>> x = choice(['Hello, world!', [1, 2, 'e', 'e', 4]])
>>> x.count('e')
2
# 案例二 加法运算夫
>>> 1 + 2
3
>>> 'Fish' + 'license'
'Fishlicense'
# 案例三 repr
def length_message(x):
print("The length of", repr(x), "is", len(x))
>>> length_message('Fnord')
The length of 'Fnord' is 5
>>> length_message([1, 2, 3])
The length of [1, 2, 3] is 3
7.1.2 封装
多态 让你无需知道对象所属的类(对象的类型)就能调用其方法
封装 让你无需知道对象的构造就能使用它。
- 问题引入
已知一个名为OpenObject的类
>>> o = OpenObject() # 对象就是这样创建的
>>> o.set_name('Sir Lancelot')
>>> o.get_name()
'Sir Lancelot'
当创建多个对象时,出现问题,因为他们公用同一个变量
>>> o1 = OpenObject()
>>> o2 = OpenObject()
>>> o1.set_name('Robin Hood')
>>> o2.get_name()
'Robin Hood'
如上代码所见,设置一个对象的名称时,将自动设置另一个对象的名称。
解决办法:将其作为一个属性即可 ,属性是归属于对象的变量,就像方法一样。
对象有自己的状态 。对象的状态由其属性(如名称)描述。对象的方法可能修改这些属性,因此对象将一系列函数(方法)组合起来,并赋予它们访问一些变量(属性)的权限,而属性可用于在两次函数调用之间存储值。
使用属性而非全局变量重新编写前面的类,并将其重命名为ClosedObject,就可像下面这样使用它:
>>> c = ClosedObject()
>>> c.set_name('Sir Lancelot')
>>> c.get_name()
'Sir Lancelot'
>>> r = ClosedObject()
>>> r.set_name('Sir Robin')
r.get_name()
'Sir Robin'
>>> c.get_name()
'Sir Lancelot'
7.1.3 继承
见7.2.5章节、7.2.6章节
7.2 类
7.2.1 类的创建
类:类型,一种对象。每个对象都属于特定的类,并被称为该类的实例。
例如:鸟就是鸟类的实例。
鸟类是一个非常通用(抽象)的类,它有多个子类:你看到的那只鸟可能属于子类“云雀”。你可将“鸟类”视为由所有鸟组成的集合,而“云雀”是其一个子集。一个类的对象为另一个类的对象的子集时,前者就是后者的子类。因此“云雀”为“鸟类”的子类,而“鸟类”为“云雀”的超类。
类的所有实例都有该类的所有方法,因此子类的所有实例都有超类的所有方法
- 定义一个类:Person
class Person:
def set_name(self, name):
self.name = name
def get_name(self):
return self.name
def greet(self):
print("Hello, world! I'm {}.".format(self.name))
class语句创建独立的命名空间,用于在其中定义函数
>>> foo = Person()
>>> bar = Person()
>>> foo.set_name('Luke Skywalker')
>>> bar.set_name('Anakin Skywalker')
>>> foo.greet()
Hello, world! I'm Luke Skywalker.
>>> bar.greet()
Hello, world! I'm Anakin Skywalker.
对foo调用set_name和greet时,foo都会作为
第一个参数自动 传递给它们。我将这个参数命名为self,这非常贴切。实际上,可以随便给这个参数命名,但鉴于它总是指向对象本身,因此习惯上将其命名为self。
self很有用,甚至必不可少。如果没有它,所有的方法都无法访问对象本身——要操作的属性所属的对象
# 外部访问属性
>>> foo.name
'Luke Skywalker'
>>> bar.name = 'Yoda'
>>> bar.greet()
Hello, world! I'm Yoda.
7.2.2 属性、函数、方法
- 实例展示
# 案例一
>>> class Class:
... def method(self):
... print('I have a self!')
...
>>> def function():
... print("I don't...")
...
>>> instance = Class()
>>> instance.method()
I have a self!
>>> instance.method = function
>>> instance.method()
I don't...
# 案例二
>>> class Bird:
... song = 'Squaawk!'
... def sing(self):
... print(self.song)
...
>>> bird = Bird()
>>> bird.sing()
Squaawk!
>>> birdsong = bird.sing
>>> birdsong()
Squaawk!
7.2.3 隐藏–不能从外部访问⭐
解释何为:不能从外部访问?
>>> c.name
'Sir Lancelot'
>>> c.name = 'Sir Gumby'
>>> c.get_name()
'Sir Gumby'
毕竟,如果能直接访问ClosedObject(对象c所属的类)的属性name,就不需要创建方法setName和getName了。
例如,ClosedObject可能在对象修改其名称时向管理员发送电子邮件。这种功能可能包含在方法set_name中 。但如果直接设置c.name ,结果将如何呢?什么都不会发生——根本不会发送电子邮件。为避免这类问题,可将属性定义为私有。
私有属性不能从对象外部访问,而只能通过存取器方法(如get_name和set_name)来访问。
class Secretive:
def __inaccessible(self):
print("Bet you can't see me ...")
def accessible(self):
print("The secret message is:")
# 现在从外部不能访问__inaccessible,但在类中(如accessible中)依然可以使用它。
self.__inaccessible()
>>> s = Secretive()
>>> s.__inaccessible()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: Secretive instance has no attribute '__inaccessible'
>>> s.accessible()
The secret message is:
Bet you can't see me ...
7.2.4 类的命名空间
在class语句中定义的代码都是在一个特殊的命名空间(
类的命名空间)内执行的,而类的所有成员都可访问这个命名空间。类定义其实就是要执行的代码段。
class MemberCounter:
members = 0
def init(self):
MemberCounter.members += 1
>>> m1 = MemberCounter()
>>> m1.init()
>>> MemberCounter.members
1
>>> m2 = MemberCounter()
>>> m2.init()
>>> MemberCounter.members
2
>>> m1.members
2
>>> m2.members
2
>>> m1.members = 'Two'
>>> m1.members
'Two'
>>> m2.members
2
7.2.5 指定超类⭐
子类扩展了超类的定义。要指定超类,可在class语句中的
类名后加上超类名,并将其用圆括号括起。
# Filter 是一个过滤序列的通用类 实际上,它不会过滤掉任何东西
class Filter:
def init(self):
self.blocked = []
def filter(self, sequence):
return [x for x in sequence if x not in self.blocked] # 返回的是列表
class SPAMFilter(Filter): # SPAMFilter是Filter的子类
def init(self): # 重写超类Filter的方法init
self.blocked = ['SPAM']
>>> f = Filter()
>>> f.init()
>>> f.filter([1, 2, 3])
[1, 2, 3]
Filter类的用途在于可用作其他类(将’SPAM’从序列中过滤掉的SPAMFilter类)的基类(超类)。
>>> s = SPAMFilter()
>>> s.init()
>>> s.filter(['SPAM', 'SPAM', 'SPAM', 'SPAM', 'eggs', 'bacon', 'SPAM'])
['eggs', 'bacon']
请注意SPAMFilter类的定义中有两个要点。
- 以提供新定义的方式
重写了Filter类中方法init的定义。- 直接从Filter类
继承了方法filter的定义,因此无需重新编写其定义。
第二点说明了继承很有用的原因:可以创建大量不同的过滤器类,它们都从Filter类派生而来,并且都使用已编写好的方法filter。这就是懒惰的好处。
7.2.6 深入探讨继承
- 内置方法 issubclass
要确定一个类是否是另一个类的子类,可使用内置方法
issubclass。
>>> issubclass(SPAMFilter, Filter)
True
>>> issubclass(Filter, SPAMFilter)
False
如果你有一个类,并想知道它的基类 ,可访问其特殊属性
__bases__。
# 类SPAMFilter的基类是Filter
>>> SPAMFilter.__bases__
(<class __main__.Filter at 0x171e40>,)# FIlter
>>> Filter.__bases__
(<class 'object'>,)
- 使用isinstance 判断是否是实例
>>> s = SPAMFilter()
>>> isinstance(s, SPAMFilter)
True
>>> isinstance(s, Filter)
True
>>> isinstance(s, str)
False
使用isinstance通常不是良好的做法,依赖多态在任何情况下都是更好的选择。一个重要的例外情况是使用抽象基类和模块abc时。
- class
如果你要
获悉对象属于哪个类,可使用属性
>>> s.__class__
<class __main__.SPAMFilter at 0x1707c0>
# 对象s属于类SPAMFilter
还可使用
type(s)来获悉其所属的类
7.2.7 多个超类
- 多重继承
class Calculator:
def calculate(self, expression):
self.value = eval(expression)
class Talker:
def talk(self):
print('Hi, my value is', self.value)
class TalkingCalculator(Calculator, Talker):
pass
>>> tc = TalkingCalculator()
>>> tc.calculate('1 + 2 * 3')
>>> tc.talk()
Hi, my value is 7
eval() 函数用来执行一个字符串表达式,并返回表达式的值。
>>>x = 7
>>> eval( '3 * x' )
21
>>> eval('pow(2,2)')
4
>>> eval('2 + 2')
4
>>> n=81
>>> eval("n + 4")
85
注意:应避免使用多重继承,因为在有些情况下,它可能带来意外的“并发症”。
如果多个超类以不同的方式实现了同一个方法(即有多个同名方法),必须在class语句中小心排列这些超类,因为位于前面的类的方法将覆盖位于后面的类的方法 。
在前面的示例中,如果Calculator类包含方法talk,那么这个方法将覆盖Talker类的方法talk(导致它不可访问 )
如下反转超类的排列顺序:导致Talker的方法talk是可以访问的。
class TalkingCalculator(Talker, Calculator):
pass
7.2.8 抽象基类
一般而言,抽象类是不能(至少是不应该)实例化的类,其职责是定义子类应实现的一组抽象方法
from abc import ABC, abstractmethod
class Talker(ABC):
@abstractmethod
def talk(self):
pass
使用@abstractmethod来将方法标记为抽象的——在子类中必须实现的方法。
抽象类(即包含抽象方法的类)最重要的特征是不能实例化
若派生出的子类没有重写抽象方法,则该类也是抽象的,不能被实例化。
- 实例展示
>>> Talker()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: Can't instantiate abstract class Talker with abstract methods talk
# 翻译:无法用抽象方法talk实例化抽象类Talker
# instantiate :例示;用具体例子说明
# 派生出子类 该类也是抽象的,无法实例化--->因为没有重写talk()方法,不能实例化
class Knigget(Talker):
pass
# 重写talk()方法 让其可以实例化
class Knigget(Talker):
def talk(self):
print("Ni!")
- 注册机制
已知有一个类Herring,假设该类是从他人的模块中导入的。这样无法通过派生的方法。
解决问题:可将Herring注册为Talker(而不从Herring和Talker派生出子类),这样所有Herring对象都将被视为Talker对象。
class Herring:
def talk(self):
print("Blub.")
>>> h = Herring()
>>> isinstance(h, Talker)
False
>>> Talker.register(Herring)# 注册机制
<class '__main__.Herring'>
>>> isinstance(h, Talker)
True
>>> issubclass(Herring, Talker)
True
>>> class Clam:
... pass
...
>>> Talker.register(Clam)
<class '__main__.Clam'>
>>> issubclass(Clam, Talker)
True
>>> c = Clam()
>>> isinstance(c, Talker)
True
# 缺点:直接从抽象类派生提供的保障没有了
>>> c.talk()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'Clam' object has no attribute 'talk'
八、魔法方法、特性和迭代器
8.1 构造函数⭐
其实就是本书前面一些示例中使用的初始化方法,只是命名为
__init__
构造函数不同于 普通方法的地方在于,将在对象创建后自动调用它们
class FooBar:
def __init__(self):
self.somevar = 42
>>> f = FooBar() # 创建实例时,自动调用__init__
>>> f.somevar
42
8.1.1 重写特殊的构造函数
重写构造函数时,必须调用超类(继承的类)的构造函数,否则可能无法正确地初始化对象。
- 问题引入
# 超类
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):
self.sound = 'Squawk!'
def sing(self):
print(self.sound)
-----------------------------------------------------
# 调用
>>> b = Bird()
>>> b.eat()
Aaaah ...
>>> b.eat()
No, thanks!
>>> sb = SongBird()
>>> sb.sing()
Squawk!
>>> 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中重写了构造函数 ,但新的构造函数没有包含任何初始化属性hungry的代码。要消除这种错误,SongBird的构造函数必须调用其超类(Bird)的构造函数,以确保基本的初始化得以执行。
- 方案一:调用未关联的超类构造函数
class SongBird(Bird):
def __init__(self):
Bird.__init__(self)# 增加的一行
self.sound = 'Squawk!'
def sing(self):
print(self.sound)
# 调用
>>> sb = SongBird()
>>> sb.sing()
Squawk!
>>> sb.eat()
Aaaah ...
>>> sb.eat()
No, thanks!
对实例调用方法时,方法的参数self将自动关联到实例(称为
关联的方法)。
如果你通过类调用方法(如Bird.init ),就没有实例与其相关联。在这种情况下,你可随便设置参数self。这样的方法称为未关联的。
通过将这个未关联方法的self参数设置为当前实例,将使用超类的构造函数来初始化SongBird对象 。这意味着将设置其属性hungry。
- 方案二:使用函数super()
调用这个函数时,将当前类和当前实例作为参数。对其返回的对象调用方法时,调用的将是超类(而不是当前类)的方法
在Python 3中调用函数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!
即便有多个超类,也只需调用函数super一次(条件是所有超类的构造函数也使用函数super)

8.2 元素访问⭐🌙
在Python中,协议通常指的是规范行为的规则 ,有点类似于第7章提及的接口。**协议指定应实现哪些方法以及这些方法应做什么。**在Python中,多态仅仅基于对象的行为(而不基于祖先,如属于哪个类或其超类等) ,因此这个概念很重要:其他的语言可能要求对象属于特定的类或实现了特定的接口,而Python通常只要求对象遵循特定的协议。因此,要成为序列,只需遵循序列协议即可 。

- 创建无序序列
def check_index(key):
"""
指定的键是否是可接受的索引?
键必须是非负整数,才是可接受的。如果不是整数,
将引发TypeError异常;如果是负数,将引发Index
Error异常(因为这个序列的长度是无穷的)
"""
if not isinstance(key, int):
raise TypeError
if key < 0:
raise IndexError
class ArithmeticSequence:
def __init__(self, start=0, step=1):
"""
初始化这个算术序列
start -序列中的第一个值
step -两个相邻值的差
changed -一个字典,包含用户修改后的值
"""
self.start = start # 存储起始值
self.step = step # 存储步长值
self.changed = {} # 没有任何元素被修改
def __getitem__(self, key):
"""
从算术序列中获取一个元素
"""
check_index(key)
try:
return self.changed[key] # 修改过?
except KeyError: # 如果没有修改过,
return self.start + key * self.step # 就计算元素的值
def __setitem__(self, key, value):
"""
修改算术序列中的元素
"""
check_index(key)
self.changed[key] = value # 存储修改后的值
# 调用
>>> s = ArithmeticSequence(1, 2)
>>> s[4]
9
>>> s[4] = 2
>>> s[4]
2
>>> s[5]
11
# 请注意,我要禁止删除元素,因此没有实现__del__:
>>> del s[4]
Traceback (most recent call last):
File "<stdin>", line 1, in ?
AttributeError: ArithmeticSequence instance has no attribute '__delitem__'
# 另外,这个类没有方法__len__,因为其长度是无穷的。如果所使用索引的类型非法,将引发TypeError异常;如果索引的类型正确,但不在允许的范围内(即为负数),将引发IndexError异常。
>>> s["four"]
Traceback (most recent call last):
File "<stdin>", line 1, in ?
File "arithseq.py", line 31, in __getitem__
check_index(key)
File "arithseq.py", line 10, in checkIndex
if not isinstance(key, int): raise TypeError
TypeError
>>> s[-42]
Traceback (most recent call last):
File "<stdin>", line 1, in ?
File "arithseq.py", line 31, in __getitem__
check_index(key)
File "arithseq.py", line 11, in checkIndex
if key < 0: raise IndexError
IndexError
8.3 特性
Python能够替你隐藏存取方法,让所有的属性看起来都一样。通过存取方法定义的属性通常称为特性(property)。
- 案例展示
# 代码一
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
--------------------------------------
# 代码二
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)# 重点
# 调用
>>> r = Rectangle()
>>> r.width = 10
>>> r.height = 5
>>> r.size
(10, 5)
>>> r.size = 150, 100
>>> r.width
150
通过调用函数property并将存取方法作为参数(
获取方法在前,设置方法在后)创建了一个特性,然后将名称size 关联到这个特性。这样,你就能以同样的方式对待width、height和size,而无需关心它们是如何实现的。
8.4 迭代器
- “列表”为斐波那契数列,表示该数列的迭代器如下:
class Fibs:
def __init__(self):
self.a = 0
self.b = 1
def __next__(self):
self.a, self.b = self.b, self.a + self.b
return self.a
def __iter__(self):
return self
这个迭代器实现了方法__iter__,而这个方法返回迭代器本身。推荐在迭代器中也实现方法__iter__(并像刚才那样让它返回self),这样迭代器就可直接用于for循环中。
注意 :更正规的定义是,实现了方法__iter__的对象是可迭代的,而实现了方法__next__的对象是迭代器。
# 首先,创建一个Fibs对象。
>>> fibs = Fibs()
# 然后就可在for循环中使用这个对象,如找出第一个大于1000的斐波那契数。
>>> for f in fibs:
... if f > 1000:
... print(f)
... break
...
1597
# 这个循环之所以会停止,是因为其中包含break语句;否则,这个for循环将没完没了地执行。
- 迭代器创建序列
>>> 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]
8.5 生成器
首先迭代所提供嵌套列表中的所有子列表 ,然后按顺序迭代每个子列表的元素。
包含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]
8.5.1 递归式生成器
- 不知道多少层嵌套时,使用递归
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]
在函数flatten中,不应该对类似于字符串的对象进行迭代,主要原因有两个。首先,你想将类似于字符串的对象视为原子值,而不是应该展开的序列。其次,对这样的对象进行迭代会导致无穷递归,因为字符串的第一个元素是一个长度为1的字符串,而长度为1的字符串的第一个元素是字符串本身!
处理这种问题,必须在生成器开头进行检查。要检查对象是否类似于字符串,最简单、最快捷的方式是,尝试将对象与一个字符串拼接起来,并检查这是否会引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
print(list(flatten(['foo', ['bar', ['baz']]])))
# 调用
>>> list(flatten(['foo', ['bar', ['baz']]]))
['foo', 'bar', 'baz']
8.5.2 通用生成器
生成器是包含关键字
yield的函数,但被调用时不会执行函数体内的代码,而是返回一个迭代器
yield意味着应生成一个值,而return意味着生成器应停止执行(即不再生成值;仅当在生成器调用return时,才能不提供任何参数)
生成器由两个单独的部分组成:生成器的函数 和生成器的迭代器 ,生成器的函数是由def语句定义的,其中包含yield。生成器的迭代器是这个函数返回的结果。这两个实体通常被视为一个,通称为生成器。
>>> def simple_generator():
yield 1
...
>>> simple_generator
<function simple_generator at 153b44>
>>> simple_generator()
<generator object at 1510b0>
8.6 八皇后问题⭐
- 问题引入
你需要将8个皇后放在棋盘上,条件是任何一个皇后都不能威胁其他皇后,即任何两个皇后都不能吃掉对方。[任意两个皇后都不能处于同一行、同一列或同一斜线上]
- 思路解析
在棋盘的第一行尝试为第一个皇后选择一个位置,再在第二行尝试为第二个皇后选择一个位置,依次类推。在发现无法为一个皇后选择合适的位置后,回溯到前一个皇后,并尝试为它选择另一个位置。最后,要么尝试完所有的可能性,要么找到了答案。
- 状态表示
可简单地使用元组(或列表)来表示可能的解(或其一部分),其中每个元素表示相应行中皇后所在的位置(即列)。如果state[0] == 3,就说明第1行的皇后放在第4列(还记得吧,我们从0开始计数) 。
完全可以使用列表(而不是元组)来表示状态,具体使用哪个完全取决于你的喜好。一般而言,如果序列较小且是静态的,使用元组可能是不错的选择
状态元组state的长度小于8(即皇后总数)
- 检测冲突
要找出没有冲突(即任何一个皇后都吃不到其他皇后)的位置组合,首先必须定义冲突 :
- 如果下一个皇后与当前皇后的x坐标相同或在同一条对角线上,将发生冲突,因此返回True;如果没有发生冲突,就返回False。
# nextX表示下一个皇后的水平位置(x坐标,即列)
# nextY为下一个皇后的垂直位置(y坐标,即行)
# 状态元组state的长度小于8(即皇后总数)
组的长度小于8(即皇后总数)。
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
'''
abs(state[i] - nextX) in (0, nextY - i) 解释:
state[0] = 3:皇后的位置第一行第四列
如果差值等于0,两个皇后在同一列, 则代表冲突, 返回True;
如果列的差值等与行的差, 两个皇后在对角线上, 则代表冲突, 返回True;
'''
- 基线条件
最后一个皇后。对于这个皇后,你想如何处理呢?
def queens(num, state):
if len(state) == num-1:
for pos in range(num):
if not conflict(state, pos):
yield pos
这段代码的意思是,如果只剩下最后一个皇后没有放好,就遍历所有可能的位置,并返回那些不会引发冲突的位置。
- 参数num为皇后总数
- 参数state是一个元组,包含已放好的皇后的位置。
-
- 示例如下:
>>> list(queens(4, (1, 3, 0)))
[2]
在这个示例中,只有一个位置符合条件
- 递归条件
对于递归调用,向它提供的是由当前行上面的皇后位置组成的元组。对于当前皇后的每个合法位置,递归调用返回的是由下面的皇后位置组成的元组。为了让这个过程不断进行下去,只需将当前皇后的位置插入返回的结果开头,
def queens(num=8, state=()):
"""
采用生成器的方式来产生每一个皇后的位置,并用递归来实现下一个皇后的位置。
num: 皇后的数量
state: 标记已经排好的每个皇后的位置
"""
# # 八皇后的数量N=0, 1, 2, 3, 4, 5, 6 , 7 你要在哪一列放置皇后
for pos in range(num):
# 如果不冲突,则递归构造棋盘
if not conflict(state, pos):
# # 如果棋盘状态state已经等于num-1,即到达倒数第二行,而这时最后一行皇后又没冲突,直接yield,打出其位置(pos, )
if len(state) == num-1:
yield (pos,)
else:
for result in queens(num, state + (pos,)):
yield (pos,) + result
# 对于 state + (pos,): state是状态元组
>>> (1,3,0)+(2,)
(1, 3, 0, 2)
- 棋盘展示
def prettyprint(solution):
"""
友好展示: 为了直观表现棋盘,用X表示每个皇后的位置
"""
def line(pos, length=len(solution)):
return '. ' * (pos) + 'X ' + '. ' * (length-pos-1)
for pos in solution:
print(line(pos))
-
- 测试数据
>>> 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,
-
- 结果展示
>>> import random
>>> prettyprint(random.choice(list(queens(8))))
. . . . . X . .
. X . . . . . .
. . . . . . X .
X . . . . . . .
. . . X . . . .
. . . . . . . X
. . . . X . . .
. . X . . . . .
random.choice(seq):
从非空序列 中随机选取一个数据并带回,该序列可以是list、tuple、str、set。
如果序列为空,则弹出IndexError错误。
>>> len(list(queens(8)))
92
# random.choice(seq)则会从92个数据中随机选择一个待会进行棋盘打印展示

