我有一个函数m_chain,它指的是未定义的两个函数bind
和unit
。我想在一些提供这些函数定义的上下文中包装此函数 - 您可以将它们视为我想动态提供实现的接口。
def m_chain(*fns):
"""what this function does is not relevant to the question"""
def m_chain_link(chain_expr, step):
return lambda v: bind(chain_expr(v), step)
return reduce(m_chain_link, fns, unit)
在Clojure中,这是用宏完成的。在python中执行此操作的一些优雅方法是什么?我考虑过了:
self.bind
和self.unit
的方法,其实现由子类提供with
界面,以便我可以修改环境地图,然后在我完成后清理理想情况下,我根本不想修改m_chain,我想按原样使用该定义,并且所有上述选项都需要更改定义。这有点重要,因为还有其他m_ *函数引用了在运行时提供的附加函数。
我如何最好地构建这个,以便我可以很好地传递bind和unit的实现?尽管执行复杂,但m_chain的最终用法非常容易使用,这一点非常重要。
编辑:这是另一种有效的方法,这很丑陋,因为它需要m_chain被cur成一个没有args的函数。但这是一个最低限度的工作实例。
def domonad(monad, cmf):
bind = monad['bind']; unit = monad['unit']
return cmf()
identity_m = {
'bind':lambda v,f:f(v),
'unit':lambda v:v
}
maybe_m = {
'bind':lambda v,f:f(v) if v else None,
'unit':lambda v:v
}
>>> domonad(identity_m, lambda: m_chain(lambda x: 2*x, lambda x:2*x)(2))
8
>>> domonad(maybe_m, lambda: m_chain(lambda x: None, lambda x:2*x)(2))
None
答案 0 :(得分:8)
在Python中,您可以编写所需的所有代码,这些代码指的是不存在的东西;具体而言,您可以编写引用没有绑定值的名称的代码。你可以编译该代码。唯一的问题是在运行时发生,如果名称仍然没有绑定到它们的值。
以下是您可以运行的代码示例,在Python 2和Python 3下进行了测试。
def my_func(a, b):
return foo(a) + bar(b)
try:
my_func(1, 2)
except NameError:
print("didn't work") # name "foo" not bound
# bind name "foo" as a function
def foo(a):
return a**2
# bind name "bar" as a function
def bar(b):
return b * 3
print(my_func(1, 2)) # prints 7
如果您不希望名称只是绑定在本地名称空间中,但您希望能够根据函数对它们进行微调,我认为Python中的最佳实践是使用命名参数。您总是可以关闭函数参数并返回一个新的函数对象,如下所示:
def my_func_factory(foo, bar):
def my_func(a, b):
return foo(a) + bar(b)
return my_func
my_func0 = my_func_factory(lambda x: 2*x, lambda x:2*x)
print(my_func0(1, 2)) # prints 6
编辑:以下是您的示例,使用上述想法进行修改。
def domonad(monad, *cmf):
def m_chain(fns, bind=monad['bind'], unit=monad['unit']):
"""what this function does is not relevant to the question"""
def m_chain_link(chain_expr, step):
return lambda v: bind(chain_expr(v), step)
return reduce(m_chain_link, fns, unit)
return m_chain(cmf)
identity_m = {
'bind':lambda v,f:f(v),
'unit':lambda v:v
}
maybe_m = {
'bind':lambda v,f:f(v) if v else None,
'unit':lambda v:v
}
print(domonad(identity_m, lambda x: 2*x, lambda x:2*x)(2)) # prints 8
print(domonad(maybe_m, lambda x: None, lambda x:2*x)(2)) # prints None
请告诉我这对你有用。
编辑:好的,你的评论后还有一个版本。您可以按照此模式编写任意m_
个函数:他们会检查kwargs
以获取密钥"monad"
。必须将其设置为命名参数;由于*fns
参数将所有参数收集到列表中,因此无法将其作为位置参数传递。我提供了bind()
和unit()
的默认值,以防它们未在monad中定义,或者未提供monad;那些可能不会做你想要的,所以用更好的东西替换它们。
def m_chain(*fns, **kwargs):
"""what this function does is not relevant to the question"""
def bind(v, f): # default bind if not in monad
return f(v),
def unit(v): # default unit if not in monad
return v
if "monad" in kwargs:
monad = kwargs["monad"]
bind = monad.get("bind", bind)
unit = monad.get("unit", unit)
def m_chain_link(chain_expr, step):
return lambda v: bind(chain_expr(v), step)
return reduce(m_chain_link, fns, unit)
def domonad(fn, *fns, **kwargs):
return fn(*fns, **kwargs)
identity_m = {
'bind':lambda v,f:f(v),
'unit':lambda v:v
}
maybe_m = {
'bind':lambda v,f:f(v) if v else None,
'unit':lambda v:v
}
print(domonad(m_chain, lambda x: 2*x, lambda x:2*x, monad=identity_m)(2))
print(domonad(m_chain, lambda x: None, lambda x:2*x, monad=maybe_m)(2))
答案 1 :(得分:2)
好的,这是我对这个问题的最终答案。
您需要至少在某些时候能够重新绑定某些功能。你的黑客,备份.__globals__
值并粘贴新值,是丑陋的:缓慢,非线程安全,特定于CPython。我已经考虑过这个问题了,没有Pythonic解决方案以这种方式工作。
在Python中,你可以重新绑定任何函数,但你必须明确地执行它,并且某些函数不是重新绑定的好主意。例如,我喜欢内置all()
和any()
,我认为如果你可以悄悄地重新绑定它们会很可怕,而且不会很明显。
您希望某些功能可以重新绑定,我认为您不需要将它们全部重新绑定。因此,以某种方式标记可重新绑定的函数是完全合理的。显而易见和Pythonic的方法是使它们成为我们可以调用Monad
的类的方法函数。您可以将标准变量名称m
用于Monad
的实例,然后当有人尝试阅读并理解他们的代码时,他们会知道名为m.unit()
的函数可能是通过传入的其他Monad
实例可重新绑定。
如果遵守这些规则,它将是纯Python,并且完全可移植:
m.bind()
然后"bind"
必须出现在.__dict__
中
Monad
的实例。Monad
的函数必须使用命名
参数m=
,或者使用*args
功能的函数,
必须使用**kwargs
参数并检查其中是否有名为"m"
的键。这是我想到的一个例子。
class Monad(object):
def __init__(self, *args, **kwargs):
# init from each arg. Try three things:
# 0) if it has a ".__dict__" attribute, update from that.
# 1) if it looks like a key/value tuple, insert value for key.
# 2) else, just see if the whole thing is a dict or similar.
# Other instances of class Monad() will be handled by (0)
for x in args:
if hasattr("__dict__", x):
self.__dict__.update(x.__dict__)
else:
try:
key, value = x
self.__dict__[key] = value
except TypeError:
self.__dict__.update(x)
self.__dict__.update(kwargs)
def __identity(x):
return x
def __callt(v, f):
return f(v)
def __callt_maybe(v, f):
if v:
return f(v)
else:
return None
m_identity = Monad(bind=__callt, unit=__identity)
m_maybe = Monad(bind=__callt_maybe, unit=__identity)
def m_chain(*fns, **kwargs):
"""what this function does is not relevant to the question"""
m = kwargs.get("m", m_identity)
def m_chain_link(chain_expr, step):
return lambda v: m.bind(chain_expr(v), step)
return reduce(m_chain_link, fns, m.unit)
print(m_chain(lambda x: 2*x, lambda x:2*x, m=m_identity)(2)) # prints 8
print(m_chain(lambda x: None, lambda x:2*x, m=m_maybe)(2)) # prints None
以上是干净的,Pythonic,并且应该像在CPython下一样在IronPython,Jython或PyPy下运行。在m_chain()
内,表达式m = kwargs.get("m", m_identity)
尝试读出指定的monad参数;如果找不到,则将monad设置为m_identity
。
但是,你可能想要更多。您可能希望Monad
类仅支持可选地覆盖函数名称;你可能愿意坚持CPython。这是上面的一个棘手的版本。在此版本中,当评估表达式m.some_name()
时,如果Monad
实例m
在其some_name
中没有绑定名称.__dict__
,则会显示在来电者的本地人和some_name
。
globals()
在这种情况下,表达式m.some_name()
表示“m
可以覆盖some_name
但不必覆盖; some_name
可能不在m
中,在这种情况下,some_name
将被查找,就像它没有m.
“前缀一样。魔术在函数.__getattr__()
中,它使用sys._getframe()
来查看调用者的本地人。 .__getattr__()
仅在本地查找失败时调用,因此我们知道Monad
实例没有name
绑定.__dict__
;所以使用sys._getframe(1).f_locals
查看属于调用者的本地人;如果不这样做,请查看globals()
。只需将其插入上面源代码中Monad
的类定义中。
def __getattr__(self, name):
# if __getattr__() is being called, locals() were already checked
d = sys._getframe(1).f_locals
if name in d:
return d[name]
d = globals()
if name in d:
return d[name]
mesg = "name '%s' not found in monad, locals, or globals" % name
raise NameError, mesg
答案 2 :(得分:0)
这是我最终如何做到这一点。不知道这是不是一个好主意。但它让我编写完全独立于unit / bind实现的m_ *函数,并且完全独立于在python中完成monad的方式的任何实现细节。正确的事情就在词汇范围内。
class monad:
"""Effectively, put the monad definition in lexical scope.
Can't modify the execution environment `globals()` directly, because
after globals().clear() you can't do anything.
"""
def __init__(self, monad):
self.monad = monad
self.oldglobals = {}
def __enter__(self):
for k in self.monad:
if k in globals(): self.oldglobals[k]=globals()[k]
globals()[k]=self.monad[k]
def __exit__(self, type, value, traceback):
"""careful to distinguish between None and undefined.
remove the values we added, then restore the old value only
if it ever existed"""
for k in self.monad: del globals()[k]
for k in self.oldglobals: globals()[k]=self.oldglobals[k]
def m_chain(*fns):
"""returns a function of one argument which performs the monadic
composition of fns."""
def m_chain_link(chain_expr, step):
return lambda v: bind(chain_expr(v), step)
return reduce(m_chain_link, fns, unit)
identity_m = {
'bind':lambda v,f:f(v),
'unit':lambda v:v
}
with monad(identity_m):
assert m_chain(lambda x:2*x, lambda x:2*x)(2) == 8
maybe_m = {
'bind':lambda v,f:f(v) if v else None,
'unit':lambda v:v
}
with monad(maybe_m):
assert m_chain(lambda x:2*x, lambda x:2*x)(2) == 8
assert m_chain(lambda x:None, lambda x:2*x)(2) == None
error_m = {
'bind':lambda mv, mf: mf(mv[0]) if mv[0] else mv,
'unit':lambda v: (v, None)
}
with monad(error_m):
success = lambda val: unit(val)
failure = lambda err: (None, err)
assert m_chain(lambda x:success(2*x), lambda x:success(2*x))(2) == (8, None)
assert m_chain(lambda x:failure("error"), lambda x:success(2*x))(2) == (None, "error")
assert m_chain(lambda x:success(2*x), lambda x:failure("error"))(2) == (None, "error")
from itertools import chain
def flatten(listOfLists):
"Flatten one level of nesting"
return list(chain.from_iterable(listOfLists))
list_m = {
'unit': lambda v: [v],
'bind': lambda mv, mf: flatten(map(mf, mv))
}
def chessboard():
ranks = list("abcdefgh")
files = list("12345678")
with monad(list_m):
return bind(ranks, lambda rank:
bind(files, lambda file:
unit((rank, file))))
assert len(chessboard()) == 64
assert chessboard()[:3] == [('a', '1'), ('a', '2'), ('a', '3')]
答案 3 :(得分:0)
Python已经晚了。这里没有必要做任何工作:
def m_chain(*args):
return bind(args[0])
sourcemodulename = 'foo'
sourcemodule = __import__(sourcemodulename)
bind = sourcemodule.bind
print m_chain(3)