自定义 Python 列表:继承自list与UserList

675 阅读19分钟

F-Strings in Python 3.6

自定义 Python 列表:继承自list与UserList

在你的 Python 编码冒险中的某个时刻,你可能需要创建具有修改行为、新功能或两者的自定义列表类。要在 Python 中做到这一点,你可以继承一个抽象的基类,直接子类化内置的list 类,或者继承UserList ,它位于collections 模块中。

在本教程中,你将学习如何。

  • 通过继承内置的list来创建自定义的列表类
  • 通过子类化建立类似列表的自定义类 UserList构建自定义的列表类,从collections 模块中进行子类化

你还会写一些例子,帮助你决定在创建你的自定义列表类时使用哪个父类,listUserList

为了从本教程中获得最大的收获,你应该熟悉 Python 的内置 list类和它的标准功能。你还需要知道面向对象编程的基础知识,并了解继承在Python中是如何工作的。

在 Python 中创建类似列表的类

内置的 list类是 Python 中的一个基本数据类型。列表在很多情况下都很有用,有大量的实际用例。在某些用例中,Pythonlist 的标准功能可能是不够的,你可能需要创建自定义的类列表来解决手头的问题。

你通常会发现至少有两个原因需要创建自定义列表类。

  1. 通过添加新的功能来扩展常规列表
  2. 修改标准列表的功能

你也可能面临既需要扩展需要修改列表的标准功能的情况。

根据你的具体需要和技能水平,你可以使用一些策略来创建你自己的自定义列表类。你可以

注意:面向对象的编程中,通常的做法是互换地使用继承子类这两个动词。

当你选择使用适当的策略时,有一些考虑。继续阅读以了解更多细节。

从一个抽象基类建立一个类似列表的类

你可以通过继承一个合适的抽象基类 (ABC) 来创建你自己的列表类,例如 MutableSequence.这个 ABC 提供了大多数list 方法的通用实现,除了 .__getitem__(), .__setitem__(), .__delitem__, .__len__(), , 和.insert() 。所以,当继承这个类时,你必须自己实现这些方法。

为所有这些特殊方法编写你自己的实现是一项相当大的工作。它很容易出错,需要对 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 ,这意味着它将继承一个标准 Pythonlist 的所有功能。因为你希望你的列表以字符串的形式存储项目,所以你需要修改所有在底层列表中添加或修改项目的方法。这些方法包括以下内容。

  • .__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 的实例时,该类的初始化器负责转换。

当你对该类的实例进行追加、插入、扩展或分配新值时,支持每种操作的方法将负责字符串的转换过程。这样一来,你的列表将始终以字符串对象的形式存储其项目。

子类化UserListcollections

另一种创建自定义列表类的方法是使用 UserList类,该类来自collections 模块。这个类是内置的list 类型的一个包装器。它被设计用来创建类似列表的对象,当时还不可能直接继承内置的list 类。

尽管对这个类的需求已经部分地被直接子类化内置的list 类所取代,但为了方便和向后兼容,UserList 仍然在标准库中可用。

UserList 的显著特点是它可以让你访问它的.data 属性,这可以方便你创建你的自定义列表,因为你不需要使用 super()所有的时间。.data 属性持有一个常规的 Pythonlist ,它默认为空。

下面是你如何通过继承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() 这样的高级工具。你只需要在类的初始化器中调用这个函数,以防止在进一步的继承情况下出现问题。在其余的方法中,你只是利用了.data ,它持有一个普通的 Python 列表。与列表打交道是你可能已经掌握的一项技能。

注意:在上面的例子中,如果你使用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']

暴露.dataUserList 最相关的特性,正如你已经学到的。这个属性可以简化你的类,因为你不需要一直使用super() 。你只需利用.data ,使用熟悉的list 接口来处理这个属性。

编码类似列表的类:实际例子

当你需要创建自定义的类似列表的类来增加或修改list 的标准功能时,你已经知道如何使用listUserList

诚然,当你考虑创建一个类列表类时,继承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 方法,等等。

现在,为了确保每个输入项都是数字,你需要在所有支持添加新项目或更新列表中现有项目的操作的方法中验证每个项目。所需的方法与在Inheriting From Python's Built-Inlist class部分后面的StringList 例子中的相同。

为了验证输入的数据,你可以使用一个叫做._validate_number() 的辅助方法。这个方法使用内置的 isinstance()函数来检查当前输入值是否是intfloatcomplex 的一个实例,这些是 Python 中代表数值的内置类。

注意:在Python中,检查一个值是否是数字的更通用的方法是使用 Number模块中的 numbers模块。这将使你能够验证 FractionDecimal对象。

如果输入值是一个数字数据类型的实例,那么你的辅助函数就会返回这个值本身。否则,该函数会引发一个带有适当错误信息的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

一个使用UserListNumberList 的替代实现可以是这样的。

# 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 可以说是简化了你的代码。

注意,你只在类的初始化器中使用super().__init__() 。当你在Python中处理继承问题时,这是一个最佳实践。它允许你在父类中正确地初始化属性,而不会破坏事情。

一个具有附加功能的列表

现在,假设你需要一个具有常规 Python 的所有标准功能的列表类list 。你的类还应该提供一些从JavaScriptArray数据类型借来的额外功能。例如,你需要有像下面这样的方法。

  • .join()将列表中的所有项目串联成一个字符串。
  • .map(action)产生新的项目,这些项目是对底层列表中的每个项目应用action() 可调用的结果。
  • .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() 必须是一个布尔值函数,根据应用于输入项的某些条件,返回TrueFalse

最后,.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!

在这些例子中,你首先调用.join()words 。这个方法返回一个唯一的字符串,这个字符串是将底层列表中的所有项目连接起来的结果。

.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 子类。这一变化使你的代码更加明确。

考虑到性能:listUserList

到此为止,你已经学会了如何通过继承listUserList 来创建自己的列表类。你还知道这两个类之间唯一可见的区别是UserList 暴露了.data 属性,这可以促进编码过程。

在这一节中,你将考虑一个重要的方面,即决定是使用list 还是UserList 来创建你的自定义列表类。那就是性能!

为了评估继承自listUserList 的类之间是否存在性能差异,你将使用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_listStringList_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 类来创建类似列表的类。
  • 通过子类化建立类似列表的类 UserListcollections 模块子类化来建立类似列表的类。

现在你已经为创建你自己的自定义列表做了更好的准备,使你能够充分利用 Python 中这种有用的、常见的数据类型的全部力量。