跳转到主要内容

Python中的暗黑魔法乐趣

项目描述

sorcery

Build Status Coverage Status Supports Python 3.5+, including PyPy

此包允许您使用和编写名为 '咒语' 的可调用项,这些可调用项知道它们被调用的位置,并可以使用该信息执行其他不可能的事情。

注意:之前咒语有一个复杂的实现,这限制了它们的调用方式。现在咒语只是 executing 的一个薄包装,这要好得多。根据您的使用情况,您可能更愿意直接使用 executing。此仓库现在主要是关于如何使用它的有趣事物的集合。

安装

pip install sorcery

快速示例

请参阅文档字符串以获取更多详细信息。

from sorcery import (assigned_names, unpack_keys, unpack_attrs,
                     dict_of, print_args, call_with_name,
                     delegate_to_attr, maybe, select_from)

assigned_names

而不是

foo = func('foo')
bar = func('bar')

编写

foo, bar = [func(name) for name in assigned_names()]

而不是

class Thing(Enum):
    foo = 'foo'
    bar = 'bar'

编写

class Thing(Enum):
    foo, bar = assigned_names()

unpack_keysunpack_attrs

而不是

foo = d['foo']
bar = d['bar']

编写

foo, bar = unpack_keys(d)

同样,而不是

foo = x.foo
bar = x.bar

编写

foo, bar = unpack_attrs(x)

dict_of

而不是

dict(foo=foo, bar=bar, spam=thing())

编写

dict_of(foo, bar, spam=thing())

(另请参阅:magic_kwargs)

print_args

为了便于调试,而不是

print("foo =", foo)
print("bar() =", bar())

编写

print_args(foo, bar())

要编写此版本的自己的版本(例如,如果您想添加颜色),请使用 args_with_source

如果您喜欢这个,我推荐snoop库中的 pp 函数。

call_with_namedelegate_to_attr

有时,您可能想要创建许多方法,这些方法之间唯一的区别在于一个字符串参数,该参数等于方法的名称。给定此类

class C:
    def generic(self, method_name, *args, **kwargs):
        ...

在类定义内部,而不是

    def foo(self, x, y):
        return self.generic('foo', x, y)

    def bar(self, z):
        return self.generic('bar', z)

编写

    foo, bar = call_with_name(generic)

对于特定的常见用例

class Wrapper:
    def __init__(self, thing):
        self.thing = thing

    def foo(self, x, y):
        return self.thing.foo(x, y)

    def bar(self, z):
        return self.thing.bar(z)

您可以改写为

    foo, bar = delegate_to_attr('thing')

为了更具体的例子,这里有一个类,它包装一个列表并具有所有常规列表方法,同时确保任何通常创建新列表的方法实际上都创建一个新的包装器

class MyListWrapper(object):
    def __init__(self, lst):
        self.list = lst

    def _make_new_wrapper(self, method_name, *args, **kwargs):
        method = getattr(self.list, method_name)
        new_list = method(*args, **kwargs)
        return type(self)(new_list)

    append, extend, clear, __repr__, __str__, __eq__, __hash__, \
        __contains__, __len__, remove, insert, pop, index, count, \
        sort, __iter__, reverse, __iadd__ = spells.delegate_to_attr('list')

    copy, __add__, __radd__, __mul__, __rmul__ = spells.call_with_name(_make_new_wrapper)

当然,还有不那么神奇的DRY方法可以实现这一点(例如,遍历一些字符串并使用setattr),但它们不会告诉您的IDE/linter MyListWrapper 具有哪些或没有哪些方法。

maybe

在我们等待来自PEP 505?.运算符的同时,这里有一个替代方案。而不是

None if foo is None else foo.bar()

编写

maybe(foo).bar()

如果您想要一个稍微不那么神奇版本,请考虑pymaybe

timeit

而不是

import timeit

nums = [3, 1, 2]
setup = 'from __main__ import nums'

print(timeit.repeat('min(nums)', setup))
print(timeit.repeat('sorted(nums)[0]', setup))

编写

import sorcery

nums = [3, 1, 2]

if sorcery.timeit():
    result = min(nums)
else:
    result = sorted(nums)[0]

switch

而不是

if val == 1:
    x = 1
elif val == 2 or val == bar():
    x = spam()
elif val == dangerous_function():
    x = spam() * 2
else:
    x = -1

编写

x = switch(val, lambda: {
    1: 1,
    {{ 2, bar() }}: spam(),
    dangerous_function(): spam() * 2
}, default=-1)

这实际上会像上面的if/elif链那样表现。字典只是有些语法上的美感,但实际上从未创建过字典。键仅在需要时按顺序评估,并且仅评估匹配的值。

select_from

