Python 中的纯静态 classes - 使用 metaclass、class 装饰器或其他东西?

Purely static classes in Python - Use metaclass, class decorator, or something else?

在我正在开发的程序的一部分中,我想使用作为数据集某些函数的项执行线性回归 X。所使用的确切模型可由用户配置,特别是要使用的术语(或术语集)。这涉及生成矩阵 X',其中 X' 的每一行都是 X 对应行的函数。 X' 的列将作为我的回归的预测变量。

例如,假设我的数据集是二维的(X2 列)。如果我们将 xx' 表示为 XX' 的对应行,则假设 x 是二维的 x' 可能类似于

[ 1, x[0], x[1], x[0] * x[1], sqrt(x[0]), sqrt(x[1]), x[0]**2, x[1]**2 ]

您可以看到这些术语成组出现。首先是 1(常量),然后是未转换的数据(线性),然后是两个数据元素的乘积(如果 x 具有两个以上的维度,则所有成对乘积),然后是平方根和平方个别条款。

我需要在 python 中以某种方式定义所有这些术语集,这样每个术语都有一个用户可读的名称、生成术语的函数、从输入,根据数据的列标签为术语生成标签的函数等。从概念上讲,这些都感觉它们应该是 TermSet class 或类似的东西的实例,但这并不完全工作,因为他们的方法需要不同。我的第一个想法是使用这样的东西:

termsets = {} # Keep track of sets

class SqrtTerms:
    display = 'Square Roots' # user-readable name

    @staticmethod
    def size(d):
        """Number of terms based on input columns"""
        return d

    @staticmethod
    def make(X):
        """Make the terms from the input data"""
        return numpy.sqrt(X)

    @staticmethod
    def labels(columns):
        """List of term labels based off of data column labels"""
        return ['sqrt(%s)' % c for c in columns]

termsets['sqrt'] = SqrtTerms # register class in dict


class PairwiseProductTerms:
    display = 'Pairwise Products'

    @staticmethod
    def size(d):
        return (d * (d-1)) / 2

    @staticmethod
    def make(X):
        # Some more complicated code that spans multiple lines
        ...

    @staticmethod
    def labels(columns):
        # Technically a one-liner but also more complicated
        return ['(%s) * (%s)' % (columns[c1], columns[c2])
            for c1 in range(len(columns)) for c2 in range(len(columns))
            if c2 > c1]

termsets['pairprod'] = PairwiseProductTerms

这行得通:我可以从字典中检索 classes,将我想使用的那些放在列表中,然后对每个调用适当的方法。不过,创建仅包含静态属性和方法的 classes 看起来很丑陋而且不 pythonic。我想出的另一个想法是创建一个 class 装饰器,可以像这样使用:

# Convert bound methods to static ones, assign "display" static
# attribute and add to dict with key "name"
@regression_terms(name='sqrt', display='Square Roots')
class SqrtTerms:
    def size(d):
        return d
    def make(X):
        return numpy.sqrt(X)
    def labels(columns):
        return ['sqrt(%s)' % c for c in columns]

这给出了相同的结果,但更清晰,更易读和写(对我自己而言)(尤其是当我需要大量这些内容时)。然而,事情在引擎盖下的实际工作方式是模糊的,任何阅读本文的人可能很难一开始就弄清楚到底发生了什么。我还想过为这些创建一个 metaclass 但这听起来有点矫枉过正。我应该在这里使用更好的模式吗?

总有人会说这是滥用语言。我说 Python 被设计成可滥用的,并且能够创建不需要解析器但看起来不像 lisp 的 DSL 是它的核心优势之一。

如果你真的有很多这样的东西,那就选择 metaclass。如果这样做,除了拥有术语词典之外,您还可以拥有引用这些术语的属性。这真的很好,因为你可以有这样的代码:

print Terms.termsets
print Terms.sqrt
print Terms.pairprod
print Terms.pairprod.size(5)

return 结果如下:

{'pairprod': <class '__main__.PairwiseProductTerms'>,
 'sqrt': <class '__main__.SqrtTerms'>}
<class '__main__.SqrtTerms'>
<class '__main__.PairwiseProductTerms'>
10

完整的代码在这里:

from types import FunctionType

class MetaTerms(type):
    """
    This metaclass will let us create a Terms class.
    Every subclass of the terms class will have its
    methods auto-wrapped as static methods, and
    will be added to the terms directory.
    """
    def __new__(cls, name, bases, attr):
        # Auto-wrap all methods as static methods
        for key, value in attr.items():
            if isinstance(value, FunctionType):
                attr[key] = staticmethod(value)
        # call types.__new__ to finish the job
        return super(MetaTerms, cls).__new__(cls, name, bases, attr)

    def __init__(cls, name, bases, attr):
        # At __init__ time, the class has already been
        # built, so any changes to the bases or attr
        # will not be reflected in the cls.
        # Call types.__init__ to finish the job
        super(MetaTerms, cls).__init__(name, bases, attr)
        # Add the class into the termsets.
        if name != 'Terms':
            cls.termsets[cls.shortname] = cls

    def __getattr__(cls, name):
        return cls.termsets[name]

class Terms(object):
    __metaclass__ = MetaTerms
    termsets = {} # Keep track of sets


class SqrtTerms(Terms):
    display = 'Square Roots' # user-readable name
    shortname = 'sqrt'  # Used to find in Terms.termsets

    def size(d):
        """Number of terms based on input columns"""
        return d

    def make(X):
        """Make the terms from the input data"""
        return numpy.sqrt(X)

    def labels(columns):
        """List of term labels based off of data column labels"""
        return ['sqrt(%s)' % c for c in columns]


class PairwiseProductTerms(Terms):
    display = 'Pairwise Products'
    shortname = 'pairprod'

    def size(d):
        return (d * (d-1)) / 2

    def make(X):
        pass

    def labels(columns):
        # Technically a one-liner but also more complicated
        return ['(%s) * (%s)' % (columns[c1], columns[c2])
            for c1 in range(len(columns)) for c2 in range(len(columns))
            if c2 > c1]

print Terms.termsets
print Terms.sqrt
print Terms.pairprod
print Terms.pairprod.size(5)

如果您将元class 和基本术语class 隐藏在一个单独的模块中,那么没有人需要查看它——只需from baseterm import Terms。您还可以做一些很酷的自动发现/自动导入,其中将模块转储到正确的目录中会自动将它们添加到您的 DSL 中。

有了元class,功能集可以很容易地有机地增长,因为你发现了你想让你的迷你语言做的其他事情。