14. 尽量用异常来表示特殊情况,而不返回 None

  • 用 None 这个返回值来表示特殊意义的函数,很容易使调用者犯错,因为 None 和0以及空字符之类的值,在条件表达式里都会评估为 False
  • 函数在遇到特殊情况时,应该抛出异常,而不要返回 None.调用者看到该函数的文档中所描述的异常之后,应该就会编写相应的代码来处理它们了

如两个数字进行相除的函数

def divide(a, b):
    """求两个数相除"""
    try:
        return a / b
    except ZeroDivisionError as e:
        raise ValueError('Invalid inputs') from e
        
x, y = 5, 2
try:
    result = divide(x, y)
except ValueError:
    print('Invalid inputs')
else:
    print('Result is {}'.format(result))

15. 了解如何在闭包里使用外围作用域中的变量

  • 对于定义在某作用域内的闭包来说,它可以引用这些作用域中的变量
  • 使用默认方式对闭包内的变量赋值,不会影响外围作用域中的同名变量
  • 在 python3中,程序可以在闭包内用 nonlocal 语句来修饰某个名称,使该闭包能够修改外围作用域中的同名变量
  • 在 python2中,程序可以使用可变值(如,包含单个元素的列表)来实现与 nonlocal 语句相仿的机制
  • 处理那种比较简单的函数,尽量不要用 nonlocal 语句

示例:假如有一份列表,其中的元素都是数字,现在要对其排序,但排序时,要把出现在某个群组内的数字,放在群组外的那些数字之前。

numbers = [8, 3, 1, 2, 5, 4, 7, 6]
group = {2, 3, 5, 7}

def sort_priority(values, group):
    def helper(x):
        if x in group:
            return (0, x)
        return (1, x)
    values.sort(key=helper)
sort_priority(numbers, group)
>>> [2, 3, 5, 7, 1, 4, 6, 8]

# 这个山水能正常工作是因为
# 1. python 支持闭包:闭包是一种定义在某个作用域中的函数,这种函数引用了那个作用域里面的变量。helper 函数之所以能够访问 sort_priority 的 group 参数,原因就在于它是闭包
# 2. python 的函数是一级对象,也就是说,我们可以直接饮用函数,把函数赋给变量、把函数当成参数传递给其它函数,并通过表达式及 if 语句对其进行比较和判断,等待。于是,我们可以把 helper 这个闭包函数,传递个 sort 方法的 key 参数
# 3. python 使用特殊的规则来比较两个元组。它首先比较各元组中下标为0的对应元素,如果相等,在比较下表为1的对应元素,依次类推

改进 sort_priority函数,返回一个值用来表示用户界面里是否出现了优先级较高的原件,使得该函数的调用者,可以根据这个返回值做出相应的处理。

numbers = [8, 3, 1, 2, 5, 4, 7, 6]
group = {2, 3, 5, 7}

def sort_priority(values, group):
    found = False
    def helper(x):
        if x in group:
            found = True    # 闭包里面的赋值,相当于定义了一个新的变量 found 
                            # 不会修改sort_priority函数中的 found 值
            return (0, x)
        return (1, x)
    values.sort(key=helper)
    return found
    
sort_priority(numbers, group)

>>> 
Found: False # 期望返回 True,返回了 False
[2, 3, 5, 7, 1, 4, 6, 8]

# 之所以出现上面的问题,是Python 语言故意这么设计的。这样做可以防止函数中的局部变量污染函数外面的那个模块。

python3中获取闭包内的数据,可以使用 nonlocal。也就是给相关变量赋值的时候,应该在上层作用域中查找该变量。nonlocal 的唯一限制在于,它不能延伸到模块级别,这是为了防止污染全局作用域。

def sort_priority(values, group):
    found = False
    def helper(x):
        nonlocal found
        if x in group:
            found = True
            return (0, x)
        return (1, x)
    values.sort(key=helper)
    return found

python2中不支持 nonlocal 关键字,为了实现类似的功能,我们需要利用 python 的作用域规则来解决。

