Python学习笔记九1:魔法方法、特性和迭代器


  在Python中,有些名称很特别,开头和结尾都是两个下划线。这样的拼写表示名称有特殊意义,因此绝不要在程序中创建这样的名称。在这样的名称中,很大一部分都是魔法(特殊)方法的名称。如果你的对象实现了这些方法,它们将在特定情况下(具体是哪种情况取决于方法的名称)被Python调用,而几乎不需要直接调用。
  本章讨论几个重要的魔法方法,其中最重要的就是 __init__以及一些处理元素访问的方法(它们让你能够创建序列或映射)。本章还将讨论两个相关的主题:特性(property)和迭代器(iterator)。前者现在通过函数property处理,而后者使用魔法方法 __iter__,这让其可用于for循环中。在本章最后,将通过一个内容丰富的示例演示如何使用已有知识来解决非常棘手的问题。

一、如果你使用的不是Python3

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

二、构造函数

  构造函数其实就是前面一些示例中使用的初始化方法,只是命名为__init__。然而,构造函数不同于普通方法的地方在于,将在对象创建后自动调用它们。因此,无需采用前面一直用的方法:

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

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

>>> f = FooBar()

  在Python中,创建构造函数很容易,只要将方法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魔法方法中,__init__绝对是你用得最多的。
  注意:Python提供了魔法方法__del__,也称作析构函数。这个方法在对象被销毁前被调用,但鉴于你无法知道准确的调用时间,建议尽可能不要使用它。
  一个类中可定义多个构造方法,但实例化类时只实例化最后的构造方法。即后面的构造方法会覆盖前面的构造方法,并且需要根据最后一个构造方法的形式进行实例化。建议一个类中只定义一个构造函数。

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

  我们已经了解了继承。每个类都有一个或多个超类,并从它们那里继承行为。对类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'
  要在子类中添加功能,一种基本方式是添加方法。然而,你可能想重写超类的某些方法,以定制继承而来的行为。

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

  这样修改后,b.hello()的结果将会不同

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

  重写是继承机制的一个重要方面,对构造函数来说尤其重要。构造函数用于初始化新建对象的状态,而对大多数子类来说,除超类的初始化代码外,还需要有自己的初始化代码。虽然所有方法的重写机制都相同,但与普通方法相比,重写构造函数时更有可能遇到一个特别的问题:重写构造函数时,必须调用超类(继承的类)的构造函数,否则可能无法正确地初始化对象。
请看下面的Bird类:

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

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

>>> b = Bird()
>>> b.eat()		#Aaah…
>>> b.eat()		#No,thanks!

  鸟进食后就不再饥饿了。下面来看子类SongBird,它新增了鸣叫功能。

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

  SongBird类使用起来与Bird类一样容易:

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

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

>>> sb.eat()
#AttributeError:SongBird instance has no attribute 'hungry'

  异常清楚地指出了:SongBird没有属性hungry。为何会这样呢?因为在SongBird中重写了构造函数,但新的构造函数没有包含任何初始化属性hungry的代码。要消除这种错误,SongBird的构造函数必须调用其超类(Bird)的构造函数,以确保基本的初始化得以执行。为此,有两种方法:调用未关联的超类构造函数,以及使用函数super。

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

  本节介绍的方法主要用于解决历史遗留问题。在较新的Python版本中,显然应使用函数super。然而,很多既有代码使用的都是本节介绍的方法,因此你必须对其有所了解。另外,这种方法也极具启迪意义,淋漓尽致地说明了关联方法和未关联方法之间的差别。
  调用超类的构造函数实际上很容易,也很有用。下面先给出前一节末尾问题的解决方案。

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

  在SongBird类中,只添加了一行,其中包含代码Bird.__init__(self)。先来证明这确实管用,再解释这到底意味着什么。

>>> sb = SongBird()
>>> sb.sing()	#Squawk!
>>> sb.eat()	#Aaah…
>>> sb.eat()	#No,thanks!

  对实例调用方法时,方法的参数self将自动关联到实例(称为关联的方法),这样的示例你见过多个。然而,如果你通过类调用方法(如Bird.init),就没有实例与其相关联。在这种情况下,你可随便设置参数self。这样的方法称为未关联的。
  通过将这个未关联方法的self参数设置为当前实例,将使用超类的构造函数来初始化SongBird对象。这意味着将设置其属性hungry。

