《Effective python》-第2章 函数

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

可参考文章:python的闭包问题
总结:可使用nonlocal语句,改为辅助类,使用单元素列表或集合等获取外围作用域的变量。

def sort_priority(values,group): # 外部作用域
"""功能:把在group中的元素排在所有值的前面,若存在group范围内的值,found为True"""
    found = False
    def helper(x): # 闭包
        if x in group:
            found = True # 实际上并未修改上面的值
            return(0,x)
        return(1,x)
    values.sort(key=helper)
    return found

numbers = [8,3,1,2,5,4,7,6]
group ={2,3,5,7}
print(sort_priority(numbers,group))  # False
print(numbers)  # [2, 3, 5, 7, 1, 4, 6, 8]

上面的代码实际上无法完成需要的功能:found 不会发生变化。

在表达式中引用变量时,Python解释器将按如下顺序遍历各作用域,以解析该引用:

  1. 当前函数的作用域。
  2. 任何外围作用域(例如,包含当前函数的其他函数)。
  3. 包含当前代码的那个模块的作用域(也叫全局作用域,global scope)。
  4. 内置作用域(也就是包含len及str等函数的那个作用域)。

如果上面这些地方都没有定义过名称相符的变量,那就抛出NameError异常。

变量赋值时,规则有所不同。如果当前作用域内已经定义了这个变量,那么该变量就会具备新值。若是当前作用域内没有这个变量,Python则会把这次赋值视为对该变量的定义。而新定义的这个变量,其作用域就是包含赋值操作的这个函数
获取闭包内数据的方法:
方法1:使用nonlocal语句

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

numbers = [8,3,1,2,5,4,7,6]
group ={2,3,5,7}
print(sort_priority(numbers,group))  # True
print(numbers)  # [2, 3, 5, 7, 1, 4, 6, 8]

方法2:把函数定义为辅助类

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)

numbers = [8,3,1,2,5,4,7,6]
group ={2,3,5,7}
soter = Sorter(group)
numbers.sort(key=soter)
print(numbers) # [2, 3, 5, 7, 1, 4, 6, 8]
print(soter.found) # True

方法3:把变量改为单元素的列表,也可以使用字典、集合或类是实例

found = [False] 
found[0] = True # 相当于引用

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

总结: 列表需要存储的内容太大时,推荐使用生成器,可使用itertools.islice(iter,start,end)来获取指定范围的数据。但是需要注意,迭代器是有状态的,不应该反复调用。

使用列表存储结果的函数:
列表需要存所有结果:如果输入量非常大,那么程序可能耗尽内存并崩溃。

def index_words(text):
    """返回单词首字母的位置索引"""
    result = []
    if text:                             # 句首特殊情况
        result.append(0)
    for index,letter in enumerate(text): # 其他情况
        if letter == ' ':
            result.append(index+1)
    return result

address = "Four score and seven years ago..."
result = index_words(address)[1:4]
print(result)

结果:[5, 11, 15]

使用生成器替代列表的函数:

import itertools
def index_words2(text):
    """返回单词首字母的位置索引"""
    if text:
        yield 0
    for index,letter in enumerate(text):
        if letter == ' ':
            yield index+1

address = "Four score and seven years ago..."
result = list(itertools.islice(index_words2(address),1,4)) # 仅仅获取[1,4)的结果
print(result)

结果:[5, 11, 15]

应用
从文件读取每行的内容,逐个处理每行的单词。
该函数执行时所耗费的内存,由单行输入的最大字符数决定。

def index_words3(text):
    """返回文件每个单词首字母的位置索引"""
    offset = 0
    for line in text:
        if line:
            yield offset
        for letter in enumerate(line):
            offset+=1       # 记录当前字母的下一个位置
            if letter == ' ':
                yield offset

import itertools
with open('my.txt','r') as f:
    it = index_words3(f)
    results = itertools.islice(it,1,4)
    print(list(results))

17.在迭代器上面反复迭代会出错

总结:如果函数的参数是迭代器,那么在上面反复迭代时会出现不正确的结果。解决方法包括:
(1)转为list,再反复迭代,但是这样就没有必要使用迭代器了;
(2)使用lambda语句;
(3)构造类,实现__iter__方法,产生生成器。

迭代器是有状态的:如果你遍历一个迭代器或者生成器本身已经引发了一个StopIteration的异常,你就不可能获得任何数据了

def read_visits(data_path):
    with open(data_path,'r') as f:
        for line in f:
            yield int(line)

it = read_visits('my.txt')
print(list(it))  # [1, 2, 3, 4, 5] 遍历完成,产生StopIteration异常
print(list(it))  # []

下面的代码输出 [ ]:

def normalize(numbers):
    total = sum(numbers)               # 第一次遍历
    result = []
    for value in numbers:
        print(value)
        percent = 100 * value / total  # 第二次遍历
        result.append(percent)
    return result

def read_visits(data_path):
    with open(data_path,'r') as f:
        for line in f:
            yield int(line)

it = read_visits('my.txt')
percentages = normalize(it)
print(percentages) # []

