高级编程#

1. 几种方法#

1.1. 实例方法#

当方法所实现功能与具体实例有关时,通常定义为实例方法。如下,在列表中存储待办事项。add_event() 方法是实例方法,因为不同的日程表对象会存储不同的待办事项。

class Calendar:
    def __init__(self):
        self.events = []

    def add_event(self, event):
        self.events.append(event)

    def is_weekend(self, date):
        return date.weekday() > 4

1.2. 静态方法#

因为 is_weekend() 不关心具体的日程表实例,即和 event 没有关系,通过删除其第一个参数 self,并使用@staticmethod 装饰器定义,is_weekend() 成为了一个静态方法。

class Calendar:
    def __init__(self):
        self.events = []

    def add_event(self, event):
        self.events.append(event)

    @staticmethod
    def is_weekend(date):
        return date.weekday() > 4

约定成俗的是,使用类名直接调用会更加地明确。在一些特殊场景,如,在类的实例方法定义中调用静态方法时,会使用实例进行调用。

from datetime import date
today = date.today()

Calendar.is_weekend(today)

静态方法并不常用,从语法上来说静态方法完全可以从 class 中提取出,单独作为一个函数存在。通常当某函数与某类 class 的功能有业务上的紧密或唯一联系时,才会以静态方法形式作为类的一部分存在。静态方法体中没有使用任何的类或者对象属性。

1.3. 类方法#

假设我们可以从本地文件中读取构建返回一个日程表实例,如何实现?

class Calendar:
    def __init__(self):
        self.events = []

    def add_event(self, event):
        self.events.append(event)

    @staticmethod
    def is_weekend(date):
        return date.weekday() > 4

    @staticmethod
    def from_json(filename):
        c = Calendar()
        ...  # 读取文件、添加待办事项等操作,省略
        return c  # 构建日程表对象实例 c 并返回

从文件中读取信息构建实例,定义一个静态方法 from_json(),以上程序在功能实现上暂时没有问题。 注意到 from_json() 中日程表实例的实现是 c = Calendar(),而这在类的继承中会带来一个问题。

class WorkCalendar(Calendar):
    pass

c = WorkCalendar.from_json("mycal.calendar")

c = WorkCalendar.from_json("mycal.calendar") 子类 WorkCalendar 调用 from_json(),实际创建返回的是父类 Calendar 的对象实例,因为 from_json() 方法体中写死了 c = Calendar(),而我们期待的是返回 WorkCalendar 类的对象实例。

使用场景:类方法十分地常用,尤其常作为替代构建函数(alternative constructors)使用,类方法名中通常带有 from_xx()make_xx()create_xx() 等,功能是使用不同的参数集构建对象。 类方法会把调用自己的类作为第一个方法参数 cls

class Calendar:
    def __init__(self):
        self.events = []

    def add_event(self, event):
        self.events.append(event)

    @staticmethod
    def is_weekend(date):
        return date.weekday() > 4

    @classmethod
    def from_json(cls, filename):
        c = cls()
        ...  # 读取文件、添加待办事项等操作,省略
        return c  # 构建日程表对象实例 c 并返回

同静态方法的调用一样,通常直接使用类调用类方法,不过使用实例调用类方法也完全没有问题。在 Python 中,静态方法和类方法都是通过描述符协议(descriptor protocol)实现。

2. 迭代控制#

2.1. 索引与切片#

要表现得像列表那样按照下标取出元素,需要实现 __getitem__() 方法。但,__getitem__() 传入的参数可能是一个 int,也可能是一个切片对象 slice,故要做判断:

class Fib():

    def __getitem__(self, n):
        if isinstance(n, int): # n 是索引
            a, b = 1, 1
            for x in range(n):
                a, b = b, a + b
            return a

        if isinstance(n, slice): # n 是切片
            start = n.start
            stop = n.stop
            if start is None:
                start = 0
            a, b = 1, 1
            L = []
            for x in range(stop):
                if x >= start:
                    L.append(a)
                a, b = b, a + b
            return L

f = Fib()
print(f[0:5])
print(f[:10])

2.2. 迭代器#

魔法方法

类方法

异步方法

__iter__

iter()

__aiter__

__reversed__

reversed()

__next__

next()

__anext__

__concat__

+

若一个类想被用于 for 循环,就必须实现一个 __iter__() 方法,该方法返回一个迭代对象,然后,Python 的 for 循环就会不断调用该迭代对象的 __next__() 方法拿到循环的下一个值,直到遇到 StopIteration 错误时退出循环。

构建可迭代的对象和迭代器时经常会出现错误,原因是混淆了二者。要知道,可迭代的对象有个 __iter__ 方法,每次都实例化一个新的迭代器;而迭代器要实现 __next__ 方法, 返回单个元素,此外还要实现 __iter__ 方法,返回迭代器本身。

