Python有不可变列表吗?

129

Python是否有不可变的列表?

假设我想要一个元素有序的集合,但我希望它保持不变,应该如何实现?列表是有序的,但是它们是可变的。


3
Python中不可变类型的主要动机在于它们可以用作字典键和集合中的元素。为了保持原意,我没有改变句子结构或词汇,只是尽力使其更通俗易懂。 - Sven Marnach
9个回答

132

是的,它被称为 元组

所以,[1,2] 是一个可以被修改的 列表,而 (1,2) 是一个不可以被修改的 元组


更多信息:

一个只有一个元素的 元组 不能写成 (1),你需要写成 (1,)。这是因为解释器对括号有其他用途。

你也可以完全省略括号:1,2(1,2) 是一样的。

请注意,元组 不完全等同于一个不可变的 列表。点击这里阅读有关 列表和元组之间的差异 的更多信息。


7
如果您将本质上可变的对象指针放入元组中(例如([1,2],3)),则该元组就不再是真正的不可变,因为列表对象只是对可变对象的指针,虽然指针是不可变的,但所引用的对象并非如此。 - Nisan.H
3
实际上,一个空元组也可以写成()。这是唯一需要括号的情况。 - RemcoGerlich
13
不可变的列表和元组是两个非常不同的东西。元组是不可变的,但不能迭代,也无法对单个元组执行map/reduce/filter等操作,但您应该能够在单个可变/不可变列表上执行这些操作。请查看其他更严谨地推崇不可变性和函数式编程的语言,您会发现不可变列表是必须的。 - Kane
12
元组不是列表,它们没有兼容的行为,也不能通过多态性使用它们。 - jeremyjjbrown
17
Python 的一个大问题是它太容易使用了,以至于没有人正确地使用它。如果你尝试正确使用它,你会发现你不能这样做。这个问题是关于 Python 是否存在不可变列表(immutable list)。被接受的回答是使用元组(tuple)。但几乎所有理解语义的人都同意,元组并不是列表。(Guido 也曾说过。)所以最终问题还是没有得到解答:如何获得一个不可变的列表,类似于集合类型中的 frozenset - Garret Wilson
显示剩余9条评论

18

随着类型注解和通过mypy进行类型检查变得更加流行,这个问题值得现代化的回答。

在使用类型注解时,用元组替换List[T]可能不是理想的解决方案。从概念上讲,列表具有1个通用元数的特性,即它们具有单一的通用参数T(当然,此参数可以是Union[A, B, C, ...],以考虑异构类型列表)。相比之下,元组本质上是多态泛型Tuple[A, B, C, ...]。这使得元组成为一个笨拙的列表替代品。

事实上,类型检查提供了另一种可能性:可以使用typing.Sequence将变量注释为不可变列表,它对应于不可变接口collections.abc.Sequence的类型。例如:

from typing import Sequence


def f(immutable_list: Sequence[str]) -> None:
    # We want to prevent mutations like:
    immutable_list.append("something")


mutable_list = ["a", "b", "c"]
f(mutable_list)
print(mutable_list)

当然,就运行时行为而言,这并不是不可变的,即Python解释器会轻易地改变immutable_list,输出结果将是["a", "b", "c", "something"]

然而,如果你的项目使用像mypy这样的类型检查器,它将拒绝此代码并显示以下信息:

immutable_lists_1.py:6: error: "Sequence[str]" has no attribute "append"
Found 1 error in 1 file (checked 1 source file)

在幕后,您仍然可以继续使用常规列表,但类型检查器可以在类型检查时有效地防止任何变异。

同样,您可以通过不可变的数据类来防止修改列表成员(请注意,实际上可以在运行时防止对冻结数据类的字段进行赋值):

@dataclass(frozen=True)
class ImmutableData:
    immutable_list: Sequence[str]


def f(immutable_data: ImmutableData) -> None:
    # mypy will prevent mutations here as well:
    immutable_data.immutable_list.append("something")

