Python 3中的自定义列表类,带有__get__和__set__属性

时间:2017-04-03 02:30:56

标签: python python-3.x oop

我想在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的方式,但它的工作方式与我预期的一样。

2 个答案:

答案 0 :(得分:2)

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

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

这是不可能的。在Python中,对一个简单名称的简单赋值(例如,x = ...,与x.blah = ...x[0] = ...不同)是对名称的操作,而不是值,因此没有办法对于任何挂钩到名称绑定过程的对象。无论x = [2]的值是什么,x这样的赋值都会以相同的方式工作(无论x是否已经有值,或者这是否是第一个值,确实以相同的方式工作被分配到x)。

答案 1 :(得分:1)

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

这并不是说自定义描述符无用。您在property类中使用的Foo是描述符。如果你愿意,你可以编写自己的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__方法指定为类变量时,将在描述符上调用find_name方法。如果您只需要支持Python 3.6及更高版本,则可以删除类中的许多代码(您不需要__get__或在__set__和{{1}中调用它的任何代码}})。

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

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