3. 使用函数super

  这个函数只适用于新式类,而你无论如何都应使用新式类。调用这个函数时,将当前类和当前实例作为参数。对其返回的对象调用方法时,调用的将是超类(而不是当前类)的方法。因此,在SongBird的构造函数中,可不使用Bird,而是使用super(SongBird,self)。另外,可像通常那样(也就是像调用关联的方法那样)调用方法__init__。在Python3中调用函数super时,可不提供任何参数(通常也应该这样做),而它将像变魔术一样完成任务。
  下面是前述示例的修订版本:

class Bird:
	def __init__(self):
		self.hungry = True
	def eat(self):
		if self.hungry:
			print('Aaah…')
			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()	#Aaah…
>>> sb.eat()	#No,thanks

(1) 使用函数super有何优点

  在我看来,相比于直接对超类调用未关联方法,使用函数super更直观,但这并非其唯一的优点。实际上,函数super很聪明,因此即便有多个超类,也只需调用函数super一次(条件是所有超类的构造函数也使用函数super)。
  函数super返回的到底是什么呢?通常,你无需关心这个问题,只管假定它返回你所需的超类即可。实际上,它返回的是一个super对象。这个对象将负责为你执行方法解析。当你访问它的属性时,它将所有的超类(以及超类的超类,等等)中查找,知道找到指定的属性或引发AttributeError异常

三、元素访问

本节将介绍一组很有用的魔法方法,让你能够创建行为类似于序列或映射的对象。
基本的序列和映射协议非常简单,但要实现序列和映射的所有功能,需要实现很多魔法方法。所幸有一些捷径可走,我马上就会介绍。
注意:在Python中,协议通常指的是规范行为的规则,有点类似于第7章提及的接口。协议指定应实现哪些方法以及这些方法应该做什么。在Python中,多态仅仅基于对象的行为(而不基于祖先,如属于哪个类或其超类等),因此这个概念很重要:其他的的语言可能要求对象属于特定的类或实现了特定的接口,而Python通常只要求对象遵循特定的协议。因此,要成为序列,只需遵循序列协议即可。

1. 基本序列和映射协议

序列和映射基本上是元素的集合,要实现它们的基本行为(协议),不可变对象需要实现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异常。

  下面来试一试,看看能否创建一个无穷序列。
  自定义一个序列类型,在序列初始化的时候可以指定增量的步长,当向列表查询元素时,getitem函数会首先判断查询的下标是否是整型元素,若不是则会则会报类型错误。若是整型元素且该下标对应的值存在于列表中则会输出该元素值,若不在列表中则会根据规定的计算规则计算该下标索引对应的元素值并记录到列表索引对应的位置。

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					# 存储修改后的值

  这些代码实现的是一个算术序列,其中任何两个相邻数字的差都相同。第一个值是由构造函数的参数start(默认为0)指定的,而相邻值之间的差是由参数step(默认为1)指定的。你允许用户修改某些元素,这是通过将不符合规则的值保存在字典changed中实现的。如果元素未被修改,就使用公式self.start + key * self.step来计算它的值。
  下面的示例演示了如何使用这个类:

>>> s = ArithmeticSequence(1, 2)
>>> s[4]	#9
>>> s[4] = 2		# changed—{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 "C:/Users/MIC/Desktop/test1.py", line 57, in <module>
    s["four"]
TypeError: 'ArithmeticSequence' object is not subscriptable
>>> s[-42]
Traceback (most recent call last):
  File "C:/Users/MIC/Desktop/test1.py", line 58, in <module>
    s[-42]
TypeError: 'ArithmeticSequence' object does not support indexing

索引检查是由我为此编写的辅助函数check_index负责的。

