pythonでサブクラスを定義するときにオプションを渡せるようにしてみる

そういえば、継承時にbase classの他にオプションを取る定義が書けるのわりと最初ビックリするけれどvalidなpythonのコード。

3.6なら__init_subclass__のフックを使うのが楽かもしれない。

たとえば、以下の様なコードを書いてみる。

  • 自身を継承したクラスをchildrenという変数に格納する
  • 継承時にnameというオプションを与えた時にはかわりにその名前を格納する

3.6の場合

3.6以降の場合

class A:
    children = set()

    def __init_subclass__(cls, name=None):
        name = name or cls.__name__
        cls.children.add(name)


class B(A):
    pass


class C(A, name="MaybeA"):
    pass


print(A.children)

# {'B', 'MaybeA'}

3.6以前の場合

メタクラスを使って対応することもできる。ただ自分自身を格納しないようにするのがちょっとトリッキー

class AMeta(type):
    def __new__(self, clsname, bases, attrs, name=None):
        instance = super().__new__(self, clsname, bases, attrs)
        if "children" not in instance.__dict__:
            instance.children.add(name or clsname)
        return instance


class A(metaclass=AMeta):
    children = set()


class B(A):
    pass


class C(A, name="MaybeA"):
    pass


print(A.children)

# {'B', 'MaybeA'}

emacs上でmozcを使ったときにC-nとC-pで変換候補中の移動をできるようにする

emacs上での日本語入力にmozcを使うようにしてみた。macではなくlinux上での環境の話。

通常の設定では以下だけなのだけれど(mozcをビルド後に mozc_emacs_helper が使えるようにしないとだめ)。

