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

标准namedtuple似乎是几乎完美的解决方案,除了它们不能与PyCharm之类的现代Python IDE很好地配合使用。我的意思是,IDE不会真正提供有关定义为namedtuple的类的任何有用的见解。虽然可以使用以下技巧将docstring附加到此类:

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

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

这段代码中有一些类型信息,但不是很有用:
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类或namedtuple一样容易定义
  • 提供值语义(相等,哈希,不变性)
  • 易于记录,可以与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
    

    列出所有属性总共有四次:__slots__,ctor参数,docstring和ctor正文。到目前为止,我还不知道如何减轻它的尴尬。

    最佳答案

    您的要求尽管经过了精心表达,但对我来说却不太清楚,部分原因是我不使用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提供类型注释提示。

    关于python - 如何在Python中定义对PyCharm友好的值对象?,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/19508572/

    10-12 17:01
    查看更多