欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 汽车 > 时评 > 《Python基础教程》第9章笔记:魔法方法、特性和迭代器

《Python基础教程》第9章笔记:魔法方法、特性和迭代器

2025/3/9 22:31:31 来源:https://blog.csdn.net/holeer/article/details/146014690  浏览:    关键词:《Python基础教程》第9章笔记:魔法方法、特性和迭代器

《Python基础教程》第1章笔记👉https://blog.csdn.net/holeer/article/details/143052930

第9章 魔法方法、特性和迭代器

在Python中,有些名称很特别,开头和结尾都是两个下划线。这样的名称很大一部分都是魔法(特殊)方法的名称。如果你的对象实现了这些方法,它们将在特定情况下被Python调用,而几乎不需要直接调用。

本章讨论几个重要的魔法方法,其中最重要的是__init__以及一些处理元素访问的方法(它们让你能够创建序列或映射)。本章还将讨论两个相关的主题:特性(property)和迭代器(iterator)。前者以前是通过魔法方法处理的,但现在通过函数property处理,而后者使用魔法方法__iter__,这让其可用于for循环中。

9.2 构造函数

在对象创建后,将自动调用构造函数__init__。相应地,在对象被销毁(作为垃圾被收集)前会调用析构函数__del__

Python的每个类都有一个或多个超类,并从它们那里继承行为。要在子类中添加功能,一种基本方式是添加方法。然而,你可能想重写超类的某些方法,以定制继承而来的行为。重写是继承机制的一个重要方面,对构造函数来说尤其重要。对大多数子类来说,除了要自己的初始化代码之外,还必须调用超类的构造函数,否则可能无法正确地初始化对象。对此有两种方法。

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

这种方法主要用于解决历史遗留问题。请看示例代码。

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

在倒数第四行,通过类名而不是实例调用方法,此时认为被调用的方法是未关联的。通过将代指子类的self传入超类的构造方法,完成了子类实例中超类部分的初始化。

(2)使用函数super:将以上程序的倒数第四行改为super().__init__()即可。

9.3 元素访问

基本的序列和映射协议非常简单,但要实现序列和映射的所有功能,需要实现很多魔法方法。

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

9.3.1 基本的序列和映射协议

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

(1)__len__(self):这个方法应返回集合包含的项数,对序列来说为元素个数,对映射来说为键-值对数。如果__len__返回零(且没有实现覆盖这种行为的__nonzero__),对象在布尔上下文中将被视为False(就像空的列表、元组、字符串和字典一样)。
(2)__getitem__(self, key):这个方法应返回与指定键相关联的值。对序列来说,键应该是0~n-1的整数(也可以是负数),其中n为序列的长度。对映射来说,键可以是任何类型。
(3)__setitem__(self, key, value):存储值。
(4)__delitem__(self, key):删除值。

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

下面的示例创建了一个无穷序列。

def check_index(key):if not isinstance(key, int): raise TypeErrorif key < 0: raise IndexError
class ArithmeticSequence:def __init__(self, start=0, step=1):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

9.3.2 从list、dict和str派生

除了基本协议指定的4个方法之外,但序列还有很多其他有用的魔法方法。要实现所有这些方法,不仅工作量大,而且难度不小。如果只想定制某种操作的行为,继承已有类型即可。下面是一个简单的示例:一个带访问计数器的列表。

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

9.5 特性

第7章提到了存取方法,它们是名称类似于getHeight和setHeight的方法,用于获取或设置属性(这些属性可能是私有的)。如果访问给定属性时必须采取特定的措施,那么封装状态变量(属性)很重要。例如,请看下面的Rectangle类:

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

以上代码是存在缺陷的。代码中提供了假想属性size的存取方法,但是却没有提供属性width和height的存取方法。建议的规范是,应该让类的外部调用者能够以同样的方式对待所有的属性。为了满足规范,一种做法是为所有其他属性补充存取方法,在补充的方法中除了获取或设置属性外什么都不做。这种做法显然很糟糕。所幸Python能够替你隐藏存取方法,让所有的属性看起来都一样。通过存取方法定义的属性通常称为特性(property)。特性可通过函数property定义。

9.5.1 函数property

对于上面的Rectangle类,只需在类体末尾添加一行代码: size = property(get_size, set_size)。这样做了之后,就可以将属性size当作普通属性读写,如下面的示例所示。

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

实际上,调用函数property时,还可不指定参数、指定一个参数、指定三个参数或指定四个参数。如果没有指定任何参数,创建的特性将既不可读也不可写。如果只指定一个参数,创建的特性将是只读的。第三个参数是可选的,指定用于删除属性的方法。第四个参数也是可选的,指定一个文档字符串。

9.5.2 静态方法和类方法

静态方法和类方法是这样创建的:将它们分别包装在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)

像这样手工包装和替换方法有点繁琐,你还可以使用一种名为装饰器的语法。装饰器可用于包装任何可调用的对象,并且可用于方法和函数。

class MyClass:@staticmethoddef smeth():print('This is a static method')@classmethoddef cmeth(cls):print('This is a class method of', cls)

在Python中,静态方法和类方法一直都不太重要,主要原因是:从某种程度上说,总是可以使用函数或关联的方法替代它们。不过,它们确实有用武之地(如工厂函数)。

9.5.3 __getattr____setattr__等方法

要在属性被访问时执行一段代码,必须使用一些魔法方法。下面的四个魔法方法提供了你需要的所有功能。

__getattribute__(self, name):在属性被访问时自动调用。
__getattr__(self, name):在属性被访问而对象没有这样的属性时自动调用。
__setattr__(self, name, value):试图给属性赋值时自动调用。
__delattr__(self, name):试图删除属性时自动调用。

9.6 迭代器

