跳转到主要内容

stdlib (和你的) 对象的Python包装器,以提供流畅的接口。

项目描述

fluentpy - 流畅Python库

Fluentpy提供对现有API(如stdlib库)的流畅接口,允许您以面向对象和流畅的方式使用它们。

Fluentpy受到JavaScript的jQueryunderscore / lodash的启发,并从Ruby和SmallTalk的集合API中汲取了一些灵感。

请注意:此库基于一个包装器,它为任何对包装值的操作返回另一个包装器。有关详细信息,请参阅下面的注意事项部分。

请参阅Fowler维基百科中关于流畅接口的定义。

Documentation Status CircleCI Dependable API Evolution

动机:为什么使用fluentpy

许多最有用的标准库方法,如mapzipfilterjoin,要么是自由函数,要么在错误类型或模块中可用。这阻止了流畅的方法链。

让我们考虑这个例子

>>> list(map(str.upper, sorted("ba,dc".split(","), reverse=True)))
['DC', 'BA']

为了理解这段代码,我必须从中间的"ba,dc".split(",")开始,然后回溯到sorted(…, reverse=True),然后到list(map(str.upper, …))。同时确保所有的括号都匹配。

如果我们能够按照同样的顺序思考和编写代码,那岂不是很好?就像在其他语言中那样编写这个吗?

>>> _("ba,dc").split(",").sorted(reverse=True).map(str.upper)._
['DC', 'BA']

“为什么不行,Python 有列表推导式来处理这个”,你可能会这样说?让我们看看

>>> [each.upper() for each in sorted("ba,dc".split(","), reverse=True)]
['DC', 'BA']

这显然更好:为了阅读它,我需要来回跳转的次数更少。尽管如此,它仍然有改进的空间。另外,添加过滤到列表推导式中并不能解决问题。

>>> [each.upper() for each in sorted("ba,dc".split(","), reverse=True) if each.upper().startswith('D')]
['DC']

回溯问题仍然存在。此外,如果过滤必须应用于处理过的版本(例如 each.upper().startswith()),则该操作必须应用两次——这很糟糕,因为你需要写两次,计算两次。

解决方案?嵌套它们!

>>> [each for each in 
        (inner.upper() for inner in sorted("ba,dc".split(","), reverse=True))
        if each.startswith('D')]
['DC']

这又回到了嵌套语句和手动检查括号关闭的初始问题。

比较一下这个

>>> processed = []
>>> parts = "ba,dc".split(",")
>>> for item in sorted(parts, reverse=True):
>>>     uppercases = item.upper()
>>>     if uppercased.startswith('D')
>>>         processed.append(uppercased)

使用基本的 Python,这是代码在执行顺序中读取的最接近的方式。所以这通常是我会做的事。

但是它有一个巨大的缺点:它不是一个表达式——它是一系列语句。这使得它很难与高阶方法或生成器结合和抽象。为了编写它,你必须为没有文档用途的中间变量发明名字,但在阅读时又必须记住它们。

(掌声):解析这个仍然需要一些回溯,特别是构建阅读时的心理状态。

哦,好吧。

所以让我们回到这个

>>> (
    _("ba,dc")
    .split(",")
    .sorted(reverse=True)
    .map(str.upper)
    .filter(_.each.startswith('D')._)
    ._
)
('DC',)

虽然一开始你可能不习惯,但考虑一下它的优点。中间变量名被抽象掉了——数据完全自然地通过方法流动。根本不需要来回跳转来解析这个。它只是按照计算的顺序读取和写入。作为额外的好处,没有需要跟踪的括号堆栈。而且它还更简洁!

那么,所有这一切的本质是什么?

Python 是一种面向对象的语言——但它并没有真正使用面向对象教给我们的关于如何在与之前语言中一起使用集合和高阶方法的知识(我想到了 SmallTalk,但近年来也看到了 Ruby)。为什么我不能像 30 年前的 SmallTalk 那样在 Python 中创建那些美丽的流畅调用链?

现在我可以,你也可以。

特性

导入库

建议在导入时重命名库

>>> import fluentpy as _

或者

>>> import fluentpy as _f

我更喜欢为小型项目使用 _,为大型项目(其中使用 gettext)使用 _f

超级简单的流畅链

_ 实际上是 fluentpy 模块中的 wrap 函数,它是一个工厂函数,返回 Wrapper 的子类。这是该库的基本和主要对象。

