15

我很好奇在 Python 中定义值对象的好方法。根据维基百科:“值对象是一个小对象,它代表一个简单实体,其相等性不基于身份:即两个值对象在具有相同值时相等,不一定是同一个对象”。在 Python 中,这本质上意味着重新定义__eq____hash__方法,以及不变性。

标准namedtuple似乎几乎是完美的解决方案,但它们不能很好地与 PyCharm 等现代 Python IDE 配合使用。我的意思是 IDE 不会真正提供关于定义为namedtuple. 虽然可以使用以下技巧将文档字符串附加到此类:

class Point2D(namedtuple("Point2D", "x y")):
    """Class for immutable value objects"""
    pass

根本没有地方可以放置构造函数参数的描述并指定它们的类型。PyCharm 足够聪明,可以猜测Point2D“构造函数”的参数,但在类型方面它是盲目的。

这段代码推入了一些类型信息,但它不是很有用:

class Point2D(namedtuple("Point2D", "x y")):
    """Class for immutable value objects"""
    def __new__(cls, x, y):
        """
        :param x: X coordinate
        :type x: float

        :param y: Y coordinate
        :type y: float

        :rtype: Point2D
        """
        return super(Point2D, cls).__new__(cls, x, y)

point = Point2D(1.0, 2.0)

PyCharm 在构造新对象时会看到类型,但不会掌握 point.x 和 point.y 是浮点数,因此无助于检测它们的滥用。而且我也不喜欢在常规基础上重新定义“魔术”方法的想法。

所以我正在寻找的东西是:

  • 就像普通的 Python 类或命名元组一样容易定义
  • 提供值语义(平等、散列、不变性)
  • 易于以与 IDE 完美配合的方式记录

理想的解决方案可能如下所示:

class Point2D(ValueObject):
    """Class for immutable value objects"""
    def __init__(self, x, y):
        """
        :param x: X coordinate
        :type x: float

        :param y: Y coordinate
        :type y: float
        """
        super(Point2D, self).__init__(cls, x, y)

或者那个:

class Point2D(object):
    """Class for immutable value objects"""

    __metaclass__ = ValueObject

    def __init__(self, x, y):
        """
        :param x: X coordinate
        :type x: float

        :param y: Y coordinate
        :type y: float
        """
        pass

我试图找到这样的东西,但没有成功。我认为在自己实施之前寻求帮助是明智的。

更新:在 user4815162342 的帮助下,我设法想出了一些可行的方法。这是代码:

class ValueObject(object):
    __slots__ = ()

    def __repr__(self):
        attrs = ' '.join('%s=%r' % (slot, getattr(self, slot)) for slot in self.__slots__)
        return '<%s %s>' % (type(self).__name__, attrs)

    def _vals(self):
        return tuple(getattr(self, slot) for slot in self.__slots__)

    def __eq__(self, other):
        if not isinstance(other, ValueObject):
            return NotImplemented
        return self.__slots__ == other.__slots__ and self._vals() == other._vals()

    def __ne__(self, other):
        return not self == other

    def __hash__(self):
        return hash(self._vals())

    def __getstate__(self):
        """
        Required to pickle classes with __slots__
        Must be consistent with __setstate__
        """
        return self._vals()

    def __setstate__(self, state):
        """
        Required to unpickle classes with __slots__
        Must be consistent with __getstate__
        """
        for slot, value in zip(self.__slots__, state):
            setattr(self, slot, value)

这与理想的解决方案相去甚远。类声明如下所示:

class X(ValueObject):
    __slots__ = "a", "b", "c"

    def __init__(self, a, b, c):
        """
        :param a:
        :type a: int
        :param b:
        :type b: str
        :param c:
        :type c: unicode
        """
        self.a = a
        self.b = b
        self.c = c

列出所有属性总共有四次: in __slots__, in ctor arguments, in docstring 和 in ctor body。到目前为止,我不知道如何使它不那么尴尬。

4

3 回答 3

6

3.5 版新功能:typing模块和NamedTuple

在 3.5 版本中,typing添加了该模块,在其中,您将找到一个完全适合您需求的类。

新的NamedTuple