def sort_priority(numbers, group):
    found = [False]
    def helper(x):
        if x in group:
            found[0] = True
            return (0, x)
        return (1, x)
    numbers.sort(key=helper)
    return found[0]

如果使用 nonlocal 的那些代码,已经写得原来越复杂,那就应该将其相关的状态封装成辅助类。下面定义的类,与 nonlocal 所表达的功能形同。

class Sorter(object):
    def __init__(self, group):
        self.group = group
        self.found = False
    
    def __call__(self, x):
        if x in self.group:
            self.found = True
            return (0, x)
        return (1, x)

sorter = Sorter(group)
numbers.sort(key=sorter)
assert sorter.found is True

16. 考虑用生成器来改写直接返回列表的函数

  • 使用生成器比把收集到的结果放入列表里返回给调用者更加清晰
  • 由生成器函数返回的那个迭代器,可以把生成器函数体重,传给 yield 表达式的那些纸,逐次产生出来
  • 无论输入量有多大,生成器都能产生一系列输出,因为这些输入量和输出量,都不会影响它在执行时所耗的内存

例如: 我们要查出字符串中每个词的首字母,在整个字符串里的位置。

def index_words(text):
    result = []
    if text:
        result.append(0)
    for index, letter in enumerate(text):
        if letter == ' ':
            result.append(index + 1)
    return result
    
# 把这个函数改为生成器来写会更好。生成器是使用 yield 表达式的函数。调用生成器函数时,
# 它不会真的运行,而是会返回迭代器。每次在这个迭代器上面调用内置的 next 函数时,迭代
# 器会把生成器推进到下一个 yield 表达式那里。生成器传给 yield 的每一个值,都会由迭代
# 器返回给调用者

def index_words_iter(text):
    if text:
        yield 0
    for index, letter in enumerate(text):
        if letter == ' ':
            yield index + 1

result = list(index_words_iter(address))

# 下面定义一个生成器,从文件里面依次读入各行内容,然后逐个处理每行汇总的单词,并产生
# 相应结果。该函数执行时所耗的内存,由单行输入值的最大字符数来界定

def index_file(handle):
    offset = 0
    for line in handle:
        if line:
            yield offset
        for letter in line:
            offset += 1
            if letter == ' ':
                yield offset

