Python0902-魔术方法-上下文管理

上下文管理

文件IO操作可以对文件对象使用上下文管理,使用with … as 语法。

with open('test') as f:
    pass

仿照上例写一个自己的类,实现上下文管理

class Point:
    pass


with Point() as f:  # AttributeError: __exit__
    pass

提示属性错误, 没有exit,看了需要这个属性

上下文管理对象

当一个对象同时实现了enter()和exit()方法,它就属于上下文管理的对象

方法意义
__enter__进入与此对象相关的上下文。如果存在该方法,with语法会把该方法的返回值作为绑定到as子句中指定的变量上
__exit__退出与此对象相关的上下文
class Point:
    def __init__(self):
        print('init')


    def __enter__(self):
        print('enter')


    def __exit__(self, exc_type, exc_val, exc_tb):
        print('exit')


with Point() as f:
    print('do sth.')
~~~~~~~~~~~~~~~~~~~~~~~~~~~
init
enter
do sth.
exit

实例化对象的时候,并不会调用enter,进入with语句块调用enter方法,然后执行语句体,最后离开with语句块的时候,调用exit方法。
with可以开启一个上下文运行环境,在执行前做一些准备工作,执行后做一些收尾工作。

上下文管理的安全性

看看异常对上下文的影响。

class Point:
    def __init__(self):
        print('init')


    def __enter__(self):
        print('enter')


    def __exit__(self, exc_type, exc_val, exc_tb):
        print('exit')


with Point() as f:
    raise Exception('error')
    print('do sth.')
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
init
enter
exit

raise Exception('error')

可以看出在enter和exit照样执行,上下文管理是安全的

极端的例子
调用sys.exit(), 它会推出当前解释器。
打开Python解释器,在里面敲入sys.exit(),窗口直接关闭了。也就是说碰到这一句,Python运行环境直接退出了。

import sys


class Point:
    def __init__(self):
        print('init')


    def __enter__(self):
        print('enter')


    def __exit__(self, exc_type, exc_val, exc_tb):
        print('exit')


with Point() as f:
    sys.exit(-100)
    print('do sth.')


print('outer')
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
init
enter
exit

Process finished with exit code -100

从执行结果来看,依然执行了exit函数,哪怕是退出Python运行环境。
说明上下文管理很安全

with语句

class Point:
    def __init__(self):
        print('init')


    def __enter__(self):
        print('enter')


    def __exit__(self, exc_type, exc_val, exc_tb):
        print('exit')


p = Point()
with Point() as f:
    print(p == f)  # 为什么不相等
    print('do sth.')
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
init
init
enter
False
do sth.
exit

问题在于enter方法上,它将自己的返回值赋给f。修改上例

class Point:
    def __init__(self):
        print('init')


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


    def __exit__(self, exc_type, exc_val, exc_tb):
        print('exit')


p = Point()
with p as f:
    print(p == f)
    print('do sth.')
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
init
enter
True
do sth.
exit

enter方法和exit方法的参数

enter方法,没有其他参数。
exit方法有3个参数:
exit(self, exc_type, exc_val, exc_tb):
这三个参数都与异常有关。
如果该上下文退出时没有异常,这3个参数都为None。
如果有异常,参数意义如下:

参数意义
exc_type异常类型
exc_value异常的值
exc_tb异常的追踪信息

exit方法返回一个等效True的值,则压制异常;否则抛出异常

class Point:
    def __init__(self):
        print('init')


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


    def __exit__(self, exc_type, exc_val, exc_tb):
        print(exc_type)
        print(exc_val)
        print(exc_tb)
        print('exit')
        return "abc"


p = Point()
with p as f:
    raise Exception('New Error')
    print('do sth.')
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
init
enter
<class 'Exception'>
New Error
<traceback object at 0x00000145D258F808>
exit

练习

  • 为加法函数计时
    • 方法1:使用装饰器显示该函数的执行时长
    • 方法2:使用上下文管理方法来显示该函数的执行时长
import time

def add(x, y):
    time.sleep(2)
    return x + y

装饰器实现1

import time
import datetime
from functools import wraps


def timeit(fn):
    @wraps(fn)
    def wrapper(*args, **kwargs):
        start = datetime.datetime.now()
        ret = fn(*args, **kwargs)
        delta = (datetime.datetime.now() - start).total_seconds()
        print("{} took {}s".format(fn.__name__, delta))
        return ret
    return wrapper


@timeit
def add(x, y):
    time.sleep(2)
    return x + y

print(add(4, 5))
~~~~~~~~~~~~~~~~~~~~~~~~~~
add took 2.000429s
9

