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”函数添加到管道,最后, Ω
函数终止管道并使其进行评估。
评估包括获取输入并应用流水线中的所有功能(在这种情况下,只是双功能)。 最后,我们将结果存储在名为x的变量中并进行打印。
Python类
Python支持类,并且具有非常复杂的面向对象模型,其中包括多重继承,mixin和动态重载。 __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”是指“双下划线”。 这些方法,例如“ __eq __”,“ __ gt__”和“ __or__”,使您可以使用“ ==”,““>”和“ |”之类的运算符。 与您的类实例(对象)。 让我们看看它们如何与A类一起工作。
如果尝试将A的两个不同实例进行相互比较,则无论x的值如何,结果始终为False:
>>> print(A() == A())
False
这是因为Python默认情况下会比较对象的内存地址。 假设我们要比较x的值。 我们可以添加一个特殊的“ __eq__”运算符,该运算符采用两个参数“ self”和“ other”,并比较它们的x属性:
def __eq__(self, other):
return self.x == other.x
让我们验证一下:
>>> print(A() == A())
True
>>> print(A(4) == A(6))
False
将管道实现为Python类
既然我们已经介绍了Python中的类和自定义运算符的基础知识,让我们使用它来实现我们的管道。 __init__()
构造函数带有三个参数:函数,输入和终端。 “函数”参数是一个或多个函数。 这些功能是流水线中对输入数据进行操作的阶段。
“输入”参数是管道将在其上操作的对象的列表。 输入的每一项将由所有管道功能处理。 “ terminals”参数是一个函数列表,遇到其中一个函数时,管道会进行自我评估并返回结果。 默认情况下,终端只是打印功能(在Python 3中,“打印”是一个功能)。
请注意,在构造函数内部,将神秘的“Ω”添加到端子。 接下来,我会解释。
管道构造器
这是类定义和__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__”运算符
这是Pipeline类的核心。 为了使用“ |” (管道符号),我们需要覆盖几个运算符。 “ |” Python将symbol用于按位或整数。 在我们的例子中,我们想重写它以实现功能链,以及在流水线的开头输入输入。 这是两个单独的操作。
只要第二个操作数不是Pipeline实例,就调用“ __ror__”运算符。 它将第一个操作数视为输入,并将其存储在self.input
属性中,然后将Pipeline实例返回(自身)。 这样可以在以后链接更多功能。
def __ror__(self, input):
self.input = input
return self
这是一个将调用__ror__()
运算符的示例: 'hello there' | Pipeline()
'hello there' | Pipeline()
当第一个操作数是管道时(即使第二个操作数也是管道),也会调用“ __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的内置数据结构,例如元组,列表和字典。 但是,设计和实现自己的数据结构可以通过提高抽象级别并向用户隐藏内部详细信息,使您的系统更加简单易用。 试试看。
翻译自: https://code.tutsplus.com/tutorials/how-to-implement-your-own-data-structure-in-python--cms-28723