Python中使用自定义__str__()方法的描述符协议?

eimct9ow  于 2023-05-30  发布在  Python
关注(0)|答案(1)|浏览(110)

更新问题:在Python中,我可以创建一个自定义的数据结构dict,但是我可以将其获取并设置为set,并且可以为其创建自定义的__str__表示吗?

我想要一个class属性,它在结构上是一个dict{str:list[str]},但用户界面(因为缺乏更好的词)将其视为一个set[str]。而且我想打印它像一个字典与自定义格式。

尝试解决方案:我实现了一个Descriptor,但我还没有弄清楚如何自定义__str__,所以我认为Descriptor实际上不是我应该尝试的。

class TreatDictLikeSet():  # The Descriptor I wish existed
    def __set_name__(self, owner, name):
        self.name = name

    def __get__(self, obj, type=None) -> object:
        my_dict = obj.__dict__.get(self.name) or {}
        return [e for v in my_dict.values() for e in v]

    def __set__(self, obj, value) -> None:
        value = ...<rules to insert set values into a dict>...
        obj.__dict__[self.name] = value

class Foo():
    my_dict = TreatDictLikeSet()
uyhoqukh

uyhoqukh1#

如果您想要的只是行为 “assign set,but get dict,我不确定您是否需要处理描述符。
看起来简单的property就可以了:

class Foo:
    _my_set: set[str]

    @property
    def my_dict(self) -> dict[str, list[str]]:
        return {f"key_{i}": [value] for i, value in enumerate(self._my_set)}

    @my_dict.setter
    def my_dict(self, value: set[str]) -> None:
        self._my_set = value

foo = Foo()
foo.my_dict = {'a', 'b', 'c'}
print(f"{foo.my_dict}")  # {'key_0': ['a'], 'key_1': ['c'], 'key_2': ['b']}

更新

如果你想要一个行为像标准集合类的东西(例如a set),一个好的起点通常是collections.abc模块。
例如,你可以子类化MutableSet,实现它的抽象方法(__contains____iter____len__adddiscard),还可以为它实现你自己的__init__**和__str__**方法:

from collections.abc import Iterable, Iterator, MutableSet
from typing import TypeVar

T = TypeVar("T")

class SetButAlsoDictOfLists(MutableSet[T]):
    _data: dict[str, list[T]]

    def __init__(self, values: Iterable[T] = ()) -> None:
        self._data = {}
        for value in values:
            self.add(value)

    def __str__(self) -> str:
        return str(self._data)

    def __contains__(self, value: object) -> bool:
        return any(value in list_ for list_ in self._data.values())

    def __iter__(self) -> Iterator[T]:
        return (list_[0] for list_ in self._data.values())

    def __len__(self) -> int:
        return len(self._data)

    def add(self, value: T) -> None:
        self._data[f"key_{value}"] = [value]

    def discard(self, value: T) -> None:
        del self._data[f"key_{value}"]

如您所愿,底层数据结构是一个列表字典。我只是实现了一些任意的规则来创建字典键,以用于演示目的。
正如@ Blckknight在评论中指出的那样,您在下面使用不同的数据结构意味着操作的运行时可能会非常不同。具体来说,正如你所看到的,我在这里实现__contains__的方式是 O(n),而不是实际集合的 O(1)。这是因为我循环遍历dict的整个values视图来查找某个值,而不是像使用集合那样进行散列和查找。
另一方面,即使删除原则上同样昂贵,由于dict键逻辑的 * 这个特定 * 实现,删除(discard)也同样有效,因为值是键的一部分。
当然,你也可以将这些值存储在字典旁边的一个 actual set中,这样可以再次提高这些操作的效率,但是这显然会为每个值占用两倍的内存。
无论哪种方式,你现在都可以将这个类作为一个常规的(可变的)集合来使用,但是它的字符串表示是底层字典的字符串表示:

obj = SetButAlsoDictOfLists({"a", "b", "d"})
print(obj.isdisjoint(["x", "y"]))  # True
obj.add("c")
obj.remove("d")
print(obj)  # {'key_b': ['b'], 'key_a': ['a'], 'key_c': ['c']}

现在,如果您出于某种原因仍然需要描述符魔法,您可以编写一个在后台使用此类的描述符,即。在它的__set__中初始化一个新对象,并在它的__get__方法中返回它:

from typing import Generic, TypeVar

# ... import SetButAlsoDictOfLists

_T = TypeVar("_T")

class Descriptor(Generic[_T]):
    name: str

    def __set_name__(self, owner: type, name: str) -> None:
        self.name = name

    def __get__(
        self,
        instance: object,
        owner: type | None = None,
    ) -> SetButAlsoDictOfLists[_T]:
        return instance.__dict__.get(self.name, SetButAlsoDictOfLists())

    def __set__(self, instance: object, value: Iterable[_T]) -> None:
        instance.__dict__[self.name] = SetButAlsoDictOfLists(value)

然后这样使用:

class Foo:
    my_cool_set = Descriptor[str]()

foo = Foo()
print(foo.my_cool_set)  # {}
foo.my_cool_set = {"a", "b"}
print(foo.my_cool_set)  # {'key_b': ['b'], 'key_a': ['a']}
foo.my_cool_set |= ["b", "c"]
print(foo.my_cool_set)  # {'key_b': ['b'], 'key_a': ['a'], 'key_c': ['c']}

相关问题