Python基础教程-第9章-魔法方法、特性和迭代器

本文详细介绍了Python中的魔法方法,包括构造函数`__init__`,以及如何处理元素访问,如序列和映射协议。文章讨论了特性(property)和迭代器,展示了如何创建和使用生成器解决八皇后问题。此外,还探讨了构造函数中的super函数用法及其优势,以及静态方法和类方法的概念。
摘要由CSDN通过智能技术生成

在Python中,有些名称很特别,开头和结尾都是两个下划线。你在本书前面已经见过一些, 如__future__。
在这样的名称中,很大一部分都是魔法(特殊)方法的名称。如果你的对象实现了这些方法,它们将在特定情况下(具体是哪种情况取决于方法的名称)被Python调用,而几乎不需要直接调用
本章讨论几个重要的魔法方法,其中最重要的是__init__以及一些处理元素访问的方法(它 们让你能够创建序列或映射)。本章还将讨论两个相关的主题:特性(property)和迭代器(iterator)。

9.1 构造函数

我们要介绍的第一个魔法方法是构造函数。你可能从未听说过构造函数(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

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

第7章介绍了继承。每个类都有一个或多个超类,并从它们那里继承行为。对类B的实例调用方法(或访问其属性)时,如果找不到该方法(或属性),将在其超类A中查找。

class A:
    def hello(self):
        print("Hello, I'm A.")
class B(A): 
    pass
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

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

重写构造函数时,必须调用超类(继承的类)的构造函数,否则可能无法正确地初始化对象。

class Bird:							# 父类
    def __init__(self):
        self.hungry = True 
    def eat(self):
        if self.hungry: 
            print('Aaaah ...')
            self.hungry = False
        else:
            print('No, thanks!')
bird = Bird()
bird.eat()
bird.eat()
class SongBird(Bird):      			# 子类
    def __init__(self):
        self.sound = 'Squawk!' 
    def sing(self):
        print(self.sound)
sb = SongBird()
sb.sing()
sb.eat()
# 报错 'SongBird' object has no attribute 'hungry'

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

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

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

对实例调用方法时,方法的参数self将自动关联到实例(称为关联的方 法),这样的示例你见过多个。
然而,如果你通过类调用方法(如Bird._init_),就没有实例 与其相关联。在这种情况下,你可随便设置参数self。这样的方法称为未关联的。

9.1.3 使用函数 super

新式类中应该使用super。
调用这个函数时,将当前类和当前实例作为参数。对其返回的对象调用方法时,调用的将是超类(而不是当前类)的方法。因此,在SongBird的构造函数中,可不使用Bird, 而是使用super(SongBird, self)。另外,可像通常那样(也就是像调用关联的方法那样)调用方法__init__。

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

使用super的优点

  • 使用函数super更直观
  • 即便有多个超类,也只需调用函数super一次(条件 是所有超类的构造函数也使用函数super)

9.2 元素访问

9.2.1 基本的序列和映射协议

序列和映射基本上是元素(item)的集合。

  • _len_(self) 这个方法应返回集合包含的项数,对序列来说为元素个数,对映射来说 为键值对数。
  • _getitem_(self, key):这个方法应返回与指定键相关联的值
  • _setitem_(self, key, value):这个方法应以与键相关联的方式存储值,以便以后能够 使用__getitem__来获取。
  • _delitem_(self, key):这个方法在对对象的组成部分使用__del__语句时被调用,应 删除与key相关联的值。
    示例代码
class ArithmeticSequence:
    def __init__(self, start=0, step=1):
        self.start = start 
        self.step = step
        self.changed = {}
    
    def __getitem__(self,key):
        try: 
            return self.changed[key] 
        except KeyError:
            return self.start + key * self.step
        
    def __setitem__(self, key, value):
        self.changed[key] = value
s = ArithmeticSequence(1, 2)
s[4]     # 9
s[10000] # 20001

上面这个代码实现的是一个无穷序列。通过魔方方法来实现索引不存在时的处理

9.2.2 从list、dict和str派生

序列中还有很多的魔法方法可以通过继承得到。重写其中的魔法方法来进行修改,这里可以从list、dict和str中继承得到
来看一个简单的示例——一个带访问计数器的列表。

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)
cl = CounterList(range(10))       # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] 
cl[0] + cl[2]				      # 2
cl.counter			# 访问计数器

9.3 特性

第7章中提到了存取方法,它们是名称类似于getHeight和setHeight的方法,用于获取或设置属性。
下面是示例

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
print(r.get_size())   # (10, 5)
r.set_size((150, 100))
r.width			      # 150

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

9.3.1 函数property

函数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
    size = property(get_size, set_size)

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

9.3.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:
    @staticmethod 
    def smeth():
        print('This is a static method')
        
    @classmethod
    def cmeth(cls):
        print('This is a class method of', cls)

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

MyClass.smeth()
MyClass.cmeth()

9.3.3 _getattr_、__setattr__等方法

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

9.4 迭代器

9.4.1 迭代器协议

迭代(iterate)意味着重复多次,就像循环那样。本书前面只使用for循环迭代过序列和字典, 但实际上也可迭代其他对象:实现了方法__iter__的对象。
方法__iter__返回一个迭代器,它是包含方法__next__的对象,当你调用方法__next__时,迭代器应返回其下一个值。
你还可使用内置的便利函数next,在这种情况下,next(it)与 it.next()等效。
更正规的定义是,实现了方法__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
fibs = Fibs()
for f in fibs:
    if f > 1000: 
        print(f)
        break

9.4.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]

9.5 生成器

生成器是一个相当复杂的概 念,你可能需要花些功夫才能明白其工作原理和用途。虽然生成器让你能够编写出非常优雅的代 码,但请放心,无论编写什么程序,都完全可以不使用生成器。

9.5.1 创建生成器

下面用一个示例说明,顺序提供列表的列表中的元素

def flatten(nested):
	for sublist in nested:
		for element in sublist: 
			yield element
nested = [[1, 2], [3, 4], [5]]
for num in flatten(nested):
	print(num)

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

9.5.2 递归式生成器

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

def flatten(nested): 
    try:
        for sublist in nested:
            for element in flatten(sublist): 
                yield element 
    except TypeError:
        yield nested
list(flatten([[[1], 2], 3, 4, [5, [6, 7]], 8]))

9.5.3 通用生成器

生成器是包含关键字 yield的函数,但被调用时不会执行函数体内的代码,而是返回一个迭代器。
每次请求值时,都将执行生成器的代码,直到遇到yield或return。yield意味着应生成一个值,而return意味着生成器应停止执行(即不再生成值;仅当在生成器调用return时,才能不提供任何参数)。
生成器由两个单独的部分组成:生成器的函数和生成器的迭代器。生成器的函数是由def语句定义的,其中包含yield。

def simple_generator(): 
 	yield 1

9.5.4 生成器的方法

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

  • 外部世界 外部世界可访问生成器的方法send,这个方法类似于next,但接受一个参数(要 发送的“消息”,可以是任何对象)。
  • 生成器 在挂起的生成器内部,yield可能用作表达式而不是语句。当生成器重新运行时,yield将返回一个值–通过send从外部世界发送的值
def repeater(value):
    while True:
        new = (yield value)
        if new is not None: 
            value = new
r = repeater(42)
next(r)
r.send("Hello, world!")

9.6 八皇后问题

9.6.1 生成器的回溯

对于逐步得到结果的复杂递归算法,非常适合使用生成器来实现。
要在不使用生成器的情况 下实现这些算法,通常必须通过额外的参数来传递部分结果,让递归调用能够接着往下计算。通过使用生成器,所有的递归调用都只需生成其负责部分的结果。
对于需要尝试所有组合直到找到答案的问题,这种回溯策略对其解决很有帮助。

9.6.2 问题

国际象棋中的皇后比中国象棋里的大车还厉害,皇后能横向,纵向和斜向移动,在这三条线上的其他棋子都可以被吃掉。所谓八皇后问题就是:将八位皇后放在一张8x8的棋盘上,使得每位皇后都无法吃掉别的皇后,(即任意两个皇后都不在同一条横线,竖线和斜线上),问一共有多少种摆法。此问题是在1848年由棋手马克思·贝瑟尔提出的,后面陆续有包括高斯等大数学家们给出自己的思考和解法,所以此问题不只是有年头了,简直比82年的拉菲还有年头,我们今天不妨尝尝这老酒。
我们先举例来理解一下这个问题的场景到底是什么样子的,下面的绿色格子是一个皇后在棋盘上的“封锁范围”,其他的皇后不能放置在这些绿格子中:


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

9.6.3 状态表示

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

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

9.6.5 基线条件

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

def queens(num, state):
    if len(state) == num-1:
	    for pos in range(num):
	        if not conflict(state, pos):
	            yield pos
list(queens(4, (1, 3, 0)))
# 2

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

9.6.6 递归条件

处理好基线条件后,可在递归条件中假设来自更低层级(编号更大的皇后)的结果都是正确的。因此,只需在函数queens的前述实现中给if语句添加 一个else子句。
对于递归调用,向它提供的是由当前行上面的皇后位置组成的元组。对于当前皇后的 每个合法位置,递归调用返回的是由下面的皇后位置组成的元组。为了让这个过程不断进行下去, 只需将当前皇后的位置插入返回的结果开头。

def queens(num=8, state=()):
    for pos in range(num):
        if not conflict(state, pos):
            if len(state) == num-1:
                yield (pos,)       # (pos,)中的逗号必不可少(不能仅用圆括号将pos括起),这样得到的才是元组
            else:
                for result in queens(num, state + (pos,)):
                    yield (pos,) + result
list(queens(4))
# [(1, 3, 0, 2), (2, 0, 3, 1)]
len(list(queens(8)))
# 92 				八皇后问题有92个解法
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值