魔法方法
魔法方法(Magic methods)在Python中是一类特殊的方法,它们的形式就是前后有两个下划线,例如__init__
这个方法,这个方法是我们最常见的魔法方法之一。
构造函数
构造函数(constructor)是Python中的一个魔法方法,例如我们在创建某个类的时候使用的初始化方法__init__
。构造函数
不同的于普通方法的地方在于,对象创建后会自动调用魔法方法,例如常规的方法使用如下:
|
|
但是构造函数的使用如下所示:
|
|
从上面可以看出,构造函数不用初始化,只要创建这个对象,它自己就自动初始化了,来看一个简单的案例:
|
|
运行结果如下所示:
|
|
从上面的结果可以看出来,在创建了f
这个对象后,我们并没有给f.somevar
添加属性,但是在__init__
中我们已经初始化了这个属性,因此不用再次定义属性。
再给这个构造函数添加几个参数,如下所示:
|
|
运行结果如下所示:
|
|
从这个案例中可以知道,如果指定了参数,那么最终属性显示的结果就是添加的指定的参数。
重写普通方法和特殊的构造函数
在面向对象的编程中,每个类都有一个或多个超类,并从它们那里继承行为,对类B
的实例调用方法或访问其属性时,如果找不到该方法或属性,将在其超类A
中查找,可以看下面的案例:
|
|
运行结果如下所示:
|
|
在这个案例中,我们可以看到,由于类B
自己没有定义方法hello
,因此对其调用方法hello
时,打印的消息是Hello, I'm A.
。要在子类中添加功能,一种基本方法是添加方法,但是如果你想写超类的某些方法,以定制继承而来的行为。还以上面的案例为例说明一下,类B
可以重写方法hello
,如下述修改后的类B
定义如下所示:
|
|
运行结果如下所示:
|
|
重写是继承机制的一个重要方法,对构造函数来说万为重要,构造函数用于初始化新建对象的状态,而对于大多数子类来说,除超类的初始化代码外,还要有自己的初始化值代码。虽然所有方法的重写机制都相同,但与重写普通方法相比,重写构造函数时更有可能遇到一个特别的问题:重写构造函数时,必须调用超类(继承的类)的构建函数,否则无法正确地初始化对象,看下面的案例:
|
|
运行结果如下所示:
|
|
从这个案例中我们可以知道,初始情况下,鸟b
处于饥饿状态(hungry = True
),第一次b.eat()
后,输出的是Aaaah ...
, 此时并把hungry = True
切换为hungry = False
,当第二次b.eat()
后,就输出了No, thanks !
。
再看下面的案例:
|
|
运行结果如下所示:
|
|
在这个案例中,SongBird
是Bird
的子类,继承了方法est
,但如果你要尝试调用它,就出现了问题。
异常提示,SongBird
没有属性hungry
,这是因为,在SongBird
类中重写了构造函数,但新的构造函数中没有包含任何初始化属性hungry
的代码。如果要消除这种错误,SongBird
的构造函数必须调用其超类Bird
的构造函数,以确保基本的初始化得以执行。
为此,有两种方法:
第一,调用未关联的超类构造函数;
第二,使用函数super
。
调用未关联的超类构建函数
还以上面的案例为例说明一下,如果要调用未关联的超类方法,就要添加一行Bird.__init__(self)
,完整代码如下所示:
|
|
运行结果如下所示:
|
|
在常规的面向对象编程中,对实例调用方法时
,方法的参数self
将自动关联到实例(称为关联的方法)。
如果通过类调用方法
(例如Bird.__init__
),就没有实例与其相关联,在这种情况下,可以随便设置参数self
,这样的方法称为未关联
。参过将这个未关联方法的self
参数设置为当前实例,将使用超类的构造函数来初始化singbird
对象,这表明将设置其属性hungry
。
使用super()
函数
这一种方法只适用Python3。调用super
这个函数时,将当前类和当前实例作为参数。对其返回的对象调用方法时,调用的是将是超类(而不是当前类)的方法,因此在singbird
的构造函数中,可以不使用Bird
而是使用了super(singbird, self)
,另外,可以像常规调用方法那样(也就是调用关联的方法那样)调用方法__init__
。在Python3中调用函数super
,可以不提供任何参数,如下所示:
|
|
运行结果如下所示:
|
|
元素访问
这一部分是其他的魔法方法。
基本的序列和映射协议
序列和映射基本上是元素(item)
的集合,要实现它们的基本行为(协议),不可变对象需要实现2个方法,而可变对象要实现4个:
__len__(self)
:这个方法返回集合包含的项数,对序列来说就是元素个数,对映射来说,就是键-值
对。如果__len__(self)
返回零(且没有实现覆盖这种行为的__nonzero__
),对象在布尔上下文中将被视为假(就像空的列表、元组、字符串和字典一样)。__getitem__(self, key)
:返回与指定键相关联的值。对序列来说,键应该是0~n-1
的整数(也可以是负数),其中n
为序列的长度,对映射来说,键可以是任何类型。如果键为负责,应从末尾往前数,也就是说x[-n]
应与x[len(x)-n]
等效。__setitem__(self, key8, value)
:这个方法应以与键相关联的方式存储值,以便以后能够使用__getitem__
来获取。__delitem__(self, key)
:这个方法在对对象的组成部分使用__del__
语句时被调用,应删除与key相关联的值,同样,仅当对象可变(且允许其值被删除)时,才需要实现这个方法。
另外,如果键的类型不合适(如对序列使用字符串键),可能引发TypeError
异常。对于序列,如果索引的类型是正确的,但不在允许的范围内,会引发IndexError
异常。
下面看一个案例,观察一下能否创建一个无穷序列,代码如下所示:
|
|
运行结果如下所示:
|
|
这些代码实现的是一个算术序列
,其中任何两个相邻数字的差都相同,第一值是由构造函数的参数start
(默认为0)指定的,而相邻值之间的差是由参数step
(默认为1)指定的。允许允许修改某些元素,这是通过将不符合规则的值保存在字典changed
中实现的。如果元素未被修改,就用公式self.start + key*self.step
来计算它的值。
这段代码中禁止删除元素,因此没有实现__del__
,如下所示:
|
|
另外这个类中没有方法__len__
,因此其长度是无穷的,如果所使用考虑综的类型非法,将引发TypeError异常,如果索引的类型正确,但不在允许的范围内(即为负数),将引发IndexError异常,其中索引的检查是源代码中的check_index
这个函数负责的,如果出现索引错误,就是如下所示:
|
|
从list、dict和str派生
关于序列/映射的一些方法可以还从已有方法中继承,在标准库collections
这个模块中提供了抽象和具体的基类,用户也能继承内置类型。因此如果要实现一种行为类似于内置列表的序列类型,可以直接继承list,在下面这个案例中,来实现一个带访问计数器的列表,如下所示:
|
|
在上面的这段代码中,CounterList
类依赖于其超类List
的行为。CounterList
没有重写的方法(如append
、extend
,index
等)都可以直接使用。在两个被重写的方法中,使用super
来调用超类的相应方法,并添加了必要的行为:初始化属性counter
(在__init__
中)和更新属性counter
(在__getitem__
)。需要注意的是,重写__getitem__
并不能保证一定会捕捉用户的访问操作,因为还有其他访问列表内容的方法,例如通过方法pop
,下面来演示一下CounterList
的可能用法,如下所示:
|
|
运行结果如下所示:
|
|
在上面的案例中,CounterList
的行为在大多数方面的都类型于列表,但它有一个counter
属性(其初始值为0),每当访问列表元素时,这个属性的值就都加1,执行加法运算cl[4] + cl[2]
后,counter
的值递增了2次,变成了2。
特性
存取方法
是名称类似于getHeight
和setHeight
的方法,用于获取或设置属性,如果访问给定属性时,必须采取特定的措施,那么像这样的封装状态变量(属性)很重要,例如看下面的案例:
|
|
运行结果如下所示:
|
|
get_size
和set_size
是假想属性size
的存取方法,这个属性是一个由width
和height
组成的元组,这些代码并非完全错误,但存在缺陷。使用这个类时,用户无需关心它是如何被封装的。如果用户想要修改实现,让size成为真正的属性,而width和height是动态计算出来的,就需要提供用于访问width和height的存取方法,使用这个类的程序也必须重写。应当客户代码能够以同样的方式对待所有的属性。
那么如何解决这个问题呢?给所有的属性都提供存取方法并不现实。而Python能够隐藏存取方法,让所有的属性看起来都一样,通过存取方法定义的属性通常称为特性(property)
。在Python中有两种创建特性的机制,但Python中只用property
函数。
property
函数
以前面的Rectangle
类为例说明一下,在使用property
函数时,只需要添加一行代码,即size = property(get_size, set_size)
即可,完整代码如下所示:
|
|
在上面的这段代码中,通过调用property
函数,将存取方法作为参数(获取方法
在前,设置方法
在后)创建了一个特性,然后将名称size
关联到这个特性。这样就能以同样的方式对待width、height和size了,而无需关心它们是如何实现的,如下所示:
|
|
从运行结果中可以看到,属性size
依然受制于get_size
和set_size
执行的计算,但是看起来就像普通属性一样。
事实上,调用函数property
时,还可以①不指定参数;②指定一个参数;③指定三个参数或四个参数。
如果没有指定任何参数,创建的特性将既不可读,也不可写。如果只指定一个参数(获取方法),创建的特性将是只读的。第三个参数是可选的,指定用于删除属性的方法(这个方法不接受任何参数)。第四个参数也是可选的,指定一个文档字符串,这些参数分别命名为fget
、fset
、fdel
和doc
。如果要创建一个只可写且带文档字符串的特性,可使用它们作为关键字参数。
property
函数的工作原理
property
其实并不一个函数,而是一个类,它的实例包含一些魔法方法,而所有的魔法都是由这些方法完成的。这些魔法方法为__get__
,__set__
和__delete__
,它们一道定义了所谓的描述符协议。只要对象实现了这些方法中的任何一个,它就是一个描述符。描述符的独特之处在于其访问方式。例如,读取属性(具体来说,是在实例中访问类中定义的属性)时,如果它关联的是一个实现了__get__
的对象,将不会返回这个对象,而是调用方法__get__
并将其结果返回。实际上,这是隐藏在特性、关联的方法、静态方法和类方法。
静态方法和类方法
静态方法和类方法是这样创建的:将它们分别包装在staticmethod
和classmethod
类的对象中。静态方法的定义中没有参数self
,可直接通过类来调用。类方法的定义中包含类似于self的参数,通常被命令为cls,对于类方法,也可通过对象直接调用,但参数cls将自动关联到类。
看下面的一个简单案例:
|
|
像这样手工包装和替换方法有点繁琐。在Python2中,引入了一种名为装饰器
的新语法,可用于像这样包装方法(实际上,装饰器可用于包装任何可调用的对象,并且可用于方法和函数)。要指定一个或多个装饰器,为此可在方法(或函数)前面使用运算符@
列出这些装饰器(指定了多少装饰器时,应用的顺序与列出的顺序相反),如下所示:
|
|
运行结果如下所示:
|
|
__getattr__
、__setattr__
等方法
可以拦截对象属性的所有访问企图、其用途之一是在旧式类中实现特性(在旧类中,函数property
的行为可能不符合预期)。要在属性被访问时执行一段代码,必须使用一些魔法方法,以下的4个魔法方法提供了你需要的所有功能:
__getattribute__(self, name)
:在属性被访问时自动调用;__getattr__(self, name)
:在属性被访问而对象没有这样的属性时自动调用;__setattr__(self, name, value)
:试图给属性赋值时自动调用;__delattr__(self, name)
:试图删除属性时自动调用。
相比函数property
,这些魔法方法使用起来比较麻烦。但它们很有用,因为用户可以在这些方法中编写处理多个特性的代码。然而在可能的情况下,最好还是首选property
函数。
还看一下前面的Rectangle
的案例,如下所示:
|
|
如上面代码所示,这个版本需要处理额外的管理细节,对于这段代码,需要注意以下两点:
- 即使涉及的属性不是
size
,也将调用方法__setattr__
。因此这个方法必须考虑如下两种情形:如果涉及的属性为size
,就执行与以前一样的操作;否则就使用魔法属性__dict__
。__dict__
属性是一个字典,其中包含所有的实例属性。使用它们而不是执行常规属性赋值,是因为旨在避免再次调用__setattr__
,进而导致无限循环。 - 仅当没有找到指定的属性时,才会调用方法
__getattr__
。这意味着如果指定的名称不是size
,这个方法将引发AttributeError
异常。这在要让类能够正确地支持__hasattr__
和__getattr__
等内置函数时非常重要。如果指定的名称为size
,就使用前一个实现中的表达式。
需要注意的是:编写方法__setattr__
时需要避开无限循环陷阱,编写__getattribute__
时也是如此。由于它拦截对所有属性的访问,因此将拦截对__dict__
的访问。在__getattribute__
中访问当前实例的属性时,唯一案例的方式是使用父类的方法__getattribute__
(使用super
)。
迭代器
有关迭代器的魔法方法是__iter__
,它是迭代器协议的基础。
迭代器协议
迭代(iterate)
意味着重复多次,就像循环那样。方法__iter__
返回一个迭代器,它是包含方法__next__
的对象,而调用这个方法时可不提供任何参数,当调用__next__
方法时,迭代器应返回其下一个值。如果迭代器没有可供返回的值,应引发StopIteration
异常。另外,还可以使用内置的函数next
,在这种情况下next(it)
与it.__next__()
等效。
这有什么意义呢,为什么不使用列表?
因为在很多情况下,使用列表都点大材小用。例如,现在有一个可逐个计算值的函数,你可能只想逐个地获取值,而不是使用列表一次性获取。这是因为如果有很多值,列表可以战用太多的内存。但还有其他原因:使用迭代器更通用与简便。
在下面的案例中就可以看到不能使用列表的案例,因为如果要使用列表,这个列表的长度必须是无穷大的,下面的这个案例是有关斐波那契数列的,代码如下所示:
|
|
这个迭代器实现了方法__iter__
,而这个方法返回迭代器本身,在很多情况下,都在另一个
对象中实现返回迭代器的方法__iter__
,并在for循环中使用这个对象。在推荐在迭代器中也实现方法__iter__
(并像刚才那样让它返回self),这样迭代器就可直接用于for循环中。
有关迭代
更正规的定义是,实现了方法__iter__
的对象是可迭代的,而实现了方法__next__
的对象是迭代器。
来看一下迭代器的for循环案例,如下所示:
|
|
运行结果如下所示:
|
|
这个循环会停止是因为其中包含break语句,否则这个for循环会一直执行下去。
通过对可迭代对象调用内置函数iter
就可以获得一个迭代器,如下所示:
|
|
从迭代器创建序列
除了对迭代器和可迭代对象进行迭代外,还可以将它们转换为序列。在可以使用序列的情况下,大多数也可以使用迭代器或可迭代对象(索引和切片这种操作除外)。
看下面的一个案例,使用构造函数list显式地将迭代器转换为列表:
|
|
运行结果如下所示:
|
|
生成器
生成器是一种使用普通函数语法定义的迭代器。
创建生成器
现在创建一个将嵌套列表展开的函数,这个函数以类似于下面的列表作为参数:
|
|
上面的这个参数其实是一个列表的列表,现在定义这样的函数:
|
|
这个函数首选迭代所提供嵌套列表中的所有子列表,然后按顺序迭代每个子列表的元素。
这个函数的最后一行含有yield
语句,像含有yield
语句的函数都被称为生成器。生成器与普通函数的区别在于,生成器不是使用return
返回一个值,而是生成多个值,一次一个。每次使用yield
生成一个值后,函数都将冻结,即在此停止执行,等待被重新唤醒,被唤醒后,函数将从停止的地方开始继续执行。
为使用所有的值,可对生成器进行迭代,如下所示:
|
|
运行结果如下所示:
|
|
生成器推导
生成器推导也叫生成器表达式,这是一个类似于列表推导的概念。其工作原理与列表推导相同,但不是创建一个列表,而是返回一个生成器,让你能够逐步执行计算,如下所示:
|
|
在这段代码中,使用的是圆括号。像在这种简单的情形下,不如使用列表推导;但如果要包装可迭代对象(可能生成大量的值),使用列表推导将立即实例化一个列表,从而丧失迭代的优势。另一个好处是,直接在一对既有的圆括号内(如在函数中调用)使用生成器推导时,无需再添加一对圆括号。也就是可编写下面这样的代码:
|
|
递归生成器
前面的生成器是使用了2个for循环实现的,如果要处理更多层的嵌套列表,此时就需要使用递归的思路,如下所示:
|
|
调用flatten
时,有两种可能性:
第一,基线条件。在基线条件下,要求这个函数展开单个元素,例如一个数,在这种情况下,for循环将引发TypeError异常(因为用户此时试图迭代一个数),而这个生成器只生成一个元素。
第二,递归条件。如果要展开的是一个列表,就需要做这些工作:遍历所有的子列表,并对它们调用flatten
,然后使用另一个for循环生成展开后的子列表中的所有元素,如下所示:
|
|
但是这种方案存在一个问题,如果nested
是字符串或类似于字符串的对象,它就属于序列,因此不会引发TypeError异常。要处理这种问题,必须要在生成器开头进行检查。要检查对象是否类似于字符串,最简单的方式就是尝试将对象与一个字符串拼接起来,并检查这是否会引发TypeError异常,添加后的代码如下所示:
|
|
运行结果如下所示:
|
|
在这个案例中,如查表达式nested + ""
引发了TypeError异常,就忽略这种异常;如果没有引发TypeError异常,内部try
语句中的else语句就引发TypeError异常,这样将在外部的except中生成类似于字符串的对象。
需要注意的是,这段代码没有进行类型检查,也就是说没有检查nested
是否为字符串,而是只检查了它的行为是否类似于字符串,即能否与字符串拼接。对于这种检查,更符合常规思路的方案是使用isinstance
以及字符串和类似于字符串的对象的一些抽象父类,但是Python中没这样的标准类。
通用生成器
这里再描述一下生成器,生成器是含有yield
的函数,但被调用时不会执行函数体内的代码,而是返回一个迭代器,每次请求值时,都将执行生成器的代码,直到遇到yield
或return
,yield
意味着应生成一个值,而return
意味着生成器应停止执行(即不再生成值,仅当在生成器调用return
时,才能不提供任何参数)。
换句话讲就是,生成器由两个单独的部分构成:①生成器的函数和②生成器的迭代器。生成器的函数是由def
语句定义的,其中包含yield
。生成器迭代器是这个函数返回的结果。用不太精准的话讲,生成器就是这两个东西的组合,看下面的代码:
|
|
生成器的方法
在生成器开始运行后,可使用生成器和外部之间的通信渠道向它提供值,这个通信渠道包含如下两个商战:
- 外部世界:外部世界可访问生成器的方法
send
,这个方法类似于next
,但接受一个参数(要发挥的消息
可以是任何对象)。 - 生成器:在挂起的生成器内部,
yield
可能用作表达式而不是语句。也就是说,在生成器重新运行时,yield
返回一个值,也就是通过send
从外部世界发达的值。如果使用的是next
,yield
将返回None
。需要注意的是,仅当生成器被挂起(也就是遇到第一个yield
)后,使用send
(而不是next
)才有意义。要在此之前向生成器提供消息,可使用生成器的函数的参数,下面的一个简单的案例就说明了这个机制:
|
|
运行结果如下所示:
|
|
在上面代码中,使用圆括号将yield
表达式圈了起来。
生成器还有另外两种方法,分别是throw
与close
。其中:
throw
方法用于在生成器中(yield表达式处)引发异常调用时可提供一个异常类型、一个可选值和一个traceback
对象。
close
方法用于停止生成器,调用时无需提供任何参数。方法close
也是基于异常的。在yield
处引发GeneratorExit
异常。因此如果要在生成器中提供一些清理代码,可将yield
放在一条try/finally
语句中。如果愿意,也可以捕获GeneratorExit
异常,但随后必须重新引发它(可能在清理后)、引发其他 异常或直接返回。对生成器调用close
方法后,再试图从它那里获取值将导致RuntimeError
异常。
生成器的使用案例——八皇后问题
现在使用生成器来解决一个经典的编程问题。
问题背景
对于逐步得到结果的复杂递归算法,非常适合使用生成器来实现。要在不使用生成器的情况下实现这些算法,通常必须通过额外的参数来传递部分结果,让递归调用能够接着往下计算。通过使用生成器,所有的递归调用进乞只需要生成其负责部分的结果。在前面的递归函数flatten
就是这样做的,可以使用这种策略来遍历图结构
和树结构
。
然而在有些应用程序中,你不能马上得到答案。你必须尝试多次,且在每个递归层级中都如此。
这里讲一个简单的案例。假设你要去参加一个很重要的会议,你不知道会议在哪里召开,但前面有两扇门,而会议室就在其中一扇门的后面。你选择进入左边的那扇门后,又看到两扇门。你再次选择进入左边的那扇门,但发现走错了。因此你往回走,并进入右边那扇门,但发现也走错了。因此你继续往回走到起始,现在可以尝试进入右边那扇门了。
对于需要尝试所有组合直到找到答案的问题,这种回溯策略对其解决很有帮助。这种问题的解决方案类似于以下代码:
|
|
如果要直接使用for循环来实现,就要写许多层,此时就可以使用递归。
问题描述
将8个皇后(这个皇后指的是国际象棋中的皇后)放在棋盘上,条件是任何一个皇后都不能威胁其他皇后,即任何两个皇后都不能吃掉对方。
这里再讲一下国际象棋的部分规则。
第一,国际象棋的棋盘是8 x 8
的方格,如下所示:
第二,皇后是国际象棋中的一个柜子,并且是棋局中实力最强的一种棋子,但同时也是最容易被吃掉的棋子。皇后可横着走,可以直着走,可以斜着走,且格数不限。
第三,因此如果不想让8个皇后中的任何1个被其他的皇后吃掉,那么要满足:①任意2个皇后不能处于同一条横线上;②任意2个皇不能处于同一条竖线上;③任意2个皇后不能处于同一条斜线上。
解决思路
八皇后问题是一个典型的回溯问题:
第一,在棋盘的第一行尝试为第一个皇后选择一个位置;
第二,在第二行尝试为第二个皇后选择一个位置,依次类推;
第三,当发现无法为一个皇后选择合适的位置后,回溯到前一个皇后,并尝试为它选择另外一个位置;
第四,最终要么要尝试完所有的可能性,要么找到答案。
代码实现
现在使用Python来解决一下这个问题。
状态表示
可以简单地使用元组或列表来表示可能的解,其中每个元素表示相应行中皇后所在的位置(即列)。因此如果state[0] = 3
就说明第1行的皇后放在第4列。在特定的递归层级(特定的行),你只需要知道上面各皇后的位置即可,因此状态元组的长度是小于8(即皇后的总数)。
检测冲突
先来做些简单的抽象。要找到没有冲突,即任何一个皇后都无法吃掉其他皇后的位置组合,首选必须定义冲突是什么。此时我们就可以使用一个函数conflict
来定义一下。
函数conflict
接受现有皇后的位置,并确定下一个皇后的位置是否会导致冲突,如下所示:
|
|
参数描述:
nextX
表示下一个皇后的水平位置,也就是x坐标;nextY
表示下一个皇后的垂直位置,也就是y坐标;- 这个函数对现有的每个皇后执行简单的检查:如果下一个皇后与当前皇后的
x
坐标相同或在同一条对角线上,将发生冲突,因此会返回True
,如果没有发生冲突,就返回False
; abs(state[i] - nextX) in (0, nextY - i)
表示:如果下一个皇后和当前皇后的水平距离为0,就表示他们同一列;或者是他们的垂直距离相等,这表示他们位于同一条对角线上,这个表达式就为真,否则为假。
基线条件
基线条件:最后一个皇后,对于这个皇后,代码如下所示:
|
|
这段代码的意思是:
- 如果只剩下最后一个皇后没有放好,就遍历所有可能的位置,并返回那些不会引发冲突的位置;
- 参数
num
是皇后总数,而参数state
是一个元组,包含已经放好的皇后位置,例如,假设总共有4个皇后,而前3个皇后的位置分别是1、3和0,如下所示:
从上图可知,每个皇后都占据一行,而皇后的位置是从0开始编号的,queens
函数的代码运行如下所示:
|
|
使用list旨在让生成器生成所有的值,在这个案例中,只有一个位置符合条件,就是上面白色棋子的位置。
递归条件
处理好基线条件后,可在递归条件中假设来自更低层级(编号更大的皇后)的结果都是正确的,因此,只需要在函数queens的前述实现中给if语句添加一个else语句。最终我们想要的返回结果是当前行下面所有皇后的位置,假设位置是以元组的形式返回的,因此需要修改基线条件,使其返回一个(长度为1的)元组。
因此,对于递归调用,向它提供的是当前行上面的皇后位置组成的元组。对于当前皇后的每个合法位置,递归调用返回的是由下面的皇后位置组成的元组。为了让这个过程不断进行下去,只需将当前皇后的位置插入返回结果的开头即可,如下所示:
|
|
这里的for pos
和前面的if not conflict
部分与前面相同,因此哦可以稍微简化一下代码,另外还可以给参数指定默认值,如下所示:
|
|
扫尾工作
在代码结束部分,可以让输出更容易理解一些,如下所示:
|
|
参考资料
- MagnusLieHetland. Python基础教程.第3版[M]. 人民邮电出版社, 2018.