Python 中的纯静态类 - 使用元类、类装饰器还是其他东西?

Posted

技术标签:

【中文标题】Python 中的纯静态类 - 使用元类、类装饰器还是其他东西?【英文标题】:Purely static classes in Python - Use metaclass, class decorator, or something else? 【发布时间】:2015-11-04 08:21:24 【问题描述】:

在我正在开发的程序的一部分中,我想使用作为数据集 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 类的实例或类似的东西,但这并不完全有效,因为它们的方法需要不同。我的第一个想法是使用这样的东西:

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

这很有效:我可以从字典中检索类,将我想要使用的类放在一个列表中,然后对每个类调用适当的方法。尽管如此,创建仅具有静态属性和方法的类看起来很丑陋且不符合 Python 标准。我想出的另一个想法是创建一个可以像这样使用的类装饰器:

# 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]

这给出了相同的结果,但是(对我自己而言)阅读和写作更干净、更好(尤其是当我需要很多这些时)。然而,事情实际上在幕后工作的方式是模糊的,任何阅读这篇文章的人可能一开始很难弄清楚发生了什么。我也想过为这些创建一个元类,但这听起来有点矫枉过正。我应该在这里使用更好的模式吗?

【问题讨论】:

有功能的模块呢? 纯静态类的标准替代方案是模块,但在这种情况下,我有许多具有相同名称的函数和属性的类。这一切都已经在一个模块中,但我绝对可以通过将它变成一个包并为每组术语创建一个模块来完成我想要的。不过,这真的完全不像您应该使用模块。 【参考方案1】:

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

如果您真的有很多这些,请使用元类。如果这样做,除了拥有术语字典之外,您还可以拥有引用这些术语的属性。真的很好,因为你可以有这样的代码:

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

返回结果如下:

'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)

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

使用元类,功能集可以很容易地有机地增长,因为您可以找到其他您希望您的迷你语言做的事情。

【讨论】:

我认为你是对的,很多人会说这是对语言的滥用,但总的来说,这看起来是最干净的解决方案。比我想出的装饰器要好。我特别喜欢 getattr 的实现。

以上是关于Python 中的纯静态类 - 使用元类、类装饰器还是其他东西?的主要内容,如果未能解决你的问题,请参考以下文章

Python之路:描述符,类装饰器,元类

Python - 元类装饰器 - 如何使用 @classmethod

Python基础(30)——上下文管理,描述符,类装饰器,元类

在类中的所有实例方法自动调用之后调用方法:元类还是装饰器?

使用装饰器设置类的元类

使用两种不同的装饰器实现来装饰所有类方法的元类