同样的原理也可以通过typing.Mapping用于字典。


“Sequence”并不完全表示它是不可变的,它只是意味着它没有可用于更改其值的方法。底层变量仍然可以是可变的。 - Timmmm
1
@Timmmm 我知道,这就是为什么我写了:_当然,在运行时行为方面,这并不是不可变的,也就是说,Python解释器会愉快地改变immutable_list_。如果你的CI强制执行严格的类型检查,这在现今许多项目中都是如此,它就相对安全了。 - bluenote10
1
这实际上是一个非常好的答案,但是针对不同的问题。 - Hacker

13
在这里是一个 ImmutableList 的实现。底层列表没有以任何直接数据成员的形式暴露出来,但可以使用成员函数的闭包属性进行访问。如果我们遵循不使用上述属性修改闭包内容的惯例,则此实现将达到预期的目的。这个 ImmutableList 类的实例可以在任何需要正常的Python列表的地方使用。
from functools import reduce

__author__ = 'hareesh'


class ImmutableList:
    """
    An unmodifiable List class which uses a closure to wrap the original list.
    Since nothing is truly private in python, even closures can be accessed and
    modified using the __closure__ member of a function. As, long as this is
    not done by the client, this can be considered as an unmodifiable list.

    This is a wrapper around the python list class
    which is passed in the constructor while creating an instance of this class.
    The second optional argument to the constructor 'copy_input_list' specifies
    whether to make a copy of the input list and use it to create the immutable
    list. To make the list truly immutable, this has to be set to True. The
    default value is False, which makes this a mere wrapper around the input
    list. In scenarios where the input list handle is not available to other
    pieces of code, for modification, this approach is fine. (E.g., scenarios
    where the input list is created as a local variable within a function OR
    it is a part of a library for which there is no public API to get a handle
    to the list).

    The instance of this class can be used in almost all scenarios where a
    normal python list can be used. For eg:
    01. It can be used in a for loop
    02. It can be used to access elements by index i.e. immList[i]
    03. It can be clubbed with other python lists and immutable lists. If
        lst is a python list and imm is an immutable list, the following can be
        performed to get a clubbed list:
        ret_list = lst + imm
        ret_list = imm + lst
        ret_list = imm + imm
    04. It can be multiplied by an integer to increase the size
        (imm * 4 or 4 * imm)
    05. It can be used in the slicing operator to extract sub lists (imm[3:4] or
        imm[:3] or imm[4:])
    06. The len method can be used to get the length of the immutable list.
    07. It can be compared with other immutable and python lists using the
        >, <, ==, <=, >= and != operators.
    08. Existence of an element can be checked with 'in' clause as in the case
        of normal python lists. (e.g. '2' in imm)
    09. The copy, count and index methods behave in the same manner as python
        lists.
    10. The str() method can be used to print a string representation of the
        list similar to the python list.
    """

    @staticmethod
    def _list_append(lst, val):
        """
        Private utility method used to append a value to an existing list and
        return the list itself (so that it can be used in funcutils.reduce
        method for chained invocations.

        @param lst: List to which value is to be appended
        @param val: The value to append to the list
        @return: The input list with an extra element added at the end.

        """
        lst.append(val)
        return lst

    @staticmethod
    def _methods_impl(lst, func_id, *args):
        """
        This static private method is where all the delegate methods are
        implemented. This function should be invoked with reference to the
        input list, the function id and other arguments required to
        invoke the function

        @param list: The list that the Immutable list wraps.

        @param func_id: should be the key of one of the functions listed in the
            'functions' dictionary, within the method.
        @param args: Arguments required to execute the function. Can be empty

        @return: The execution result of the function specified by the func_id
        """

        # returns iterator of the wrapped list, so that for loop and other
        # functions relying on the iterable interface can work.
        _il_iter = lambda: lst.__iter__()
        _il_get_item = lambda: lst[args[0]]  # index access method.
        _il_len = lambda: len(lst)  # length of the list
        _il_str = lambda: lst.__str__()  # string function
        # Following represent the >, < , >=, <=, ==, != operators.
        _il_gt = lambda: lst.__gt__(args[0])
        _il_lt = lambda: lst.__lt__(args[0])
        _il_ge = lambda: lst.__ge__(args[0])
        _il_le = lambda: lst.__le__(args[0])
        _il_eq = lambda: lst.__eq__(args[0])
        _il_ne = lambda: lst.__ne__(args[0])
        # The following is to check for existence of an element with the
        # in clause.
        _il_contains = lambda: lst.__contains__(args[0])
        # * operator with an integer to multiply the list size.
        _il_mul = lambda: lst.__mul__(args[0])
        # + operator to merge with another list and return a new merged
        # python list.
        _il_add = lambda: reduce(
            lambda x, y: ImmutableList._list_append(x, y), args[0], list(lst))
        # Reverse + operator, to have python list as the first operand of the
        # + operator.
        _il_radd = lambda: reduce(
            lambda x, y: ImmutableList._list_append(x, y), lst, list(args[0]))
        # Reverse * operator. (same as the * operator)
        _il_rmul = lambda: lst.__mul__(args[0])
        # Copy, count and index methods.
        _il_copy = lambda: lst.copy()
        _il_count = lambda: lst.count(args[0])
        _il_index = lambda: lst.index(
            args[0], args[1], args[2] if args[2] else len(lst))

        functions = {0: _il_iter, 1: _il_get_item, 2: _il_len, 3: _il_str,
                     4: _il_gt, 5: _il_lt, 6: _il_ge, 7: _il_le, 8: _il_eq,
                     9: _il_ne, 10: _il_contains, 11: _il_add, 12: _il_mul,
                     13: _il_radd, 14: _il_rmul, 15: _il_copy, 16: _il_count,
                     17: _il_index}

        return functions[func_id]()

    def __init__(self, input_lst, copy_input_list=False):
        """
        Constructor of the Immutable list. Creates a dynamic function/closure
        that wraps the input list, which can be later passed to the
        _methods_impl static method defined above. This is
        required to avoid maintaining the input list as a data member, to
        prevent the caller from accessing and modifying it.

        @param input_lst: The input list to be wrapped by the Immutable list.
        @param copy_input_list: specifies whether to clone the input list and
            use the clone in the instance. See class documentation for more
            details.
        @return:
        """

        assert(isinstance(input_lst, list))
        lst = list(input_lst) if copy_input_list else input_lst
        self._delegate_fn = lambda func_id, *args: \
            ImmutableList._methods_impl(lst, func_id, *args)

    # All overridden methods.
    def __iter__(self): return self._delegate_fn(0)

    def __getitem__(self, index): return self._delegate_fn(1, index)

    def __len__(self): return self._delegate_fn(2)

    def __str__(self): return self._delegate_fn(3)

    def __gt__(self, other): return self._delegate_fn(4, other)

    def __lt__(self, other): return self._delegate_fn(5, other)

    def __ge__(self, other): return self._delegate_fn(6, other)

    def __le__(self, other): return self._delegate_fn(7, other)

    def __eq__(self, other): return self._delegate_fn(8, other)

    def __ne__(self, other): return self._delegate_fn(9, other)

    def __contains__(self, item): return self._delegate_fn(10, item)

    def __add__(self, other): return self._delegate_fn(11, other)

    def __mul__(self, other): return self._delegate_fn(12, other)

    def __radd__(self, other): return self._delegate_fn(13, other)

    def __rmul__(self, other): return self._delegate_fn(14, other)

    def copy(self): return self._delegate_fn(15)

    def count(self, value): return self._delegate_fn(16, value)

    def index(self, value, start=0, stop=0):
        return self._delegate_fn(17, value, start, stop)


