Python - 版本化列表而不是不可变列表?

时间:2017-06-13 22:40:42

标签: python list

更新

  • 从CPython 3.6开始,字典有一个版本(谢谢pylang向我展示)。
  • 如果他们将相同的版本添加到列表并公开,我原来的帖子中的所有3个断言都会通过!它肯定会满足我的需求。它们的实现与我设想的不同,但我喜欢它。
  • 实际上,我觉得我不能使用字典版本:
    • 不公开。 Jake Vanderplas展示了如何在post中展示它,但他警告说:绝对不是你应该用于任何目的的代码,而不仅仅是玩得开心。我同意他的理由。
    • 在我的所有用例中,数据在概念上是元素数组,每个元素都具有相同的结构。元组列表很自然。使用字典会使代码不那么自然,而且可能更麻烦。
  • 有没有人知道是否有计划将版本添加到列表中?
  • 是否有计划将其公之于众?

如果有计划将 version 添加到列表并公开,我会觉得现在提出一个不兼容的VersionedList很尴尬。我只是实现我需要的最低限度并且过去了。

以下原帖

事实证明,很多时候我想要一个不可变的列表, VersionedList 几乎同样有效(有时甚至更好)。

  • 是否有人实施了版本化列表?
  • 是否有符合我需求的更好,更Pythonic的概念? (见下面的动机。)

