4

我有一个函数 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 中有哪些优雅的方法可以做到这一点?我考虑过:

  • 多态性:将 m_chain 转换为引用self.bindand的方法self.unit,其实现由子类提供
  • 实现with接口,这样我就可以修改环境映射,然后在我完成后清理
  • 更改 m_chain 的签名以接受单元并绑定作为参数
  • 要求使用 m_chain 由装饰器包裹,该装饰器将做某事或其他事情 - 不确定这是否有意义

理想情况下,我根本不想修改 m_chain,我想按原样使用定义,并且上述所有选项都需要更改定义。这一点很重要,因为还有其他 m_* 函数引用了在运行时提供的附加函数。

我如何最好地构建它,以便我可以很好地传递绑定和单元的实现?尽管实现复杂,但 m_chain 的最终用法必须非常易于使用,这一点很重要。

编辑:这是另一种可行的方法,它非常丑陋,因为它需要将 m_chain 柯里化为没有参数的函数。但这是一个最小的工作示例。

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
4

4 回答 4

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_函数:它们检查kwargskey "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))
于 2012-07-21T00:12:02.960 回答
2

好的,这是我对这个问题的最终答案。

您需要至少在某些时候能够重新绑定某些功能。您的 hack,备份.__globals__值并粘贴新值是丑陋的:速度慢,非线程安全,并且特定于 CPython。我已经考虑过这一点,并且没有以这种方式工作的 Pythonic 解决方案。

在 Python 中,您可以重新绑定任何函数,但您必须显式执行此操作,并且某些函数重新绑定不是一个好主意。例如,我喜欢内置的all()any(),我认为如果你可以偷偷地重新绑定它们会很可怕,而且不会很明显。

您希望某些功能可以重新绑定,我认为您不需要它们都可以重新绑定。因此,以某种方式标记可重新绑定的功能是非常有意义的。显而易见的 Pythonic 方法是让它们成为我们可以调用的类的方法函数Monadm您可以为 的实例使用标准变量名称Monad,然后当有人尝试阅读和理解他们的代码时,他们将知道名称为 like 的函数m.unit()可能可以通过传入的其他Monad实例重新绑定。

如果您遵守以下规则,它将是纯 Python 并且完全可移植:

  1. 所有函数都必须绑定在 monad 中。如果你提到 m.bind()then"bind"必须出现在.__dict__的实例中Monad
  2. 函数 usingMonad必须接受一个命名参数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 的,在 IronPython、Jython 或 PyPy 下运行应该和在 CPython 下运行一样好。在内部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绑定 in .__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
于 2012-07-23T08:14:23.347 回答
0

这就是我最终的做法。不知道这是否是个好主意。但它让我编写我的 m_* 函数完全独立于单元/绑定的实现,也完全独立于在 python 中完成单子的方式的任何实现细节。正确的东西就在词法范围内。

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')]
于 2012-07-21T16:33:37.240 回答
0

Python 已经很晚了。这里不需要做任何工作:

def m_chain(*args):
    return bind(args[0])

sourcemodulename = 'foo'
sourcemodule = __import__(sourcemodulename)
bind = sourcemodule.bind

print m_chain(3)
于 2012-07-21T23:00:03.330 回答