(require 'mozc)
(set-language-environment 'japanese)
(setq default-input-method 'japanese-mozc)

このままの場合には、変換候補中のカーソル移動は矢印キーに限定される。 個人的には、C-p, C-nemacsのバッファ上の通常のカーソル移動ではなく、変換候補のリスト上の移動になってほしかった。

advice

なので以下のようなadviceを追加した。

(defun advice:mozc-key-event-with-ctrl-key--with-ctrl (r)
  (cond ((and (not (null (cdr r))) (eq (cadr r) 'control) (null (cddr r)))
         (case (car r)
           (102 r) ; C-f
           (98 r) ; C-b
           (110 '(down)) ; C-n
           (112 '(up))  ; C-p
           (t r)
           ))
        (t r)))

(advice-add 'mozc-key-event-to-key-and-modifiers :filter-return 'advice:mozc-key-event-with-ctrl-key--with-ctrl)
;; (advice-remove 'mozc-key-event-to-key-and-modifiers 'mozc-key-event-with-ctrl-key)

最近は、advice.elではなくnadvice.elの方を使うっぽい。

EIN(Emacs IPython Notebook)を試してみる

2つrepositoryがあり後者の方がmelpaで入るもの。どうやら後者は前者のforkらしい。

install

package-installで入れる。

M-x package-install ein

注意点としてemacsのversionを上げないとうまくいかない模様。25に上げる必要があった。結構アグレッシブ。

how to use

使い方は以下を何処かに記述してから。

(require 'ein)
  1. jupyter-notebookを立ち上げる
  2. einからjupyterのserverにログイン
  3. notebookの一覧表示
  4. notebookを開く

jupyter-notebookを立ち上げる。

 $ jupyter-notebook
[I 21:40:56.465 NotebookApp] Serving notebooks from local directory: ~/venvs/my3/nbreversible/examples/00ipython
[I 21:40:56.465 NotebookApp] 0 active kernels
[I 21:40:56.465 NotebookApp] The Jupyter Notebook is running at: http://localhost:8888/?token=4bf5221877274cbb6d1efd9971e6b5999643d9c19f09449e
[I 21:40:56.465 NotebookApp] Use Control-C to stop this server and shut down all kernels (twice to skip confirmation).
[C 21:40:56.467 NotebookApp]

    Copy/paste this URL into your browser when you connect for the first time,
    to login with a token:
        http://localhost:8888/?token=4bf5221877274cbb6d1efd9971e6b5999643d9c19f09449e

ここでtokenの4bf5221877274cbb6d1efd9971e6b5999643d9c19f09449eをコピーしておく。

その後emacs上で M-x ein:notebooklist-login を実行しパスワードの箇所でコピーしておいたトークンを入力する。

ログインが成功したら M-x ein:notebooklist-open で一覧画面を開く。

Jupyter v5 Notebook list

 | [Home] |

[New Notebook] [Reload List] [Open In Browser]

Create New Notebooks Using Kernel: 
( ) Python 3

 [Open] [Stop] [Delete] : 00pretty.ipynb
 [Open] [Delete] : 00pretty.md
 [Open] [Delete] : 00pretty.py
 [Open] [Delete] : Makefile
 [Open] [Delete] : requirements.txt

---------- All Opened Notebooks ----------

[Open][Close] : *ein: http://127.0.0.1:8888/00pretty.ipynb*

雰囲気で使い方はわかると思う。OpenのところでEnterを押すとnotebookが開く。

with notebook

notebookを開いたら通常のnotebookとおおよそ同様の画面に辿り着く。こういうような。

In [1]

ここで最低限覚えて置かなければいけないのは以下の4(3)つ。

key 説明
C-c C-c 現在のcellを評価する
C-c C-a 現在のcellの上に新しいcellを作る
C-c C-b 現在のcellの下に新しいcellを作る
C-c C-k 現在のcellを削除する

詳しいkey-bindingはこのあたりに

変更の保存はもちろん C-x C-s

tab (worksheet)

ちょっとした作業にworksheetを行き来するのが楽なので便利。これは以下の様にして使う。

key 説明
C-c + 新しいworksheetの作成
C-c 1 worksheet1に移動
C-c 2 worksheet2に移動
C-c 3 worksheet3に移動
.. ..
C-c worksheetに移動
C-c - worksheetの削除

dictknifeのclickへの依存を無くした

dictknifeのclickへの依存を無くした。その過程で思ったことは記事にした。

まだpypiの方は更新していない。

変化

こういう操作をしたときに読み込まれるモジュールが少なくなった。

$ dictknife concat a.json b.json --dst x.json

これが

こうなった

不要なimportをlazyにしたい(ついでにちょっとしたsetupも呼びたい)

不要なimportをlazyにしたい場合がある。例えばdictknife.loadではフォーマットとしてjson,yaml,tomlをサポートしているのだけれど。 jsonだけで十分の処理でyamlやtomlをimportを行いたくない。

ふつうのlazy import

ふつうのlazy importとして関数の中でimportする方法がある。

foo.py

def use_json(data):
    import json
    return json.dumps(data)

例えばこういうコード(foo.py)は import foo されたタイミングではjsonがimportされずに、use_json()が使われたタイミングでimportされる。

reifyが便利

とは言え、関数の中で個別にimportしまくるというのも何だかそれはそれで微妙なのでもうちょっと楽な方法がないかなみたいな事を考えてみた。 reifyを使うと良い。

reifyというのはeval onceなpropertyのようなもの。一度実行した結果をキャッシュし続ける。

class reify(object):
    def __init__(self, wrapped):
        self.wrapped = wrapped
        try:
            self.__doc__ = wrapped.__doc__
        except:
            pass

    def __get__(self, inst, objtype=None):
        if inst is None:
            return self
        val = self.wrapped(inst)
        setattr(inst, self.wrapped.__name__, val)
        return val

pyramidというweb frameworkで見つけてから、とても便利で気に入っている。なので個人的に至る所で使っている(とは言えさすがにreifyのためだけにpyramidに依存したくないという気持ちは強い)。

reifyを使ってlazy module

reifyを使ったlazy importは、仮想的なmoduleを作って上げる感じ。moduleと言っても単なるsingleton object。

class Loading:
    @reify
    def json(self):
        import json
        return json

    @reify
    def yaml(self):
        import yaml
        return yaml

    @reify
    def toml(self):
        import toml
        return toml


m = Loading()  # singleton

このようにしてあげると、m.jsonm.yaml とアクセスしたときにはじめてmoduleがimportされる。

利用例

例えば、以下の様なコードにすると、tomlを使った場合にはyaml,jsonがimportされない。

00lazy.py

def run(loading):
    import sys
    data = {"person": {"name": "foo", "age": 20}}
    loading.dump(data, sys.stdout)
    sys.stdout.write("\n")
    sys.stdout.flush()


if __name__ == "__main__":
    import argparse
    parser = argparse.ArgumentParser()
    parser.add_argument("--format", default="json", choices=["json", "yaml", "toml"])

    args = parser.parse_args()
    loading = getattr(m, args.format)

    run(loading)

こういう感じ。

$ python 00lazy.py --format toml
[person]
age = 20
name = "foo"

補足1: setupも一緒に書けるので便利

例えば現状のyamlのoutputは以下の様になっている。

$ python 00lazy.py --format yaml
person: {age: 20, name: foo}

これを以下の様なコードを追加した後に実行する

--- 00lazy.py    2017-08-05 20:09:37.000000000 +0900
+++ 01lazy.py 2017-08-05 20:22:13.000000000 +0900
@@ -23,6 +23,13 @@
     @reify
     def yaml(self):
         import yaml
+        from functools import partial
+
+        class Dumper(yaml.Dumper):
+            def represent_mapping(self, tag, mapping, flow_style=False):
+                return super().represent_mapping(tag, mapping, flow_style=flow_style)
+
+        yaml.dump = partial(yaml.dump, Dumper=Dumper)
         return yaml
 
     @reify

defaultがblock styleになる。

$ python 01lazy.py --format yaml
person:
  age: 20
  name: foo

補足2: 本当に他のmoduleがimportされていないの?

importされているモジュールが何か確認する方法は幾つかある。例えば起動時に -v オプションをつけて結果をgrepしてみるだとか。

$ python -v 00lazy.py --format=toml 2>&1 | grep import | grep -P 'json|yaml|toml'
import 'toml' # <_frozen_importlib_external.SourceFileLoader object at 0x103210438>

個人的に作っているmoduleknifeというので可視化するのもありかも?

$ modulegraph --outfile=withtoml.dot 00lazy.py --format=toml
$ dot -Tpng withtoml.dot > withtoml.png

withtoml

argparseでsubcommandを作るためのユーティリティ

サブコマンドを作る時に何らかのライブラリに依存して良いならclickがオススメではあるけれど。 使いたくない場合もあったりする。そういう時にどうするとまだましになるかみたいな事を考えたりしていた。

argparseでのサブコマンド

argparseでのサブコマンドの定義の仕方は以下のような感じ。

def foo():
    print("foo")


def bar():
    print("bar")


if __name__ == "__main__":
    import argparse

    parser = argparse.ArgumentParser()
    subparsers = parser.add_subparsers(dest="subcommand")
    subparsers.required = True

    foo_parser = subparsers.add_parser("foo")
    foo_parser.set_defaults(fn=foo)

    bar_parser = subparsers.add_parser("bar")
    bar_parser.set_defaults(fn=bar)

    args = parser.parse_args()
    args.fn()

こうするとfooとbarが使える

$ python 00subcommands.py  -h
usage: 00subcommands.py [-h] {foo,bar} ...

positional arguments:
  {foo,bar}

optional arguments:
  -h, --help  show this help message and exit

引数の数が違う場合

引数の数が違う場合にちょっと大変になる。

例えばこういう風にfooではx,yでbarにはx,y,zの引数が用意されていた場合など。

    foo_parser = subparsers.add_parser("foo")
    foo_parser.add_argument("-x")
    foo_parser.add_argument("-y")
    foo_parser.set_defaults(fn=foo)

    bar_parser = subparsers.add_parser("bar")
    bar_parser.add_argument("-x")
    bar_parser.add_argument("-y")
    bar_parser.add_argument("-z")
    bar_parser.set_defaults(fn=bar)

if文とかで頑張らないとだめ。

if args.fn == foo:
    return foo(args.x, args.y)
elif args.fn == bar:
    return bar(args.x, args.y, args.z)

ユーティリティ

以下の様なユーティリティを書くとマシかもしれない。面倒なのは parser.parse_args() で手に入った Namespace オブジェクトの取扱い。 add_argumentで返されるactionのdestの値がシステム名的なものなのでそれを利用する関数を登録する。

クラスなどにしてラップしても良いけれど。contextmanagerを使うのが楽な気がした。

import contextlib

@contextlib.contextmanager
def subparser(subparsers, fn, *args, **kwargs):
    parser = subparsers.add_parser(fn.__name__, *args, **kwargs)
    dests = []
    arrived = set()

    def add_argument(*args, **kwargs):
        ac = parser.add_argument(*args, **kwargs)
        if ac.dest not in arrived:
            arrived.add(ac.dest)
            dests.append(ac.dest)
        return ac

    yield add_argument

    def run(args):
        return fn(**{name: getattr(args, name) for name in dests})

    parser.set_defaults(fn=run)

こんな感じで使う。

def foo(*, x, y):
    print("foo", x, y)


def bar(*, x, y, z):
    print("bar", x, y, z)


def main():
    import argparse
    parser = argparse.ArgumentParser()
    parser.add_subparsers
    parser = argparse.ArgumentParser()
    subparsers = parser.add_subparsers(dest="subcommand")
    subparsers.required = True

    with subparser(subparsers, foo) as add_argument:
        add_argument("-x", default=None)
        add_argument("-y", default=None)

    with subparser(subparsers, bar) as add_argument:
        add_argument("-x", default=None)
        add_argument("-y", default=None)
        add_argument("-z", default=None)

    args = parser.parse_args()
    return args.fn(args)
$ python 02* foo -h
usage: 02withutil.py foo [-h] [-x X] [-y Y]

optional arguments:
  -h, --help  show this help message and exit
  -x X
  -y Y
$ python 02* bar -h
usage: 02withutil.py bar [-h] [-x X] [-y Y] [-z Z]

optional arguments:
  -h, --help  show this help message and exit
  -x X
  -y Y
  -z Z

# 引数多すぎ
$ python 02* foo -x 10 -y 20 -z 30
usage: 02withutil.py [-h] {foo,bar} ...
02withutil.py: error: unrecognized arguments: -z 30

# ok
$ python 02* foo -x 10 -y 20
foo 10 20

補足

foo,barの引数の定義にkeyword only argumentsを使うと良い。引数の数が会わない時のエラーがわかりやすいので。

例えば、fooにも -z オプションを追加してしまった場合には以下の様になる。

$ python 02* foo
Traceback (most recent call last):
  File "02withutil.py", line 56, in <module>
    main()
  File "02withutil.py", line 52, in main
    return args.fn(args)
  File "02withutil.py", line 20, in run
    return fn(**{name: getattr(args, name) for name in dests})
TypeError: foo() got an unexpected keyword argument 'z'

あー。zオプションなんて定義していなかったと気付ける。あと引数の順序を気にする必要がなくなるし。

mongodbのsparse indexと `{$ne: null}` 的なquery

疎なfield(ほとんどがnullでごく少数のdocumentだけ値が入る)があったとして。 これに対するindexはsparse indexで十分じゃないかなと思っていたのだけれど。

試してみたらだめなqueryだったという話。

explainの見方

その前にexplainの見方をメモ

db.<collection>.find(q).explain("executionStats")
  • winningPlanが採用されたもの(rejectedPlanが非採用)
  • 使われているindexはinpuStageがIXSCANの時のindexName(IXSCAN=index scan, COLLSCAN=collection scan)
  • totalDocsExaminedはindexで絞り込まれたdocument数
  • nReturnedは返されたdocument数

要はIXSCANになっていればOK。

sparse index

部分関数みたいないもの。sparse indexとdense indexで対応している。通常のindexがdense index。 値が入っていない部分に関してはindexを作成しないというもの。indexのサイズを小さくできることが利点(逆に言うとsparse indexにしたからといって早くなることはない。メモリーに乗り切らなくなるみたいなすごいきわどい状況の話でなければ)。

例えばKというfieldがありこれがnull等の場合に貼るindexを考えてみたときに以下の様な形になる。

value dense index sparse index
{K: 1} true true
{K: null} true true
{} true false

fieldが存在しない場合にはindexは生成されないけれど、nullの場合は作成される。 (ちなみにunique:trueを付けたときにsparse:trueを付けると、該当のfieldを持たないdocumentはunique制約の対象外にできるみたいな話もあったりする)

{$ne: null} もしくは {$exists: true}

sparse index

ある値が存在するのならばというqueryは {$exists: true} という形になるし。これはindexがあるかで判定ができる。 一方で {$exists: false} はindexの対象外の値を見ているのでindexだけをみて判断できない。

sparse indexが賢いなら、全部nullのfieldになっていた場合に、 {$ne: null} でもなんか良い感じにindex使われないかなと思ったけれど。そんなことはなかった。 考えてみたら当たり前で null以外は値を持たないも含んでしまうのでsparse indexではまかないきれない範囲になっている。

代わりに {$exists: true} とのandを取ってあげるとindexを使ってくれる。

dense index

一方dense indexというか通常のindexは全部の情報を持っているので {$ne: null} だけでindexが効く。 考えてみれば当たり前の話ではあるけれど。

実験

entriesというidとnameとmarkedAtだけで作られたdocumentを格納するcollectionを作成。 markedAtは1つだけに値を入れる(それ以外はnullかunset)。 markedAtにindexを貼ってみて上手くindexが使われるか調べる。

db.createCollection("entries");

db.entries.insert([
  {"_id": ObjectId(), "name": "foo"},
  {"_id": ObjectId(), "name": "bar"},
  {"_id": ObjectId(), "name": "boo"},
]);

// create sparse index
db.entries.createIndex({"markedAt": 1}, {sparse: true});

// markedAt 1 and other items don't have markedAt
db.entries.updateOne({}, {$set: {"markedAt": ISODate()}})
db.entries.find({markedAt: {$exists: true}}).explain("executionStats") // IXSCAN

// markedAt 1 and other items markedAt are null
db.entries.updateMany({}, {$set: {"markedAt": null}});
db.entries.updateOne({}, {$set: {"markedAt": ISODate()}})
db.entries.find({markedAt: {$ne: null}}).explain("executionStats") // COLLSCAN
db.entries.find({$and: [{markedAt: {$ne: null}}, {markedAt: {$exists: true}}]}).explain("executionStats") // IXSCAN

// create dense index
db.entries.dropIndex("markedAt_1");
db.entries.createIndex({"markedAt": 1});


// markedAt 1 and other items markedAt are null
db.entries.find({markedAt: {$ne: null}}).explain("executionStats") // IXSCAN

// markedAt 1 and other items don't have markedAt
db.entries.updateMany({}, {$unset: {"markedAt": 1}});
db.entries.updateOne({}, {$set: {"markedAt": ISODate()}})
db.entries.find({markedAt: {$exists: false}}).explain("executionStats") // IXSCAN

sparse indexにする時には、$exists: true を忘れずにという話