它就像您期望的那样工作:

  • 简单类型定义:

    from typing import NamedTuple
    class DownloadableFile(NamedTuple):
        file_path: str
        download_url: str
    
  • 在 PyCharm 中得到认可:

    PyCharm 完成 PyCharm 类型检测


注意
截至今天,API 仍处于临时阶段。这意味着发布新版本时不能保证向后兼容。虽然不期望对界面进行更改。我个人的看法是:鉴于设计的简单性,如果有变化,我相信这将是一个简单的重构;)

于 2018-11-11T11:49:37.830 回答
4

您的要求虽然经过仔细表达,但对我来说不是很清楚,部分原因是我不使用 PyCharm GUI。但这是一个尝试:

class ValueObject(object):
    __slots__ = ()

    def __init__(self, *vals):
        if len(vals) != len(self.__slots__):
            raise TypeError, "%s.__init__ accepts %d arguments, got %d" \
                % (type(self).__name__, len(self.__slots__), len(vals))
        for slot, val in zip(self.__slots__, vals):
            super(ValueObject, self).__setattr__(slot, val)

    def __repr__(self):
        return ('<%s[0x%x] %s>'
                % (type(self).__name__, id(self),
                   ' '.join('%s=%r' % (slot, getattr(self, slot))
                            for slot in self.__slots__)))

    def _vals(self):
        return tuple(getattr(self, slot) for slot in self.__slots__)

    def __eq__(self, other):
        if not isinstance(other, ValueObject):
            return NotImplemented
        return self.__slots__ == other.__slots__ and self._vals() == other._vals()

    def __ne__(self, other):
        return not self == other

    def __hash__(self):
        return hash(self._vals())

    def __setattr__(self, attr, val):
        if attr in self.__slots__:
            raise AttributeError, "%s slot '%s' is read-only" % (type(self).__name__, attr)
        super(ValueObject, self).__setattr__(attr, val)

用法是这样的:

class X(ValueObject):
  __slots__ = 'a', 'b'

这将为您提供一个具有两个只读插槽和一个自动生成的构造函数的具体值类__eq__, 和__hash__。例如:

>>> x = X(1.0, 2.0, 3.0)
Traceback (most recent call last):
  File "<input>", line 1, in <module>
  File "<input>", line 5, in __init__
TypeError: X.__init__ accepts 2 arguments, got 3
>>> x = X(1.0, 2.0)
>>> x
<X[0x4440a50] a=1.0 b=2.0>
>>> x.a
1.0
>>> x.b
2.0
>>> x.a = 10
Traceback (most recent call last):
  File "<input>", line 1, in <module>
  File "<input>", line 32, in __setattr__
AttributeError: X slot 'a' is read-only
>>> x.c = 10
Traceback (most recent call last):
  File "<input>", line 1, in <module>
  File "<input>", line 33, in __setattr__
AttributeError: 'X' object has no attribute 'c'
>>> dir(x)
['__class__', '__delattr__', '__dict__', '__doc__', '__eq__', '__format__', '__getattribute__', '__hash__', '__init__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__slots__', '__str__', '__subclasshook__', '__weakref__', '_vals', 'a', 'b']
>>> x == X(1.0, 2.0)
True
>>> x == X(1.0, 3.0)
False
>>> hash(x)
3713081631934410656
>>> hash(X(1.0, 2.0))
3713081631934410656
>>> hash(X(1.0, 3.0))
3713081631933328131

如果您愿意,您可以__init__使用(可能)为您的 IDE 提供类型注释提示的文档字符串定义您自己的文档字符串。

于 2013-10-22T17:50:33.437 回答
3

更新解决方案:

从 python 3.7 开始,有一个名为的新内置模块dataclasses,其中包含dataclass类。

Pycharm 支持它并且知道如何使用它。

它非常适合值对象,因为它已经定义了很多您将为值对象定义的内容,而且语法非常简短:

  • 代表
  • 情商
  • 通过传递使类不可变Frozen=True

例子:

from dataclasses import dataclass
@dataclass(frozen=True)
class Point2D:
    x: float
    y: float

两者之间存在差异NamedTupleDataclasses最值得注意的是一个基于元组,另一个基于字典。

在我看来,dataclassATM 是创建值对象的最佳代码生成器。

有关更多信息,请继续阅读有关如何使用dataclass.

于 2020-04-13T10:36:33.697 回答