《Python有什么好学的》之上下文管理器

“Python有什么好学的”这句话可不是反问句,而是问句哦。

主要是煎鱼觉得太多的人觉得Python的语法较为简单,写出来的代码只要符合逻辑,不需要太多的学习即可,即可从一门其他语言跳来用Python写(当然这样是好事,谁都希望入门简单)。

于是我便记录一下,如果要学Python的话,到底有什么好学的。记录一下Python有什么值得学的,对比其他语言有什么特别的地方,有什么样的代码写出来更Pythonic。一路回味,一路学习。

引上下文管理器

太极生两仪,两仪为阴阳。

道有阴阳,月有阴晴,人有生死,门有开关。

你看这个门,它能开能关,就像这个对象,它能创建能释放。(扯远了

编程这行,几十年来都绕不开内存泄露这个问题。内存泄露的根本原因,就是把某个对象创建了,但是却没有去释放它。直到程序结束前那一刻,这个未被释放的对象还一直占着内存,即使程序已经不用这个对象了。泄露的量少的话还好,量大的话就直接打满内存,然后程序就被kill了。

聪明的程序员经过了这十几年的努力,创造出很多高级编程语言,这些编程语言已经不再需要让程序员过度关注内存的问题了。但是在编程时,一些常见的对象释放、流关闭还是要程序员显式地写出来。

最常见的就是文件操作了。

常见的文件操作方式

原始的Python文件操作方式,很简单,也很common(也很java):

def read_file_1():
    f = open('file_demo.py', 'r')
    try:
        print(f.read())
    except Exception as e:
        pass
    f.close()

就是这么简简单单的,先open然后读写再close,中间读写加个异常处理。

其中close就是释放资源了,在这里如果不close,可能:

  1. 资源不释放,直到不可控的垃圾回收来了,甚至直到程序结束
  2. 中间对文件修改时,修改的信息还没来得及写入文件
  3. 整个代码显得不规范

因此写上close函数理论上已经必须的了,可是xxx.close()这样写上去,在逻辑复杂的时候让人容易遗漏,同时也显得不雅观。

这时,各种语言生态有各种解决方案。

像Java,就直接jvm+依赖注入,直接把对象的生命周期管理接管了,只留下对象的使用功能给程序员;像golang,defer一下就好。而python最常用的则是with,即上下文管理器

使用上下文管理器

用with之后的文件读写会变成:

def read_file_2():
    with open('file_demo.py', 'r') as f:
        print(f.read())

我们看到用了with之后,代码没有了open创建,也没有了close释放。而且也没有了异常处理,这样子我们一看到代码,难免会怀疑它的健壮性。

为了更好地理解上下文管理器,我们先实现试试。

实现上下文管理器

我们先感性地对with进行猜测。

从调用with的形式上看,with像是一个函数,包裹住了open和close:

# 大概意思而已 with = open + do + close
def with():
    open(xxxx)
    doSomething(xxxx)
    close(xxxx)

而Python的库中已有的方案(contextmanager)也和上面的伪代码具有一定的相似性:

from contextlib import contextmanager

@contextmanager
def c(s):
    print(s + 'start')
    yield s
    print(s + 'end')

“打印start”相当于open,而“打印end”相当于close,yield语法和修饰器(@)不熟悉的同学可以复习一下这些文章:生成器修饰器

然后我们调用这个上下文管理器试试,注意煎鱼还给上下文管理器加了参数s,输出的时候会带上:

def test_context():
    with c('123') as cc:
        print('in with')
        print(type(cc))

if __name__ == '__main__':
    test_context()
image

我们看到,start和end前都有实参s=123。

现实一个上下文管理器就是这么简单。

异常处理

但是我们必须要注重异常处理,假如上面的上下文管理器中抛异常了怎么办呢:

def test_context():
    with c('123') as cc:
        print('in with')
        print(type(cc))
        raise Exception

结果:


image

显然,这样弱鸡的异常处理,煎鱼时忍不了的。而且最重要的是,后面的close释放居然没有执行!

我们可以在实现上下管理器时,接入异常处理:

@contextmanager
def c():
    print('start')
    try:
        yield
    finally:
        print('end')
        
def test_except():
    try:
        with c() as cc:
            print('in with')
            raise Exception

    except:
        print('catch except')

调用test_except函数输出:


image

我们在上下文管理器的实现中加入了try-finally,保证出现异常的时候,上下文管理器也能执行close。同时在调用with前也加入try结构,保证整个函数的正常运行。

然而,加入了这些东西之后,整个函数变得复杂又难看。

因此,煎鱼觉得,想要代码好看,抽象的逻辑需要再次升华,即从函数的层面升为对象(类)的层面。

实现上下文管理器类

其实用类实现上下文管理器,从逻辑理解上简单了很多,而且不需要引入那一个库:

class ContextClass(object):
    def __init__(self, s):
        self.s = s

    def __enter__(self):
        print(self.s + 'call enter')
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        print(self.s + 'call exit')

    def test(self):
        print(self.s + 'call test')

从代码的字面意思上,我们就能感受得出来,__enter__即为我们理解的open函数,__exit__就是close函数。

接下来,我们调用一下这个上下文管理器:

def test_context():
    with ContextClass('123') as c:
        print('in with')
        c.test()
        print(type(c))
        print(isinstance(c, ContextClass))

    print('')
    c = ContextClass('123')
    print(type(c))
    print(isinstance(c, ContextClass))

if __name__ == '__main__':
    test_context()

输出结果:

image

功能上和直接用修饰器一致,只是在实现的过程中,逻辑更清晰了。

异常处理

回到我们原来的话题:异常处理。

直接用修饰器实现的上下文管理器处理异常时可以说是很难看了,那么我们的类选手表现又如何呢?

为了方便比较,煎鱼把未进行异常处理的和已进行异常处理的一起写出来,然后煎鱼调用一个不存在的方法来抛异常:

class ContextClass(object):
    def __init__(self, s):
        self.s = s

    def __enter__(self):
        print(self.s + 'call enter')
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        print(self.s + 'call exit')

class ContextExceptionClass(object):
    def __init__(self, s):
        self.s = s

    def __enter__(self):
        print(self.s + 'call enter')
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        print(self.s + 'call exit')
        return True
        
def test_context():
    with ContextExceptionClass('123') as c:
        print('in with')
        t = c.test()
        print(type(t))

    # with ContextClass('456') as c:
        # print('in with')
        # t = c.test()
        # print(type(t))

if __name__ == '__main__':
    test_context()

输出不一样的结果:


image
image

结果发现,看了半天,两个类只有最后一句不一样:异常处理的类中__exit__函数多一句返回,而且还是return了True。

而且这两个类都完成了open和close两部,即使后者抛异常了。

而在__exit__中加return True的意思就是不把异常抛出。

如果想要详细地处理异常,而不是像上面治标不治本的隐藏异常,则需要在__exit__函数中处理异常即可,因为该函数中有着异常的信息。

不信?稍微再改改:

class ContextExceptionClass(object):
    def __init__(self, s):
        self.s = s

    def __enter__(self):
        print(self.s + 'call enter')
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        print(self.s + 'call exit')
        print(str(exc_type) + ' ' + str(exc_val) + ' ' + str(exc_tb))
        return True

输出与预期异常信息一致:

image

先这样吧

若有错误之处请指出,更多地请关注造壳

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容

  • 满眼深黛胜似春,布谷之声处处闻。 茶花抿嘴羞涩起,香樟伸头窥游人。 精光运竹节节须,发财树下富贵人。 一切浮华订四...
    阿丙11阅读 208评论 0 0
  • paidaren阅读 127评论 0 0
  • 夜半窗外风怒号, 岁末心头鼓急敲。 犹记豪情放新鹞, 可怜躯残断老腰。 卧床拜仙求神术, 冰冻斗志不枯槁。 只待天...
    半是瞎忙半是闲阅读 346评论 3 3
  • “与你年轻时的容貌相比,我更爱你现在备受岁月摧残的容颜”每每看到这句,其实都带给我不小的感触,每个人都是视觉动物,...
    巴拉巴拉炸阅读 252评论 0 2
  • 金帆塑业阅读 124评论 0 0