装饰器实现2

import time
import datetime


def timeit(output=lambda fn, delta: print("{} took {}s".format(fn.__name__, delta))):
    def timeit(fn):
        def wrapper(*args, **kwargs):
            start = datetime.datetime.now()
            ret = fn(*args, **kwargs)
            delta = (datetime.datetime.now() - start).total_seconds()
            output(fn, delta)
            return ret
        return wrapper
    return timeit

@timeit()  # add = timeit(add)
def add(x, y):
    time.sleep(2)
    return x + y


add(4, 5)  # wrapper
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
add took 2.016718s

上下文管理

基本实现
import time
import datetime
from functools import wraps


def timeit(fn):
    @wraps(fn)
    def wrapper(*args, **kwargs):
        start = datetime.datetime.now()
        ret = fn(*args, **kwargs)
        delta = (datetime.datetime.now() - start).total_seconds()
        print("{} took {}s".format(fn.__name__, delta))
        return ret
    return wrapper


@timeit
def add(x, y):
    time.sleep(2)
    return x + y


class TimeIt:
    def __init__(self, fn):
        self.fn = fn

    def __enter__(self):
        self.start = datetime.datetime.now()
        return self.fn

    def __exit__(self, exc_type, exc_val, exc_tb):
        delta = (datetime.datetime.now() - self.start).total_seconds()
        print("{} took {}s".format(self.fn.__name__, delta))


with TimeIt(add) as fn:
    print(fn(4, 6))
    print(add(4, 5))
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
add took 2.000976s
10
add took 2.000243s
9
add took 4.001219s
另一种实现,使用可调用对象实现
import time
import datetime
from functools import wraps


def timeit(fn):
    @wraps(fn)
    def wrapper(*args, **kwargs):
        start = datetime.datetime.now()
        ret = fn(*args, **kwargs)
        delta = (datetime.datetime.now() - start).total_seconds()
        print("{} took {}s".format(fn.__name__, delta))
        return ret
    return wrapper


@timeit
def add(x, y):
    time.sleep(2)
    return x + y


class TimeIt:
    def __init__(self, fn):
        self.fn = fn

    def __enter__(self):
        self.start = datetime.datetime.now()
        return self.fn

    def __exit__(self, exc_type, exc_val, exc_tb):
        delta = (datetime.datetime.now() - self.start).total_seconds()
        print("{} took {}s".format(self.fn.__name__, delta))


    def __call__(self, x, y):
        print(x, y)
        return self.fn(x, y)

with TimeIt(add) as timeitobj:
    print(timeitobj(4, 6))
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
add took 2.000261s
10
add took 2.000261s
根据上面的代码,能不能把类当做装饰器用?
import time
import datetime
from functools import wraps


class TimeIt:
    """This is TimeIt function."""
    def __init__(self, fn):
        self.fn = fn

    def __enter__(self):
        self.start = datetime.datetime.now()
        return self.fn

    def __exit__(self, exc_type, exc_val, exc_tb):
        delta = (datetime.datetime.now() - self.start).total_seconds()
        print("{} took {}s".format(self.fn.__name__, delta))


    def __call__(self, *args, **kwargs):
        self.start = datetime.datetime.now()
        ret = self.fn(*args, **kwargs)
        self.delta = (datetime.datetime.now() - self.start).total_seconds()
        print("{} took {}s. call".format(self.fn.__name__, self.delta))
        return ret


@TimeIt
def add(x, y):
    """This is add function."""
    time.sleep(2)
    return x + y


add(4, 5)
print(add.__doc__)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
add took 2.000246s. call
This is TimeIt function.
思考:如何解决文档字符串问题?
方法一:直接修改doc
class TimeIt:
    """This is TimeIt function."""
    def __init__(self, fn):
        self.fn = fn
        # 把函数对象的文档字符串赋给类
        self.__doc__ = fn.__doc__
方法二:使用functools.wraps函数
import time
import datetime
from functools import wraps, update_wrapper


class TimeIt:
    """This is TimeIt function."""
    def __init__(self, fn):
        self.fn = fn
        # 把函数对象的文档字符串赋给类
        # self.__doc__ = fn.__doc__
        # update_wrapper(self, fn)
        wraps(fn)(self)

    def __enter__(self):
        self.start = datetime.datetime.now()
        return self.fn

    def __exit__(self, exc_type, exc_val, exc_tb):
        delta = (datetime.datetime.now() - self.start).total_seconds()
        print("{} took {}s".format(self.fn.__name__, delta))


    def __call__(self, *args, **kwargs):
        self.start = datetime.datetime.now()
        ret = self.fn(*args, **kwargs)
        self.delta = (datetime.datetime.now() - self.start).total_seconds()
        print("{} took {}s. call".format(self.fn.__name__, self.delta))
        return ret


