【问题标题】:Custom list class in Python 3 with __get__ and __set__ attributesPython 3 中具有 __get__ 和 __set__ 属性的自定义列表类
【发布时间】:2017-04-03 02:30:55
【问题描述】:

我想在 Python 3 中编写一个自定义列表类,就像这个问题 How would I create a custom list class in python? 一样,但与那个问题不同,我想实现 __get____set__ 方法。虽然我的类与列表类似,但是这些方法背后隐藏着一些神奇的操作。所以我想使用这个变量,就像我的程序的main 一样(见下文)。我想知道,如何将__get____set__ 方法(分别为fgetfset)从Foo 类移动到MyList 类,使其只有一个类。

我当前的解决方案(另外,为了清楚起见,我为每个操作添加了输出):

class MyList:

    def __init__(self, data=[]):
        print('MyList.__init__')
        self._mylist = data 

    def __getitem__(self, key):
        print('MyList.__getitem__')
        return self._mylist[key]

    def __setitem__(self, key, item):
        print('MyList.__setitem__')
        self._mylist[key] = item

    def __str__(self):
        print('MyList.__str__')
        return str(self._mylist)


class Foo:

    def __init__(self, mylist=[]):
        self._mylist = MyList(mylist)

    def fget(self):
        print('Foo.fget')
        return self._mylist

    def fset(self, data):
        print('Foo.fset')
        self._mylist = MyList(data)

    mylist = property(fget, fset, None, 'MyList property')


if __name__ == '__main__':
    foo = Foo([1, 2, 3])
    # >>> MyList.__init__
    print(foo.mylist)
    # >>> Foo.fget
    # >>> MyList.__str__
    # >>> [1, 2, 3]
    foo.mylist = [1, 2, 3, 4]
    # >>> Foo.fset
    # >>> MyList.__init__
    print(foo.mylist)
    # >>> Foo.fget
    # >>> MyList.__str__
    # >>> [1, 2, 3, 4]
    foo.mylist[0] = 0
    # >>> Foo.fget
    # >>> MyList.__setitem__
    print(foo.mylist[0])
    # >>> Foo.fget
    # >>> MyList.__getitem__
    # >>> 0

提前感谢您的帮助。


如何将 __get____set__ 方法(分别为 fget 和 fset)从 Foo 类移动到 MyList 类以只有一个类?


UPD:

非常感谢@Blckknght!我试图理解他的回答,这对我来说效果很好!这正是我所需要的。结果,我得到以下代码:

class MyList:
    def __init__(self, value=None):
        self.name = None
        if value is None:
            self.value = []
        else:
            self.value = value

    def __set_name__(self, owner, name):
        self.name = "_" + name

    def __get__(self, instance, owner):
        return getattr(instance, self.name)

    def __set__(self, instance, value):
        setattr(instance, self.name, MyList(value))

    def __getitem__(self, key):
        return self.value[key]

    def __setitem__(self, key, value):
        self.value[key] = value

    def append(self, value):
        self.value.append(value)

    def __str__(self):
        return str(self.value)


class Foo:
    my_list = MyList()

    def __init__(self):
        self.my_list = [1, 2, 3]
        print(type(self.my_list))       # <class '__main__.MyList'>
        self.my_list = [4, 5, 6, 7, 8]
        print(type(self.my_list))       # <class '__main__.MyList'>
        self.my_list[0] = 10
        print(type(self.my_list))       # <class '__main__.MyList'>
        self.my_list.append(7)
        print(type(self.my_list))       # <class '__main__.MyList'>
        print(self.my_list)             # [10, 5, 6, 7, 8, 7]

foo = Foo()

我不知道,这是否是 Pythonic 方式,但它按我的预期工作。

【问题讨论】:

  • "但与那个问题不同,我想实现 __get____set__ 方法" - 你为什么要这样做?你确定你了解__get____set__ 是什么吗?
  • 不要问如何使用__get____set__,你能解释一下你想要实现的行为吗?为什么在现有示例中使用__get____set__?如果您只是删除 Foo 并直接使用 MyList 实例,您希望您的列表类能够做什么?
  • @RomanAlexeev:你甚至想象那会做什么?如果你认为你可以做类似x = MyList([1]); x = [2] 的事情,然后让x 成为MyList,那么不会。 Python 不是这样工作的。您不能像在 C++ 中那样覆盖赋值运算符。
  • 你想要什么还不清楚。你说你想“只有一堂课”,但你也说你想做foo.mylist = ...。如果没有 foofoo.mylist 不同,你显然不能做 foo.mylist = ...。那么,如果你“只有一个类”,你想怎么用呢?
  • @RomanAlexeev 你不能那样做。 __get____set__ 方法覆盖类/实例属性的分配,而不是作为本地或全局命名空间中的变量。

标签: python python-3.x oop


【解决方案1】:

在评论中,您解释了您真正想要的内容:

x = MyList([1])
x = [2]
# and have x be a MyList after that.