2. 从list、dict 和str 派生

  基本的序列/映射协议指定的4个方法能够让你走很远,但序列还有很多其他有用的魔法方法和普通方法,其中包括将在9.6节介绍的方法__iter__。要实现所有这些方法,不仅工作量大,而且难度不小。如果只想定制某种操作的行为,就没有理由去重新实现其他所有方法。这就是程序员的懒惰(也是常识)。
  那么该如何做呢?“咒语”就是继承。在能够继承的情况下为何去重新实现呢?在标准库中,模块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__中)
  注意:重写__getitem__并不能保证一定会捕捉用户的访问操作,因为还有其他访问列表内容的方式,如通过方法pop
  下面的示例演示了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

  如你所见,CounterList的行为在大多数方面都类似于列表,但它有一个counter属性(其初始值为0)每当你访问列表元素时,这个属性的值都加1。执行加法运算cl[4] + cl[2]后,counter的值递增两次,变成了2。

四、其他魔法方法

  特殊(魔法)名称的用途很多,前面展示的只是冰山一角。魔法方法大多是为非常高级的用途准备的,因此这里不详细介绍。然而,如果你感兴趣,可以模拟数字,让对象像函数一样被调用,影响对象的比较方式,等等。要更详细地了解有哪些魔法方法,可参阅“Python Reference Manual”的Special method names一节

1. str

  在开始介绍之前,我们先定义一个Student类。

class Student(object):
	def __init__(self,name):
		self.name = name
>>> print(Student('xiaozhi'))
	#<__main__.Student object at 0x000001EA1E0C8CF8>

  执行结果过输出一堆字符串,一般人看不懂。但是我们定义好__str__()方法,返回一个好看的字符串就好。

class Student(object):
	def __init__(self,name):
		self.name = name
	def __str__(self):
		return '学生名称:%s'%self.name
>>> print(Student('xiaozhi'))
	#学生名称:xiaozhi

  如果在交互模式下输入如下:

>>> s = Student('xiaozhi')
>>> s
	#<__main__.Student object at 0x000001EA1E0C8CF8>

  这是因为直接显示变量调用的不是__str__,而是__repr__,两者的区别在于__str__()返回用户看到的字符串,而__repr__()返回程序开发者看到的字符串。
解决办法是再定义一个__repr__()。通常,__str__()__repr__()代码是一样的,所以有一个偷懒的写法。

class Student(object):
	def __init__(self,name):
		self.name = name
	def __str__(self):
		return '学生名称:%s'%self.name
	__repr__ = __str__

1. call

  一个实例对象可以有自己的属性和方法,调用实例的方法时使用instance.method()调用。能不能直接在实例本身调用呢?答案是可以的。
  任何类,只需要定义一个__call__()方法,就可以直接对实例进行调用,例如:

class Student(object):
	def __init__(self,name):
		self.name = name
	def __call__(self):
		print('学生名称:%s'%self.name)
>>> stu = Student()
>>> stu('xiaozhi')
	#学生名称:'xiaozhi'

  由输出结果看到,可以直接对实例进行调用并得到结果。
  __call__()还可以定义参数。对实例进行直接调用就像对一个函数调用一样,完全可以把对象看成函数,把函数看成对象,因为这两者本来就有根本区别。
  如果把对象看成函数,函数本身就可以在运行期间动态创建出来,因为类的实例都是运行运行期间创建出来的。这样一来,就模糊了对象和函数的界限。
  怎么判断一个变量是对象还是函数呢?
  很多时候判断一个对象是否能被能被调用,可以使用Callable()函数,比如函数和上面定义的带有__call__()的类实例。输入如下:

>>> callable(Student('xiaozhi'))	#True
>>> callable(max)	#True
>>> callable([1,2,3])	#False
>>> callable(None)	#False
>>> callable('a')	#False

  由操作结果看到,通过callable()函数可以判断一个对象是否为"可调用"对象。