解决办法1:使用迭代器制作一个list,使用这个list进行迭代。缺点在于复制迭代器的大量数据会导致内存耗尽。

def normalize_copy(numbers):
    numbers = list(numbers)  # 复制一个列表
    total = sum(numbers)
    result = []
    for value in numbers:
        percent = value * 100 / total
        result.append(percent)
    return result

解决办法2:通过参数来接受另外一个函数,这个函数每次调用都返回新的迭代器。这里的lambda表达式会调用生成器,以便每次都产生新的迭代器。(不是很理解)

def read_visits(data_path):
    with open(data_path,'r') as f:
        for line in f:
            yield int(line)
            
def normalize_func(get_iter):
    total = sum(get_iter()) # 一个新的迭代器
    result = []
    for value in get_iter(): # 又一个新的迭代器
        percent = 100 * value / total
        result.append(percent)
    return result

percentages = normalize_func(lambda: read_visits('my.txt'))  # 注意这里需要使用lambda
print(percentages)

结果:[6.666666666666667, 13.333333333333334, 20.0, 26.666666666666668, 33.333333333333336]
解决办法3:新建一个实现迭代器协议的容器类。
for循环会调用iter(对象),然后调用__iter__方法,实际反复调用__next__,直到耗尽。这里我们自己的类把__iter__方法实现为生成器。

def normalize(numbers):
    total = sum(numbers)               # 第一次遍历
    result = []
    for value in numbers:
        percent = 100 * value / total  # 第二次遍历
        result.append(percent)
    return result

class ReadVisitors(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)

visits = ReadVisitors('my.txt')
percentages = normalize(visits)
print(percentages)

结果:[6.666666666666667, 13.333333333333334, 20.0, 26.666666666666668, 33.333333333333336]

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

总结:对于函数中的动态参数要默认为None,因为参数的默认值是在模块加载时赋值的。

from time import sleep
from _datetime import datetime

def log(message, when=None):  # 这里的when要先设置为None
    when = datetime.now() if when is None else when
    print("%s: %s" %(when, message))
    
log('Hi there!')
sleep(0.1)
log('Hi again!')
# 2020-02-18 18:04:23.032938: Hi there!
# 2020-02-18 18:04:23.132944: Hi again!

如果直接在参数中写when = datetime.now(),那么参数的值就不会发生改变。

from time import sleep
from _datetime import datetime

def log(message, when = datetime.now()):  # 这里的when要先设置为None
    print("%s: %s" % (when, message))

log('Hi there!')
sleep(0.1)
log('Hi again!')
# 2020-02-18 18:05:00.198064: Hi there!
# 2020-02-18 18:05:00.198064: Hi again!

反复调用decode函数,最后返回的default都是一个对象

import json

def decode(data, default={}):  # 
    try:
        return json.loads(data)
    except ValueError:
        return default

foo = decode('bad data')
foo['stuff'] = 5
bar = decode('also bad')
bar['meep'] = 1

print('Foo:', foo)  # Foo: {'stuff': 5, 'meep': 1}
print('Bar:', bar)  # Bar: {'stuff': 5, 'meep': 1}
assert foo is bar   # foo和bar实际上是同一个对象

下面是修改动态参数为None:

import json

def decode(data,default=None):
    if default == None:
        default = {}
    try:
        return json.loads(data)
    except ValueError:
        return default

foo = decode('bad data')
foo['stuff'] = 5
bar = decode('also bad')
bar['meep'] = 1

print('Foo:', foo)  # Foo: {'stuff': 5}
print('Bar:', bar)  # Bar: {'meep': 1}

当然可以直接删除动态参数:

import json

def decode(data):
    default = {}
    try:
        return json.loads(data)
    except ValueError:
        return default

foo = decode('bad data')
foo['stuff'] = 5
bar = decode('also bad')
bar['meep'] = 1

print('Foo:', foo)  # Foo: {'stuff': 5}
print('Bar:', bar)  # Bar: {'meep': 1}

21.用关键字参数确保代码清晰

在位置参数和关键字参数中,使用*,可以强制要求调用时关键字参数必须写明关键字。
下面函数的正确调用是:

result = safe_division(1, 10**500, ignore_overflow=True, ignore_zero_division=True)

错误调用:

result = safe_division(1, 10**500, True, True)  
错误提示:TypeError: safe_division() takes 2 positional arguments but 4 were given
def safe_division(number, divisor,*, ignore_overflow=False, ignore_zero_division=False):
    try:
        return number / divisor
    except OverflowError:           # 忽略除数的float overflow,并返回0
        if ignore_overflow:
            return 0
        else:
            raise
    except ZeroDivisionError:        # 忽略除数为零,返回无限值
        if ignore_zero_division:
            return float('inf')
        else:
            raise
        
result = safe_division(1, 10**500, ignore_overflow=True, ignore_zero_division=True)
print(result)
result = safe_division(1, 0,ignore_overflow=True, ignore_zero_division=True)
print(result)
# 0.0
# inf
发布了119 篇原创文章 · 获赞 4 · 访问量 5475

猜你喜欢

转载自blog.csdn.net/qq_27921205/article/details/104365259