def main():
    lst1 = ['a', 'b', 'c']
    lst2 = ['p', 'q', 'r', 's']

    imm1 = ImmutableList(lst1)
    imm2 = ImmutableList(lst2)

    print('Imm1 = ' + str(imm1))
    print('Imm2 = ' + str(imm2))

    add_lst1 = lst1 + imm1
    print('Liist + Immutable List: ' + str(add_lst1))
    add_lst2 = imm1 + lst2
    print('Immutable List + List: ' + str(add_lst2))
    add_lst3 = imm1 + imm2
    print('Immutable Liist + Immutable List: ' + str(add_lst3))

    is_in_list = 'a' in lst1
    print("Is 'a' in lst1 ? " + str(is_in_list))

    slice1 = imm1[2:]
    slice2 = imm2[2:4]
    slice3 = imm2[:3]
    print('Slice 1: ' + str(slice1))
    print('Slice 2: ' + str(slice2))
    print('Slice 3: ' + str(slice3))

    imm1_times_3 = imm1 * 3
    print('Imm1 Times 3 = ' + str(imm1_times_3))
    three_times_imm2 = 3 * imm2
    print('3 Times Imm2 = ' + str(three_times_imm2))

    # For loop
    print('Imm1 in For Loop: ', end=' ')
    for x in imm1:
        print(x, end=' ')
    print()

    print("3rd Element in Imm1: '" + imm1[2] + "'")

    # Compare lst1 and imm1
    lst1_eq_imm1 = lst1 == imm1
    print("Are lst1 and imm1 equal? " + str(lst1_eq_imm1))

    imm2_eq_lst1 = imm2 == lst1
    print("Are imm2 and lst1 equal? " + str(imm2_eq_lst1))

    imm2_not_eq_lst1 = imm2 != lst1
    print("Are imm2 and lst1 different? " + str(imm2_not_eq_lst1))

    # Finally print the immutable lists again.
    print("Imm1 = " + str(imm1))
    print("Imm2 = " + str(imm2))

    # The following statemetns will give errors.
    # imm1[3] = 'h'
    # print(imm1)
    # imm1.append('d')
    # print(imm1)