五、特性

  第7章提到了存取方法,它们是名称类似于getHeightsetHeight的方法,用于获取或设置属性(这些属性可能是私有的,详情请参阅7.2.4节)。如果访问给定属性时必须采取特定的措施,那么像这样封装状态变量(属性)很重要。例如,请看下面的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_sizeset_size是假想属性size的存取方法,这个属性是一个由widthheight组成的元组。这些代码并非完全错误,但存在缺陷。使用这个类时,程序员应无需关心它是如何实现的(封装)。如果有一天你想修改实现,让size成为真正的属性,而widthheight是动态计算出来的,就需要提供用于访问widthheight的存取方法,使用这个类的程序也必须重写。应让客户端代码(使用你所编写代码的代码)能够以同样的方式对待所有的属性。
  那么如何解决这个问题呢?给所有的属性都提供存取方法吗?这当然并非不可能,但如果有大量简单的属性,这样做就不现实(而且有点傻),因为将需要编写大量这样的存取方法,除了获取或设置属性外什么都不做。这将引入复制并粘贴(重复代码)的坏味,显然很糟糕(虽然在有些语言中,这样的问题很常见)。所幸Python能够替你隐藏存取方法,让所有的属性看起来都一样。通过存取方法定义的属性通常称为特性(property)
  在Python中,实际上有两种创建特定的机制,我将重点介绍较新的那种——函数property,它只能用于新式类。随后,我将简单说明如何使用魔法方法来实现特性。

1. 函数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)

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

>>> r = Rectangle()
>>> r.width = 10
>>> r.height = 5
>>> r.size		#(10, 5)
>>> r.size = 150, 100
>>> r.width		#150

  如你所见,属性size依然受制于get_sizeset_size执行的计算,但看起来就像普通属性一样。
  注意:如果特性的行为怪异,务必确保你使用的是新式类(通过直接或间接地继承object或直接设置__metaclass__)。不然,特性的获取方法依然正常,但设置方法可能不正常(是否如此取决于使用的Python版本)。这可能有点令人迷惑。
实际上,调用函数property时,还可不指定参数、指定一个参数、指定三个参数或指定四个参数。

  • 如果没有指定任何参数,创建的特性将既不可读也不可写。
  • 如果只指定一个参数(获取方法),创建的特性将是只读的。
  • 第三个参数是可选的,指定用于删除属性的方法(这个方法不接受任何参数)。
  • 第四个参数也是可选的,指定一个文档字符串。

  这些参数分别名为fgetfsetfdeldoc。如果你要创建一个只可写且带文档字符串的特性,可使用它们作为关键字参数来实现。
  本节虽然很短(旨在说明函数property很简单),却非常重要。这里要说明的是,对于新式类,应使用特性而不是存取方法。

2. 函数property的工作原理

  property其实并不是函数,而是一个类。它的实例包含一些魔法方法,而所有的魔法都是由这些方法完成的。这些魔法方法为__get____set____delete__,它们一道定义了所谓的描述符协议。只要对象实现了这些方法中的任何一个,它就是一个描述符。描述符的独特之处在于其访问方式。
  例如,读取属性(具体来说,是在实例中访问类中定义的属性)时,如果它关联的是一个实现了__get__的对象,将不会返回这个对象,而是调用方法__get__并将其结果返回。实际上,这是隐藏在特性、关联的方法、静态方法和类方法(详细信息请参阅下一小节)以及super后面的机制。

3. 静态方法和类方法

  讨论旧的特性实现方式之前,先来说说另外两种实现方式类似于新式特性的功能。静态方法和类方法是这样创建的:将它们分别包装在staticmethodclassmethod类的对象中。静态方法的定义中没有参数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'>

  在Python中,静态方法和类方法以前一直都不太重要,主要是因为从某种程度上说,总是可以使用函数或关联的方法替代它们,而且早期的Python版本并不支持它们。因此,虽然较新的代码没有大量使用它们,但它们确实有用武之地(如工厂函数),因此你或许应该考虑使用它们。
注意:实际上,装饰器语法也可用于特性,详情请参阅有关函数property的文档。

4. getattr、__setattr__等方法

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

  • __getattribute__(self, name):在属性被访问时自动调用(只适用于新式类)。
  • __getattr__(self, name):在属性被访问而对象没有这样的属性时自动调用。
  • __setattr__(self, name, value):试图给属性赋值时自动调用。
  • __delattr__(self, name):试图删除属性时自动调用。
      相比函数property,这些魔法方法使用起来要棘手些(从某种程度上说,效率也更低),但它们很有用,因为你可在这些方法中编写处理多个特性的代码。然而,在可能的情况下,还是使用函数property吧。
      再来看前面的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__。因此这个方法必须考虑如下两种情形:如果涉及的属性为size,就执行与以前一样的操作;否则就使用魔法属性__dict____dict__属性是一个字典,其中包含所有的实例属性。之所以使用它而不是执行常规属性赋值,是因为旨在避免再次调用__setattr__,进而导致无限循环。
  • 仅当没有找到指定的属性时,才会调用方法__getattr__。这意味着如果指定的名称不是size,这个方法将引发AttributeError异常。这在要让类能够正确地支持hasattr和getattr等内置函数时很重要。如果指定的名称为size,就使用前一个实现中的表达式。

注意:前面说过,编写方法__setattr__时需要避开无限循环陷阱,编写__getattribute__时亦如此。由于它拦截对所有属性的访问(在新式类中),因此将拦截对__dict__的访问!在__getattribute__中访问当前实例的属性时,唯一安全的方式是使用超类的方法__getattribute__(使用super)。

六、迭代器

  本书前面粗略地提及了迭代器(和可迭代对象),本节将更详细地介绍。对于魔法方法,这里只介绍__iter__,它是迭代器协议的基础。

1. 迭代器协议

  迭代(iterate)意味着重复多次,就像循环那样。本书前面只使用for循环迭代过序列和字典,但实际上也可迭代其他对象:实现了方法__iter__的对象。
  迭代器对象要求支持迭代器协议的对象,在Python中,支持迭代器协议就是实现对象的__iter__()__next__()方法。
  方法__iter__()返回一个迭代器对象本身,它是包含方法__next__()的对象,而调用这个方法时可不提供任何参数。当你调用方法__next__()时,迭代器应返回其下一个值。如果迭代器没有可供返回的值,应引发StopIteration异常。你还可使用内置的便利函数next,在这种情况下,next(it)it.__next__()等效。iter([1,2,3])与[1,2,3].iter()等效。

>>> a=iter([1,2,3])
>>> next(a)		#1

  这有什么意义呢?为何不使用列表呢?因为在很多情况下,使用列表都有点像用大炮打蚊子。
  例如,如果你有一个可逐个计算值的函数,你可能只想逐个地获取值,而不是使用列表一次性获取。这是因为如果有很多值,列表可能占用太多的内存。但还有其他原因:使用迭代器更通用、更简单、更优雅。
  list只有__iter__(),没有__next__(),而list.__iter__()是一个迭代器。实现迭代器__next__()
  下面来看一个不能使用列表的示例,因为如果使用,这个列表的长度必须是无穷大的!
  这个“列表”为斐波那契数列,表示该数列的迭代器如下:

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__,并在for循环中使用这个对象。但推荐在迭代器中也实现方法__iter__(并像刚才那样让它返回self),这样迭代器就可直接用于for循环中。
  注意:更正规的定义是,实现了方法__iter__的对象是可迭代的,而实现了方法__next__的对象是迭代器。
  首先,创建一个Fibs对象

>>> fibs = Fibs()

  然后就可在for循环中使用这个对象,如找出第一个大于1000的斐波那契数。

>>> for f in fibs:
		if f > 1000:
			print(f)
			break
	#1597

  这个循环之所以会停止,是因为其中包含break语句;否则,这个for循环将没完没了地执行。
  提示:通过对可迭代对象调用内置函数iter,可获得一个迭代器。

>>> it = iter([1, 2, 3])
>>> next(it)	#1
>>> next(it)	#2

  还可使用它从函数或其他可调用对象创建可迭代对象,详情请参阅库参考手册。

2. 从迭代器创建序列

  除了对迭代器和可迭代对象进行迭代(通常这样做)之外,还可将它们转换为序列。在可以使用序列的情况下,大多也可使用迭代器或可迭代对象(诸如索引和切片等操作除外)。一个这样的例子是使用构造函数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]

