【问题标题】:Python - Versioned list instead of immutable list?Python - 版本列表而不是不可变列表?
【发布时间】:2017-06-15 22:05:29
【问题描述】:

更新:

  • 从 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() 将实现上述目的并满足以下动机中确定的所有需求。我们需要以一种不会有所有与“hash()”相同的问题的方式定义“version()”。

    如果两个列表中的相同数据极不可能发生,除非在初始化时,我们将没有理由测试深度相等。来自 (https://docs.python.org/3.5/reference/datamodel.html#object.hash) 唯一需要的属性是比较相等的对象具有相同的哈希值。如果我们不对'version()' 强加这个要求,'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__ 始终返回元素的副本,这对于我能想到的所有用例来说都是非常低效的。我认为我们需要将元素限制为不可变对象(深度不可变,例如:排除带有列表元素的元组)。我可以想到 3 种方法来实现这一点:

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

动机:

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

    如果在几个步骤结束时(例如:5)结果我需要执行不同的分析(例如:回到步骤 4),我想知道步骤 3 中的数据集没有被意外更改.这样我就可以从第 4 步开始,而不是重复第 1-3 步。

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

    control-points() 取决于: knots, algorithm_enum
    first-derivative() 取决于: control-points(), knots
    @ 987654340@ 取决于: first-derivative(), control-points(), knots, offset_distance
    outline() 取决于: offset(), end_type_enum

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

    如果“结”发生变化,我需要重新计算所有内容,而不是依赖于之前的结果“数组”。

    要实现这一点,knots 和所有“数组值”对象都可以是 VersionedList。

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

【问题讨论】:

  • 你能澄清最后一段代码吗?它不是有效的 Python。向我们展示您将使用的代码,完全按照您的设想使用。
  • knots[] 应该是什么?看起来您正试图将 knots 声明为一个数组,但 Python 中没有变量声明,您可能的意思是它是一个列表,而不是一个数组...但是您这样做 提到数组,但你到底是什么意思? array.array? numpy.ndarray?
  • 在实际实现中,所有的数组都是对象的成员。这使得拥有独立实例变得容易。所以,调用有空的参数列表。为了阐明依赖关系,实际代码必须包含大类的大部分。
  • 查看对我的评论的编辑,数组是什么意思
  • 我在过去使用dict 完成了此操作,解决方案是使用“已删除”元素来表示已删除的密钥,并将每个新版本中的更新密钥存储为单独的dicts,然后进行深度树展平以获得当前版本。使用列表会更复杂,因为您还需要保持顺序和关系,而不仅仅是在一个级别上 - 这将很快让您意识到您需要构建一个状态机(CPU 密集型)或创建一个新的将每个破坏性更改复制到原始列表(内存密集型)。鉴于这些问题,我怀疑这种结构的实用性。

标签: python list


【解决方案1】:

3.6 中的私有字典

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

  1. 确定字典何时更新
  2. 保留订单

例子

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() 是一种猴子补丁方法,适用于任何字典,在 Python 3.6 中使用以下从 Jake VanderPlas 的博客文章派生的修改代码实现。此代码在调用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)

【讨论】:

  • 谢谢pylang,这很令人兴奋。我不认为我可以将它用于我的项目,因为Jake Vanderplas' post states:绝对不是您应该将其用于任何目的的代码,他这么说的理由令人信服。
  • CPython 实现当然应该临时应用,因为它是相当新的。这是否会在未来版本中公开用于 dicts 或应用于列表,目前尚有推测。至于 Python 3.6,它应该可以工作,尽管它是前卫的。
猜你喜欢
  • 2013-06-13
  • 2013-09-26
  • 2014-04-15
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多