文章目录
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解释器将按如下顺序遍历各作用域,以解析该引用:
- 当前函数的作用域。
- 任何外围作用域(例如,包含当前函数的其他函数)。
- 包含当前代码的那个模块的作用域(也叫全局作用域,global scope)。
- 内置作用域(也就是包含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