这做了两件事:首先,它确保了对包装对象的每个属性访问、项访问或方法调用也将返回一个包装对象。这意味着,一旦你包装了某物,除非你显式地通过 ._.unwrap.to(a_type) 解包它,否则它将保持包装状态——几乎无论你对它做什么。第二件事是,它返回一个具有特定方法的 Wrapper 子类,这些方法取决于包装的类型。我设想它在未来会扩展,但目前最有用的包装器是:添加了所有 Python 集合函数(map、filter、zip、reduce 等)以及来自 itertools 的许多方法的 IterableWrapper,以及一些额外的额外方法。CallableWrapper,其中添加了 .curry().compose(),以及 TextWrapper,其中添加了大多数正则表达式方法。

一些示例

# View documentation on a symbol without having to wrap the whole line it in parantheses
>>> _([]).append.help()
Help on built-in function append:

append(object, /) method of builtins.list instance
    Append object to the end of the list.

# Introspect objects without awkward wrapping stuff in parantheses
>>> _(_).dir()
fluentpy.wrap(['CallableWrapper', 'EachWrapper', 'IterableWrapper', 'MappingWrapper', 'ModuleWrapper', 'SetWrapper', 'TextWrapper', 'Wrapper', 
'_', '_0', '_1', '_2', '_3', '_4', '_5', '_6', '_7', '_8', '_9', 
…
, '_args', 'each', 'lib', 'module', 'wrap'])
>>> _(_).IterableWrapper.dir()
fluentpy.wrap(['_', 
…, 
'accumulate', 'all', 'any', 'call', 'combinations', 'combinations_with_replacement', 'delattr', 
'dir', 'dropwhile', 'each', 'enumerate', 'filter', 'filterfalse', 'flatten', 'freeze', 'get', 
'getattr', 'groupby', 'grouped', 'hasattr', 'help', 'iaccumulate', 'icombinations', '
icombinations_with_replacement', 'icycle', 'idropwhile', 'ieach', 'ienumerate', 'ifilter', 
'ifilterfalse', 'iflatten', 'igroupby', 'igrouped', 'imap', 'ipermutations', 'iproduct', 'ireshape', 
'ireversed', 'isinstance', 'islice', 'isorted', 'issubclass', 'istar_map', 'istarmap', 'itee', 
'iter', 'izip', 'join', 'len', 'map', 'max', 'min', 'permutations', 'pprint', 'previous', 'print', 
'product', 'proxy', 'reduce', 'repr', 'reshape', 'reversed', 'self', 'setattr', 'slice', 'sorted', 
'star_call', 'star_map', 'starmap', 'str', 'sum', 'to', 'type', 'unwrap', 'vars', 'zip'])

# Did I mention that I hate wrapping everything in parantheses?
>>> _([1,2,3]).len()
3
>>> _([1,2,3]).print()
[1,2,3]

# map over iterables and easily curry functions to adapt their signatures
>>> _(range(3)).map(_(dict).curry(id=_, delay=0)._)._
({'id': 0, 'delay': 0}, {'id': 1, 'delay': 0}, {'id': 2, 'delay': 0})
>>> _(range(10)).map(_.each * 3).filter(_.each < 10)._
(0, 3, 6, 9)
>>> _([3,2,1]).sorted().filter(_.each<=2)._
[1,2]

# Directly work with regex methods on strings
>>> _("foo,  bar,      baz").split(r",\s*")._
['foo', 'bar', 'baz']
>>> _("foo,  bar,      baz").findall(r'\w{3}')._
['foo', 'bar', 'baz']

# Embedd your own functions into call chains
>>> seen = set()
>>> def havent_seen(number):
...     if number in seen:
...         return False
...     seen.add(number)
...     return True
>>> (
...     _([1,3,1,3,4,5,4])
...     .dropwhile(havent_seen)
...     .print()
... )
(1, 3, 4, 5, 4)

等等。 探索方法文档,了解你可以做什么

导入为表达式

导入语句在Python中是(嗯哼)语句。这没问题,但有时真的很烦人。

_lib 对象,它是Python导入机制的包装器,允许导入任何可以通过导入访问的内容,并将其作为内联使用的表达式导入。

所以,而不是

>>> import sys
>>> input = sys.stdin.read()

你可以这样做

>>> lines = _.lib.sys.stdin.readlines()._

作为额外的好处,通过lib导入的任何内容都已预先包装,因此你可以立即从中链式调用。

从表达式生成lambda表达式

lambda很棒——它通常正是你所需要的。但是,如果你只是想对集合中的每个对象获取属性或调用方法,每次都必须写下来,它也会很烦人。例如

>>> _([{'fnord':'foo'}, {'fnord':'bar'}]).map(lambda each: each['fnord'])._
('foo', 'bar')

>>> class Foo(object):
>>>     attr = 'attrvalue'
>>>     def method(self, arg): return 'method+'+arg
>>> _([Foo(), Foo()]).map(lambda each: each.attr)._
('attrvalue', 'attrvalue')