而不是

cursor.execute('''
    SELECT foo, bar
    FROM my_table
    WHERE spam = ?
      AND thing = ?
    ''', [spam, thing])

for foo, bar in cursor:
    ...

编写

for foo, bar in select_from('my_table', where=[spam, thing]):
    ...

如何编写自己的咒语

使用@spell装饰一个函数。将FrameInfo类的实例传递给函数的第一个参数,而其他参数将从调用中获取。例如

from sorcery import spell

@spell
def my_spell(frame_info, foo):
    ...

将被调用为my_spell(foo)

您最可能使用的重要信息是frame_info.call。这是调用咒语的ast.Call节点。这里有一些有用的文档,用于在AST中进行导航。每个节点还添加了一个parent属性。

frame_info.frame是调用咒语时的执行框架 - 请参阅inspect文档了解您可以做什么。

这些都是基本内容。请参阅各种咒语的源代码以获取一些示例,它并不复杂。

在咒语中使用其他咒语

有时您想在一个咒语中重用另一个咒语的魔法。简单地调用另一个咒语不会做您想要的事情 - 您想要告诉另一个咒语它就像是从您自己的咒语被调用的地方一样行动。为此,在您使用的咒语及其参数之间添加.at(frame_info)

让我们看看一个具体的例子。以下是咒语args_with_source的定义

@spell
def args_with_source(frame_info, *args):
    """
    Returns a list of pairs of:
        - the source code of the argument
        - the value of the argument
    for each argument.

    For example:

        args_with_source(foo(), 1+2)

    is the same as:

        [
            ("foo()", foo()),
            ("1+2", 3)
        ]
    """
    ...

args_with_source的魔法在于它查看其被调用的地方的参数并提取它们的源代码。以下是使用该魔法的简化实现的print_args咒语

@spell
def simple_print_args(frame_info, *args):
    for source, arg in args_with_source.at(frame_info)(*args):
        print(source, '=', arg)

然后当您调用simple_print_args(foo(), 1+2)时,该表达式的Call节点将传递给args_with_source.at(frame_info),以便从正确的参数中提取源代码。简单地写作args_with_source(*args)是错误的,因为这会给出源代码"*args"

其他辅助工具

这实际上就是您开始编写咒语所需的一切,但这里有一些可能有助于您的东西的指针。请参阅文档字符串以获取详细信息。

sorcery.core模块有这些辅助函数

  • node_names(node: ast.AST) -> Tuple[str]
  • node_name(node: ast.AST) -> str
  • statement_containing_node(node: ast.AST) -> ast.stmt

FrameInfo有这些方法

  • assigned_names(...)
  • get_source(self, node: ast.AST) -> str

我真的应该使用这个库吗?

如果您仍然在学习Python,那么不是。这将导致您对Python中的正常和预期内容产生混淆,并阻碍您的学习。

在严肃的商业或生产环境中,除非您非常小心,否则我不建议使用大多数咒语。它们不寻常的特性可能会让其他阅读代码的人感到困惑,而且将代码的行为与变量名等事物联系起来可能不利于代码的可读性和重构。尽管如此,也有一些例外。

  • call_with_namedelegate_to_attr
  • assigned_names 用于创建 Enum
  • 在调试时使用 print_args

如果您编写的代码对性能和稳定性不是那么关键,例如,如果您只是为了娱乐或者只想尽可能快地写下一些代码并在以后进行润色,那么请尝试使用它。

这个库的目的不仅仅是用于实际代码。它是一种探索和思考API和语言设计、可读性以及Python自身限制的方法。创建它很有趣,我希望其他人也能在玩弄它时感到乐趣。来聊天,谈谈您认为哪些咒语很酷,您希望Python有哪些功能,或者您想创造哪些疯狂的项目。

如果您对这类东西感兴趣,尤其是对Python AST的创意使用,您可能还会对以下内容感兴趣:

  • executing:该库的核心
  • snoop:一个功能丰富、方便的调试库,它也使用了 executing 以及其他各种魔法和技巧
  • birdseye:一个记录每个表达式值的调试器
  • MacroPy:通过在导入时转换AST实现的Python语法宏

项目详情


下载文件

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

源分布

sorcery-0.2.2.tar.gz (23.5 kB 查看哈希)

上传时间

构建分布

sorcery-0.2.2-py3-none-any.whl (16.3 kB 查看哈希)

上传时间 Python 3

由以下机构支持

AWS AWS 云计算和安全赞助商 Datadog Datadog 监控 Fastly Fastly CDN Google Google 下载分析 Microsoft Microsoft PSF赞助商 Pingdom Pingdom 监控 Sentry Sentry 错误日志 StatusPage StatusPage 状态页面