我有一个类,它本质上是一个集合/列表。但是我想在这个列表中添加一些额外的功能。我想要的是以下内容:
li = MyFancyList()
。每当我将其用作列表时,变量li
应该像列表一样:[e for e in li]
,li.expand(...)
,for e in li
。 li.fancyPrint()
,li.getAMetric()
,li.getName()
。我目前使用以下方法:
class MyFancyList:
def __iter__(self):
return self.li
def fancyFunc(self):
# do something fancy
这可以用作[e for e in li]
之类的迭代器,但我没有像li.expand(...)
这样的完整列表行为。
首先猜测是将list
继承到MyFancyList
。但这是推荐的pythonic方式吗?如果是,那要考虑什么?如果不是,那会是更好的方法吗?
答案 0 :(得分:61)
如果您只想要列表行为的一部分,请使用合成(即您的实例包含对实际列表的引用)并仅实现所需行为所需的方法。这些方法应该将工作委托给实际列表,任何类的实例都包含对它的引用,例如:
def __getitem__(self, item):
return self.li[item] # delegate to li.__getitem__
单独实现__getitem__
将为您提供惊人数量的功能,例如迭代和切片。
>>> class WrappedList:
... def __init__(self, lst):
... self._lst = lst
... def __getitem__(self, item):
... return self._lst[item]
...
>>> w = WrappedList([1, 2, 3])
>>> for x in w:
... x
...
1
2
3
>>> w[1:]
[2, 3]
如果您想要列表的完整行为,请继承collections.UserList
。 UserList
是list数据类型的完整Python实现。
那么为什么不直接从list
继承?
直接从list
(或用C编写的任何其他内置函数)继承的一个主要问题是内置函数的代码可能会也可能不会调用在用户定义的类中重写的特殊方法。以下是pypy docs的相关摘录:
正式地说,CPython根本没有规则来确定是否隐式调用了内置类型的子类的完全重写方法。作为近似,这些方法永远不会被同一对象的其他内置方法调用。例如,dict的子类中被覆盖的
__getitem__
将不会被例如调用。内置的get
方法。
另一句话,来自Luciano Ramalho的Fluent Python,第351页:
直接对dict或list或str等内置类型进行子类化是错误的 - 容易因为内置方法大多忽略用户定义 覆盖。不是子类化内置函数,而是派生你的类 来自集合的UserDict,UserList和UserString 模块,设计易于扩展。
......等等,第370页+:
行为不当的内置插件:错误或功能? 内置的dict,list和str类型是Python本身的基本构建块,所以 他们必须快速 - 任何性能问题都会严重影响 其他一切。这就是CPython采用导致其内置的快捷方式的原因 通过不与子类重写的方法合作来行为不端的方法。
在玩了一下之后,list
内置的问题似乎不那么重要了(我试图在Python 3.4中破解它一段时间,但没有发现真正明显的意外行为),但我仍然我想发布原则上会发生什么的演示,所以这里有一个dict
和UserDict
:
>>> class MyDict(dict):
... def __setitem__(self, key, value):
... super().__setitem__(key, [value])
...
>>> d = MyDict(a=1)
>>> d
{'a': 1}
>>> class MyUserDict(UserDict):
... def __setitem__(self, key, value):
... super().__setitem__(key, [value])
...
>>> m = MyUserDict(a=1)
>>> m
{'a': [1]}
如您所见,来自__init__
的{{1}}方法忽略了重写的dict
方法,而__setitem__
方法中的__init__
方法则没有。< / p>
答案 1 :(得分:5)
这里最简单的解决方案是继承list
类:
class MyFancyList(list):
def fancyFunc(self):
# do something fancy
然后,您可以使用MyFancyList
类型作为列表,并使用其特定方法。
继承在您的对象和list
之间引入了强大的耦合。您实现的方法基本上是一个代理对象。
大量使用的方式取决于您使用对象的方式。如果必须 一个列表,那么继承可能是一个不错的选择。
编辑:正如@acdr所指出的,应该覆盖一些返回列表副本的方法,以便返回MyFancyList
而不是list
。
实现该方法的简单方法:
class MyFancyList(list):
def fancyFunc(self):
# do something fancy
def __add__(self, *args, **kwargs):
return MyFancyList(super().__add__(*args, **kwargs))
答案 2 :(得分:4)
如果您不想重新定义list
的每种方法,我建议您使用以下方法:
class MyList:
def __init__(self, list_):
self.li = list_
def __getattr__(self, method):
return getattr(self.li, method)
这会使append
,extend
等方法开箱即用。但请注意,魔术方法(例如__len__
,__getitem__
等)在这种情况下不起作用,因此您至少应该重新声明它们:
class MyList:
def __init__(self, list_):
self.li = list_
def __getattr__(self, method):
return getattr(self.li, method)
def __len__(self):
return len(self.li)
def __getitem__(self, item):
return self.li[item]
def fancyPrint(self):
# do whatever you want...
请注意,在这种情况下,如果您想覆盖list
(例如extend
)的方法,您可以声明自己的方式,以便通话无法通过通过__getattr__
方法。例如:
class MyList:
def __init__(self, list_):
self.li = list_
def __getattr__(self, method):
return getattr(self.li, method)
def __len__(self):
return len(self.li)
def __getitem__(self, item):
return self.li[item]
def fancyPrint(self):
# do whatever you want...
def extend(self, list_):
# your own version of extend
答案 3 :(得分:3)
根据您在帖子中添加的两个示例方法(fancyPrint
,findAMetric
),您似乎不需要在您的帖子中存储任何额外的州名单。如果是这种情况,你最好简单地将这些声明为自由函数并完全忽略子类型;这完全避免了诸如list
vs UserList
之类的问题,脆弱的边缘情况,例如__add__
的返回类型,意外的Liskov问题,以及c。相反,您可以编写函数,为输出编写单元测试,并确保一切都能按预期工作。
作为一个额外的好处,这意味着您的函数将使用任何可迭代类型(例如生成器表达式),而无需任何额外的努力。