七、生成器

  生成器是一个相对较新的Python概念。由于历史原因,它也被称为简单生成器(simplegenerator)。生成器和迭代器可能是近年来引入的最强大的功能,但生成器是一个相当复杂的概念,你可能需要花些功夫才能明白其工作原理和用途。虽然生成器让你能够编写出非常优雅的代码,但请放心,无论编写什么程序,都完全可以不使用生成器。
  生成器是一种使用普通函数语法定义的迭代器。生成器的工作原理到底是什么呢?通过示例来说明最合适。下面先来看看如何创建和使用生成器,然后再看看幕后的情况。

1. 创建生成器

  在Python中,使用生成器可以很方便的支持迭代器协议。
  生成器创建起来与函数一样简单。你现在肯定厌烦了老套的斐波那契数列,所以下面换换口味,创建一个将嵌套列表展开的函数。这个函数将一个类似于下面的列表作为参数:

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

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

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

  简单示例:

>>> def simple_generator_function():
>>>    yield 1
>>>    yield 2
>>>    yield 3

  它首先迭代所提供嵌套列表中的所有子列表,然后按顺序迭代每个子列表的元素。倘若最后一行为print(element),这个函数将容易理解得多,不是吗?
  在这里,你没有见过的是yield语句。包含yield语句的函数都被称为生成器。这可不仅仅是名称上的差别,生成器的行为与普通函数截然不同。差别在于,生成器不是使用return返回一个值,而是可以生成多个值,每次一个。每次使用yield生成一个值后,函数都将冻结,即在此停止执行,等待被重新唤醒。被重新唤醒后,函数将从停止的地方开始继续执行。也就是说,yield是一个语法糖,内部实现支持了迭代器协议,同时yield内部是一个状态机,维护着挂起和继续的状态。
为使用所有的值,可对生成器进行迭代。

def fun():
    i = 0
    while i < 5:
        print('jianeng')
        yield 'stop' #把这个函数变成了生成器
        i+=1
        print('lucky',i)
>>> a = fun()
>>> next(a)
jianeng
'stop'
>>> next(a)
lucky 1
jianeng
'stop'

  要注意的是生成器就是一类特殊的迭代器。作为一个迭代器,生成器必须要定义一些方法,其中一个就是__next__()。如同迭代器一样,我们可以使用__next__()函数来获取下一个值。

简单生成器

  在Python 2.4中,引入了一个类似于列表推导(参见第5章)的概念:生成器推导(也叫生成器表达式)。其工作原理与列表推导相同,但不是创建一个列表(即不立即执行循环),而是返回一个生成器,让你能够逐步执行计算。

>>> g = ((i + 2) ** 2 for i in range(2, 27))
>>> next(g)		#16

  如你所见,不同于列表推导,这里使用的是圆括号。在像这样的简单情形下,还不如使用列表推导;但如果要包装可迭代对象(可能生成大量的值),使用列表推导将立即实例化一个列表,从而丧失迭代的优势。
  另一个好处是,直接在一对既有的圆括号内(如在函数调用中)使用生成器推导时,无需再添加一对圆括号。换而言之,可编写下面这样非常漂亮的代码:
sum(i ** 2 for i in range(10))

2. 递归式生成器

  前一节设计的生成器只能处理两层的嵌套列表,这是使用两个for循环来实现的。如果要处理任意层嵌套的列表,该如何办呢?
  例如,你可能使用这样的列表来表示树结构(也可以使用特定的树类,但策略是相同的)。对于每层嵌套,都需要一个for循环,但由于不知道有多少层嵌套,你必须修改解决方案,使其更灵活。该求助于递归了。

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异常,可你并不想对其进行迭代。
  注意:在函数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

  如你所见,如果表达式nested + ''引发了TypeError异常,就忽略这种异常;如果没有引发TypeError异常,内部try语句中的else子句将引发TypeError异常,这样将在外部的excpet子句中原封不动地生成类似于字符串的对象。
下面的示例表明,这个版本也可用于字符串:

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

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

3. 通用生成器

  如果你按前面的例子做了,就差不多知道了如何使用生成器。你知道,生成器是包含关键字yield的函数,但被调用时不会执行函数体内的代码,而是返回一个迭代器。每次请求值时,都将执行生成器的代码,直到遇到yieldreturn。yield意味着应生成一个值,而return意味着生成器应停止执行(即不再生成值;仅当在生成器调用return时,才能不提供任何参数)。
  换而言之,生成器由两个单独的部分组成:生成器的函数和生成器的迭代器。生成器的函数是由def语句定义的,其中包含yield。生成器的迭代器是这个函数返回的结果。用不太准确的话说,这两个实体通常被视为一个,通称为生成器。

