扇出差¶
设一列表的数字,为每个数字生成它与它在列表后 \(n_{fanout}\)(叫做扇出(fanout)值)个数字之间的差的列表。返回一个列表,其成员为为每一个数字生成的列表(也就是说返回的是成员为列表的列表)。如果列表成员之后的成员数小于 \(n_{fanout}\),计算尽可能多的差。比如说,假设我们想要计算列表 [3, 2, 4, 6, 1]
的扇出差,且扇出值为3,那我们应该计算:
\(3 \rightarrow [2 - 3, 4 - 3, 6 - 3]\)
\(2 \rightarrow [4 - 2, 6 - 2, 1 - 2]\)
\(4 \rightarrow [6 - 4, 1 - 4]\)
\(6 \rightarrow [1 - 6]\)
\(1 \rightarrow []\)
# 范例行为
>>> difference_fanout([3, 2, 4, 6, 1], 3)
[[-1, 1, 3], [2, 4, -1], [2, -3], [-5], []]
你会想要先阅读本文对列表,索引和切片列表,以及for循环的讨论,然后再解决本问题。
如果想要获得额外分(以及额外的乐趣!)的话,尝试仅仅使用列表理解来编写你的函数。
解:使用for循环计算扇出差¶
我们将会使用最直接了当的嵌套for循环来解决本问题。外层for循环会迭代列表中的每一个数字。我们这个数字将其称为“基础数”(base number)。我们将想要内层for循环迭代基础数之后的数字,这样我们可以计算基础数和它 \(n_{fanout}\) 个邻居们的差。我们将会为每一个基础数重新初始化中间的列表,不然每个差都会被附加到一个很长的列表后。
def difference_fanout(l, fanout):
""" 返回列表,其成员为值和它后面成员之间的差
Parameters
----------
l: List[Number]
作为输入的基础数列表
fanout: int
和多少个邻居计算差
Returns
-------
List[List[Number]]
"""
all_fanouts = [] # 会储存每一个扇出差的列表
for i, base_number in enumerate(l):
# `base_fanout` 会储存基础数和它之后邻居的差
base_fanout = []
for neighbor in l[i+1: i+1+fanout]:
base_fanout.append(neighbor - base_number)
all_fanouts.append(base_fanout)
return all_fanouts
请注意我们使用了enumerate;这允许我们同时访问我们的基础数(来求差)以及它在列表 l
中的索引(来确定它的邻居是哪些)。
你可能会担心我们的内循环会试图迭代列表结尾之后的数字。考虑 base_number
为 l
的最后成员的情况。那么 l[i+1: i+1+fanout]
等值于 l[len(l): len(l)+fanout]
——这个切片的结尾索引明显超过了 l
的长度(假设 fanout > 0
)。幸运的是,这并不是我们的疏忽。虽然索引列表范围外的成员会导致错误,但是请回忆:切片会自动将其限制在序列的边界之中。也就是说,l[i+1: i+1+fanout]
其实行为和
l[min(i, len(l)-1): min(len(l), i+1+fanout)]
一样(假设我们仅仅在处理正索引和非空的列表)。因此我们的内循环会自然地限制自身。在 base_number
为 l
最后成员的情况下,内循环会立刻退出,使得 base_fanout
为空。虽然有一点费解,但是这是一个值得记忆的Python切片特征。
解:使用列表理解计算扇出差¶
我们可以审慎地嵌套列表理解来简化我们的答案。虽然这语法可能第一眼看上去有一点复杂,但是它允许我们不用担心初始化多个列表并在嵌套for循环中正确的地方向它们附加内容。
def difference_fanout(l, fanout):
""" 返回列表,其成员为值和它后面成员之间的差
Parameters
----------
l: List[Number]
作为输入的基础数列表
fanout: int
和多少个邻居计算差
Returns
-------
List[Number]
"""
return [[neighbor - base for neighbor in l[i+1:i+1+fanout]]
for i,base in enumerate(l)]
注意最外层的列表理解迭代了基础数,如上一个答案的外层for循环一般,而里层的列表理解达成了和上一个答案的里层for循环一样的目的。
在这个版本的解中可能导致错误的地方比前一个答案少,因为其简短性使得你不需要担心之前答案需要处理的“中间部分”。这应该演示了理解表达式语法的强大。
扩展¶
回忆前文,函数其实就是提供了“调用函数”这个特殊功能的对象。这意味着你可以将函数作为参数输入到别的函数中。这一点极其强大,因为这将允许我们扩大我们函数的运用场合。比如说,我们不需要限制我们的函数来仅仅计算成员和之后项目的差;我们可以使用任何二元运算。与其计算差,我们可以计算和或乘积或甚至在字符串列表的情况下粘连字符串。可能性无限;有限的仅仅是你的想象力。
在了解了这一点后,我们可以归纳我们的代码。
def apply_fanout(l, fanout, op):
""" 返回一个列表,成员为值和在它后面成员之间
进行二元运算的结果
Parameters
----------
l: List[Any]
输入列表
fanout: int
和多少个邻居进行运算
op: Callable[[Any, Any], Any]
任何用来对 `l` 中对象组成的扇出对操作的二元运算
Returns
-------
List[List[Any]]
"""
return [[op(neighbor, base) for neighbor in l[i+1:i+1+fanout]]
for i,base in enumerate(l)]
现在,我们可以简单地重写 difference_fanout
为:
def subtract(a, b):
return a - b
def difference_fanout(l, fanout):
return apply_fanout(l, fanout, subtract)
我们可以轻松地将 subtract
换成其它函数来达到完全不同的目的。