我想在Python 3中编写一个自定义列表类,就像在这个问题How would I create a custom list class in python?中一样,但与该问题不同,我想实现__get__
和__set__
方法。虽然我的类类似于列表,但是这些方法背后隐藏着一些神奇的操作。所以我想像列表一样处理这个变量,比如我程序的main
(见下文)。我想知道,如何将__get__
和__set__
方法(分别为fget
和fset
)从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的方式,但它的工作方式与我预期的一样。
答案 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__
方法中解决此问题。