>>> _([Foo(), Foo()]).map(lambda each: each.method('arg'))._
('method+arg', 'method+arg')

当然,它 works,但如果我们能保存一个变量并稍微缩短这个表达式会更好吗?

Python确实有attrgetteritemgettermethodcaller——它们只是有点不便使用

>>> from operator import itemgetter, attrgetter, methodcaller
>>> __([{'fnord':'foo'}, {'fnord':'bar'}]).map(itemgetter('fnord'))._
('foo', 'bar')
>>> _([Foo(), Foo()]).map(attrgetter('attr'))._
('attrvalue', 'attrvalue')

>>> _([Foo(), Foo()]).map(methodcaller('method', 'arg'))._
('method+arg', 'method+arg')

_([Foo(), Foo()]).map(methodcaller('method', 'arg')).map(str.upper)._
('METHOD+ARG', 'METHOD+ARG')

为了简化这个过程,_.each被提供。 each为这些(以及其他运算符)提供了语法糖。基本上,你为_.each做的一切都会被记录,稍后当你通过将其展开或应用运算符(如`+ - * / <`)来生成可调用对象时,会“播放”这些记录。

>>>  _([1,2,3]).map(_.each + 3)._
(4, 5, 6)

>>> _([1,2,3]).filter(_.each < 3)._
(1, 2)

>>> _([1,2,3]).map(- _.each)._
(-1, -2, -3)

>>> _([dict(fnord='foo'), dict(fnord='bar')]).map(_.each['fnord']._)._
('foo', 'bar')

>>> _([Foo(), Foo()]).map(_.each.attr._)._
('attrvalue', 'attrvalue')

>>> _([Foo(), Foo()]).map(_.each.method('arg')._)._
('method+arg', 'method+arg')

>>> _([Foo(), Foo()]).map(_.each.method('arg').upper()._)._
('METHOD+ARG', 'METHOD+ARG')
# Note that there is no second map needed to call `.upper()` here!

规则是,你必须展开._each对象,以生成一个可调用的对象,然后你可以将其传递给.map().filter()或任何你想使用它的地方。

方法返回None时的链式调用

使用流畅接口的一个主要烦恼是返回None的方法。遗憾的是,Python中的许多方法在主要对对象产生副作用时返回None。例如,考虑一下list.sort()。但所有没有return语句的方法都返回None。虽然这比Ruby好得多(Ruby会返回最后一个表达式的值——这意味着对象会不断泄露内部信息),但如果你想要链式调用这些方法之一,这会非常烦恼。

不过,不要担心,Fluentpy已经为你解决了这个问题。 :)

Fluent包装对象将有一个self属性,允许你继续链式调用之前的'self'对象。

>>> _([3,2,1]).sort().self.reverse().self.call(print)

尽管sort()reverse()都返回None

当然,如果你在任何时候使用.unwrap._展开,你将得到真正的返回值None

使用Python轻松进行Shell过滤

使用一点点Python,在shell上完成某些事情可能非常容易。但是,编写时的回溯以及Python命令倾向于跨越多行(导入、函数定义等),使得这通常不够实用,以至于你不会这样做。

这就是为什么fluentpy是一个可执行模块,因此你可以在shell上像这样使用它

$ echo 'HELLO, WORLD!' \
    | python3 -m fluentpy "lib.sys.stdin.readlines().map(str.lower).map(print)"
hello, world!

在这种模式下,变量lib_each将被注入到第一个位置参数所提供的python命令的命名空间中。

考虑这个shell文本过滤器,我用它从我心爱的但遗憾的是相当传统的del.icio.us账户中提取数据。格式如下

$ tail -n 200 delicious.html|head
<DT><A HREF="http://intensedebate.com/" ADD_DATE="1234043688" PRIVATE="0" TAGS="web2.0,threaded,comments,plugin">IntenseDebate comments enhance and encourage conversation on your blog or website</A>
<DD>Comments on static websites
<DT><A HREF="http://code.google.com/intl/de/apis/socialgraph/" ADD_DATE="1234003285" PRIVATE="0" TAGS="api,foaf,xfn,social,web">Social Graph API - Google Code</A>
<DD>API to try to find metadata about who is a friend of who.
<DT><A HREF="http://twit.tv/floss39" ADD_DATE="1233788863" PRIVATE="0" TAGS="podcast,sun,opensource,philosophy,floss">The TWiT Netcast Network with Leo Laporte</A>
<DD>Podcast about how SUN sees the society evolve from a hub and spoke to a mesh society and how SUN thinks it can provide value and profit from that.
<DT><A HREF="http://www.xmind.net/" ADD_DATE="1233643908" PRIVATE="0" TAGS="mindmapping,web2.0,opensource">XMind - Social Brainstorming and Mind Mapping</A>
<DT><A HREF="http://fun.drno.de/pics/What.jpg" ADD_DATE="1233505198" PRIVATE="0" TAGS="funny,filetype:jpg,media:image">What.jpg 480×640 pixels</A>
<DT><A HREF="http://fun.drno.de/pics/english/What_happens_to_your_body_if_you_stop_smoking_right_now.gif" ADD_DATE="1233504659" PRIVATE="0" TAGS="smoking,stop,funny,informative,filetype:gif,media:image">What_happens_to_your_body_if_you_stop_smoking_right_now.gif 800×591 pixels</A>
<DT><A HREF="http://www.normanfinkelstein.com/article.php?pg=11&ar=2510" ADD_DATE="1233482064" PRIVATE="0" TAGS="propaganda,israel,nazi">Norman G. Finkelstein</A>