9.6.1 迭代器协议

迭代(iterate)意味着重复多次,就像循环那样。本书前面只使用for循环迭代过序列和字典,但实际上也可迭代其他对象:实现了方法__iter__的对象。

方法__iter__返回一个迭代器,它是包含方法__next__的对象,而调用这个方法时可不提供任何参数。当你调用方法__next__时,迭代器应返回其下一个值。如果迭代器没有可供返回的值,则会引发StopIteration异常。此外还可使用内置函数next,next(it)it.__next__()等效。

与列表相比,迭代器更通用、更简单、更优雅。 如果有很多值,列表可能占用太多的内存。此外,有些用迭代器实现的功能不能用列表实现,比如下面的例子。

class Fibs:def __init__(self):self.a = 0self.b = 1def __next__(self):self.a, self.b = self.b, self.a + self.breturn self.adef __iter__(self):return self

以上类定义了斐波那契数列,它实现了方法__iter__,而这个方法返回迭代器本身。在很多情况下,都在另一个对象中实现返回迭代器的方法__iter__,并在for循环中使用这个对象。因此更正规的定义是,实现了方法__iter__的对象是可迭代的,而实现了方法__next__的对象是迭代器。

如果要找出第一个大于1000的斐波那契数,你应该这样做:

fibs = Fibs()
for f in fibs:if f > 1000:print(f)break

9.6.2 从迭代器创建序列

除了对迭代器和可迭代对象进行迭代之外,还可将它们转换为序列。在可以使用序列的情况下,大多也可使用迭代器或可迭代对象(诸如索引和切片等操作除外)。比如,使用构造函数list显式地将有穷迭代器转换为列表。

class TestIterator:value = 0def __next__(self):self.value += 1if self.value > 10: raise StopIterationreturn self.valuedef __iter__(self):return selfti = TestIterator()
print(list(ti))

以上代码的期望输出为:[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

9.7 生成器

生成器是一种使用普通函数语法定义的迭代器。它是一个相当复杂的概念,虽然它让你能够编写出非常优雅的代码,但你也可以完全不使用它。

9.7.1 创建生成器

假设我们有一个嵌套列表,比如:nested = [[1, 2], [3, 4], [5]]。接着我们要创建一个将嵌套列表列表展开的函数。下面是一种解决方案:

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

在这里,你没有见过的是yield语句。包含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]

生成器推导(也叫生成器表达式)是一个类似于列表推导的概念。其工作原理与列表推导相同,但不是创建一个列表(即不立即执行循环),而是返回一个生成器,让你能够逐步执行计算,如下面代码的第一行所示。另外,直接在一对既有的圆括号内使用生成器推导时,无需再添加一对圆括号。这可以让你写出非常漂亮的代码,如下面代码的第二行所示。

g = ((i + 2) ** 2 for i in range(2, 27))
sum(i ** 2 for i in range(10))

9.7.2 递归式生成器

前一小节设计的生成器只能处理两层的嵌套列表,这是使用两个for循环来实现的。如果要处理任意层嵌套的列表(这样的列表可以用来表示树结构),由于嵌套层数是未知的,你需要使用递归,如下所示。

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

以上生成器同时适用于元素为数或字符串类型的输入参数nested,这段代码可能有点费解。在nested对调用flatten时,有两种可能性 (处理递归时都如此):基线条件和递归条件。在基线条件下,要求这个函数展开单个元素。在这种情况下,for循环将引发TypeError异常。然而,如果要展开的是一个列表,则需要做些工作:遍历所有的子列表并对它们调用flatten,然后使用另一个for循环生成展开后的子列表中的所有元素。

9.8 八皇后问题

(1)生成器的回溯

对于逐步得到结果的复杂递归算法,非常适合使用生成器来实现。要在不使用生成器的情况下实现这些算法,通常必须通过额外的参数来传递部分结果,让递归调用能够接着往下计算。通过使用生成器,所有的递归调用都只需生成其负责部分的结果。前面的递归版flatten就是这样做的,你可使用这种策略来遍历图结构和树结构。

然而,在有些应用程序中,你不能马上得到答案。你必须尝试多次,且在每个递归层级中都如此。对于需要尝试所有组合直到找到答案的问题,回溯策略很有帮助。这种问题的解决方案类似于下面这样:

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

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

(2)问题

八皇后问题如下:你需要将8个皇后放在国际象棋棋盘上,并保证任何一个皇后都不能威胁其他皇后。(HDOJ-2553提供了标准化的问题描述)

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

(3)状态表示

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

(4)冲突检测

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

def conflict(state, nextX):nextY = len(state)for i in range(nextY):# 如果下一个皇后和当前皇后的水平距离为0(在同一列)或与它们的垂直距离相等(位于一条对角线上)if abs(state[i] - nextX) in (0, nextY - i):return Truereturn False

(5)基线条件:只剩下最后一个皇后没有放好。此时遍历所有可能的位置,并返回那些不会引发冲突的位置。代码如下。

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

(6)递归条件:基线条件的对立。因此递归部分为不满足基线条件的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

(7)结果验证

为了验证结果的正确性,下面定义了一个打印函数,并从解集中随机选择了一个解来打印。

def prettyprint(solution):def line(pos, length=len(solution)):return '. ' * (pos) + 'X ' + '. ' * (length-pos-1)for pos in solution:print(line(pos))
import random
prettyprint(random.choice(list(queens(8))))

以下是一个可能的结果。

 . . . . . X . .. X . . . . . .. . . . . . X .X . . . . . . .. . . X . . . .. . . . . . . X. . . . X . . .. . X . . . . .

这个结果对应的国际象棋棋盘如下图所示。

在这里插入图片描述

你可以下载作为本文附件的代码,然后尝试运行。

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com

热搜词