>>> def simple_generator():
		yield 1
>>> simple_generator
	#<function simple_generator at 153b44>
>>> simple_generator()
	#<generator object at 1510b0>

  对于生成器的函数返回的迭代器,可以像使用其他迭代器一样使用它。

4. 生成器的方法

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

  • 外部世界:外部世界可访问生成器的方法send,这个方法类似于next,但接受一个参数(要发送的“消息”,可以是任何对象)。
  • 生成器:在挂起的生成器内部,yield可能用作表达式而不是语句。换而言之,当生成器重新运行时,yield返回一个值——通过send从外部世界发送的值。如果使用的是next,yield将返回None。

  请注意,仅当生成器被挂起(即遇到第一个yield)后,使用send(而不是next)才有意义。要在此之前向生成器提供信息,可使用生成器的函数的参数。
  注意:如果一定要在生成器刚启动时对其调用方法send,可向它传递参数None。
  下面的示例很傻,但说明了这种机制:

def repeater(value):
	while True:
		new = (yield value)
		if new is not None: value = new

  下面使用了这个生成器:

>>> r = repeater(42)
>>> next(r)		#42
>>> r.send("Hello, world!")		#"Hello, world!"

  注意到使用圆括号将yield表达式括起来了。在有些情况下,并非必须这样做,但小心驶得万年船。如果要以某种方式使用返回值,就不管三七二十一,将其用圆括号括起吧。
生成器还包含另外两个方法。
方法throw:用于在生成器中(yield表达式处)引发异常,调用时可提供一个异常类型、一个可选值和一个traceback对象。
方法close:用于停止生成器,调用时无需提供任何参数。
方法close(由Python垃圾收集器在需要时调用)也是基于异常的:在yield处引发GeneratorExit异常。因此如果要在生成器中提供一些清理代码,可将yield放在一条try/finally语句中。如果愿意,也可捕获GeneratorExit异常,但随后必须重新引发它(可能在清理后)、引发其他异常或直接返回。对生成器调用close后,再试图从它那里获取值将导致RuntimeError异常。

5. 模拟生成器

  如果你使用的是较老的Python版本,就无法使用生成器。下面是一个简单的解决方案,让你能够使用普通函数模拟生成器。
  首先,在函数体开头插入如下一行代码:
result = []
  如果代码已使用名称result,应改用其他名称。(在任何情况下,使用更具描述性的名称都是不错的主意。)接下来,将类似于yield some_expression的代码行替换为如下代码行:
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

八、八皇后问题

学习各种魔法方法后,该付诸应用了。本节将演示如何使用生成器来解决一个经典的编程问题。

1.生成器的回溯

对于逐步得到结果的复杂递归算法,非常适合使用生成器来实现。要在不使用生成器的情况下实现这些算法,通常必须通过额外的参数来传递部分结果,让递归调用能够接着往下计算。通过使用生成器,所有的递归调用都只需生成其负责部分的结果。前面的递归版flatten就是这样做的,你可使用这种策略来遍历图结构和树结构。
然而,在有些应用程序中,你不能马上得到答案。你必须尝试多次,且在每个递归层级中都如此。打个现实生活中的比方吧,假设你要去参加一个很重要的会议。你不知道会议在哪里召开,但前面有两扇门,而会议室就在其中一扇门的后面。你选择进入左边那扇门后,又看到两扇门。你再次选择进入左边那扇门,但发现走错了。因此你往回走,并进入右边那扇门,但发现也走错了。因此你继续往回走到起点,现在可以尝试进入右边那扇门。
对于需要尝试所有组合直到找到答案的问题,这种回溯策略对其解决很有帮助。这种问题的
解决方案类似于下面这样:

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

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

2. 问题

