Python中的暗黑魔法乐趣
项目描述
sorcery
此包允许您使用和编写名为 '咒语' 的可调用项,这些可调用项知道它们被调用的位置,并可以使用该信息执行其他不可能的事情。
注意:之前咒语有一个复杂的实现,这限制了它们的调用方式。现在咒语只是 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_keys
和 unpack_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_name
和 delegate_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_name
和delegate_to_attr
assigned_names
用于创建Enum
。- 在调试时使用
print_args
。
如果您编写的代码对性能和稳定性不是那么关键,例如,如果您只是为了娱乐或者只想尽可能快地写下一些代码并在以后进行润色,那么请尝试使用它。
这个库的目的不仅仅是用于实际代码。它是一种探索和思考API和语言设计、可读性以及Python自身限制的方法。创建它很有趣,我希望其他人也能在玩弄它时感到乐趣。来聊天,谈谈您认为哪些咒语很酷,您希望Python有哪些功能,或者您想创造哪些疯狂的项目。
如果您对这类东西感兴趣,尤其是对Python AST的创意使用,您可能还会对以下内容感兴趣:
项目详情
下载文件
下载适用于您平台文件。如果您不确定选择哪个,请了解更多关于 安装包 的信息。
源分布
构建分布
sorcery-0.2.2.tar.gz 的哈希
算法 | 哈希摘要 | |
---|---|---|
SHA256 | 84198c9502e0e7818df2e023b55831fd14bd2a36b19f0e58f363ec82c5606be7 |
|
MD5 | 8e32431a2586bfefd8b64309d32b9143 |
|
BLAKE2b-256 | f78c15a9df10a6a2c6667a0df1079df1493831e43ecea5133f4a781646b16c1a |
sorcery-0.2.2-py3-none-any.whl 的哈希
算法 | 哈希摘要 | |
---|---|---|
SHA256 | 299f23db2b7d4506df79a6e8688dfadc0e61f188029675f4b1a1d7b2e97c385c |
|
MD5 | 2ebb67366426fd7776cd930450fc3011 |
|
BLAKE2b-256 | 62c7b57116a13cacceeb83b50b84373effc3aef2a7568a742b8596769d1d20c0 |