仅作举例说明,我采用了@ballsdotballs的答案中的numpy
版本,并制作了一个伴随的C实现,可通过ctypes
调用。由于numpy
是一个高度优化的工具,我的C代码很难像它一样高效,但应该会接近一些。这里的重要优势在于,通过使用C类型的示例,可以帮助您了解如何将自己的个人C函数连接到Python,而不需要太多的开销。当您只想通过将某些C源码中的小片段编写成C来优化大型计算中的小部分时,这特别有用。大多数情况下,简单地使用numpy
就能解决问题,但对于那些不真正需要全部numpy
并且不想添加耦合以要求在整个代码中使用numpy
数据类型的情况,知道如何降级到内置的ctypes
库并自己完成任务非常方便。
首先让我们创建我们的C源文件,称为haversine.c
:
#include <stdlib.h>
#include <stdio.h>
#include <math.h>
int haversine(size_t n,
double *lon1,
double *lat1,
double *lon2,
double *lat2,
double *kms){
if ( lon1 == NULL
|| lon2 == NULL
|| lat1 == NULL
|| lat2 == NULL
|| kms == NULL){
return -1;
}
double km, dlon, dlat;
double iter_lon1, iter_lon2, iter_lat1, iter_lat2;
double km_conversion = 2.0 * 6367.0;
double degrees2radians = 3.14159/180.0;
int i;
for(i=0; i < n; i++){
iter_lon1 = lon1[i] * degrees2radians;
iter_lat1 = lat1[i] * degrees2radians;
iter_lon2 = lon2[i] * degrees2radians;
iter_lat2 = lat2[i] * degrees2radians;
dlon = iter_lon2 - iter_lon1;
dlat = iter_lat2 - iter_lat1;
km = pow(sin(dlat/2.0), 2.0)
+ cos(iter_lat1) * cos(iter_lat2) * pow(sin(dlon/2.0), 2.0);
kms[i] = km_conversion * asin(sqrt(km));
}
return 0;
}
int main(void) {
double lat1[2] = {16.8, 27.4};
double lon1[2] = {8.44, 1.23};
double lat2[2] = {33.5, 20.07};
double lon2[2] = {14.88, 3.05};
double kms[2] = {0.0, 0.0};
size_t arr_size = 2;
int res;
res = haversine(arr_size, lon1, lat1, lon2, lat2, kms);
printf("%d\n", res);
int i;
for (i=0; i < arr_size; i++){
printf("%3.3f, ", kms[i]);
}
printf("\n");
}
请注意,我们正在努力遵循C语言的惯例。明确通过引用传递数据参数,使用
size_t
作为大小变量,并期望我们的
haversine
函数通过改变其中一个传递的输入来工作,以便在退出时它将包含预期的数据。该函数实际上返回一个整数,这是一个成功/失败标志,可以被其他C级别的函数调用者使用。
我们需要找到一种方法来处理Python中所有这些小的C特定问题。
接下来让我们把我们的
numpy
版本的函数连同一些导入和一些测试数据放到一个名为
haversine.py
的文件中:
import time
import ctypes
import numpy as np
from math import radians, cos, sin, asin, sqrt
def haversine(lon1, lat1, lon2, lat2):
"""
Calculate the great circle distance between two points
on the earth (specified in decimal degrees)
"""
lon1, lat1, lon2, lat2 = map(np.radians, [lon1, lat1, lon2, lat2])
dlon = lon2 - lon1
dlat = lat2 - lat1
a = (np.sin(dlat/2)**2
+ np.cos(lat1) * np.cos(lat2) * np.sin(dlon/2)**2)
c = 2 * np.arcsin(np.sqrt(a))
km = 6367 * c
return km
if __name__ == "__main__":
lat1 = 50.0 * np.random.rand(1000000)
lon1 = 50.0 * np.random.rand(1000000)
lat2 = 50.0 * np.random.rand(1000000)
lon2 = 50.0 * np.random.rand(1000000)
t0 = time.time()
r1 = haversine(lon1, lat1, lon2, lat2)
t1 = time.time()
print t1-t0, r1
我选择让经度和纬度(以度为单位)在0到50之间随机选择,但对于本说明并不太重要。
接下来我们需要做的是编译我们的C模块,使得它可以被Python动态加载。我使用的是Linux系统(你可以很容易地在Google上找到其他系统的例子),所以我的目标是将haversine.c
编译成共享对象,如下:
gcc -shared -o haversine.so -fPIC haversine.c -lm
我们还可以编译成可执行文件并运行它,以查看C程序的main
函数显示的内容:
> gcc haversine.c -o haversine -lm
> ./haversine
0
1964.322, 835.278,
现在我们已经编译了共享对象haversine.so
,我们可以使用ctypes
在Python中加载它,我们需要提供文件路径来实现:
lib_path = "/path/to/haversine.so"
haversine_lib = ctypes.CDLL(lib_path)
现在,haversine_lib.haversine
函数的作用与 Python 函数基本相同,只是我们可能需要进行一些手动类型转换以确保正确解释输入和输出。
numpy
实际上提供了一些不错的工具来实现这一点,这里我将使用的是 numpy.ctypeslib
。我们将构建一个指针类型,使我们能够像使用指针一样传递 numpy.ndarrays
到这些 ctypes
加载的函数中。以下是代码:
arr_1d_double = np.ctypeslib.ndpointer(dtype=np.double,
ndim=1,
flags='CONTIGUOUS')
haversine_lib.haversine.restype = ctypes.c_int
haversine_lib.haversine.argtypes = [ctypes.c_size_t,
arr_1d_double,
arr_1d_double,
arr_1d_double,
arr_1d_double,
arr_1d_double]
请注意,我们告诉 haversine_lib.haversine
函数代理根据我们想要的类型解释其参数。
现在,为了从 Python 中进行测试,我们只需要创建一个大小变量和一个数组(就像在 C 代码中一样)来存储结果数据,然后调用它即可:
size = len(lat1)
output = np.empty(size, dtype=np.double)
print "====="
print output
t2 = time.time()
res = haversine_lib.haversine(size, lon1, lat1, lon2, lat2, output)
t3 = time.time()
print t3 - t2, res
print type(output), output
在haversine.py
的__main__
块中,将所有内容都放在一起,整个文件现在看起来像这样:
import time
import ctypes
import numpy as np
from math import radians, cos, sin, asin, sqrt
def haversine(lon1, lat1, lon2, lat2):
"""
Calculate the great circle distance between two points
on the earth (specified in decimal degrees)
"""
lon1, lat1, lon2, lat2 = map(np.radians, [lon1, lat1, lon2, lat2])
dlon = lon2 - lon1
dlat = lat2 - lat1
a = (np.sin(dlat/2)**2
+ np.cos(lat1) * np.cos(lat2) * np.sin(dlon/2)**2)
c = 2 * np.arcsin(np.sqrt(a))
km = 6367 * c
return km
if __name__ == "__main__":
lat1 = 50.0 * np.random.rand(1000000)
lon1 = 50.0 * np.random.rand(1000000)
lat2 = 50.0 * np.random.rand(1000000)
lon2 = 50.0 * np.random.rand(1000000)
t0 = time.time()
r1 = haversine(lon1, lat1, lon2, lat2)
t1 = time.time()
print t1-t0, r1
lib_path = "/home/ely/programming/python/numpy_ctypes/haversine.so"
haversine_lib = ctypes.CDLL(lib_path)
arr_1d_double = np.ctypeslib.ndpointer(dtype=np.double,
ndim=1,
flags='CONTIGUOUS')
haversine_lib.haversine.restype = ctypes.c_int
haversine_lib.haversine.argtypes = [ctypes.c_size_t,
arr_1d_double,
arr_1d_double,
arr_1d_double,
arr_1d_double,
arr_1d_double]
size = len(lat1)
output = np.empty(size, dtype=np.double)
print "====="
print output
t2 = time.time()
res = haversine_lib.haversine(size, lon1, lat1, lon2, lat2, output)
t3 = time.time()
print t3 - t2, res
print type(output), output
要运行它,分别运行并计时Python和ctypes
版本,然后打印一些结果,我们只需要执行以下命令:
python haversine.py
显示以下内容:
0.111340045929 [ 231.53695005 3042.84915093 169.5158946 ..., 1359.2656769
2686.87895954 3728.54788207]
=====
[ 6.92017600e-310 2.97780954e-316 2.97780954e-316 ...,
3.20676686e-001 1.31978329e-001 5.15819721e-001]
0.148446083069 0
<type 'numpy.ndarray'> [ 231.53675618 3042.84723579 169.51575588 ..., 1359.26453029
2686.87709456 3728.54493339]
预期的结果是,numpy
版本略微更快(对于长度为1百万的向量,需要0.11秒),但是我们简单粗暴的ctypes
版本也表现不俗:在同样的数据上表现出色,只需要0.148秒。
现在,让我们将这与Python中朴素的for循环解决方案进行比较:
from math import radians, cos, sin, asin, sqrt
def slow_haversine(lon1, lat1, lon2, lat2):
n = len(lon1)
kms = np.empty(n, dtype=np.double)
for i in range(n):
lon1_v, lat1_v, lon2_v, lat2_v = map(
radians,
[lon1[i], lat1[i], lon2[i], lat2[i]]
)
dlon = lon2_v - lon1_v
dlat = lat2_v - lat1_v
a = (sin(dlat/2)**2
+ cos(lat1_v) * cos(lat2_v) * sin(dlon/2)**2)
c = 2 * asin(sqrt(a))
kms[i] = 6367 * c
return kms
当我将这个放到与其他代码放在同一个Python文件中,并对同样的百万元素数据进行计时,我总是看到我的机器上大约2.65秒的时间。
因此,通过快速切换到ctypes
,我们将速度提高了约18倍。对于许多可以受益于访问裸、连续数据的计算,您通常会看到比这更高的收益。
仅仅为了非常明确,我并不完全认可这比仅仅使用numpy
更好。这正是numpy
所致力于解决的问题,因此在以下两种情况下自己开发ctypes
代码既不太有效(a)在您的应用程序中合并numpy
数据类型有意义;(b)存在一种简单的方法将您的代码映射到numpy
等效形式中。
但对于那些希望以C语言编写某些内容但又需要在Python中调用它的场合(或在嵌入式系统中无法安装numpy
的情况下),掌握如何做这件事仍然非常有帮助。
apply
操作,可以直接使用数据框中的列。 - reptilicus