4

i have a function, m_chain, which refers to two functions bind and unit which are not defined. i want to wrap this function in some context which provides definitions for these functions - you can think of them as interfaces for which i want to dynamically provide an implementation.

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)

In Clojure, this is done with macros. what are some elegant ways of doing this in python? i have considered:

  • polymorphism: turn m_chain into a method referring to self.bind and self.unit, whose implementations are provided by a subclass
  • implementing the with interface so i can modify the environment map and then clean up when i'm done
  • changing the signature of m_chain to accept unit and bind as arguments
  • requiring usage of m_chain be wrapped by a decorator which will do something or other - not sure if this even makes sense

ideally, i do not want to modify m_chain at all, i want to use the definition as is, and all of the above options require changing the definition. This is sort of important because there are other m_* functions which refer to additional functions to be provided at runtime.

How do i best structure this so i can nicely pass in implementations of bind and unit? its important that the final usage of m_chain be really easy to use, despite the complex implementation.

edit: here's another approach which works, which is ugly as all hell because it requires m_chain be curried to a function of no args. but this is a minimum working example.

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

これがどのように機能するか教えてください。

編集:さて、あなたのコメントの後にもう1つのバージョン。このパターンに従って任意の関数を書くことができます: それらは keym_をチェックします。これは、名前付き引数として設定する必要があります。すべての引数をリストに集める引数のため、位置引数として渡す方法はありません。モナドで定義されていない場合、またはモナドが提供されていない場合に備えて、 andのデフォルト値を提供しました。それらはおそらくあなたが望むことをしないので、より良いものに置き換えてください。kwargs"monad"*fnsbind()unit()

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

さて、これがこの質問に対する私の最後の答えです。

少なくとも時々、いくつかの関数を再バインドできる必要があります。値をバックアップして.__globals__新しい値を貼り付けるというあなたのハックは醜いです: 遅く、非スレッドセーフで、CPython に固有です。私はこれについて考えましたが、このように機能する Pythonic ソリューションはありません。

Python では、任意の関数を再バインドできますが、明示的に再バインドする必要があり、一部の関数は再バインドすることをお勧めしません。たとえば、私はビルトインall()とが大好きで、any()これらをこっそりと再バインドできて、それが明らかでなかったら怖いと思います。

一部の機能を再バインド可能にしたい場合、それらすべてを再バインド可能にする必要はないと思います。したがって、何らかの方法で再バインド可能な関数をマークすることは完全に理にかなっています。これを行うための明白で Pythonic な方法は、呼び出し可能なクラスのメソッド関数にすることMonadです。mのインスタンスに標準の変数名を使用できます。そうすればMonad、誰かが自分のコードを読んで理解しようとすると、 のような名前の関数が、渡された他のインスタンスm.unit()を介して潜在的に再バインド可能であることがわかります。Monad

これらのルールに従えば、それは純粋な Python であり、完全に移植可能です。

  1. すべての関数はモナドにバインドする必要があります。を参照する場合 m.bind()は、 のインスタンスの に"bind"表示する必要があります。.__dict__Monad
  2. を使用する関数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 であり、IronPython、Jython、または PyPy でも CPython と同じように動作するはずです。内部m_chain()では、式m = kwargs.get("m", m_identity)は指定されたモナド引数を読み取ろうとします。見つからない場合、モナドは に設定されm_identityます。

しかし、もっと欲しいかもしれません。Monadオプションで関数名のオーバーライドのみをサポートするクラスが必要な場合があります。そして、CPython だけに固執することを厭わないかもしれません。これは、上記のよりトリッキーなバージョンです。このバージョンでは、式m.some_name()が評価されるときに、Monadインスタンスの に名前がバインドされてmいない場合、呼び出し元のローカルと.some_name.__dict__some_nameglobals()

この場合、式m.some_name()は "mをオーバーライドできますsome_nameが、オーバーライドする必要はありません。 に含まれていないsome_name可能性があります。このm場合、 は "some_nameというプレフィックスが付いていないかのように検索されm.ます。魔法は、呼び出し元のローカルをのぞくため .__getattr__()に使用する関数にあります。ローカル ルックアップが失敗した場合にのみ呼び出されるため、インスタンスがバインドされていないことがわかります。;を使用して、呼び出し元に属するローカルを見てください。それができない場合は、 を参照してください。これを上記のソース コードののクラス定義に挿入するだけです。sys._getframe().__getattr__()Monadname.__dict__sys._getframe(1).f_localsglobals()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 に答える