Python 基础学习11:魔法方法、特性、迭代器

在 Python 中开头和结尾都使用两个下划线的名称很特别具有特殊的含义,在自己编写程序时不要创建类似的名称。这种名称通常称为魔法方法。

构造函数

构造函数在对象创建后会自动调用它,构造函数命名为 __init__

class FooBar:
    def __init__(self):
        self.somevar = 66


f = FooBar()
f.somevar
66

给构造函数添加参数:

class Foobar:
    def __init__(self, value=66):
        self.somevar = value


f = FooBar()
f.somevar
66

当然也可以指定参数:

class FooBar:
    def __init__(self, value=66):
        self.somevar = value


f = FooBar('This is new value')
f.somevar
'This is new value'

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

重写是继承机制的一个重要特性,重写构造函数时会遇到必须调用超类的构造函数,否则可能无法正确的初始化对象。

class Brid:
    def __init__(self):
        self.hungry = True
    def eat(self):
        if self.hungry:
            print('Aaaah...')
        else:
            print('No, thanks')


class SongBrid(Brid):
    def __init__(self):
        self.sound = 'Squawk!'
    def sing(self):
        print(self.sound)


sb = SongBrid()
sb.sing()
Squawk!

sb.eat()
Traceback (most recent call last):
  File "/usr/lib64/python3.12/idlelib/run.py", line 580, in runcode
    exec(code, self.locals)
  File "<pyshell#23>", line 1, in <module>
  File "<pyshell#14>", line 5, in eat
AttributeError: 'SongBrid' object has no attribute 'hungry'

SongBrid 继承了 Brid ,但是在 SongBrid 中也有 __init__ 方法,导致 Brid 原有的 __init__ 方法被重写了,所以在后续调用 eat 方法时报错。

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

SongBrid 的构造函数中添加一行即可实现对超类调用构造函数:

class SongBrid(Brid):
    def __init__(self):
        self.sound = 'Squawk!'
        Brid.__init__(self)
    def sing(self):
        print(self.sound)


sb = SongBrid()
sb.eat()
Aaaah...

通过类调用方法(Brid.__init__)就没有实例与其关联,这种情况下可以随便设置参数 self,这样的方法称为未关联的。

使用 super 函数

调用这个函数时,将当前类和当前实例作为参数,对其返回的对象掉眼泪能够方法时,调用的将是超类的方法。所以在前面的例子中可以不适用 Brid.__init__ 而是使用 super(Brid, self)
调用 super 函数时,可以不提供任何参数:

class Brid:
    def __init__(self):
        self.hungry = True
    def eat(self):
        if self.hungry:
            print('Aaaah...')
        else:
            print('No, thanks')


class SongBrid(Brid):
    def __init__(self):
        super().__init__()
        self.sound = 'Squawk!'
    def sing(self):
        print(self.sound)


sb = SongBrid()
sb.eat()
Aaaah...

即使多个超类,也只需调用函数 super 一次。

元素访问

在类中有一些有用的魔术方法,能够创建行为类似于序列或映射的对象。

基本的序列和映射协议

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

  • __len__(self):此方法应返回集合包含的 item 数,如果返回零(且没有实现覆盖这种行为的 __nozero__)对象在布尔上下文中被视为假(类似于空列表字典一样)。
  • __getitem__(self), key:返回与指定键相关联的值。
  • __setitem__(self, key, value) :以与键相关联的方式存储值。
  • __delitem__(self, key):删除与 key 相关联的值,仅当对象可变时才需要。

示例:

def check_index(key):
    if not isinstance(key, int): raise TypeError
    if 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

s.changed
{4: 2}

s[-1]
Traceback (most recent call last):
  File "/usr/lib64/python3.12/idlelib/run.py", line 580, in runcode
    exec(code, self.locals)
  File "<pyshell#23>", line 1, in <module>
  File "<pyshell#17>", line 7, in __getitem__
  File "<pyshell#3>", line 3, in check_index
IndexError

s['four']
Traceback (most recent call last):
  File "<pyshell#25>", line 1, in <module>
    s['four']
  File "<pyshell#17>", line 7, in __getitem__
    check_index(key)
  File "<pyshell#3>", line 2, in check_index
    if not isinstance(key, int): raise TypeError
TypeError

从 list dict 和 str 派生

基本的四个方法可以满足基本的需求,但序列还有很多其它有用的魔法方法,一一手动去实现不太现实,最方便的方法就是继承。

如果要实现一种类似于内置列表的序列类型,可以直接继承 list
示例:

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


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]
cl.counter
0
cl[2] + cl[8]
8
cl.counter
2

特性

如果访问个定属性时必须采取特定的措施,那么封装状态变量很重要。
示例:

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((20,10))
r.get_size()
(20, 10)

get_sizeset_sizesize 的存取方法,Python 可以隐藏存储方法,让所有的属性看起来都一样。
通过存取方法定义的属性称为特性(property)。

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


