簡體   English   中英

使用屬性裝飾器的 Python 只讀列表

[英]Python read-only lists using the property decorator

精簡版

我可以使用 Python 的屬性系統創建一個只讀列表嗎?

長版

我創建了一個 Python 類,其中有一個列表作為成員。 在內部,我希望它在每次修改列表時都做一些事情。 如果這是 C++,我會創建 getter 和 setter,它們允許我在調用 setter 時進行簿記,並且我會讓 getter 返回一個const引用,這樣如果我嘗試修改,編譯器就會對我大喊大叫通過 getter 的列表。 在 Python 中,我們有屬性系統,因此不再需要(謝天謝地)為每個數據成員編寫普通的 getter 和 setter。

但是,請考慮以下腳本:

def main():

    foo = Foo()
    print('foo.myList:', foo.myList)

    # Here, I'm modifying the list without doing any bookkeeping.
    foo.myList.append(4)
    print('foo.myList:', foo.myList)

    # Here, I'm modifying my "read-only" list.
    foo.readOnlyList.append(8)
    print('foo.readOnlyList:', foo.readOnlyList)


class Foo:
    def __init__(self):
        self._myList = [1, 2, 3]
        self._readOnlyList = [5, 6, 7]

    @property
    def myList(self):
        return self._myList

    @myList.setter
    def myList(self, rhs):
        print("Insert bookkeeping here")
        self._myList = rhs

    @property
    def readOnlyList(self):
        return self._readOnlyList


if __name__ == '__main__':
    main()

輸出:

foo.myList: [1, 2, 3]
# Note there's no "Insert bookkeeping here" message.
foo.myList: [1, 2, 3, 4]
foo.readOnlyList: [5, 6, 7, 8]

這說明 Python 中缺少const的概念允許我使用append()方法修改我的列表,盡管我已經將它設為屬性。 這可以繞過我的簿記機制 ( _myList ),或者它可以用於修改可能希望為只讀的列表 ( _readOnlyList )。

一種解決方法是在 getter 方法中返回列表的深層副本(即return self._myList[:] )。 這可能意味着很多額外的復制,如果列表很大或者復制是在內部循環中完成的。 (但無論如何,過早的優化是萬惡之源。)此外,雖然深拷貝可以防止簿記機制被繞過,但如果有人調用.myList.append() ,他們的更改將被默默地丟棄,這可能會產生一些痛苦的調試。 如果提出異常就好了,這樣他們就會知道他們正在違背班級的設計。

最后一個問題的解決方法是不使用屬性系統,並制作“正常”的 getter 和 setter 方法:

def myList(self):
    # No property decorator.
    return self._myList[:]

def setMyList(self, myList):
    print('Insert bookkeeping here')
    self._myList = myList

如果用戶嘗試調用append() ,它看起來像foo.myList().append(8) ,那些額外的括號會提示他們他們可能正在獲取副本,而不是對內部列表數據的引用. 不利的是,像這樣編寫 getter 和 setter 有點不符合 Pythonic,如果該類有其他列表成員,我將不得不為那些 (eww) 編寫 getter 和 setter,或者創建接口不一致。 (我認為稍微不一致的界面可能是所有弊端中最少的。)

我還缺少其他解決方案嗎? 可以使用 Python 的屬性系統制作一個只讀列表嗎?

您可以讓方法返回原始列表周圍的包裝器—— collections.Sequence可能有助於編寫它。 或者,您可以返回一個tuple ——將列表復制到元組的開銷通常可以忽略不計。

但最終,如果用戶想要更改基礎列表,他們可以而且您實際上無法阻止他們。 (畢竟,如果他們想要的話,他們可以直接訪問self._myList )。

我認為做這樣的事情的pythonic方法是記錄他們不應該更改列表,如果這樣做,那么當他們的程序崩潰和燒毀時,這是他們的錯。

盡管我已經把它變成了財產

如果它是一個屬性並不重要,您將返回一個指向列表的指針,以便您可以修改它。

我建議創建一個列表子類並覆蓋append__add__方法

為返回返回元組或子類列表的建議解決方案似乎是不錯的解決方案,但我想知道是否將裝飾器子類化不是更容易? 不確定這可能是一個愚蠢的想法:

  • 使用這個safe_property可以防止意外發送混合 API 信號(內部不可變屬性受到所有操作的“保護”,而對於可變屬性,仍然允許使用內置的普通property進行一些操作)
  • 優點:比在任何地方實現自定義返回類型更容易使用 -> 更容易內化
  • 缺點:必須使用不同的名稱
class FrozenList(list):

    def _immutable(self, *args, **kws):
        raise TypeError('cannot change object - object is immutable')

    pop = _immutable
    remove = _immutable
    append = _immutable
    clear = _immutable
    extend = _immutable
    insert = _immutable
    reverse = _immutable


class FrozenDict(dict):

    def _immutable(self, *args, **kws):
        raise TypeError('cannot change object - object is immutable')

    __setitem__ = _immutable
    __delitem__ = _immutable
    pop = _immutable
    popitem = _immutable
    clear = _immutable
    update = _immutable
    setdefault = _immutable