if __name__ == '__main__':
    main()

这很有用,Hareesh。但是返回列表的方法不应该返回ImmutableList吗?这似乎很容易做到,例如第167行:def add(self, other): return ImmutableList(self._delegate_fn(11, other))。你为什么没有这样做呢? - Richard Pawson

9

你可以使用包含两个元素的元组模拟一个类似Lisp风格的不可变的单链表(注意:这与任意元素元组答案不同,后者创建的元组要不太灵活):

nil = ()
cons = lambda ele, l: (ele, l)

例如对于列表[1, 2, 3],你会得到以下内容:
l = cons(1, cons(2, cons(3, nil))) # (1, (2, (3, ())))

您的标准carcdr函数很简单:
car = lambda l: l[0]
cdr = lambda l: l[1]

由于此列表为单向链表,因此在前面添加的时间复杂度为O(1)。由于此列表是不可变的,如果列表中的底层元素也是不可变的,则可以安全地共享任何子列表以在另一个列表中重用。


这个实现比原生元组(a,b,c)更灵活在哪里? - Literal
@Literal 你可以在单向链表中添加前缀,而普通元组是不可变的。这就使得它们在函数式编程语言中更加灵活多变,成为其中的重要组成部分。 - Kevin Ji
1
谢谢您的回复。我仍在努力理解这种实现的好处,因为我也可以通过创建一个新的元组实例来添加一个元素:(z,) + (a,b,c)。这是性能问题吗? - Literal
@Literal 是的。在你的情况下,连接一个元素的时间复杂度是O(n),其中n是现有列表的长度,因为你需要分配一个大小为n+1的新元组,然后将元素复制过去。在这里,concat的时间复杂度是O(1),因为你只需要分配一个带有指向原始“列表”的指针的大小为2的元组。 - Kevin Ji

5

但是,如果有一个数组和元组的元组,则元组中的数组可以被修改。

>>> a
([1, 2, 3], (4, 5, 6))

>>> a[0][0] = 'one'

>>> a
(['one', 2, 3], (4, 5, 6))

12
没有真正能够使其内容不可变的集合,因为你需要一种方法来创建任意对象的不可变副本。要做到这一点,你必须复制这些对象所属的类,甚至是它们引用的内置类。即使如此,这些对象仍然可能引用文件系统、网络或其他永远可变的东西。因此,既然我们无法使任意对象都不可变,我们就必须满足于将可变对象组成的不可变集合。 - Jack O'Connor
2
@JackO'Connor 不完全同意。这完全取决于您如何对世界进行建模:外部可变性始终可以建模为随时间演变的状态,而我可以选择引用不可变的 s_t 而不是维护单个可变状态 s。"不可变对象的不可变集合" <-- 可以查看 Huskell、Scala 和其他函数式编程语言。在开始学习 Python 之前,我曾经听说 Python 具有完全支持不可变性和 fp,但事实证明并非如此。 - Kane
我应该说,在Python中实际上不可能有这样的事情。Python的不可变性依赖于程序员遵守约定(如“_private_variables”),而不是解释器的任何强制执行。 - Jack O'Connor
2
像Haskell这样的语言提供了更多的保证,尽管如果程序员真的想要做恶,他们仍然可以写入/proc/#/mem或链接不安全的库或其他任何东西来破坏模型。 - Jack O'Connor

2

列表(List)和元组(Tuple)在其工作方式上有所区别。

在列表中,我们可以在创建后进行更改。但是,如果您想要一个有序的序列,在将来无法应用任何更改,则可以使用元组。

更多信息:

 1) the LIST is mutable that means you can make changes in it after its creation
 2) In Tuple, we can not make changes once it created
 3) the List syntax is
           abcd=[1,'avn',3,2.0]
 4) the syntax for Tuple is 
           abcd=(1,'avn',3,2.0) 
      or   abcd= 1,'avn',3,2.0 it is also correct

0

示例1

def foo(array_of_strings):
    array_of_strings[0] = "Hello, Jerry"
     
bar = ['Hi', 'how', 'are', 'you', 'doing']
foo(bar)
print(bar)

输出:

['Hello, Jerry', 'how', 'are', 'you', 'doing']

使用Python 3.x中的切片功能
使用切片功能的第二个示例
def foo(array_of_strings):
    array_of_strings[0] = "Hello, Jerry"
     
bar = ['Hi', 'how', 'are', 'you', 'doing']
foo(bar[:])
print(bar)

输出:

['Hi', 'how', 'are', 'you', 'doing']

0
你可以使用frozenlist模块中的FrozenList类。要安装它,请运行:
$ pip install frozenlist

这个类实现了一个类似列表的结构,直到调用freeze方法后,列表变得可变,之后对列表的修改会引发RuntimeError异常。当列表被冻结时,frozenlist是可哈希的。
>>> from frozenlist import FrozenList
>>> fl = FrozenList([1, 2, 3])
>>> fl
&ltFrozenList(frozen=False, [1, 2, 3])>
>>> fl.append(4)
>>> fl
&ltFrozenList(frozen=False, [1, 2, 3, 4])>
>>> fl.<b>freeze()</b>
>>> fl.append(5)
Traceback (most recent call last):
  File "", line 1, in 
  File "frozenlist/_frozenlist.pyx", line 106, in frozenlist._frozenlist.FrozenList.append
  File "frozenlist/_frozenlist.pyx", line 28, in frozenlist._frozenlist.FrozenList._check_frozen
<b>RuntimeError</b>: Cannot modify frozen list.
>>> hash(fl)
590899387163067792

-1

你可以使用frozenset代替元组。frozenset创建一个不可变的集合。你可以将列表作为frozenset的成员,并使用单个for循环访问frozenset中列表的每个元素。


6
frozenset要求其集合成员是可哈希的,而列表则不具备这种特性。 - matias elgart

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接