Python高性能编程(第2版)
上QQ阅读APP看书,第一时间看更新

2.3 计算整个朱利亚集合

本节详细介绍生成朱利亚集合的代码。本章后面将以各种方式对这些代码进行分析。如示例2-1所示,在模块开头,我们为支持第一种剖析方法导入了模块time,还定义了一些坐标常量。

示例2-1 定义表示坐标空间的全局常量

"""Julia set generator without optional PIL-based image drawing"""
import time
 
# area of complex space to investigate
x1, x2, y1, y2 = -1.8, 1.8, -1.8, 1.8
c_real, c_imag = -0.62772, -.42193

为生成图形,需要创建两个包含输入数据的列表。第一个列表为zs(复平面坐标z),第二个列表为cs(表示初始条件的复数)。这两个列表都是不变的,且对于列表cs,可将其优化为单个常量值c。这里创建两个输入列表,旨在提供一些看起来合理的数据,以便本章后面能够剖析RAM占用情况。

要想创建列表zs和cs,需要知道每个z的坐标。在示例2-2中,使用了xcoord和ycoord以及步长x_step和y_step来生成这些坐标。这里所做的设置工作有点烦琐,但有助于将代码移植到其他工具(如numpy)和其他Python环境中,因为明确地定义了方方面面,这有助于调试。

示例2-2 创建用作计算函数输入的坐标列表

def calc_pure_python(desired_width, max_iterations):
"""Create a list of complex coordinates (zs) and complex parameters (cs),
build Julia set"""
    x_step = (x2 - x1) / desired_width
    y_step = (y1 - y2) / desired_width
    x = []
    y = []
    ycoord = y2
while ycoord > y1:
        y.append(ycoord)
        ycoord += y_step
    xcoord = x1
while xcoord < x2:
        x.append(xcoord)
        xcoord += x_step
# build a list of coordinates and the initial condition for each cell.
# Note that our initial condition is a constant and could easily be removed,
# we use it to simulate a real-world scenario with several inputs to our
# function
    zs = []
    cs = []
for ycoord in y:
for xcoord in x:
            zs.append(complex(xcoord, ycoord))
            cs.append(complex(c_real, c_imag))
print("Length of x:", len(x))
print("Total elements:", len(zs))
    start_time = time.time()
    output = calculate_z_serial_purepython(max_iterations, zs, cs)
    end_time = time.time()
    secs = end_time - start_time
print(calculate_z_serial_purepython._ _name_ _ + " took", secs, "seconds")
 
# This sum is expected for a 1000^2 grid with 300 iterations
# It ensures that our code evolves exactly as we'd intended
assert sum(output) == 33219980

创建列表zs和cs后,我们输出一些关于列表长度的信息,并调用函数calculate_z_serial_purepython生成列表output。最后,我们对列表output中的值求和,并使用assert确认结果与预期的输出值相同。这里这样做旨在确认书中没有出现错误。

由于上述代码是完整且正确的,因此可以对计算得到的值求和来验证函数是否按预期工作。这种完整性检查很有用——当我们修改代码时,检查我们没有破坏算法是明智的。理想情况下,应使用单元测试进行检查,还应检查多个方面。

接下来,我们定义函数calculate_z_serial_purepython,它扩展了本章前面讨论的算法,如示例2-3所示。注意,这个函数开头定义了列表output,其长度与输入列表zs和cs相同。

示例2-3 CPU密集型计算函数

def calculate_z_serial_purepython(maxiter, zs, cs):
"""Calculate output list using Julia update rule"""
    output = [0] * len(zs)
for i in range(len(zs)):
        n = 0
        z = zs[i]
        c = cs[i]
while abs(z) < 2 and n < maxiter:
            z = z * z + c
            n += 1
        output[i] = n
return output

现在可以调用calc_pure_python了,如示例2-4所示。将调用该函数的语句放在一条执行_ _main_ _检查的if语句中,这样可在尝试剖析方法时安全地导入这个模块,而不启动计算。请注意,这里没有列出绘制输出的方法。

示例2-4 主函数

if _ _name_ _ == "_ _main_ _":
# Calculate the Julia set using a pure Python solution with
# reasonable defaults for a laptop
    calc_pure_python(desired_width=1000, max_iterations=300)

如果此时运行这些代码,我们将看到有关问题复杂性的输出:

    # running the above produces:
    Length of x: 1000
    Total elements: 1000000
    calculate_z_serial_purepython took 8.087012767791748 seconds

在图2-1所示的伪灰度图中,颜色对比度较高,这让我们知道在各个地方函数开销的变化速度是快还是慢。图2-3显示的是线性颜色映射图,其中的黑色表示计算时间短,而白色表示计算时间长。

图2-3 纯灰度的朱利亚集合图

通过呈现数据的两种表示方式,我们可以看到线性颜色映射会丢失大量的细节。在有些情况下,使用不同的表示方式对研究函数开销很有帮助。