class safe_property(property):

    def __get__(self, obj, objtype=None):
        candidate = super().__get__(obj, objtype)
        if isinstance(candidate, dict):
            return FrozenDict(candidate)
        elif isinstance(candidate, list):
            return FrozenList(candidate)
        elif isinstance(candidate, set):
            return frozenset(candidate)
        else:
            return candidate


class Foo:

    def __init__(self):
        self._internal_lst = [1]

    @property
    def internal_lst(self):
        return self._internal_lst

    @safe_property
    def internal_lst_safe(self):
        return self._internal_lst


if __name__ == '__main__':

    foo = Foo()

    foo.internal_lst.append(2)
    # foo._internal_lst is now [1, 2]
    foo.internal_lst_safe.append(3)
    # this throws an exception

對這方面的其他意見非常感興趣,因為我還沒有在其他地方看到這實現。

為什么列表比元組更可取? 對於大多數意圖和目的而言,元組是“不可變列表”——因此它們本質上將充當無法直接設置或修改的只讀對象。 在這一點上,人們只需要不為該屬性編寫所述設置器。

>>> class A(object):
...     def __init__(self, list_data, tuple_data):
...             self._list = list(list_data)
...             self._tuple = tuple(tuple_data)
...     @property
...     def list(self):
...             return self._list
...     @list.setter
...     def list(self, new_v):
...             self._list.append(new_v)
...     @property
...     def tuple(self):
...             return self._tuple
... 
>>> Stick = A((1, 2, 3), (4, 5, 6))
>>> Stick.list
[1, 2, 3]
>>> Stick.tuple
(4, 5, 6)
>>> Stick.list = 4 ##this feels like a weird way to 'cover-up' list.append, but w/e
>>> Stick.list = "Hey"
>>> Stick.list
[1, 2, 3, 4, 'Hey']
>>> Stick.tuple = 4
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: can't set attribute
>>> 

這可以通過使用Sequence類型提示來完成,與list不同的是,它是不可修改的:

from typing import Sequence

def foo() -> Sequence[int]:
    return []

result = foo()
result.append(10)
result[0] = 10

mypypyright在嘗試修改帶有Sequence提示的列表時都會出錯:

$ pyright /tmp/foo.py
  /tmp/foo.py:7:8 - error: Cannot access member "append" for type "Sequence[int]"
    Member "append" is unknown (reportGeneralTypeIssues)
  /tmp/foo.py:8:1 - error: "__setitem__" method not defined on type "Sequence[int]" (reportGeneralTypeIssues)
2 errors, 0 warnings, 0 informations 

然而,Python 本身忽略了這些提示,因此必須采取一些措施來確保這些類型檢查器中的任何一個定期運行或作為構建過程的一部分。

還有一個Final類型提示,其作用類似於 C++ const ,但它僅提供對存儲列表引用的變量的保護,而不是對列表本身,因此它在這里沒有用,但可能在其他方面有用情況。

兩個主要建議似乎是使用元組作為只讀列表或子類化列表。 我喜歡這兩種方法。

從 getter 返回一個元組,或者首先使用一個元組,可以防止使用+=運算符,這可能是一個有用的運算符,並且還可以通過調用 setter 來觸發簿記機制。 然而,返回一個元組是一個單行更改,如果您想進行防御性編程但認為將整個其他類添加到您的腳本可能會不必要地復雜,這很好。

這是一個說明這兩種方法的腳本:

import collections


def main():

    foo = Foo()
    print('foo.myList:', foo.myList)

    try:
        foo.myList.append(4)
    except RuntimeError:
        print('Appending prevented.')

    # Note that this triggers the bookkeeping, as we would like.
    foo.myList += [3.14]
    print('foo.myList:', foo.myList)

    try:
        foo.readOnlySequence.append(8)
    except AttributeError:
        print('Appending prevented.')
    print('foo.readOnlySequence:', foo.readOnlySequence)


class UnappendableList(collections.UserList):
    def __init__(self, *args, **kwargs):
        data = kwargs.pop('data')
        super().__init__(self, *args, **kwargs)
        self.data = data

    def append(self, item):
        raise RuntimeError('No appending allowed.')


class Foo:
    def __init__(self):
        self._myList = [1, 2, 3]
        self._readOnlySequence = [5, 6, 7]

    @property
    def myList(self):
        return UnappendableList(data=self._myList)

    @myList.setter
    def myList(self, rhs):
        print('Insert bookkeeping here')
        self._myList = rhs

    @property
    def readOnlySequence(self):
        # or just use a tuple in the first place
        return tuple(self._readOnlySequence)


if __name__ == '__main__':
    main()

輸出:

foo.myList: [1, 2, 3]
Appending prevented.
Insert bookkeeping here
foo.myList: [1, 2, 3, 3.14]
Appending prevented.
foo.readOnlySequence: (5, 6, 7)

該答案作為對Python read-only lists using the property decorator問題的編輯發布,由 CC BY-SA 3.0 下的 OP ngb發布。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM