FluentPythonCh15

21st December 2017 at 12:01pm

Do This, Then That: else Blocks Beyond if

else 除了用在 if / else 块中,还可以跟 for / while / try 一起使用。

for / while 后使用,表示循环正常退出:

for item in my_list:
    if item.flavor == 'banana':
        break
else:
    raise ValueError('No banana flavor found!')

try 后使用,表示没有异常抛出。对比这两段代码:

# Code 1
try:
    dangerous_call()
    after_call()
except OSError:
    log('OSError...')

# Code 2
try:
    dangerous_call()
except OSError:
    log('OSError...')
else:
    after_call()

# 这两段代码功能基本一致,但是第二段代码在表义上更清晰

try/except 这种模式,体现了 Python 更喜欢 EAFP (easier to ask for forgiveness than permission),而不是传统 C 风格的 LBYL (look before you leap)。传统 C 风格的程序需要很多 if 来判断操作成功了没,很啰嗦,比如:

int main(int argc, char *argv[])
{
    int fd;

    fd = open(argv[1], O_RDONLY);
    if (-1 == fd) {
        printf("\n open() failed with error [%s]\n",strerror(errno));
        return 1;
    }
    // ...
}

Context Managers and with Blocks

with 语句,是用来简化 try/finally pattern 的,一般用来做资源自动释放。

context manager 跟 with 搭配使用,主流有两种写法,一种基于类,一种用函数。

class LookingGlass:
    def __enter__(self):   
        import sys
        self.original_write = sys.stdout.write
        sys.stdout.write = self.reverse_write
        # 在 __enter__ 中返回的内容,会被成 as 后的值
        return 'JABBERWOCKY'
        
    def reverse_write(self, text):   
        self.original_write(text[::-1])
        
    def __exit__(self, exc_type, exc_value, traceback):
        # sys.exc_info() 返回的内容即是 (exc_type, exc_value, traceback),
        # 这不是巧合,因为大家经常在 finally 块中调用 sys.exc_info() 来判断做什么 clean-up
        import sys
        sys.stdout.write = self.original_write   
        if exc_type is ZeroDivisionError:   
            print('Please DO NOT divide by zero!')
            return True  # 返回 True 表示异常已经被处理好。否则异常还会被抛出。

>>> from mirror import LookingGlass
>>> with LookingGlass() as what:   
...      print('Alice, Kitty and Snowdrop')   
...      print(what)
...
pordwonS dna yttiK ,ecilA   
YKCOWREBBAJ
>>> what   
'JABBERWOCKY'
>>> print('Back to normal.')   
Back to normal.

# 第二种写法,用函数,yield 前的类似 __enter__,后面类似 __exit__
import contextlib

@contextlib.contextmanager
def looking_glass():
    import sys
    original_write = sys.stdout.write   
    def reverse_write(text):   
        original_write(text[::-1])
    sys.stdout.write = reverse_write   
    yield 'JABBERWOCKY'   
    sys.stdout.write = original_write   

上面的函数写法有个问题,一旦 with 块里抛异常,就会通过 yield 继续抛出。把 yield 包上 try 块可以解决:

@contextlib.contextmanager
def looking_glass():
    import sys
    original_write = sys.stdout.write
    
    def reverse_write(text):
        original_write(text[::-1])
        
    sys.stdout.write = reverse_write
    msg = ''   
    try:
        yield 'JABBERWOCKY'
    except ZeroDivisionError:   
        msg = 'Please DO NOT divide by zero!'
    finally:
        sys.stdout.write = original_write   
        if msg:
            print(msg)

同时,在 yield 周围包上 try/finally 是一种最佳实践,因为你不知道使用者会在 with 块中做什么。

Future Reading

又有很多好东西。如果要做分享讲 with 的特性,可以从里面挖掘材料。