版本化列表的含义是:

  • 行为类似于列表的类
  • 对实例中的实例或元素进行的任何更改都会导致更新instance.version()。因此,如果alist是正常列表:

    a = VersionedList(alist)
    a_version = a.version()
    change(a)
    assert a_version != a.version()
    reverse_last_change(a)  
    
  • 如果列表是可清除的,则hash()将实现上述目标并满足以下动机中确定的所有需求。我们需要定义' version()'以某种方式没有所有与' hash()'相同的问题。

    如果在初始化时极不可能发生两个列表中的相同数据,那么我们就没有理由测试深度相等性。 From(https://docs.python.org/3.5/reference/datamodel.html#object.hash唯一必需的属性是比较相等的对象具有相同的哈希值。如果我们不将此要求强加于版本()',则可能是' version()'不会有所有相同的问题,使列表不可用。因此,与哈希不同,相同的内容不会表示相同的版本

    #contents of 'a' are now identical to original, but...
    assert a_version != a.version()
    
    b = VersionedList(alist)
    c = VersionedList(alist)
    assert b.version() != c.version()
    
  • 对于VersionList,如果任何修改__get__的结果的尝试自动导致副本而不是修改底层实现数据,那将是很好的。我认为唯一的另一种选择是让__get__总是返回元素的副本,这对于我能想到的所有用例来说都是非常低效的。我认为我们需要将元素限制为不可变对象(深度不可变,例如:使用列表元素排除元组)。我可以想出三种方法来实现这个目标:

    1. 只允许不能包含可变元素的元素(int,str等都可以,但排除元组)。 (这对我的案例来说太过限制了)
    2. 将代码添加到__init____set__等,以遍历输入以深入检查可变子元素。 (贵,有什么方法可以避免这种情况?)
    3. 还允许更复杂的元素,但要求它们是不可变的。也许要求他们公开deeply_immutable属性。 (这对我所有的用例来说都很容易)

动机:

  1. 如果我正在分析数据集,我经常需要执行多个返回大数据集的步骤(注意:由于数据集是有序,最好用List而不是集合表示)。

    如果在几个步骤结束时(例如5),我发现我需要执行不同的分析(例如:返回步骤4),我想知道步骤3中的数据集不是偶然的被改变了。这样我就可以从第4步开始,而不是重复步骤1-3。

  2. 我有依赖并返回数组值对象的函数(控制点,一阶导数,二阶导数,偏移,轮廓等)(在线性代数意义上)。基地阵列'是knots

    control-points() 取决于: knotsalgorithm_enum
    first-derivative() 取决于: control-points()knots
    offset() 取决于: first-derivative()control-points()knotsoffset_distance
    outline() 取决于: offset()end_type_enum

    如果offset_distance发生变化,我想避免重新计算一阶导数()和控制点()。为避免重新计算,我需要知道没有任何意外更改结果的数组'。

    如果打结'更改,我需要重新计算所有内容,而不是依赖于之前生成的数组'。

    为实现这一目标,我需要knots以及所有'数组值'对象可以是VersionedList。

  3. 仅供参考:我曾希望利用像numpy.ndarray这样的高效课程。在我的大多数用例中,元素在逻辑上都具有结构。必须精神上跟踪索引的多维度意味着使用ndarray实现和调试算法要困难许多倍。基于namedtuples的命名元组列表的实现证明更具可持续性。

1 个答案:

答案 0 :(得分:1)

3.6中的私人词汇

在Python 3.6中,字典现在是私有的(PEP 509)和紧凑的(issue 27350),它们分别跟踪版本和保留顺序。当使用CPython 3.6实现时,这些功能目前是正确的。尽管面临挑战,Jake VanderPlas在他的blog post演示了在普通Python中从CPython中暴露这个版本控制功能的详细演示。我们可以用他的方法:

  1. 确定字典何时更新
  2. 保留订单
  3. 实施例

    import numpy as np
    
    d = {"a": np.array([1,2,3]),
         "c": np.array([1,2,3]),
         "b": np.array([8,9,10]),
        }
    
    for i in range(3):
        print(d.get_version())                                 # monkey-patch
    # 524938
    # 524938
    # 524938
    

    请注意,在更新字典之前,版本号不会更改,如下所示:

    d.update({"c": np.array([10, 11, 12])})
    d.get_version()
    # 534448
    

    此外,保留了插入顺序(以下是在Python 3.5和3.6的重新启动的会话中测试过的):

    list(d.keys())
    # ['a', 'c', 'b']
    

    您可以利用这种新的字典行为,从而避免实现新的数据类型。

    <强>详情

    对于那些感兴趣的人,后者get_version()是任何字典的猴子修补方法,使用从Jake VanderPlas博客文章派生的以下修改代码在Python 3.6中实现。此代码在调用get_version().

    之前运行
    import types
    import ctypes
    import sys
    assert (3, 6) <= sys.version_info < (3, 7)                 # valid only in Python 3.6
    
    py_ssize_t = ctypes.c_ssize_t  
    
    # Emulate the PyObjectStruct from CPython
    class PyObjectStruct(ctypes.Structure):
        _fields_ = [('ob_refcnt', py_ssize_t),
                    ('ob_type', ctypes.c_void_p)]
    
    
    # Create a DictStruct class to wrap existing dictionaries
    class DictStruct(PyObjectStruct):
        _fields_ = [("ma_used", py_ssize_t),
                    ("ma_version_tag", ctypes.c_uint64),
                    ("ma_keys", ctypes.c_void_p),
                    ("ma_values", ctypes.c_void_p),
                   ]
    
        def __repr__(self):
            return (f"DictStruct(size={self.ma_used}, "
                    f"refcount={self.ob_refcnt}, "
                    f"version={self.ma_version_tag})")
    
        @classmethod
        def wrap(cls, obj):
            assert isinstance(obj, dict)
            return cls.from_address(id(obj))
    
    assert object.__basicsize__ == ctypes.sizeof(PyObjectStruct)
    assert dict.__basicsize__ == ctypes.sizeof(DictStruct)
    
    
    # Code for monkey-patching existing dictionaries
    class MappingProxyStruct(PyObjectStruct):
        _fields_ = [("mapping", ctypes.POINTER(DictStruct))]
    
        @classmethod
        def wrap(cls, D):
            assert isinstance(D, types.MappingProxyType)
            return cls.from_address(id(D))
    
    assert types.MappingProxyType.__basicsize__ == ctypes.sizeof(MappingProxyStruct)
    
    
    def mappingproxy_setitem(obj, key, val):
        """Set an item in a read-only mapping proxy"""
        proxy = MappingProxyStruct.wrap(obj)
        ctypes.pythonapi.PyDict_SetItem(proxy.mapping,
                                        ctypes.py_object(key),
                                        ctypes.py_object(val))
    
    mappingproxy_setitem(dict.__dict__,
                         'get_version',
                         lambda self: DictStruct.wrap(self).ma_version_tag)