这是一个深受大家喜爱的计算机科学谜题:你需要将8个皇后放在棋盘上,条件是任何一个皇后都不能威胁其他皇后,即任何两个皇后都不能吃掉对方。怎样才能做到这一点呢?应将这些皇后放在什么地方呢?
这是一个典型的回溯问题:在棋盘的第一行尝试为第一个皇后选择一个位置,再在第二行尝试为第二个皇后选择一个位置,依次类推。在发现无法为一个皇后选择合适的位置后,回溯到前一个皇后,并尝试为它选择另一个位置。最后,要么尝试完所有的可能性,要么找到了答案。
在前面描述的问题中,只有8个皇后,但这里假设可以有任意数量的皇后,从而更像现实世界的回溯问题。

3. 状态表示

可简单地使用元组(或列表)来表示可能的解(或其一部分),其中每个元素表示相应行中皇后所在的位置(即列)。因此,如果state[0] == 3,就说明第1行的皇后放在第4列(还记得吧,我们从0开始计数)。
在特定的递归层级(特定的行),你只知道上面各皇后的位置,因此状态元组的长度小于8(即皇后总数)。
注意:完全可以使用列表(而不是元组)来表示状态,具体使用哪个完全取决于你的喜好。一般而言,如果序列较小且是静态的,使用元组可能是不错的选择。

4. 检测冲突

先来做些简单的抽象。要找出没有冲突(即任何一个皇后都吃不到其他皇后)的位置组合,首先必须定义冲突是什么。为何不使用一个函数来定义呢?
函数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(在同一列)或与它们的垂直距离相等(位于一条对角线上),这个表达式就为真;否则为假。

5. 基线条件

八皇后问题解决起来有点棘手,但通过使用生成器并不太难。然而,如果你不熟悉递归,就很难自己想出这里的解决方案。另外,这个解决方案的效率不是特别高,因此皇后非常多时,其速度可能有点慢。
下面先来看基线条件:最后一个皇后。对于这个皇后,你想如何处理呢?假设你想找出所有可能的解——给定其他皇后的位置,可将这个皇后放在什么位置(可能什么位置都不行)?可以这样编写代码。

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

这段代码的意思是,如果只剩下最后一个皇后没有放好,就遍历所有可能的位置,并返回那些不会引发冲突的位置。参数num为皇后总数,而参数state是一个元组,包含已放好的皇后的位置。例如,假设总共有4个皇后,而前3个皇后的位置分别为1、3和0,如图9-1所示。(现在不用关心白色的皇后。)
在这里插入图片描述
图9-1 在一个4行4列的棋盘上放置4个皇后
从该图可知,每个皇后都占据一行,而皇后的位置是从0开始编号的(Python中通常如此)。

>>> list(queens(4, (1, 3, 0)))
	#[2]

代码的效果很好。这里使用list旨在让生成器生成所有的值。在这个示例中,只有一个位置符合条件。在图9-1中,在这个位置放置了一个白色皇后。

6. 递归条件

现在来看看这个解决方案的递归部分。处理好基线条件后,可在递归条件中假设来自更低层级(编号更大的皇后)的结果都是正确的。因此,只需在函数queens的前述实现中给if语句添加一个else子句。
你希望递归调用返回当前行下面所有皇后的位置。假设位置是以元组的方式返回的,因此需要修改基线条件,使其返回一个(长度为1的)元组,但这将在后面处理。
因此,对于递归调用,向它提供的是由当前行上面的皇后位置组成的元组。对于当前皇后的每个合法位置,递归调用返回的是由下面的皇后位置组成的元组。为了让这个过程不断进行下去,只需将当前皇后的位置插入返回的结果开头,如下所示:

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

这里的for pos和if not conflict部分与前面相同,因此可以稍微简化一下代码。另外,还可给参数指定默认值。

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

7. 扫尾工作

结束本节之前,可以让输出更容易理解些。在任何情况下,清晰的输出都是好事,因为这让
查找bug等工作更容易。

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

请注意,我在prettyprint中创建了一个简单的辅助函数。之所以将它放在prettyprint中,是因为我认为在其他地方都用不到它。下面随机地选择一个解,并将其打印出来,以确定它是正确的。

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

图9-2显示了这个解。
在这里插入图片描述
图9-2 八皇后问题的众多解之一

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值