機能も揃ってきたので真面目な例も考えようと思った結果、禁断の果実(eval)に手を伸ばしてみたり

github.com

はじめに

1週間くらい触ってきた結果それなりに機能が揃ってきたような気がする。$loadで外部のリソースから値を取ってくる事ができるようになったし。その際にjson pointerで指定した範囲の一部分を取り出すことも出来る。$concatで複数の断片をつなぎ合わせて1つにすることが出来るし。何か不足することがあれば$importで直接pythonの関数をimportすることも出来る。scopeも変数束縛も出来るようになった。

そんなわけでもう少し真面目な例を考えてみようと思った。

フィルタリング

真面目な例をということで考えている内に、フィルタリングの操作がしたいことがあるかもしれないと思い始めた。例えば、json referenceによって特定のファイルの特定の箇所のデータを取ってくることは出来るのだけれど。そのデータの内ある特定の条件を満たしたものだけを操作の対象にしたいということがしばしばある気がする。

例えば、以下のようなnums.yamlがありこの内のnums0の方を利用した設定を作りたい。この時奇数だけを取り出したいみたいな状況(通常の利用例では数値が欲しくなることは少なく、何らかの文字列(e.g. ファイル名)のリストを拡張子などで絞り込むみたいな感じかもしれない)。

nums.yaml

definitions:
  nums0:
    [1, 2, 3, 4, 5, 6]
  nums1:
    [1, 2, 3, 5, 7, 11]

この時、$loadを使えばnums0だけを取り出すことは出来る。

main0.yaml

definitions:
  nums: {$load: ./nums.yaml#/definitions/nums0}
$ zenmai main0.yaml
definitions:
  nums:
  - 1
  - 2
  - 3
  - 4
  - 5
  - 6

ただこの内、奇数だけを利用したい箇所、偶数だけを利用したい箇所というのが出てくるかもしれない。

まっとうな方法

importの際にPYTHONPATHに追加せずとも直接物理的なファイル名でimport出来るようにしている。なので以下のようにテキトウに自分でフィルタリングする関数を書いてあげてimportするというのがまっとうな方法かもしれない。

main1.yaml

code:
  $import: ./additional.py
  as: my
definitions:
  $let:
    nums: {$load: ./nums.yaml#/definitions/nums0}
  odds:
      $my.odds: {$my.get: nums}
  even:
      $my.evens: {$my.get: nums}

自分で定義したadditional.pyを読み込んで、myとしてimportしたものを利用する。additional.pyは以下の様な感じ(letで束縛された値を取り出す操作を提供し忘れたのでついでにそのための関数も定義しておく)。

additional.py

from zenmai.decorators import with_context


@with_context
def get(name, context):
    return getattr(context.scope, name)


def odds(nums):
    return [n for n in nums if n % 2 == 1]


def evens(nums):
    return [n for n in nums if n % 2 == 0]

結果は期待通りのもの

$ zenmai main1.yaml
definitions:
  odds:
  - 1
  - 3
  - 5
  even:
  - 2
  - 4
  - 6

ちょっとした怠け者の思考

まじめにpythonの関数を書くのも面倒ということもあるかもしれない?(あるかもしれない)。そんな時にちょっとした横着をする方法が提供されていても良いのかもしれない。危ないけれど。例えばこういう感じで使う。先程と同様の例。

main2.yaml

definitions:
  $let:
    nums: {$load: ./nums.yaml#/definitions/nums0}
  odds:
      $dynamic: |
        [n for n in nums if n % 2 == 1]
  even:
      $dynamic: |
        [n for n in nums if n % 2 == 0]

何だかんだでpythonの式を直接かけたりすると捗るということはある気がする。こういう機能も標準で用意しておくと便利かもしれない。。evalですね。 以下の様な関数を組み込みにしてしまうと上の例が動くようになります。

from zenmai.actions import load  # NOQA
from collections import ChainMap
from zenmai.decorators import with_context


def get_locals(scope, scopes=None):
    scopes = scopes or []
    scopes.append(scope.__dict__)
    if getattr(scope, "parent", None) is not None:
        return get_locals(scope.parent, scopes=scopes)
    else:
        return ChainMap(*scopes)


@with_context
def dynamic(transform, context):
    """teribble dangerous function"""
    local_values = get_locals(context.scope)
    r = eval(transform, {}, local_values)
    return r

組み込みの関数を定義するmoduleを変更したい場合には -m で直接moduleを指定できます(defaultはzenmai.actions)。例えば上のpythonのファイルをfns.pyとすると

$ zenmai -m ./fns.py main2.yaml
definitions:
  odds:
  - 1
  - 3
  - 5
  even:
  - 2
  - 4
  - 6

これはこれでありのような気がする。