with open('/tmp/address.txt', 'r') as f:
    it = index_file(f)
    results = islice(it, 0, 3)
    print(list(results)

17. 在参数上面迭代时,要多加小心

  • 函数在输入的参数上面多次迭代时要小心:如果参数是迭代器,那么可能会导致奇怪的行为并错失某些值
  • python 的迭代器协议,描述了容器和迭代器应该如何与iter和 nextn 内置函数、for 循环及相关表达式相互配合
  • iter 方法实现为生成器,即可定义自己的容器类型
  • 想判断某个值是迭代器还是容器,可以拿该值为参数,两次调用 iter 函数,若结果相同,则为迭代器,调用内置的 next 函数,即可令该迭代器前进一步

以统计城市旅游的人数,占总游客的百分比。

def normalize_defensive(numbers):
    if iter(numbers) is iter(numbers):
        raise TypeError('Must supply a container')
    total = sum(numbers)
    result = []
    for value in numbers:
        percent = 100 * value / total
        result.append(percent)
    return result
    
# 使用迭代器协议(iterator protocol)的容器类
class ReadVisits(object):
    def __init__(self, data_path):
        self.data_path = data_path
        
    def __iter__(self):
        with open(self.data_path) as f:
            for line in f:
                yield int(line)
                
'''
/tmp/a.data 文件中包含三行数据
15
35
80
'''

# python 在 for 循环及相关表达式中遍历某中容器的内容时,就要依靠这个迭代协议。
# 在执行类似 for x in foo 这样的语句时,python 实际上会调动 iter(foo).内置的 iter 函数又会
# 调用 foo.__iter__ 这个特殊方法。该方法必须返回迭代器对象。

visits = [15, 35, 80]
normalize_defensive(visits) # No error
visits = ReadVisits('/tmp/a.data')
normalize_defensive(visits)

18. 用数量可变的位置参数减少视觉杂讯

  • 在 def 语句中使用*args, 即可令函数接受数量可变的位置参数
  • 调用函数时,可以采用* 操作符,把序列中的元素当成位置参数,传给该函数
  • 对生成器使用 * 操作符,可能导致程序耗尽内存并崩溃
  • 在已经接受 *args 参数的函数上面继续添加位置参数,可能会产生难以排查的 bug

19. 用关键字参数来表达可选的行为

  • 函数参数可以按位置或关键字来指定
  • 只使用位置参数来调用函数,可能会导致这些参数值的含义不够明确,而关键字参数则能阐明每个参数的意图
  • 给函数添加新的行为时,可以使用带默认值的关键字参数,以便与原有的函数调用代码保持兼容
  • 可选的关键字参数,总是应该以关键字形式来指定,而不应该以位置参数的形式来指定

20. 用 None 和文档字符串来描述具有动态默认值的参数

  • 参数的默认值,只会在程序加载模块并读到本函数的定义时评估一次。对于{} 或 [] 等动态的值,可能会导致奇怪的行为
  • 对于以动态值作为实际默认值的关键字参数来说,应该把形式上的默认值写为 None,并在函数的文档字符串里面描述该默认值所对应的实际行为
from datetime import datetime
from time import sleep
def log(message, when=datetime.now()):
    print('{}: {}'.format(when, message))

log('Hi there!')
sleep(0.1)
log('Hi again!')

# 上面的例子输出的时间是一样的,因为 datetime.now 只执行了一次。
# 在 python 若想正确实动态默认值,习惯上把默认值设置为 None,并在文档字符串里面把对None 对应的实际行为
# 描述出来。编写函数代码时,如果返现该参数的值是 None,那就将其设为实际的默认值

def log(message, when=None):
    """Log a message with a timestamp.
    
    Args:
        message: Message to print.
        when: datetime of when the message occurred.
            Defaults to the present time.
    """
    from datetime import datetime
    when = datetime.now() if when is None else when
    print('{}: {}'.format(when, message))

21. 用只能以关键字形式指定的参数来确保代码清晰

  • 关键字参数能够使函数调用的意图更加明确
  • 对于各参数之间很容易混淆的函数,可以声明只能以关键字形式指定的参数,以确保调用者必须通过关键字来指定它们。对于接受多个 Boolena 标志的函数,更应该这样做
  • python3 有明确的语法来定义这种只能以关键字形式指定的参数
  • python2 的函数可以接受 **kwargs 参数,并手工抛出 TypeError 异常,以便模拟只能以关键字形式来指定的参数

python3 可以定义一种只能以关键字形式来指定的参数,从而确保调用该函数的代码读起来会比较明确。下面定义的这个 safe_division_c 函数,带有两个只能以关键字形式来指定的参数。参数列表里的*,标志着位置参数就此终结,之后的那些参数,都只能以关键字形式来指定。

# python3 写法
def safe_division_c(number, divisor, *, ignore_overflow=False,ignore_zero_division=False):
    try:
        return number /divisor
    except OverflowError:
        if ignore_overflow:
            return 0
        else:
            raise
    except ZeroDivisionError:
        if ignore_zero_division:
            return float('inf')
        else:
            raise

# python2 写法
def safe_division_d(number, divisor, **kwargs):
    ignore_overflow = kwargs.pop('ignore_overflow', False)
    ignore_zero_division = kwargs.pip('ignore_zero_division', False)
    if kwargs:
        raise TypeError('Unexpected **kwargs: %r' % kwargs)
    try:
        return number /divisor
    except OverflowError:
        if ignore_overflow:
            return 0
        else:
            raise
    except ZeroDivisionError:
        if ignore_zero_division:
            return float('inf')
        else:
            raise

继续阅读关于 的文章



Fork me on GitHub