提供不同线性代数后端的架构

13 投票
3 回答
632 浏览
提问于 2025-04-16 14:02

我正在用Python开发一个新系统,这个系统主要是处理数字计算。

一个重要的要求是能够使用不同的线性代数后端:从用户自己实现的代码到一些通用的库,比如Numpy。线性代数的实现(也就是后端)必须和接口是独立的。

我最初的架构尝试是这样的:

(1) 定义系统接口

>>> v1 = Vector([1,2,3])
>>> v2 = Vector([4,5,6])
>>> print v1 * v2
>>> # prints "Vector([4, 10, 18])"

(2) 实现代码,使得可以独立于后端使用这个接口

# this example uses numpy as the back-end, but I mean
# to do this for a general back-end
import numpy 
def numpy_array(*args): # creates a numpy array from the arguments
    return numpy.array(*args)

class VectorBase(type):
    def __init__(cls, name, bases, attrs):
        engine = attrs.pop("engine", None)
        if not engine:
            raise RuntimeError("you need to specify an engine")
        # this implementation would change depending on `engine`
        def new(cls, *args):
            return numpy_array(*args)   
        setattr(cls, "new", classmethod(new))

class Vector(object):   
    __metaclass__ = VectorBase        
    # I could change this at run time
    # and offer alternative back-ends
    engine = "numpy"  
    @classmethod
    def create(cls, v):
        nv = cls()
        nv._v = v
        return nv    
    def __init__(self, *args):  
        self._v = None
        if args:
            self._v = self.new(*args)
    def __repr__(self):
        l = [item for item in self._v]
        return "Vector(%s)" % repr(l)
    def __mul__(self, other):
        try:
            return Vector.create(self._v * other._v)
        except AttributeError:
            return Vector.create(self._v * other)
    def __rmul__(self, other):
        return self.__mul__(other)

这个简单的例子是这样工作的:Vector 类保存了一个由后端创建的向量实例的引用(在这个例子中是numpy.ndarray);所有的数学运算都是由接口来实现的,但具体的计算是交给后端去处理。

实际上,接口重载了所有合适的运算符,并将计算交给后端(这个例子只展示了__mul____rmul__,但你可以想象其他操作也是这样处理的)。

我愿意在性能上做一些牺牲,以换取可定制性。虽然我的例子可以运行,但感觉不太对——我觉得这样会让后端因为太多的构造函数调用而变得很笨重!这就需要一种不同的metaclass实现,以便更好地处理调用的延迟。

那么,你会建议我怎么实现这个功能呢?我想强调的是,保持系统中所有的Vector 实例在功能上是一致的,并且与线性代数后端是独立的,这一点非常重要。

3 个回答

2

顺便说一下,你可以很简单地配置和构建NumPy,让它使用Intel的数学核心库或者AMD的核心数学库,而不是通常的ATLAS + LAPACK。这其实很简单,只需要创建一个site.cfg文件,里面设置好blas_libslapack_libslibrary_dirsinclude_dirs这些变量就行了。(关于如何为MKL和ACML设置这些选项,网上有很多资料可以查。)把这个文件放在setup.py脚本旁边,然后像往常一样进行构建。

如果你想在这些标准的线性代数库之间切换,可以为每个库构建一个不同的NumPy实例,并使用virtualenvs来管理它们。

我知道这样做可能没有你想要的灵活性,无法使用你自己的自定义数学库,但我只是想提一下这个方法。而且虽然我没有深入研究过,但我想你可能也能让NumPy与自定义库一起构建,这样做的工作量可能比自己从头开始构建一个前端要少,尤其是如果你想保留NumPy/SciPy的丰富功能的话。

3

为什么不直接创建一个“虚拟”类(AbstractVector),这个类就像你例子中的Vector,然后为每种实现创建不同的子类呢?

你可以通过像 Vector = NumPyVector 这样的方式来选择引擎。

6

你可以去看看 PEP-3141 以及标准库模块 ABCMeta

如果想要详细了解如何使用 ABCMeta,永远都很有帮助的 PyMOTW 上有一篇不错的介绍。

撰写回答