30 KiB
自定义 Python 列表:从列表和用户列表继承
在您的 Python 编码冒险中的某个时刻,您可能需要创建定制的类似列表的类,具有修改的行为、新的功能,或者两者兼有。在 Python 中要做到这一点,你可以从一个抽象基类继承,直接继承内置list类的子类,或者从UserList继承,后者位于collections模块中。
在本教程中,您将学习如何:
- 通过继承内置的
list类来创建定制的类似列表的类 - 通过从
collections模块中子类化UserList来构建定制的列表类
您还将编写一些示例,帮助您决定在创建自定义列表类时使用哪个父类list或UserList。
为了充分利用本教程,您应该熟悉 Python 的内置 list 类及其标准特性。你还需要知道面向对象编程的基础知识,理解继承在 Python 中是如何工作的。
免费下载: 点击这里下载源代码,你将使用它来创建定制的列表类。
在 Python 中创建类似列表的类
内置的 list 类是 Python 中的基本数据类型。列表在很多情况下都很有用,并且有大量的实际用例。在某些用例中,Python list的标准功能可能不够,您可能需要创建定制的类似列表的类来解决手头的问题。
您通常会发现创建定制的类似列表的类至少有两个原因:
- 通过添加新功能来扩展常规列表
- 修改标准列表的功能
您还可能面临需要扩展和来修改列表的标准功能的情况。
根据您的具体需求和技能水平,您可以使用一些策略来创建您自己的定制列表类。您可以:
- 从适当的抽象基类继承,如
MutableSequence - 直接从 Python 内置的
list类继承 - 子类
UserList来自collections
注:在面向对象编程中,通常的做法是将动词继承和子类互换使用。
当您选择要使用的适当策略时,有一些注意事项。请继续阅读,了解更多详情。
从抽象基类构建类似列表的类
您可以通过继承适当的抽象基类(ABC) ,像 MutableSequence ,来创建自己的列表类。除了 .__getitem__() 、 .__setitem__() 、 .__delitem__ 、 .__len__() 和.insert()之外,这个 ABC 提供了大多数list方法的通用实现。因此,当从这个类继承时,您必须自己实现这些方法。
为所有这些特殊方法编写自己的实现是一项相当大的工作量。这很容易出错,并且需要 Python 及其数据模型的高深知识。这也可能意味着性能问题,因为您将使用纯 Python 编写方法。
此外,假设您需要定制任何其他标准列表方法的功能,如 .append() 或.insert()。在这种情况下,您必须覆盖默认实现,并提供一个满足您需求的合适实现。
这种创建类似列表的类的策略的主要优点是,如果您在自定义实现中遗漏了任何必需的方法,父 ABC 类会提醒您。
一般来说,只有当您需要一个与内置的list类完全不同的列表类时,您才应该采用这种策略。
在本教程中,您将通过继承内置的list类和标准库collections模块中的UserList类来创建类似列表的类。这些策略似乎是最快捷和最实用的。
从 Python 内置的list类继承而来
很长一段时间,直接继承用 C 实现的 Python 类型是不可能的。Python 2.2 修复了这个问题。现在你可以子类内置类型,包括list。这一变化给子类带来了一些技术优势,因为现在它们:
这个列表中的第一项可能是对需要 Python 内置类的 C 代码的要求。第二项允许您在标准列表行为的基础上添加新功能。最后,第三项将使您能够将子类的属性限制为那些在.__slots__中预定义的属性。
要开始创建定制的类似列表的类,假设您需要一个列表,它会自动将所有项目存储为字符串。假设您的定制列表将把数字仅仅存储为字符串,您可以创建下面的list子类:
# string_list.py
class StringList(list):
def __init__(self, iterable):
super().__init__(str(item) for item in iterable)
def __setitem__(self, index, item):
super().__setitem__(index, str(item))
def insert(self, index, item):
super().insert(index, str(item))
def append(self, item):
super().append(str(item))
def extend(self, other):
if isinstance(other, type(self)):
super().extend(other)
else:
super().extend(str(item) for item in other)
您的StringList类直接继承了list,这意味着它将继承标准 Python list的所有功能。因为您希望列表将项存储为字符串,所以需要修改所有在基础列表中添加或修改项的方法。这些方法包括以下内容:
.__init__初始化所有类的新实例。.__setitem__()允许您使用项目的索引为现有项目分配一个新值,就像在a_list[index] = item中一样。.insert()允许你使用项目的索引在底层列表的给定位置插入一个新项目。.append()在底层列表的末尾增加一个新的单项。.extend()将一系列项目添加到列表的末尾。
您的StringList类从list继承的其他方法工作得很好,因为它们不添加或更新您的自定义列表中的项目。
注意:如果你想让你的StringList类支持和加号运算符(+)的串联,那么你还需要实现其他特殊的方法,比如 .__add__() 、 .__radd__() 和 .__iadd__() 。
要在代码中使用StringList,您可以这样做:
>>> from string_list import StringList
>>> data = StringList([1, 2, 2, 4, 5])
>>> data
['1', '2', '2', '4', '5']
>>> data.append(6)
>>> data
['1', '2', '2', '4', '5', '6']
>>> data.insert(0, 0)
>>> data
['0', '1', '2', '2', '4', '5', '6']
>>> data.extend([7, 8, 9])
>>> data
['0', '1', '2', '2', '4', '5', '6', '7', '8', '9']
>>> data[3] = 3
>>> data
['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']
你的班级像预期的那样工作。它将所有输入值动态转换成字符串。那很酷,不是吗?当你创建一个新的StringList实例时,类的初始化器会负责转换。
当您向类的实例追加、插入、扩展或分配新值时,支持每个操作的方法将负责字符串转换过程。这样,您的列表将始终将其项目存储为字符串对象。
从collections 子类化UserList
另一种创建定制列表类的方法是使用collections模块中的 UserList 类。这个类是内置list类型的包装器。它是为在不可能直接从内置的list类继承时创建类似列表的对象而设计的。
尽管对这个类的需求已经被直接子类化内置的list类的可能性部分取代,但是为了方便和向后兼容,UserList仍然可以在标准库中使用。
UserList的显著特点是它允许您访问它的.data属性,这可以方便您创建自定义列表,因为您不需要一直使用 super() 。.data属性保存一个常规的 Python list,默认情况下为空。
下面是你如何通过继承UserList来重新实现你的StringList类:
# string_list.py
from collections import UserList
class StringList(UserList):
def __init__(self, iterable):
super().__init__(str(item) for item in iterable)
def __setitem__(self, index, item):
self.data[index] = str(item)
def insert(self, index, item):
self.data.insert(index, str(item))
def append(self, item):
self.data.append(str(item))
def extend(self, other):
if isinstance(other, type(self)):
self.data.extend(other) else:
self.data.extend(str(item) for item in other)
在这个例子中,访问.data属性允许您通过使用委托以更直接的方式对类进行编码,这意味着.data中的列表负责处理所有请求。
现在你几乎不用使用super()这样的高级工具了。你只需要在类初始化器中调用这个函数,以防止在进一步的继承场景中出现问题。在其余的方法中,您只需利用保存常规 Python 列表的.data。使用列表是你可能已经掌握的技能。
**注意:**在上面的例子中,你可以重用上一节中StringList的内部实现,但是把父类从list改为UserList。您的代码将同样工作。然而,使用.data可以简化列表类的编码过程。
这个新版本和你的第一个版本StringList一样。继续运行以下代码进行试验:
>>> from string_list import StringList
>>> data = StringList([1, 2, 2, 4, 5])
>>> data
['1', '2', '2', '4', '5']
>>> data.append(6)
>>> data
['1', '2', '2', '4', '5', '6']
>>> data.insert(0, 0)
>>> data
['0', '1', '2', '2', '4', '5', '6']
>>> data.extend([7, 8, 9])
>>> data
['0', '1', '2', '2', '4', '5', '6', '7', '8', '9']
>>> data[3] = 3
>>> data
['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']
正如您已经了解到的,暴露.data是UserList最相关的特性。这个属性可以简化你的类,因为你不需要一直使用super()。你可以利用.data并使用熟悉的list界面来处理这个属性。
编码列表类:实例
当您需要创建自定义的类似列表的类来添加或修改list的标准功能时,您已经知道如何使用list和UserList。
诚然,当您考虑创建一个类似列表的类时,从list继承可能比从UserList继承更自然,因为 Python 开发人员知道list。他们可能不知道UserList的存在。
您还知道这两个类的主要区别在于,当您从UserList继承时,您可以访问.data属性,这是一个常规列表,您可以通过标准的list接口对其进行操作。相比之下,从list继承需要关于 Python 数据模型的高级知识,包括像内置的super()函数和一些特殊方法这样的工具。
在接下来的部分中,您将使用这两个类编写一些实际的例子。写完这些例子后,当您需要在代码中定义定制的类似列表的类时,您可以更好地选择合适的工具。
只接受数字数据的列表
作为创建具有自定义行为的列表类的第一个例子,假设您需要一个只接受数字数据的列表。你的列表应该只存储整数、浮点数和复数。如果您试图存储任何其他数据类型的值,比如字符串,那么您的列表应该引发一个TypeError。
下面是一个具有所需功能的NumberList类的实现:
# number_list.py
class NumberList(list):
def __init__(self, iterable):
super().__init__(self._validate_number(item) for item in iterable)
def __setitem__(self, index, item):
super().__setitem__(index, self._validate_number(item))
def insert(self, index, item):
super().insert(index, self._validate_number(item))
def append(self, item):
super().append(self._validate_number(item))
def extend(self, other):
if isinstance(other, type(self)):
super().extend(other)
else:
super().extend(self._validate_number(item) for item in other)
def _validate_number(self, value):
if isinstance(value, (int, float, complex)):
return value
raise TypeError(
f"numeric value expected, got {type(value).__name__}"
)
在这个例子中,您的NumberList类直接继承自list。这意味着您的类与内置的list类共享所有核心功能。您可以迭代NumberList的实例,使用它们的索引访问和更新它的条目,调用通用的list方法,等等。
现在,为了确保每个输入项都是一个数字,您需要在支持添加新项或更新列表中现有项的操作的所有方法中验证每个项。所需的方法与从 Python 内置的list类继承而来的一节中的StringList示例相同。
为了验证输入数据,您使用一个叫做._validate_number()的助手方法。该方法使用内置的 isinstance() 函数来检查当前输入值是否是int、float或complex的实例,这些是 Python 中表示数值的内置类。
**注意:**在 Python 中检查一个值是否为数字的更通用的方法是使用 numbers 模块中的 Number 。这将允许您验证 Fraction 和 Decimal 对象。
如果输入值是数值数据类型的实例,那么您的帮助器函数将返回该值本身。否则,该函数会引发一个TypeError 异常,并显示一条适当的错误消息。
要使用NumberList,请返回到您的交互式会话并运行以下代码:
>>> from number_list import NumberList
>>> numbers = NumberList([1.1, 2, 3j])
>>> numbers
[1.1, 2, 3j]
>>> numbers.append("4.2")
Traceback (most recent call last):
...
TypeError: numeric value expected, got str
>>> numbers.append(4.2)
>>> numbers
[1.1, 2, 3j, 4.2]
>>> numbers.insert(0, "0")
Traceback (most recent call last):
...
TypeError: numeric value expected, got str
>>> numbers.insert(0, 0)
>>> numbers
[0, 1.1, 2, 3j, 4.2]
>>> numbers.extend(["5.3", "6"])
Traceback (most recent call last):
...
TypeError: numeric value expected, got str
>>> numbers.extend([5.3, 6])
>>> numbers
[0, 1.1, 2, 3j, 4.2, 5.3, 6]
在这些例子中,在numbers中添加或修改数据的操作自动验证输入,以确保只接受数值。如果你给numbers加一个字符串值,那么你得到一个TypeError。
使用UserList的NumberList的另一个实现可以是这样的:
# number_list.py
from collections import UserList
class NumberList(UserList):
def __init__(self, iterable):
super().__init__(self._validate_number(item) for item in iterable)
def __setitem__(self, index, item):
self.data[index] = self._validate_number(item)
def insert(self, index, item):
self.data.insert(index, self._validate_number(item))
def append(self, item):
self.data.append(self._validate_number(item))
def extend(self, other):
if isinstance(other, type(self)):
self.data.extend(other) else:
self.data.extend(self._validate_number(item) for item in other)
def _validate_number(self, value):
if isinstance(value, (int, float, complex)):
return value
raise TypeError(
f"numeric value expected, got {type(value).__name__}"
)
在这个新的NumberList实现中,您继承了UserList。同样,您的类将与常规的list共享所有核心功能。
在这个例子中,不是一直使用super()来访问父类中的方法和属性,而是直接使用.data属性。在某种程度上,与使用super()和其他高级工具如特殊方法相比,使用.data可以说简化了您的代码。
注意,你只在类初始化器.__init__()中使用super()。当您在 Python 中处理继承时,这是一个最佳实践。它允许您正确初始化父类中的属性,而不会破坏东西。
具有附加功能的列表
现在假设您需要一个类似列表的类,具有常规 Python list的所有标准功能。你的类还应该提供一些从 JavaScript 的数组数据类型中借用的额外功能。例如,您需要像下面这样的方法:
.join()将列表中的所有项目串联成一个字符串。.map(action)通过对底层列表中的每个项目应用一个action()callable 来产生新的项目。.filter(predicate)在调用predicate()时会产生所有返回True的物品。.for_each(func)对底层列表中的每一项都调用func()来生成一些副作用。
这里有一个通过子类化list实现所有这些新特性的类:
# custom_list.py
class CustomList(list):
def join(self, separator=" "):
return separator.join(str(item) for item in self)
def map(self, action):
return type(self)(action(item) for item in self)
def filter(self, predicate):
return type(self)(item for item in self if predicate(item))
def for_each(self, func):
for item in self:
func(item)
CustomList中的.join()方法以一个分隔符作为参数,并使用它来连接当前列表对象中的项目,该列表对象由self表示。为此,您使用带有一个生成器表达式的 str.join() 作为参数。这个生成器表达式使用str()将每一项转换成一个字符串对象。
.map()方法返回一个CustomList对象。为了构造这个对象,您使用一个生成器表达式,将action()应用到当前对象self中的每一项。请注意,该操作可以是任何可调用的操作,它将一个项作为参数并返回一个转换后的项。
.filter()方法也返回一个CustomList对象。要构建这个对象,您需要使用一个生成器表达式来生成predicate()返回True的项目。在这种情况下,predicate()必须是一个布尔值函数,它根据应用于输入项的特定条件返回True或False。
最后,.for_each()方法对底层列表中的每一项调用func()。这个调用没有返回任何东西,但是触发了一些副作用,您将在下面看到。
要在代码中使用该类,您可以执行如下操作:
>>> from custom_list import CustomList
>>> words = CustomList(
... [
... "Hello,",
... "Pythonista!",
... "Welcome",
... "to",
... "Real",
... "Python!"
... ]
... )
>>> words.join()
'Hello, Pythonista! Welcome to Real Python!'
>>> words.map(str.upper)
['HELLO,', 'PYTHONISTA!', 'WELCOME', 'TO', 'REAL', 'PYTHON!']
>>> words.filter(lambda word: word.startswith("Py"))
['Pythonista!', 'Python!']
>>> words.for_each(print)
Hello,
Pythonista!
Welcome
to
Real
Python!
在这些例子中,首先在words上调用.join()。此方法返回一个唯一的字符串,该字符串是由基础列表中的所有项串联而成的。
对.map()的调用返回一个包含大写单词的CustomList对象。这种转换是将str.upper()应用于words中的所有项目的结果。这个方法与内置的 map() 函数非常相似。主要的区别是,内置的map()函数返回一个迭代器,生成转换后的条目和,而不是返回一个列表。
.filter()方法将一个 lambda 函数作为参数。在示例中,这个lambda函数使用 str.startswith() 来选择以"Py"前缀开头的单词。注意,这个方法的工作方式类似于内置的 filter() 函数,它返回一个迭代器而不是一个列表。
最后,对words上的.for_each()的调用将每个单词打印到屏幕上,作为对底层列表中的每个项目调用 print() 的副作用。注意,传递给.for_each()的函数应该将一个项目作为参数,但它不应该返回任何有成果的值。
你也可以通过继承UserList而不是list来实现CustomList。在这种情况下,您不需要更改内部实现,只需更改基类:
# custom_list.py
from collections import UserList
class CustomList(UserList):
def join(self, separator=" "):
return separator.join(str(item) for item in self)
def map(self, action):
return type(self)(action(item) for item in self)
def filter(self, predicate):
return type(self)(item for item in self if predicate(item))
def for_each(self, func):
for item in self:
func(item)
请注意,在本例中,您只是更改了父类。没必要直接用.data。但是,如果你愿意,你可以使用它。这样做的好处是,您可以为阅读您代码的其他开发人员提供更多的上下文:
# custom_list.py
from collections import UserList
class CustomList(UserList):
def join(self, separator=" "):
return separator.join(str(item) for item in self.data)
def map(self, action):
return type(self)(action(item) for item in self.data)
def filter(self, predicate):
return type(self)(item for item in self.data if predicate(item))
def for_each(self, func):
for item in self.data: func(item)
在这个新版本的CustomList()中,唯一的变化是你用self.data替换了self,以表明你正在使用一个UserList子类。这一变化使您的代码更加清晰。
考虑性能:list vs UserList
至此,您已经学会了如何通过继承list或UserList来创建自己的列表类。您还知道这两个类之间唯一可见的区别是UserList公开了.data属性,这有助于编码过程。
在这一节中,当决定是使用list还是UserList来创建定制的类似列表的类时,您将考虑一个重要的方面。那是性能!
为了评估继承自list和UserList的类之间是否存在性能差异,您将使用StringList类。继续创建包含以下代码的新 Python 文件:
# performance.py
from collections import UserList
class StringList_list(list):
def __init__(self, iterable):
super().__init__(str(item) for item in iterable)
def __setitem__(self, index, item):
super().__setitem__(index, str(item))
def insert(self, index, item):
super().insert(index, str(item))
def append(self, item):
super().append(str(item))
def extend(self, other):
if isinstance(other, type(self)):
super().extend(other)
else:
super().extend(str(item) for item in other)
class StringList_UserList(UserList):
def __init__(self, iterable):
super().__init__(str(item) for item in iterable)
def __setitem__(self, index, item):
self.data[index] = str(item)
def insert(self, index, item):
self.data.insert(index, str(item))
def append(self, item):
self.data.append(str(item))
def extend(self, other):
if isinstance(other, type(self)):
self.data.extend(other)
else:
self.data.extend(str(item) for item in other)
这两个类的工作原理是一样的。然而,它们在内部是不同的。StringList_list继承自list,其实现基于super()。相比之下,StringList_UserList继承自UserList,它的实现依赖于内部的.data属性。
要比较这两个类的性能,应该从计时标准列表操作开始,比如实例化。然而,在这些例子中,两个初始化器是等价的,所以它们应该执行相同的操作。
测量新功能的执行时间也很有用。比如可以查看.extend()的执行时间。继续运行下面的代码:
>>> import timeit
>>> from performance import StringList_list, StringList_UserList
>>> init_data = range(10000)
>>> extended_list = StringList_list(init_data)
>>> list_extend = min(
... timeit.repeat(
... stmt="extended_list.extend(init_data)",
... number=5,
... repeat=2,
... globals=globals(),
... )
... ) * 1e6
>>> extended_user_list = StringList_UserList(init_data)
>>> user_list_extend = min(
... timeit.repeat(
... stmt="extended_user_list.extend(init_data)",
... number=5,
... repeat=2,
... globals=globals(),
... )
... ) * 1e6
>>> f"StringList_list().extend() time: {list_extend:.2f} μs"
'StringList_list().extend() time: 4632.08 μs'
>>> f"StringList_UserList().extend() time: {user_list_extend:.2f} μs"
'StringList_UserList().extend() time: 4612.62 μs'
在这个性能测试中,您使用 timeit 模块和 min() 函数来测量一段代码的执行时间。目标代码包括使用一些样本数据在StringList_list和StringList_UserList的实例上对.extend()的调用。
在这个例子中,基于list的类和基于UserList的类之间的性能差异几乎不存在。
通常,当你创建一个定制的类似列表的类时,你会期望list的子类比UserList的子类执行得更好。为什么?因为list是用 C 写的,并且针对性能进行了优化,而UserList是用纯 Python 写的包装器类。
然而,在上面的例子中,看起来这个假设并不完全正确。因此,要决定哪个超类最适合您的特定用例,请确保运行性能测试。
撇开性能不谈,继承list可以说是 Python 中的自然方式,主要是因为list作为内置类直接供 Python 开发人员使用。此外,大多数 Python 开发人员将熟悉列表及其标准特性,这将允许他们更快地编写类似列表的类。
相比之下,UserList类位于collections模块中,这意味着如果想在代码中使用它,就必须导入它。另外,并不是所有的 Python 开发者都知道UserList的存在。然而,UserList仍然是一个有用的工具,因为它可以方便地访问.data属性,这有助于创建定制的类似列表的类。
结论
现在你已经学会了如何创建定制列表类的类和修改后的新行为。为此,您已经直接子类化了内置的list类。作为一种选择,你也继承了UserList类,它在 collections 模块中可用。
从list继承和子类化UserList都是解决在 Python 中创建自己的列表类问题的合适策略。
在本教程中,您学习了如何:
- 通过继承内置
list类来创建类似列表的类 - 通过从
collections模块中子类化UserList来构建类似列表的类
现在,您可以更好地创建自己的自定义列表,从而充分利用 Python 中这种有用且常见的数据类型的全部功能。
免费下载: 点击这里下载源代码,你将使用它来创建定制的列表类。****