$ cat delicious.html | grep hosting \                                                                               :(
   | python3  -c 'import sys,re; \
       print("\n".join(re.findall(r"HREF=\"([^\"]+)\"", sys.stdin.read())))'
https://uberspace.de/
https://gitlab.com/gitlab-org/gitlab-ce
https://www.docker.io/

当然,它 works,但与我已经讨论过的所有回溯问题一样。使用fluentpy,这可以写得更优雅,也更易于阅读

 $ cat delicious.html | grep hosting \
     | python3 -m fluentpy 'lib.sys.stdin.read().findall(r"HREF=\"([^\"]+)\"").map(print)'  
https://uberspace.de/
https://gitlab.com/gitlab-org/gitlab-ce
https://www.docker.io/

注意事项和经验教训

在每个表达式的每行开始和结束Fluentpy表达式

如果你不在每个流畅语句的末尾使用._.unwrap.to(a_type)操作来获取正常的Python对象,包装器就会像病毒一样在运行时图像中扩散,“感染”越来越多的对象,导致奇怪的副作用。所以记住:在更大型的项目中使用fluentpy时,始终严格在流畅语句的末尾展开对象。

>>> _('foo').uppercase().match('(foo)').group(0)._

通常将包装对象提交给变量是个坏主意。最好直接解包。这尤其明智,因为流畅链包含所有中间值的引用,所以解包链允许垃圾回收器释放所有这些对象。

忘记解包由 _.each 生成的方法可能有点令人惊讶,因为每次调用它们只会导致更多表达式生成而不是触发它们的效果。

话虽如此,流畅包装对象的 str()repr() 输出有明显的标记,因此这很容易调试。

此外,不必解包可能非常适合短脚本和特别是一次性的shell命令。然而:明智地使用Fluentpys的力量!

将表达式链拆分为多行

较长的流畅调用链最好写在多行上。这有助于可读性,并便于对行进行注释(因为您的代码可以非常简洁)。

对于短链,一行可能就足够了。

_(open('day1-input.txt')).read().replace('\n','').call(eval)._

对于较长的链,多行会更整洁。

day1_input = (
    _(open('day1-input.txt'))
    .readlines()
    .imap(eval)
    ._
)

seen = set()
def havent_seen(number):
    if number in seen:
        return False
    seen.add(number)
    return True

(
    _(day1_input)
    .icycle()
    .iaccumulate()
    .idropwhile(havent_seen)
    .get(0)
    .print()
)

考虑Fluentpy的性能影响

这个库通过为对象上的每个属性访问、项目获取或方法调用创建其包装对象的另一个实例来工作。此外,这些对象保留了一个历史链到链中的所有先前包装器(以应对返回 None 的函数)。

这意味着在紧密的内循环中,即使分配一个额外的对象也会严重影响您代码的性能,您可能不想使用 fluentpy

此外(再次提醒),这意味着您不想将流畅对象提交给长期存在的变量,因为这可能是严重内存泄漏的来源。

在其他所有地方:尽情享受!以流畅的方式编写Python可以非常有趣!

结语

这个库试图做一点类似于JavaScript中 underscorelodashjQuery 的库所做的事情。只需提供缺失的胶水,使标准库更美好、更易于使用。享受乐趣!

我认为这个库在短Python脚本和shell单行或shell过滤器中特别有用,在Python之前,这有点难以使用,并阻止您这样做。

我还非常喜欢将其用于笔记本或Python shell中,以便轻松探索某些库、代码或概念。

项目详情


下载文件

下载适用于您平台上的文件。如果您不确定选择哪个,请了解更多关于 安装包 的信息。

源分布

fluentpy-2.1.1.tar.gz (34.6 kB 查看哈希)

上传时间

构建分布

fluentpy-2.1.1-py3-none-any.whl (22.5 kB 查看哈希)

上传时间 Python 3

由以下支持