python后期绑定 - 动态地将locals放在范围内

时间:2012-07-20 23:47:19

标签: python functional-programming monads

我有一个函数m_chain,它指的是未定义的两个函数bindunit。我想在一些提供这些函数定义的上下文中包装此函数 - 您可以将它们视为我想动态提供实现的接口。

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中执行此操作的一些优雅方法是什么?我考虑过了:

  • polymorphism:将m_chain转换为引用self.bindself.unit的方法,其实现由子类提供
  • 实施with界面,以便我可以修改环境地图,然后在我完成后清理
  • 将m_chain的签名更改为接受单位并绑定为参数
  • 要求使用m_chain由装饰器包裹,装饰器会做某事或其他 - 不确定这是否有意义

理想情况下,我根本不想修改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

4 个答案:

答案 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,并且完全可移植:

  1. 所有函数必须绑定在monad中。如果你参考 m.bind()然后"bind"必须出现在.__dict__Monad的实例。
  2. 使用Monad的函数必须使用命名 参数m=,或者使用*args功能的函数, 必须使用**kwargs参数并检查其中是否有名为"m"的键。
  3. 这是我想到的一个例子。

    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)