摘要:在本文中,我们一起来揭开迭代循环的面纱,并介绍一些能够完全取代迭代的编程概念。
链接:https://medium.com/codex/ive-almost-stopped-using-iteration-entirely-ee34f208d7ad
声明:本文为 CSDN 翻译,未经允许禁止转载。
作者 | Emmett Boudreau 译者 | 弯月 责编 | 郑丽媛
出品 | CSDN(ID:CSDNnews)
在编写软件时,我们经常需要使用包含不同类型的数据,这种数据通常被称为“结构”。更具体地说,有些特定类型的结构又称为“可迭代对象”。
可迭代对象是一种类型,其中包含名叫“元素”的数据类型,而不是字段或属性。举个例子,Julia 中的可迭代对象包括 Dict 和 Vector,就相当于 Python 中的 list 和 dict。Dict 包含 Pair 类型的元素,而 Vector 包含其参数指定的任意类型。这些类型的结构被称为“可迭代对象”,因为它们是可以迭代。
迭代是编程中的一个非常关键的概念。事实上,迭代非常重要,就连低级汇编代码中都包含类似于循环的实现。迭代,更具体地说,for 循环,是我们在学习第一门编程语言时学习的第一个知识点。然而,迭代也有其自身的一些问题。
许多进程的执行速度减慢,究其原因最大的障碍往往就是循环。因此,我们应该最大限度地提高循环的性能,避免嵌套,并尽可能避免循环。那么,为什么我们要将循环作为默认的方式之一呢?为什么不试试看其他有助于提升性能的方式呢?
有几种技巧可以避免使用循环。首先是递归,不过递归的应用范围更窄,而且性能远远比不上循环。不过,第二种方法就比较实用了。这种方法叫做“广播”(broadcasting),它将一个函数应用到给定结构中的每个元素上,因此可以很容易地一次性改变多个元素。还有一个类似的概念“映射”(mapping),从某种意义上来说,它和广播非常相似。第三种方法与它们相似,叫做“解析式”(comprehension)。这几个概念的基本思想都是将一个函数应用到所有元素上,从而在多个值上同时进行某种运算。
在编写 Python 时,最常用的方法就是 map。为了映射一个函数,我们只需用 lambda 或 def 定义它:
myfunc = lambda x: x + 1
然后将这个函数传递给 map 函数,后者将返回一个 map 对象。该对象可以转换成一个 list,从而获取其值。map 函数的第一个参数是上述函数,第二个参数是要映射的 list。
map(myfunc, [1, 2, 3, 4, 5])
<map object at 0x7f33562f8fa0>
list(map(myfunc, [1, 2, 3, 4, 5]))
[2, 3, 4, 5, 6]
虽然 Python 中的解析式也很强大,但与 Julia 等语言相比,其功能还是太受限了。因此,大部分情况下,解析式是快速操作多个元素的最佳选择。但是,由于解析式非常善于进行小型操作(如上例),我们应该在这里演示一下。
vals = [x + 1 for x in [1, 2, 3, 4, 5]]
print(vals)
[2, 3, 4, 5, 6]
Julia 也有 map 函数,而且像 Python 一样,该函数非常易于使用。不过我必须承认,我在 Julia 中使用 map 的次数远远少于广播以及更强大的解析式。Julia 的 map 的语法也和 Python 非常相似,最主要的区别就是它会返回一个 Vector:
julia> map(x -> x + 1, [1, 2, 3, 4, 5, 6])
6-element Vector{Int64}:
2
3
4
5
6
7
Julia 也支持广播。与 map 很相似,我们可以利用广播在数组内的每个元素上应用任意函数。Python 的 NumPy 也提供了此功能,但是本文主要想讨论 Julia。在 Julia 中,只需在给定函数前写一个点(.), 即可将其应用到每个元素。Julia 的基本操作符中经常被引用的一个示例如下:
x = [5, 10, 15]
y = [5, 10, 15]
x .* y
3-element Vector{Int64}:
25
100
225
对于其他通用的应用,该操作也可以使用 @. 宏完成。如果你想了解有关 Julia 中广播的更多信息,可以阅读这篇文章:https://medium.com/chifi-media/broadcasting-power-in-julia-beginner-friendly-overview-11cdd099623a。
像 Python 一样,Julia 也有解析式。虽然 Python 的解析式很有局限性,特别是其可读性并非太好,但 Julia 则截然不同。当然,没有任何规定组禁止你在 Python 的解析式之外定义函数,但这样做会导致一些问题,比如当需要在函数中引某个变量的时候就会很麻烦。尽管如此,虽然有时候 Python 的解析式需要借助其他东西才能清晰易读,但 Julia 的解析式可以像函数一样书写,可以写在多行内,而且支持多种语句!下面是一个解析式,它很像 Python:
x = [x - 1 for x in 1:5]
下面,我们来看看 Julia 的解析式究竟好在哪里。首先,我们可以利用 begin/end 语法,以函数的形式编写解析式。
x = [begin x - 1 end for x in 1:5]
在解析式中添加 begin/end 敞开了一扇大门,可以让解析式完成更多的工作。我喜欢将解析式写成这样:
x = [begin
x - 1
end for x in 1:5]
在这个新函数中,我们可以做任何循环能做的事情,除了一些例外。例如,下面是个条件语句:
julia> x = [begin
if x > 1
5
else
3
end
end for x in 1:10]
10-element Vector{Int64}:
3
5
5
5
5
5
5
5
5
5
虽然 Python 也能实现这一点,但别忘了,我们还能添加更多的语句,甚至可以使用 try/catch 和闭包!
julia> x = [begin
if x > 1
5
else
try
"hello" * 5
catch
3
end
end
end for x in 1:10]
10-element Vector{Int64}:
3
5
5
5
5
5
5
5
5
5
虽然用函数替换循环能节省不少时间,但循环在编程语言和软件中依然有着重要的作用。最值得一提的是,循环的关键字 break 和 continue 是其他方法都没有的。这可以节省很多不必要的循环。break 关键字能结束循环,而 continue 关键字可以直接跳转到下一次循环。下面是选择循环的一个例子,主要目的是在找到适当的值时,利用 break 跳出循环:
function display(d::OliveDisplay, m::MIME{<:Any}, o::Any)
T::Type = typeof(o)
mymimes = [MIME"text/html", MIME"text/svg", MIME"text/plain"]
mmimes = [m.sig.parameters[3] for m in methods(show, [IO, Any, T])]
correctm = nothing
for m in mymimes
if m in mmimes
correctm = m
break
end
end
show(d.io, correctm(), o)
end
在这个例子中,我们依次遍历 MIME,找到最有可能的值。在已有的 MIME(即 mmimes)中找到所需的 mime 后,设置 correctm,然后将其传递给 show 函数。这个例子中,相较于映射或解析式,选择迭代更合适,因为它拥有独到的优势。
一直以来,迭代式循环都是编程中的一个重要的概念,在高级语言中得到了广泛的应用。循环是从硬件层面上实现的,其重要性不言而喻。但是,许多现代编程语言完全可以不使用循环,我们可以利用函数、映射和解析式来实现相同的功能。但是,在有一些情况下,选择循环依然比解析式或映射更好。一种情况就是可以利用 break 和 continue 关键字跳过元素、加快循环。