class Fib:

    def __init__(self):
        self.a = 0
        self.b = 1  # 初始化两个计数器 a,b

    def __iter__(self):
        return self  # 实例本身就是迭代对象,故返回自己

    def __next__(self):
        self.a, self.b = self.b, self.a + self.b
        if self.a > 1000:
            raise StopIteration()
        return self.a


for n in Fib():
    print(n)

3. 上下文#

魔法方法

异步方法

__enter__

__aenter__

__exit__

__aexit__

3.1. contextmanager#

在 Python 中,读写文件这样的资源要特别注意,必须在使用完毕后正确关闭它们。正确关闭文件资源的一个方法是使用 try...finally 或简化为 with

with open('/path/to/file', 'r') as f:
    f.read()

并不是只有 open() 函数返回的 f 对象才能使用 with 语句。实际上,任何对象,只要正确实现了上下文管理,就可用于 with 语句。实现上下文管理是通过 __enter____exit__ 这两个方法实现的。这样我们就可把自己写的资源对象用于 with 语句:

class Query:

    def __init__(self, name):
        self.name = name

    def query(self):
        print(f'Query info about {self.name}...')

    def __enter__(self):
        print('Begin')
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        if exc_type:
            print('Error')
        else:
            print('End')


with Query('Bob') as q:
    q.query()

这样仍然很繁琐,因此 Python 的标准库 contextlib 提供了更简单的写法,contextmanager 这个装饰器接受一个生成器,用 yield 语句把 with ... as var 把变量输出出去,然后,with 语句就可正常地工作了:

from contextlib import contextmanager


class Query:

    def __init__(self, name):
        self.name = name

    def query(self):
        print(f'Query info about {self.name}...')


@contextmanager
def create_query(name):
    # __enter__
    print('Begin')
    q = Query(name)
    yield q
    # __exit__
    print('End')


with create_query('Bob') as q:
    q.query()

很多时候,我们希望在某段代码执行前后自动执行特定代码,也可用 @contextmanager 实现。例如:

from contextlib import contextmanager


@contextmanager
def tag(name):
    print(f"<{name}>")
    yield
    print(f"</{name}>")


with tag("h1"):
    print("hello")
    print("world")

代码的执行顺序是:

  1. with 语句首先执行 yield 之前的语句,因此打印出

  2. yield 调用会执行 with 语句内部的所有语句;

  3. 最后执行 yield 之后的语句,打印出。

故,@contextmanager 让我们通过编写生成器,它的作用就是把任意对象变为上下文对象,并支持 with 语句。来简化上下文管理。

3.2. closing()#

若一个对象没有实现上下文,我们就不能把它用于 with 语句。这个时候,可用 closing() 来把该对象变为上下文对象。例如,用 with 语句使用 urlopen()

from contextlib import closing
from urllib.request import urlopen

with closing(urlopen('https://www.python.org')) as page:
    for line in page:
        print(line)

closing 亦为一个经过 @contextmanager 装饰的生成器,它的作用就是把任意对象变为上下文对象,并支持 with 语句。

3.3. 嵌套上下文#

import contextlib


@contextlib.contextmanager
def test_context(name):
    print(f'enter, my name is {name}')

    yield

    print(f'exit, my name is {name}')


with test_context('aaa'):
    with test_context('bbb'):
        print('========== in main ============')

# 等价于
with test_context('aaa'), test_context('bbb'):
    print('========== in main ============')

4. 自定义类装饰器#

类装饰器是一个简单的函数,它接收一个类实例作为参数,并返回一个新的类或原类的修改版本。当你想用最少的模板来修改一个类的每个方法或属性时,类装饰器是很有用的。

4.1. 无参数装饰器#

基于类装饰器的实现,必须实现 __call____init__ 两个内置函数。

  • __init__ :接收被装饰函数。

  • __call__ :实现装饰逻辑。

class logger:

    def __init__(self, func):
        self.func = func

    def __call__(self, *args, **kwargs):
        print(f"[INFO]: the function {self.func.__name__}() is running...")
        return self.func(*args, **kwargs)


@logger
def say(something):
    print(f"say {something}!")

say("hello")
# [INFO]: the function say() is running...
# say hello!

4.2. 参数装饰器#

带参数和不带参数的类装饰器有很大的不同。

  • __init__ :不再接收被装饰函数,而是接收传入参数。

  • __call__ :接收被装饰函数,实现装饰逻辑。

class logger:

    def __init__(self, level='INFO'):
        self.level = level

    def __call__(self, func):  # 接受函数

        def wrapper(*args, **kwargs):
            print(
                f"[{self.level}]: the function {func.__name__}() is running..."
            )
            func(*args, **kwargs)

        return wrapper  #返回函数


@logger(level='WARNING')
def say(something):
    print("say {something}!")

say("hello")
# [WARNING]: the function say() is running...
# say hello!