这是不可能的。在 Python 中,对裸名称的简单赋值(例如,x = ...,与 x.blah = ...x[0] = ... 形成对比)只是对名称的操作,而不是对值的操作,因此任何对象都无法挂钩名称绑定过程。无论x 的值是什么,像x = [2] 这样的赋值都以相同的方式工作(并且无论x 是否已经有一个值或者这是否是分配给x 的第一个值,实际上都以相同的方式工作)。

【讨论】:

    【解决方案2】:

    虽然您可以让您的MyList 类遵循描述符协议(这是__get____set__ 方法的用途),但您可能不想这样做。这是因为,为了有用,描述符必须作为类的属性,而不是作为实例的属性。 Foo 类中的属性为每个实例创建单独的 MyList 实例。如果列表直接在 Foo 类上定义,那将无法正常工作。

    这并不是说自定义描述符没有用处。您在 Foo 类中使用的 property 是一个描述符。如果您愿意,您可以编写自己的 MyListAttr 描述符来做同样的事情。

    class MyListAttr(object):
        def __init__(self):
            self.name = None
    
        def __set_name__(self, owner, name):     # this is used in Pyton 3.6+
            self.name = "_" + name
    
        def find_name(self, cls):  # this is used on earlier versions that don't support set_name
            for name in dir(cls):
                if getattr(cls, name) is self:
                    self.name = "_" + name
                    return
            raise TypeError()
    
        def __get__(self, obj, owner):
            if obj is None:
               return self
            if self.name is None:
                self.find_name(owner)
            return getattr(obj, self.name)
    
        def __set__(self, obj, value):
            if self.name is None:
                self.find_name(type(obj))
            setattr(obj, self.name, MyList(value))
    
    class Foo(object):
        mylist = MyListAttr() # create the descriptor as a class variable
    
        def __init__(self, data=None):
            if data is None:
                data = []
    
            self.mylist = data    # this invokes the __set__ method of the descriptor!
    

    MyListAttr 类比其他情况更复杂,因为我试图让描述符对象找到它自己的名称。这在旧版本的 Python 中并不容易弄清楚。从 Python 3.6 开始,这要容易得多(因为当将描述符分配为类变量时,将在描述符上调用 __set_name__ 方法)。如果您只需要支持 Python 3.6 及更高版本(您不需要 find_name 或在 __get____set__ 中调用它的任何代码),则可以删除该类中的许多代码。

    似乎不值得编写像MyListAttr 这样的长描述符类来完成使用property 可以用更少的代码完成的事情。如果您只想在一个地方使用描述符,那可能是正确的。但是,如果您可能有许多类(或单个类中的许多属性)需要相同的特殊行为,那么您将受益于将行为打包到描述符中,而不是编写许多非常相似的property getter 和 setter 方法。

    您可能没有注意到,但我还对Foo 类进行了更改,该类与描述符的使用没有直接关系。更改为data 的默认值。使用像列表这样的可变对象作为默认参数通常是一个非常糟糕的主意,因为对没有参数的函数的所有调用都将共享同一个对象(因此所有未使用数据初始化的 Foo 实例将共享同一个列表)。最好使用标记值(如None)并将标记替换为您真正想要的值(在这种情况下为新的空列表)。您可能也应该在您的 MyList.__init__ 方法中解决此问题。

    【讨论】:

    • 非常感谢您的回答!它真的帮助了我。我并不完全理解这个魔法,但我尝试使用 Python 3.6 为我的班级实现它,它可以按我的需要工作。我根据您的回答更新了我的问题。
    • 嗯,你的实现中有一堆奇怪的东西。您将 name 属性用于描述符变量名称和实际数据。这没有任何意义。我的设计是让描述符成为与列表不同的类型,因为您不会在相同的地方使用它们。
    • 这对我来说也很奇怪。但我无法以其他方式实现它。在您的代码setattr(obj, self.name, MyList(value))MyList -> MyListAttr,我认为这是错字)中,您传递了value,但您不能,因为据我所知,__init__ 只有一个参数self。它引发了错误:TypeError: __init__() takes 1 positional argument but 2 were given。您能解释一下,如何以正确的方式传递带值的参数吗?
    • 感谢您的发言!看来我理解我的错误,你写的。我根据您的评论编辑了我的代码。如果您查看我编辑的代码并说是否正确,我将不胜感激。
    • 我认为误解是我打算将 MyListAttrMyList 分开一个类(我没有在我的代码中显示它,因为它不需要对你所拥有的进行任何更改在您的原始代码中)。您不希望将同一个类用作容器(使用__getitem____setitem__ 方法)和描述符(使用__get____set__)。让MyListAttr 创建一个MyList(你认为这是一个错字)是故意的!
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2021-12-19
    • 2011-04-17
    • 1970-01-01
    • 2017-11-15
    • 2012-12-09
    • 1970-01-01
    相关资源
    最近更新 更多