r = Rectangle()
r.width = 10
r.height = 5
r.get_size()
(10, 5)

r.size = 20,10
r.size
(20, 10)

通过调用函数 property 并将存取方法作为参数(获取方法在前,设置方法在后)创建了一个特性,然后将名称 size 关联到这个特性。
实际上,调用函数 property 还可不指定参数、指定一个参数、指定三个参数或指定四个参数:

  • 没有任何参数:创建的特性既不可读也不可写
  • 指定一个参数(获取方法):创建的特性是只读的
  • 第三个参数:可选的,指定用于删除属性的方法(这个方法不接受任何参数)
  • 第四个参数:可选的,指定一个文档字符串。
    这些参数分别名为 fget fset fdel doc

静态方法和类方法

旧的实现方式是,将静态方法和类方法分别包装到 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)


Myclass.smeth()
This is a static method
Myclass.cmeth()
This is a class method of <class '__main__.Myclass'>

手工包装和替换的方式较为繁琐,从 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 中静态方法不怎么重要,因为可以直接使用普通函数进行替代。

getattr setattr 等方法

可以拦截对对象属性的所有访问企图,用途之一是在旧式类中实现特性。
要在属性被访问时执行一段代码,必须使用一些魔法方法。
有四个魔法方法,在旧式类中,只需使用后面三个:

  • __getattribute__(self, name):在属性被访问时自动调用(只适用于新式类)
  • __getattr__(self, name):在属性被访问而对象没有这样的属性时自动调用
  • __setattr__(self, name):试图给属性赋值时自动调用
  • __delattr__(self, name):试图删除属性时自动调用
    在可能的情况下,尽可能的使用函数 property

示例:

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()


r = Rectangle()
r.size = (100, 200)
r.size
(100, 200)

迭代器

魔法方法 __iter__ ,它是迭代器协议的基础。

迭代器协议

迭代意味着重复多次,就像循环一样。除了使用 for 循环迭代序列和字典外,也可以迭代其它对象——实现了 __iter__ 的对象。

方法 __iter__ 返回一个迭代器,它是包含方法 __nexe__ 的对象,而调用这个方法时可不提供任何参数。
当调用 __nexe__ 时,迭代器应返回其下一个值,如果迭代器没有可供返回的值,将引发 StopIteration 异常。还可以使用内置的函数 next,此时 next(it)it.__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


1597

从迭代器创建序列

除了对迭代器进行迭代外,还可以将其转换为序列。
示例:

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]

生成器

生成器是一种使用普通函数语法定义的迭代器。

创建生成器

生成器创建起来与普通函数一样简单,但是函数中会包含 yield 语句,包含该语句的函数都被称为生成器。
示例:

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


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

list(flatten(nested))
[1, 2, 3, 4, 5]

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

递归式生成器

前面的例子中出现了两个 for 循环,因此只能处理两层的嵌套,如果要处理任意层的嵌套呢?就需要使用到递归。

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]))
[1, 2, 3, 4, 5, 6]

如果被处理的对象是字符串,字符串也是序列但不想对其进行迭代,就需要在生成器开头进行检查。最简单的方式就是尝试对字符串进行拼接,并检查拼接是否会引发 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


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

通用生成器

生成器是包含关键字 yield 的函数,但被调用时不会执行函数体内的代码,而是返回一个迭代器。每次请求时都会执行生成器代码,直到遇到 yieldreturn

生成器由两个部分组成:生成器函数和生成器迭代器。
生成器函数是由 def 定义的,其中包含 yield。生成器的迭代器是这个函数的返回结果。

def simple_generator():
    yield 1


simple_generator
<function simple_generator at 0x7f2ee3d6fd80>
simple_generator()
<generator object simple_generator at 0x7f2ee4c4fed0>

生成器的方法

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

  • 外部世界:外部世界可以访问生成器的方法 send ,接受一个参数(要发送的信息,可以是任何对象)
  • 生成器:在挂起的生成器内部,yield 可能用作表达式而不是语句,也就是当生成器重新运行时,yield 返回一个值——通过 send 从外部世界发送的值。
    仅当生成器被挂起时(即遇到第一个 yield),使用 send 才有意义。
def repeater(value):
    while True:
        new = (yield value)
        if new is not None: value = new


r = repeater('hi')
next(r)
'hi'
r.send('hello world')
'hello world'

此外生成器还包括两个方法:

  • throw:用于在生成器中引发异常,调用时可提供一个异常类型一个可选值和一个 traceback 对象
  • close:用于停止生成器,调用时无需提供任何参数

八皇后问题

八皇后问题用到的核心点是回溯。
示例:

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

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

list(queens(4))
[(1, 3, 0, 2), (2, 0, 3, 1)]
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. . 

发表评论

您的电子邮箱地址不会被公开。 必填项已用 * 标注

滚动至顶部