首页 > 文章列表 > 在Python中实现自己的数据结构的方法

在Python中实现自己的数据结构的方法

Python 数据结构 实现方法
365 2023-09-07

Python 为使用类和自定义运算符实现您自己的数据结构提供了全面的支持。在本教程中,您将实现一个自定义管道数据结构,该结构可以对其数据执行任意操作。我们将使用 Python 3。

管道数据结构

管道数据结构很有趣,因为它非常灵活。它由一系列任意函数组成,这些函数可以应用于对象集合并生成结果列表。我将利用Python的可扩展性并使用管道字符(|)来构建管道。

实例

在深入了解所有细节之前,让我们先看看一个非常简单的管道:

x = range(5) | Pipeline() | double | Ω
print(x)

[0, 2, 4, 6, 8]

这是怎么回事?让我们一步步分解。第一个元素 range(5) 创建一个整数列表 [0, 1, 2, 3, 4]。整数被送入由 Pipeline() 指定的空管道中。然后将 double 函数添加到管道中,最后很酷的 Ω 函数终止管道并使其自我评估。

评估包括获取输入并应用管道中的所有函数(在本例中只是 double 函数)。最后,我们将结果存储在名为 x 的变量中并打印它。

Python 类

Python 支持类并具有非常复杂的面向对象模型,包括多重继承、混合和动态重载。 __init__() 函数充当创建新实例的构造函数。 Python 还支持高级元编程模型,我们在本文中不会讨论该模型。

这是一个简单的类,它有一个 __init__() 构造函数,该构造函数采用可选参数 x (默认为 5)并将其存储在 self.x 属性。它还具有 foo() 方法,该方法返回 self.x 属性乘以 3:

class A:
    def __init__(self, x=5):
        self.x = x

    def foo(self):
        return self.x * 3

以下是如何使用和不使用显式 x 参数来实例化它:

>>> a = A(2)
>>> print(a.foo())
6

a = A()
print(a.foo())
15

自定义运算符

使用 Python,您可以为您的类使用自定义运算符以获得更好的语法。有一些称为“dunder”方法的特殊方法。 “dunder”的意思是“双下划线”。这些方法(如 __eq____gt____or__)允许您对类实例(对象)使用 ==>| 等运算符。让我们看看它们如何与 A 类一起工作。

如果您尝试将 A 的两个不同实例相互比较,则结果将始终为 False,无论 x 的值如何:

>>> print(A() == A())
False

这是因为Python默认会比较对象的内存地址。假设我们要比较 x 的值。我们可以添加一个特殊的 __eq__ 运算符,它接受两个参数 selfother,并比较它们的 x 属性:

    def __eq__(self, other):
        return self.x == other.x

让我们验证一下:

>>> print(A() == A())
True

>>> print(A(4) == A(6))
False

将管道实现为 Python 类

现在我们已经介绍了 Python 中的类和自定义运算符的基础知识,让我们使用它来实现我们的管道。 __init__() 构造函数采用三个参数:函数、输入和终端。 “functions”参数是一个或多个函数。这些函数是管道中对输入数据进行操作的阶段。

“输入”参数是管道将操作的对象列表。输入的每一项都将由所有管道函数处理。 “terminals”参数是一个函数列表,当遇到其中一个函数时,管道会评估自身并返回结果。默认情况下,终端只是 print 函数(在 Python 3 中,print 是一个函数)。

请注意,在构造函数内部,终端中添加了一个神秘的 Ω。接下来我会解释一下。

管道构造函数

这是类定义和 __init__() 构造函数:

class Pipeline:
    def __init__(self,
                 functions=(),
                 input=(),
                 terminals=(print,)):
        if hasattr(functions, '__call__'):
            self.functions = [functions]
        else:
            self.functions = list(functions)
        self.input = input
        self.terminals = [Ω] + list(terminals)

Python 3 完全支持标识符名称中的 Unicode。这意味着我们可以使用 Ω 等很酷的符号作为变量和函数名称。在这里,我声明了一个名为 Ω 的身份函数,它充当终端函数:

Ω = lambda x: x

我也可以使用传统语法:

def Ω(x):
    return x

__or__ 和 __ror__ 运算符

这里是管道类的核心。为了使用 | (管道符号),我们需要重写几个运算符。 Python 使用 | 符号来表示整数的按位或。在我们的例子中,我们希望重写它以实现函数链接以及在管道开头提供输入。这是两个独立的操作。

当第二个操作数是管道实例(只要第一个操作数不是)时,就会调用 __ror__ 运算符。它将第一个操作数视为输入并将其存储在 self.input 属性中,并返回管道实例(self)。这允许稍后链接更多函数。

def __ror__(self, input):
    self.input = input
	return self

以下是调用 __ror__() 运算符的示例: 'hello There' |管道()

当第一个操作数是管道时(即使第二个操作数也是管道),会调用 __or__ 运算符。它接受操作数为可调用函数,并断言 func 操作数确实是可调用的。

然后,它将函数附加到 self.functions 属性并检查该函数是否是终端函数之一。如果它是终端,则评估整个管道并返回结果。如果它不是终端,则返回管道本身。

def __or__(self, func):
    assert(hasattr(func, '__call__'))
	self.functions.append(func)
	if func in self.terminals:
		return self.eval()
	return self

评估管道

当您向管道添加越来越多的非终端函数时,什么也没有发生。实际评估被推迟到调用 eval() 方法为止。这可以通过向管道添加终端函数或直接调用 eval() 来实现。

评估包括迭代管道中的所有函数(包括终端函数,如果有)并在前一个函数的输出上按顺序运行它们。管道中的第一个函数接收输入元素。

def eval(self):
    result = []
	for x in self.input:
		for f in self.functions:
			x = f(x)
		result.append(x)
	return result

有效使用管道

使用管道的最佳方法之一是将其应用于多组输入。在以下示例中,定义了一个没有输入且没有终端函数的管道。它有两个函数:我们之前定义的臭名昭著的 double 函数和标准 math.floor

然后,我们为它提供三个不同的输入。在内循环中,我们在调用 Ω 终端函数时添加它以在打印之前收集结果:

p = Pipeline() | double | math.floor

for input in ((0.5, 1.2, 3.1),
    		  (11.5, 21.2, -6.7, 34.7),
			  (5, 8, 10.9)):
	result = input | p | Ω
	print(result)
	
[1, 2, 6]
[23, 42, -14, 69]
[10, 16, 21]

您可以直接使用 print 终端函数,但每个项目将打印在不同的行上:

keep_palindromes = lambda x: (p for p in x if p[::-1] == p)
keep_longer_than_3 = lambda x: (p for p in x if len(p) > 3)

p = Pipeline() | keep_palindromes | keep_longer_than_3 | list
(('aba', 'abba', 'abcdef'),) | p | print

['abba']

未来的改进

有一些改进可以使管道更有用:

  • 添加流式传输,以便它可以处理无限的对象流(例如从文件或网络事件中读取)。
  • 提供一种评估模式,将整个输入作为单个对象提供,以避免提供一个项目集合的繁琐解决方法。
  • 添加各种有用的管道函数。

结论

Python 是一种非常具有表现力的语言,并且非常适合设计您自己的数据结构和自定义类型。当语义适合这种表示法时,覆盖标准运算符的能力非常强大。例如,管道符号(|)对于管道来说是非常自然的。

许多 Python 开发人员喜欢 Python 的内置数据结构,例如元组、列表和字典。然而,设计和实现您自己的数据结构可以通过提高抽象级别并向用户隐藏内部细节来使您的系统更简单、更易于使用。尝试一下。