@TimeIt
def add(x, y):
    """This is add function."""
    time.sleep(2)
    return x + y


print(add(10, 5))
print(add.__doc__)


print(TimeIt(add).__doc__)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
add took 2.000424s. call
15
This is add function.
This is add function.

上面的类既可以用在上下文管理,有可以用作装饰器

上下文应用场景

  • 增强功能
    • 在代码执行的前后增加代码,以增强其功能。类似装饰器的功能。
  • 资源管理
    • 打开了资源需要关闭,例如文件对象、网络连接、数据库连接等
  • 权限验证
    • 在执行代码之前,做权限的验证,在enter中处理

contextlib.contextmanger

contextlib.contextmanger
它是一个装饰器实现上下文管理,装饰一个函数,而不用像类一样实现enterexit方法。
对下面的函数有要求,必须有yield,也就是这个函数必须返回一个生成器,且只有yield一个值。也就是这个装饰器接受一个生成器对象作为参数。

import contextlib

@contextlib.contextmanager
def foo():
    print('enter')  # 相当于__enter__()
    yield # yield 5, yield的值只能有一个,作为__enter__方法的返回值
    print('exit')  # 相当于__exit__()


with foo() as f:
    # raise Exception()
    print(f)
~~~~~~~~~~~~~~~~~~~~~
enter
None
exit

f接收yield语句的返回值。

上面的额程序看似不错,但是,增加一个异常试试,发现不能保证exit的执行,怎么办?增加try finally

import contextlib

@contextlib.contextmanager
def foo():
    print('enter')  # 相当于__enter__()
    try:
        yield # yield 5, yield的值只能有一个,作为__enter__方法的返回值
    finally:
        print('exit')  # 相当于__exit__()


with foo() as f:
    raise Exception()
    print(f)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
enter
None
exit

Exception

上例这么做有什么意义呢?
- 当yield发生处为生成器函数增加了上下文管理。这是为函数增加上下文机制的方法。
- 把yield之前的当做enter方法执行
- 把yield之后的当做exit方法执行
- 把yield的值作为enter的返回值

import contextlib
import datetime
import time

@contextlib.contextmanager
def add(x, y):  # 为生成器函数增加了上下文管理
    start = datetime.datetime.now()
    try:
        yield x + y# yield 5, yield的值只能有一个,作为__enter__方法的返回值
    finally:
        delta = (datetime.datetime.now() - start).total_seconds()
        print("{}s".format(delta))


with add(4, 5) as f:
    # raise Exception()
    time.sleep(2)
    print(f)
~~~~~~~~~~~~~~~~~~~~
9
2.000861s

contextlib.contextmangager总结

如果业务逻辑简单可以使用函数加contextlib.contextmanager装饰器方法,如果业务复杂,用类的方式加enterexit方法方便。

# 使用装饰器显示该函数的执行时长

import time
import datetime

def logger(fn):
    def wrapper(*args, **kwargs):
        start = datetime.datetime.now()
        x = fn(*args, **kwargs)
        end = datetime.datetime.now()
        interval = (end - start).total_seconds()
        print("function time = {}".format(interval))
        return x
    return wrapper

@logger
def add(x, y):
    time.sleep(2)
    return x + y

print(add(3, 4))
# 使用装饰器显示该函数的执行时长

import time
import datetime

class Add:
    def __init__(self, *args, **kwargs):
        self.args = args
        self.kwargs = kwargs
        args, kwargs = self.args, self.kwargs

    def logger(fn):
        def wrapper(*args, **kwargs):
            start = datetime.datetime.now()
            x = fn(*args, **kwargs)
            end = datetime.datetime.now()
            interval = (end - start).total_seconds()
            print("function time = {}".format(interval))
            return x
        return wrapper

    @logger
    def add(x, y):
        time.sleep(2)
        return x + y

addtest = Add()
print(addtest.add(3, 4))
# 使用装饰器显示该函数的执行时长

import time
import datetime

class Add:
    def __init__(self, x, y):
        self.x = x
        self.y = y


    def __enter__(self):
        start = datetime.datetime.now()
        return start



    def __exit__(self, exc_type, exc_val, exc_tb):
        end = datetime.datetime.now()
        interval = (end - start).total_seconds()
        return interval


addtest = Add()
def add(x, y):
    time.sleep(2)
    return x + y
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值