はてだBlog(仮称)

私的なブログど真ん中のつもりでしたが、気づけばWebサイト系のアプリケーション開発周りで感じたこと寄りの自分メモなどをつれづれ述べています。2020年6月現在、Elasticsearch、pandas、CMSなどに関する話題が多めです。...ですが、だんだんとより私的なプログラムのスニペット置き場になりつつあります。ブログで述べている内容は所属組織で販売している製品などに関するものではなく、また所属する組織の見解を代表するものではありません。

Pandasのapply関連の書きっぷりバリエーションと処理時間の雑な傾向確認

PandasでDataFrameのカラムAの値とBの値を結合して、新たにC列を作りたい...てなことがよくあると思います。

普段は自分の中で可読性が高いと思っているapply系の手グセで記述しているのですが、まれに、他の言語やフレームワークに置き換えるまではいかないものの、またガチのチューニングは不要なものの、2、3ステップの気の利いたタイピングで高速化されるならそうしたいという時があります。

... というときに、何が良いんじゃろうかということで雑に確認してみた...という記事です。

確認用コード

↓ Gistに貼りました。また、雑な確認の雑な結論としては、明らかに遅いなと感じたら、DataFrameの中にある(と思われる)リストデータの存在をイメージした記法を意識した演算にするとかなり高速化される場合もあるな、というところです。

Pandasのapply関連の書きっぷりバリエーションと処理時間の雑な傾向確認

実行結果 : iMac Late 2014 ( 4GHz Intel Core i7 )

ex1〜 は全て同じ要件ではないので単純比較はできないのですが、特に ex5、ex6、ex12の見比べが注目でしょうか。

ex1
    df[colname] = df['a'].apply(lambda x: foo.get(x))
CPU times: user 438 ms, sys: 1.31 ms, total: 439 ms
Wall time: 440 ms
                    a                   b                          ex1
0  0.3790985254337701  0.3790985254337701  {'b': '0.3790985254337701'}
1   0.567098167179614   0.567098167179614   {'b': '0.567098167179614'}
2  0.5955925126571219  0.5955925126571219  {'b': '0.5955925126571219'}

---------

ex2
    df[colname] = df[['a']].apply(lambda s: foo.get(s['a']), axis=1)
CPU times: user 10.6 s, sys: 15.4 ms, total: 10.6 s
Wall time: 10.6 s
                    a                   b                          ex2
0  0.3790985254337701  0.3790985254337701  {'b': '0.3790985254337701'}
1   0.567098167179614   0.567098167179614   {'b': '0.567098167179614'}
2  0.5955925126571219  0.5955925126571219  {'b': '0.5955925126571219'}

---------

ex3
        df[colname + i] = df[i].apply(lambda x: foo.get(x))
CPU times: user 895 ms, sys: 1.08 ms, total: 896 ms
Wall time: 897 ms
                    a                   b                         ex3a                         ex3b
0  0.3790985254337701  0.3790985254337701  {'b': '0.3790985254337701'}  {'b': '0.3790985254337701'}
1   0.567098167179614   0.567098167179614   {'b': '0.567098167179614'}   {'b': '0.567098167179614'}
2  0.5955925126571219  0.5955925126571219  {'b': '0.5955925126571219'}  {'b': '0.5955925126571219'}

---------

ex4
        df.loc[:, colname + i] = df[i].apply(lambda x: foo.get(x))
CPU times: user 1.07 s, sys: 15.2 ms, total: 1.09 s
Wall time: 1.09 s
                    a                   b                         ex4a                         ex4b
0  0.3790985254337701  0.3790985254337701  {'b': '0.3790985254337701'}  {'b': '0.3790985254337701'}
1   0.567098167179614   0.567098167179614   {'b': '0.567098167179614'}   {'b': '0.567098167179614'}
2  0.5955925126571219  0.5955925126571219  {'b': '0.5955925126571219'}  {'b': '0.5955925126571219'}

---------

ex5
    df[colname] = df.apply(lambda s: s['a'] + s['b'], axis=1)
CPU times: user 19 s, sys: 25.8 ms, total: 19 s
Wall time: 19 s
                    a                   b                                   ex5
0  0.3790985254337701  0.3790985254337701  0.37909852543377010.3790985254337701
1   0.567098167179614   0.567098167179614    0.5670981671796140.567098167179614
2  0.5955925126571219  0.5955925126571219  0.59559251265712190.5955925126571219

---------

ex6
    df[colname] = [s['a'][i] + s['b'][i] for i in range(l)]
CPU times: user 236 ms, sys: 20.3 ms, total: 257 ms
Wall time: 260 ms
                    a                   b                                   ex6
0  0.3790985254337701  0.3790985254337701  0.37909852543377010.3790985254337701
1   0.567098167179614   0.567098167179614    0.5670981671796140.567098167179614
2  0.5955925126571219  0.5955925126571219  0.59559251265712190.5955925126571219

---------

ex7
    df[colname] = [i['a'] + i['b'] for i in s]
CPU times: user 2.4 s, sys: 51.7 ms, total: 2.45 s
Wall time: 2.46 s
                    a                   b                                   ex7
0  0.3790985254337701  0.3790985254337701  0.37909852543377010.3790985254337701
1   0.567098167179614   0.567098167179614    0.5670981671796140.567098167179614
2  0.5955925126571219  0.5955925126571219  0.59559251265712190.5955925126571219

---------

ex8
    df[colname] = df.apply(abfunc1, axis=1)
CPU times: user 19 s, sys: 28.8 ms, total: 19 s
Wall time: 19 s
                    a                   b                                   ex8
0  0.3790985254337701  0.3790985254337701  0.37909852543377010.3790985254337701
1   0.567098167179614   0.567098167179614    0.5670981671796140.567098167179614
2  0.5955925126571219  0.5955925126571219  0.59559251265712190.5955925126571219

---------

ex9
    df[colname] = v_abfunc2(s['a'], s['b'])
CPU times: user 840 ms, sys: 274 ms, total: 1.11 s
Wall time: 1.17 s
                    a                   b                                   ex9
0  0.3790985254337701  0.3790985254337701  0.37909852543377010.3790985254337701
1   0.567098167179614   0.567098167179614    0.5670981671796140.567098167179614
2  0.5955925126571219  0.5955925126571219  0.59559251265712190.5955925126571219

---------

ex10
    df[colname] = v_abfunc2(**s)
CPU times: user 1.53 s, sys: 84.1 ms, total: 1.62 s
Wall time: 1.63 s
                    a                   b                                  ex10
0  0.3790985254337701  0.3790985254337701  0.37909852543377010.3790985254337701
1   0.567098167179614   0.567098167179614    0.5670981671796140.567098167179614
2  0.5955925126571219  0.5955925126571219  0.59559251265712190.5955925126571219

---------

ex11
    df[colname] = df['a'] + df['b']
CPU times: user 80.8 ms, sys: 1.06 ms, total: 81.9 ms
Wall time: 82.2 ms
                    a                   b                                  ex11
0  0.3790985254337701  0.3790985254337701  0.37909852543377010.3790985254337701
1   0.567098167179614   0.567098167179614    0.5670981671796140.567098167179614
2  0.5955925126571219  0.5955925126571219  0.59559251265712190.5955925126571219

---------

ex12
    df[colname] = df['a'] + ',' + df['b']
CPU times: user 152 ms, sys: 9.18 ms, total: 162 ms
Wall time: 171 ms
                    a                   b                                   ex12
0  0.3790985254337701  0.3790985254337701  0.3790985254337701,0.3790985254337701
1   0.567098167179614   0.567098167179614    0.567098167179614,0.567098167179614
2  0.5955925126571219  0.5955925126571219  0.5955925126571219,0.5955925126571219

---------

ex13
    df[colname] = df['a'].str.cat([df['b'], df['b']], sep=',')
CPU times: user 475 ms, sys: 13.9 ms, total: 489 ms
Wall time: 491 ms
                    a                   b                                               ex13
0  0.3790985254337701  0.3790985254337701  0.3790985254337701,0.3790985254337701,0.379098...
1   0.567098167179614   0.567098167179614  0.567098167179614,0.567098167179614,0.56709816...
2  0.5955925126571219  0.5955925126571219  0.5955925126571219,0.5955925126571219,0.595592...


余談(M1 macにて)

なんとなくですが、母艦のiMac(5年ものでここのところファンがうるさいが、奮発したのでそれなりのスペックと自負)と M1 mac book Air(iMacの故障に備えたつなぎのつもり)で処理時間を比較してみました。

厳密なベンチマークでもなく、条件も同一ではありませんし、複数回の計測もしていませんが、M1 mac book Air の5倍の圧勝で、うれしいようなカナシイような... (この例であれば、五分五分かと思っていましたが...)

1) iMac Late 2014 (4GHz Intel Core i7 メモリ32GB )



---------

ex5
    df[colname] = df.apply(lambda s: s['a'] + s['b'], axis=1)
CPU times: user 19 s, sys: 25.8 ms, total: 19 s
Wall time: 19 s
                    a                   b                                   ex5
0  0.3790985254337701  0.3790985254337701  0.37909852543377010.3790985254337701
1   0.567098167179614   0.567098167179614    0.5670981671796140.567098167179614
2  0.5955925126571219  0.5955925126571219  0.59559251265712190.5955925126571219

---------

2) mac book Air (M1, 2020) メモリ16GB


※  MiniForge

--------

ex5
    df[colname] = df.apply(lambda s: s['a'] + s['b'], axis=1)
CPU times: user 4.12 s, sys: 42.9 ms, total: 4.17 s
Wall time: 4.17 s
                    a                   b                                   ex5
0  0.3790985254337701  0.3790985254337701  0.37909852543377010.3790985254337701
1   0.567098167179614   0.567098167179614    0.5670981671796140.567098167179614
2  0.5955925126571219  0.5955925126571219  0.59559251265712190.5955925126571219

---------

JavaScriptの非同期処理に入門するためのPromise、async、awaitのつまみ食い

自分のために状況証拠的に学ぶJavaScriptのPromise、async、awaitメモです。

同じような境遇の方にはなんらかジャンプ台としてお役に立てるかもネと思って、まとめております。

もっと雑に分量少なめでまとめたものはこちら↓です。

itdepends.hateblo.jp

明らかなウソはないように極力努めましたが、お気に召せばうまくご参考いただきつつ、のちのち末尾の参考書籍・リンク等から公式や体系的な情報を得るようにお願いいたします。

プロローグ:Promiseとはなんぞや

Promise/Fureture(Deferなども同類と思われる。以下、代表して単にPromiseと表記)の歴史を紐解くところは正しく説明できる自信がないので行いませんが、おおよそ次のような背景を思い描いておくとありがたみが実感しやすいです。

  • Promise/Future(Deferなども同類と思われる。以下、単にPromise)は、非同期処理のデザインパターン
  • 非同期処理を同期処理風のシンタックスにみせかけるようにしてくれる便利な「何か」
  • 多くのプログラミング言語で標準の構文として取り入られつつある
  • JavaScriptでは、併せて語れることの多い、async、awaitはPromiseを下敷きにした頻出イディオムのさらなるシンタックスシュガー
  • JavaScriptの著名なライブラリのうち、位置付けからすると非同期(並行での実行)が望ましいような関数・メソッドは、処理結果そのものではなく、Promiseを戻すようにデザインされたものが増えている

Promiseの良いところ

非同期処理のコーディングって?そもそもどうなのという話

時間のかかる処理を裏街道で行いつつ、本筋をしばらく進めて最後に美味しいところををかっさらうといった、非同期処理の良さを享受したいというシーンはしばしば発生します。

ここで、ひとまず軽い気持ちで、次のような非同期の処理を含む関数を使ったロジック(注:JavaScript風の擬似コード)を考えます。

result = 非同期の処理P(arg,arg2,arg3) //・・・(1)
何か別の処理A // 同期的な処理(つまり非同期ではない処理)
何か別の処理B // 同期的な処理(つまり非同期ではない処理)
何か別の処理C // 同期的な処理(つまり非同期ではない処理)
処理A、Bなどやresultを使った後続の処理や例外処理 // ・・・(2)
あるいはresultをリターンして、呼び出し側で使う。// ・・・(3)

しかし、(擬似コードにこういう議論をするのはやや妙な話ですが)実際は上記の記述では、期待したとおりに動作しません*1

上記のPのような非同期処理は並行か並列かスレッドかプロセスかタイムシェアリングかはともかく、確かに(1)の記述の時点で実行可能にはなるのですが、非同期処理なので当然「即時完了」されるわけではなく、評価はプログラム記述上の次の行に進んでいくとともに、resultに期待したような値がセットされることは保証されません。

結果、(2)、(3)のあたりでエラーになったり、その時点では解決/未解決のある種不安定な値を扱ってしまうことになります。

コールバック関数引き渡しスタイルでの実現とコールバック地獄の出現

Promise登場以前のJavaScriptでの原始からの作法としては、非同期の処理Pの引数にコールバック関数を渡す方法で非同期処理の要件を取り扱うことができます。

非同期処理を扱う関数の戻り値を使うのではなく、非同期処理の結果を使う関数を非同期処理に渡して「いってこい」するのです。

しかしながら、要件が少しでも複雑になるとコールバック関数のネストが深くなり、可読性が著しく落ちること、いわゆるコールバック地獄を引き起こすことになります。

そこで可読性アップのために登場するのが、Promiseです。

  • Promiseデザインパターンでは、非同期処理Pが、Pの本来の処理Xとともに、Xで得られた結果の値と後述のおまじないをかけあわせたPromiseをリターンするようにする。
  • 呼び出し側のメインの処理フローでは、戻ってきたPromiseをしばらく引き回しつつ、Promiseオブジェクトの約束事として存在するthenメソッドに該当の非同期処理が完了した際のロジックを配置できる。

呼び出す方と呼ばれる方の「お約束」のもとのみんなが幸せになる仕組みです。

Promiseのある世界

Promiseにより、Promiseが存在しない例に比べると、、極力ネストが深くなることを避けた、また、ソースコード上の登場順と処理の順序がおおよそ一致する次のようなイメージの記述が可能になります。

/* 以下は擬似JavaScriptです。 */

const promise = 非同期の処理P2(arg,arg2,arg3) //・・・(1) ※P2はPromiseを戻すようにデザインされている。
// この時点では、promiseにはPromiseオブジェクトが入っている。
何か別の処理A // 同期的な処理(つまり非同期ではない処理)
何か別の処理B // 同期的な処理(つまり非同期ではない処理)
何か別の処理C // 同期的な処理(つまり非同期ではない処理)
// Promiseオブジェクトに「then」メソッドを呼び出す記述をすると、このタイミングで処理P2を同期(待ち合わせ)するように動作させることができる。
promise.then(value=>{ 
    //P2の処理が完了した場合に、このブロックが処理される。
   処理Aの結果や(1)の処理の本来の処理結果(value)や resultを使った後続の処理や例外処理 // ・・・(2)
} )

なお、thenメソッドはメソッドチェーンで記述できるので、複数の非同期処理やある種の待ち合わせ相当を安全に記述することができます。

(1)Promiseを戻すためのJavaScriptでのおまじない

前項で「Promiseをリターンするように」と言いました。

Promise自体を自前実装するのはかなりめんどくさいと思いますが、2021年のいまや、JavaScriptでは(JavaScriptが動作する「まともな環境」では)Promiseは標準サポートの範囲と前置きなく言っても良い状況だと思います。

そして、おおよそ次の要領で非同期の処理をラップしてやることで、Promiseを戻すように出来ます。

function 非同期の処理P2(引数){
    なんらかの処理
    return Promise((resolve,reject)=>{
        // Promise未使用の非同期の処理 ※さらにコールバック関数を引数にとる処理となることもある
        なんらかの処理                        
        resolve(非同期の処理の結果得られる値);  // resolve(XMLHttpRequest#responseTextを指す変数) のような記載になることが多い。
    }

}

なお、ここでは作法の説明は深追いしません。理解は無用という意味ではありません。ただ、私のような使う側に重心があるヒトは、上記を念頭におきつつも、Promiseを戻すようにデザインされた外部のライブラリにまずは慣れて実感を得るという習熟の仕方もあると思います。

(ちなみに、後述のasyncキーワードがこの「Promiseを戻す」仕組みを成立させるためのさらなるシンタックスシュガーになっています。asyncを見慣れてからからここに戻ってくるというのがおすすめです。)

なお、同じ理由で、ここまでもそうでしたが、以降も正常系の話を中心に記載することにします。

当然、異常系についても適切にハンドリングされるべきですので、ここではググリングのためのキーワードを列挙しておくに留めます。

  • Promise#reject
  • Promise#catch
  • Promise#finally
  • await式導入時のtryとcatch

(2) async、awaitの登場

なぜawaitか、そしてasyncか(Promise#thenでも満足できないヒトのために...)

Promise導入により、また、「then」メソッドを用いることで、非同期処理を含むロジックを同期的な見栄えのスタイルでコーディングできることの雰囲気を示しました。

最初は感心するものの、そのうち、これこれの場合、実際どうやって記述していけばよいのだろうという疑問がいくつか湧いてきます。

  • 非同期の処理を複数実行して、これら全てがそろったら得られた値を全て組み合わせてなんらか次の演算を行いたい。 (thenのメソッドチェーンでは非同期処理を直列化できそうだが、全部揃ったらというのには向かないかも...)
  • 時間を要する複数の非同期の処理はそれぞれで開始しておきたいが、結果をまとめる際には処理Aの結果を先に使って次に処理Bの結果を使うという順序性を制御したい。
  • エラーが発生した場合、特に複数の非同期の処理を実行している中で、thenにごにょごにょしていくことでエラーハンドリングを行うにはどうしたら良い?(うまくやりきる自身がない)。

他にも、コールバック地獄こそ回避されそうですが、Promise#thenの中で一方通行のコールバック関数を渡す形はさけられず、ロジックの最終の出口めがけてthenのメソッドチェーンが連なり、少し窮屈になりそうな予感もあります(し、実際そうです)。

つまるところ、もっと直感的にしたい、普段から見慣れているスタイルでロジック記述したいな〜と欲が出てくるのです。

そして、こういったところをより直感的なシンタックスでプログラム実現、制御できるための仕掛けとして、asyncやawaitが登場します。

async

非同期処理を呼び出し側で取り扱いやすくするためのPromiseですが、一方で、非同期処理側からすると、元からある「非Promiseな非同期処理」自体をPromiseの作法に合わせるために、return new Promise(xxx,yyy)でラッピングしてしてやる必要があります。

Promiseをnewするブロックって、ネストはさして深くないものの、その割に分かりにくいと感じません? 

asyncキーワードは、その関数がPromiseオブジェクトを戻すようにラッピングして(ある種のデコレートをして)くれるシンタックスシュガーです。

具体例としては、

async function 内部ロジックによらずPromiseを戻す関数(arg) {
    return ("解決したい値" + arg);
}

function 内部ロジックによらずPromiseを戻す関数(arg) {
    return new Promise((resolve,reject) => {
           resolve("解決したい値" + arg);
    } 
}

シンタックスシュガーです。

asyncは、別の非同期処理を含めやりたいこと自体を素直に関数化したロジックを最初に記述しておき、そこにasyncキーワードを付けるだけで、関数側に手間をかけずに、呼び出し側に「Promise」の利便性を提供できるようになるおまじないです。

await

await(await式)はそれを適用したPromise*2オブジェクトが「解決されるまで待ち」ます。

すごく雑に言うと、「then」のメソッドチェーンを作らなくても、「then」のご利益にあやかることができます。

Promise#thenメソッドでの記述スタイルは不要です。

また、「解決されるまで待つ」ので、戻り値は、Promiseが抱きかかえて抽象化してくれている非同期処理の処理結果の戻り値になります。 そうです。Promise相手とはなりますが、関数の戻り値を得て、それを次ステップの処理に利用するという、「普通」のスタイルのコーディングが可能になります。

例えば、Promiseオブジェクトが格納されている変数をprmsとすると、

const r = await prms;

で、rに処理結果が入ってきます。

Promiseを戻す非同期処理の関数の実際の処理結果を実際に使いたいタイミングで、該当のPromiseを格納した変数にawaitを適用すると、非同期処理の最終的な処理結果を戻り値として確保できます。

ちなみに、awaitはそれを適用したPromiseが「解決されるまで待つ」のですが、実際は、当該関数の評価もawait式が現れたところで一旦ストップするようです。

そのPromiseのインスタンスがFulfilledになるまで待つだけではなくて、とにかくそこで待つのです。

ここで、await式が現れると、その次のステップ以降は、評価が進んでいきません。本当にストップするみたいです。

これは、私のようなヒトの想像する挙動とある意味一致するので、それ自体は都合が良いことかと思います。

もちろん、この関数自体が自分が呼び出した非同期関数の終了を待つのは良いのですが、当該関数をさらに呼び出す側も待たされてしまうと、呼び出し元のプログラム全体が無用に停止したかのように振る舞ってしまうことになります。

それはそれで困るよね、ということで、awaitを使っているロジックを含む関数は、asyncを付けて宣言(つまり当該関数自体はPromiseを戻すように)しなさいというのが、言語仕様として規定されています。

深く考えずにawaitをおまじない的に適用した際に、asyncがないぞ!と、しばしばコンパイルエラーになります。 しかし、背景としてはこういうことだったりしますし、エラーでの指摘に従い、雰囲気でasyncを付けちゃって臭いものにフタをした気持ちになりますが、それ自体は「そうするべきもの」なので、なんら後ろめたくない話です。

(3) 実際の非同期処理制御の(筆者がよく出くわす)パターンのまとめ

講釈は以上でおわり、さいごに、非同期処理制御の(筆者がよく出くわす)パターンの早見例をまとめて記載します。

なお、以下の例のHOGE()、HOGE1()、...はPromiseを返す関数です。

サンプル...というほどのコード量ではありませんが、Node.jsのv12.16で確認しています。

例1. 非同期処理が処理するまで待つ

const rslt = await HOGE()

HOGE()が非同期処理であり(かつPromiseを戻してくれる場合)、その一方、今回の用途では終了を待った方が良いということであれば、この記述でOKです。

また、複数の非同期処理を直列化したいなら、

const rslt1 = await HOGE1()
const rslt2 = await HOGE2(rslt1)

のようにすれば良いです。

例2. IO待ちなどが発生する非同期処理をスタートさせつつIO待ちの間は他の処理を進めるようにしたい

これが一番多い例だと思いますが、1つの非同期処理を動かしておきながら、メインの同期的な処理を実施して、両方が揃った後に、次の非同期処理をしたいという例です。

await式により、必要なタイミングで処理を合流させることができそうです。

const prms1 = HOGE1()
const f1 = FUGA2()
const f2 = FUGA3()
const prms2 = await HOGE2(f1,f2,await prms1)

例3. 複数の非同期処理の実行の待ち合わせ(複数の非同期処理を開始した上で、それら全てが終了するのを待つ)

非同期処理が重要になるケースでは、順次は複数(しかも2つ以上)の処理をスタートさせておき、最後にそれらの全てが出揃ってから、全ての結果を統合して利用したいというケースが非常に多いと思います。

このような場合は、「Promise.all(Promiseオブジェクトの配列)」を使うことで、全部がでそろうまで待ち合わせが可能となります。 (また、awaitを使うとそれの見通しが非常に良くなりますので定型句として使えると思います。)*3

const tasks = [] 
tasks.push(HOGE1())
tasks.push(HOGE2())
tasks.push(HOGE3())
const rslts = await Promise.all(tasks)
 // rsltsは、tasksの配列の順番の処理の戻り値の配列。

/////ここでは説明自体の分かりやすさのため、promise1、promise2...のように直接初期化していますが、実際はtasksに
/////様々な並列実行させたい処理を抱えるPromiseを配列にappendするスタイルになると思います。

注目:上記の例では、rsltsに、HOGE1、2、3の戻り値の値が配列のこの順序でセットされます。

例4. 複数の(非同期処理の)並列実行のうち最初に完了したものを使う

await Promise.allは、「全ての終了を待つ」ですが、await Promise.raceは、どれか一つ完了するまで待つです。

浮気性な要件にはぴったりです。

const promise1 = HOGE1()
const promise2 = HOGE2()
const promise3 = HOGE3()

tasks = [promise1,promise2,promise3]
const rsltX = await Promise.race(tasks)
// rsltXには、HOGE1、HOGE2、HOGE3のうち最初に処理が完了したものの戻り値が入る。

付録(参考リンク/参考書籍)

promisesaplus.com

azu.github.io

developer.mozilla.org

developer.mozilla.org

Node.jsデザインパターン 第2版

qiita.com

*1:よりやっかいな話として、(1)が処理A、Bなどに比べて相対的に早く全てのの処理を完了した場合、一見期待したとおりに動作したように見えることもしばしば...

*2:正確には、Promiseでなくとも、「thenable」であれば良いようですが、ここでは触れません。

*3:Promise.allはawait、async以前から存在するPromiseデザインパターン側のメソッドですが、2021年においては、awaitとペアで覚えておくのが分かりやすかろうというのが筆者の感触でしたので、ここでは、await Promise.all(hogehoge)までを定型句としてご紹介しています。

時間がない人のための JavaScriptのPromise、await/asyncのシンタックスのつかみ重視の入門メモ

はじめに

JavaScriptのPromiseとawait(とasync)による非同期処理と待ち合わせのシンタックス入門メモです。

次の記事の姉妹記事です。 itdepends.hateblo.jp

非同期処理を使いたい背景など

JavaScriptを用いた要件で、非同期処理を織り交ぜて時間のかかる処理を並べて処理したり、IO待ちによるユーザーのストレスを軽減するためにsetTimeoutでコールバック関数で処理をさせる、というのはしばしば出くわすと思います。

自分で非同期処理を含む関数を実装することになるケースも多いでしょうが、サードパーティなどからライブラリ提供されるものを使いこなしてくださいというオーダーに従って、ライブラリのおおよその構造を理解しつつも、これを使ったビジネスロジックにできるだけ集中したいということもあるでしょう。

ここで、このサードパーティ提供の非同期処理の関数/APIは、「(君もよく知っている例の)Promise」を返してくるから簡単だよねと言われたりもします。

ともかく、非同期処理は非同期というぐらいなので、いつ応答が戻ってくるかは分かりません。Worker型で投げっぱなしで良いならかまいませんが、非同期処理が完了した際に得られるデータを使って、最終アウトプットの仕上げを行うというのが普通の要件でしょう。

例えば次の図のような例の要件は、世の中「あるある」の一例でしょうか。

f:id:azotar:20210105235730p:plain

非同期処理は難しい(処理モデルの理解もさることながら、言語のシンタックスのトリックとしても) → 救世主 Promiseとawait登場

やってみれば分かるのですが、手続き系のプログラミング言語では、このような例程度の非同期処理でも、スレッド(シングルスレッドの場合も含む)、プロセス、並列、並行といったことをある程度イメージしながら、テキストエディタで見たときに、上の方から処理されるような プログラムの記法で表現するのは意外に難しいです。

言語の提供される文法でどのように表記したら良いのか、思いの他パズル的な記法が求められることになります。いわゆるコールバック地獄ですね。

.... というところなのですが、2021年の現在においては、JavaScriptについても、Promiseとawait(とasync)というしかけにより、非同期処理に不慣れな方でも、少なくともawaitとPromiseの関係をつかんでおけば、先人が悩ませたこの難題から少しでも楽になれるというのが現状です*1

ということで、Promiseをリターンしてくれるサードパーティの関数とそれを待ち合わせるawait式のほぼミニマムなプログラム記述例(冒頭の挿絵のイメージとも対応)は以下のとおりです。

await記述例

// 非同期処理Xは、Promiseをリターンする標準あるいはサードパーティ等から提供のライブラリ
//  → fetchなどをイメージしてください。

async function main(){ 
    // ↑ 注:今の時点では、await式を適用するロジックを含む関数には「async」をおまじないとしてつけておく作法だと理解しておくことで良い。

    const i = 処理A();
    const promise = 非同期処理X(i,その他の引数など); //処理は完了していないが、Promiseのオブジェクトが戻ってきて、ロジックの評価が次ステップ以降に進んでいく。
    const j = 処理B(i);
    const k = 処理C(j);
    const val = await promise; // 「await Promiseオブジェクト」という記法でこのPromiseをリターンした関数の非同期処理の実際の戻り値が得られるまで、待ち合わせするよう動作する。
    //前ステップでのawait式による待ち合わせにより、処理Dは、非同期処理Xが完了するまでは処理開始されない(させない)
    const foo = 処理D(k,val);
    return foo;

    /*
     *  ここでは「べからず」のみ伝えますが、
     *  非同期処理Xでは、処理B、処理Cで値を変更するような変数を使わないようにしてください。
     */
}

main();

// node.js のv12で確認しています。

冒頭の挿絵程度であれば、たったこれだけです。

Promiseが無かった頃、await、asyncの言語組み込み以前の何がめんどくさかったのかを割愛していますので、どこが素晴らしいのか伝わりにくいですが、ともあれ、本当楽になりましたね。

なお、本記事で要点を絞る都合上、非同期処理Xでの例外発生については記載していません。

言うまでもありませんが、実際の実装では例外のハンドリングは忘れないようにしましょう。

関連記事

体系的・本格的とまではいきませんが、本記事よりももう少しPromiseを噛み砕いたり、本記事では触れていない、2つ以上の非同期処理の関数を呼び出しして並列化したものを最後にまとめあげたりという例にふれている同じテーマの別記事です。

私見では、本記事の例以外では、Promise.allを知っていれば、JavaScriptの非同期処理のおいしいところ取りができる、それだけでも応用がきくように思いますので、Promise.allについてふれています。

また、上記ではぼやかした、asyncをfunctionの前に付けている背景もそちらで述べています。

<2021/1/14 時点 作成中 >

itdepends.hateblo.jp

参考書籍・リンク

azu.github.io

developer.mozilla.org

*1:ホント先人に感謝ですね。

最近のPythonでのコンカレント(並列)の非同期処理 async/await ・asyncioについて(ver 3.8で確認)

Pythonの非同期処理については、いろいろ歴史・経緯があるようです。

しかしながら、皆さんのおかげで、部外者が安易に言うのもはばかられますが、Python3.7あたりから、私のような日曜エンジニアでもおいしいところにありつけるようになってきた様子だったので、シンタックス入門例を自分用にまとめてみました。

docs.python.org

ちなみに、非同期処理が登場する要件としてはいろいろなケースがあると思いますが、本記事では、時間のかかるIO処理を並行化するユースケースを念頭においています。さらに、サーバとクライアントでいえばどちらかといえば「使う」側の後者における非同期処理に重心があります。

また、記事作成にあたり実際はいろいろネット上の文献等を参照させていただいてはいるのですが、どこのどれにポインタを貼るべきか悩ましいところでした。

よって、予備知識や背景を勉強させていただいたことには感謝しつつ、ここではあえて他の参考リンクはしませんでした。

なお、本記事の例はPython3.8で動作確認しています。

上記の公式リファレンスは自分なりに熟読しましたが、如何せん、非同期処理は、挙動を観測するためにトリックを入れるとそれの都合で確認したい挙動が変わってしまう(かのようにログ出力されることもある)ため、Python3.8で確認したことは胸をはって宣言できるのですが、Python自体のソースコードを追ったわけではありませんので、多少割り引いてご覧ください。また、エラーハンドリングは省略していますので、言うまでもないですが、自己責任でお願いします*1

(ex.1) コルーチンオブジェクトを戻す非同期関数・メソッドを同期的に処理する

まずは、柔道で立ち技の技術を活かすために寝技に通じる、ブレーキの技術があればアグレッシブな運転ができるというところで、あえて非同期処理を同期的に扱う例から入ります。

一例として、Pythonの非同期処理を取り扱うサードパーティ他のライブラリを使用することになりました、という例をイメージください。

さて、このサードパーティ関数・メソッド・APIは、コルーチンオブジェクトを戻します... とリファレンスには記載されています*2

Pythonにおけるコルーチン・コルーチンオブジェクトが何かは、ボロが出るので深追いしませんが、どうやらこの関数で何かデータを取得したり変換した結果そのものではなく、なんらかの代理のオブジェクトのようなものを戻してくれるらしいし、この存在を認めることで、どうやら非同期処理をプログラム記述上うまく取り扱えるんだろうなということは状況証拠的に分かります。

そのような想定をしつつ、関数に不慣れなこともあり、非同期処理ではありながらも同期的に処理することにして、それ以外の本来の目的に集中しましょうというプログラム設計アプローチをとることもあるよね...

という場合に、このサードパーティ関数の実際の戻り値を取得する、また、プログラムの進行を非同期処理が応答を戻すまで確実に待機するための魔法のコトバが、「await」です。

具体的なawaitの例は次のとおりです。

■awaitで外部の非同期ライブラリを待ち合わせる例(hoge1.py)

import asyncio

async def gaibuno_asyncfunc(x, y):
    """
    擬似非同期関数です。
    データベースなどを検索して(見ての通りしてませんが)、最終的には値を戻す関数に見立ててください。
 実際は、
    import  gaibuno_asyncfunc
    している体としてご覧ください。
    """
    print(F'引数{x},{y}で呼び出されました。')
    return x + y


async def main():
    cr = gaibuno_asyncfunc(3, 10)
    print(cr)
    #  ↓
    # <coroutine object echo at 0x7ff420035640 >

    result = await cr
    print(result)
    #  ↓
    #  13


asyncio.run(main())

hoge1.pyの実行結果

<coroutine object echo at 0x7f8e38105640>
引数3,10で呼び出されました。
13

gaibuno_asyncfuncは、今回のストーリーにおける、サードパーティの「コルーチンオブジェクト」を戻すと言われている関数だと思ってください。コードの見た目が「return x + y」になっていることをうっすら念頭におきつつ、厳密なところはあまり気にしないでください。

asyncio.run(foo())の存在と、main()にasyncが付いていることはひとまず気にしないでください。

ポイント、というか目を慣らして欲しいのは次の2点です。

  1. 擬似外部サードパーティの非同期関数という体のgaibuno_asyncfuncの戻り値は、元のコードでは、「x + y」を戻すようなものでも、コルーチンオブジェクトになっていること。
  2. コルーチンオブジェクトを格納した crという変数に対して、await式を適用すると、x + yらしき値の13が得られていること。また、非同期か並行だったかというところはともかく(この例だけでは実際ははっきりしませんが)awaitで待ち合わせしたようである。

というところです。

なお、「コルーチンオブジェクトが戻る」ということを見せたくてこのような例にしましたが、awaitで外部の非同期ライブラリをあえて同期的に実行したい場合は、実際は次のように、関数呼び出しの場所でawaitで捕まえるようなコーディングとしましょう。

async def main():
    result = await gaibuno_asyncfunc(3, 10)
    print(result)

(ex.2) 非同期処理をレスポンス向上に活かす際の例

前項では、非同期処理を同期的にする例としましたが、ここからが本題です。

あらためて気持ちを棚卸ししますが、準備が整ったら非同期処理は開始して欲しいし、複数の非同期処理があればこれらもそれぞれの内部で時間のかかる外部リソースのIO待ちになれば違いに切り替えながら他の仕事をして欲しいし、 並行して走っている複数の非同期処理のうち、ある処理については完了し次第、得られた値を使って次の演算をしたいよね、という非同期処理ならではのシーンをイメージします。

例えば、なんちゃって図で書くと、深く考えずに直列化すると(処理の流れは分かりやすいものの)下記図の上の図で登場人物分の処理時間を要してしまうものの、ホントの気持ちは下部の図のようにうまくやりたいよね、という例です。

f:id:azotar:20210113225344p:plain

このような用途については、ハイレベル、ローレベル合わせていくつか方法があるようです。

Python3.7あたりでは、ハイレベルのAPIが整理されており、次のような例がおおよそ、上記の図の下の方の図のような挙動になるコーディング例です。

■随時非同期処理をスタートさせて並行処理しながら必要なタイミングで戻り値を待ち合わせ取得して利用する例(hoge2.py)

import asyncio


async def some_asyncfunc(x):
    print(F'引数{x}で呼び出されました。ただし、すぐに完了するとは限りません。')
    return x


async def mainConcurrent():
    task1 = asyncio.create_task(some_asyncfunc(333))
    task2 = asyncio.create_task(some_asyncfunc(777))

    r1 = await task1
    print(F'r1:{r1}と一緒に出力したい')
    r2 = await task2
    print(F'r1:{r1},r2:{r2}と一緒に出力したい')

asyncio.run(mainConcurrent())

hoge2.pyの実行例

引数333で呼び出されました。ただし、すぐに完了するとは限りません。
引数777で呼び出されました。ただし、すぐに完了するとは限りません。
r1:333と一緒に出力したい
r1:333,r2:777と一緒に出力したい

ここでは、「タスク」という処理モデル(処理単位?)を抽象化したものがPythonの言語側で整理されており、これが登場します。

ポイントとしては、asyncio.create_task(コルーチンオブジェクト)という形で、「タスク」を作りなさいという命令をコールすると、その時点で引数で渡したコルーチンオブジェクトが取り扱う非同期処理がスケジュール化(言語側に任されることになりますが、つまり処理がスタート)されます。

ここで、asyncio.create_taskは、Taskオブジェクトを戻します。例によって、Taskオブジェクトはそのままでは、なんらかの値を見せてくれませんが、コルーチンオブジェクトやこの記事では紹介しないPythonのFeatureのような「awaitable」なオブジェクトであり、つまるところ、awaitで終了の待機と非同期処理で取得される値を得ることができます。

注:コルーチンを戻す関数を呼び出ししただけでは、その非同期処理はスタートしない(らしい)

実はex.1もそうでしたが、コルーチンを戻す関数を呼び出し記述しただけでは、その非同期処理はスタートしないことに注意してください。

言語に「スタートしても良いよ」ということを伝えるには、create_taskや今回はご紹介していないローレベルのイベントループ制御のAPIにコルーチンオブジェクトを渡してやる必要があります。

例えば、次のプログラム例では、

async def main():
    cr1 = some_asyncfunc(333)
    cr2 = some_asyncfunc(777)

    r1 = await cr1
    print(F'r1:{r1}と一緒に出力したい')
    r2 = await cr2
    print(F'r1:{r1},r2:{r2}と一緒に出力したい')

asyncio.run(main())

次のような出力となり、

引数333で呼び出されました。ただし、すぐに完了するとは限りません。
333 r1と一緒に出力したい
引数777で呼び出されました。ただし、すぐに完了するとは限りません。
333 r1と一緒に出力したい

awaitで「終了を待ち合わせする」記述が現れた時点から非同期処理が走行し始めて、完了するまで走行するという動きになります。

(ex.3) ある程度の(例えば同じような性質の複数のリソースへの情報取得の)非同期処理を複数を並行に走らせて、それらの取得値が出揃ったら次のステップの処理をしたい

ex.2は、どちらかといえば時間のかかる処理を並行で随時開始させておきたいというところに関心がありましたが、こちらは、並列で走らせたいがそれらの間に特に順序性等はないが、全部そろうのを待つという例です。

こんな↓シナリオ例です。

f:id:azotar:20210113225928p:plain

私なぞ深く考えないヒトは、この例について、普通(?)にforループでawaitしてまわれば良いのでは?とも思ったりするのですが、そうすると実際のところ直列化されてしまいます。

やはり、このような要件は頻出なのか、asyncio.gatherというAPIが用意されています。JavaScriptにおける、Promise.allですね。

また、これはそのまま定型として覚えても良いと思われますが、「await asyncio.gather(タスクオブジェクトの一覧)」としてawaitを付けて、結果のリストを取得するようにしています。

■(この例では並列(parallel)ではないが)パラパラと随時非同期処理を多数走行させて総取りする例(hoge3.py)

import asyncio


async def myfunc(a):
    """
    擬似非同期処理
    """    
    return a


async def main():
    tsks = [asyncio.create_task(myfunc(i)) for i in [0, 1, 2, 3, 4]]
    # 通常は、res = await asyncio.gather(*tsks) で良いが、戻り値の関係が分かりやすいように、
    # 以下では引数の順番で遊んでみている。
    res = await asyncio.gather(tsks[4], tsks[2], tsks[1], tsks[0], tsks[3])
    print(res)

asyncio.run(main())

hoge3.pyの実行結果

[4, 2, 1, 0, 3]

さいごに(【補足】async def、asyncio.run(foo())について)

最後にここまでの例ではあえて説明を飛ばした事項について補足など。

asyncio

ご紹介が最後になりましたが、asyncioさんです。

忘れずにimport してください。

docs.python.org

この記事は、上記リンク中の「高レベル API 」セクションの「コルーチンとTask」の内容にそっています。

ちなみに、記事のex.3の例をもう少し複雑にしたような例だと、「キュー」を使うことになるんだと思います。

async def

async def foo(xxx)で宣言した関数は、「foo(xxx)」で記述された際に、コルーチンオブジェクトを戻り値として戻すことで、非同期処理(そして実際は非同期でなくても)を抽象化できる上に、awaitで元々の戻り値を取得する分かりやすいコーディングができる... ものです。

...と私は(使う側の立場としては)理解しているのですが、このままの説明は公式リファレンスではされていません。PEPなどの読み込みやPythonへの深い理解が足らないのかも。

ただ、入門時は一旦このようにとらえておいて良いのではと感じています。

asyncio.run(foo())

asyncio.runは、コルーチンオブジェクトを引数にとって、その非同期処理を走行させるためのAPIです。

クライアント系のプログラムであれば、複数の非同期処理を取り扱うmain関数(これ自体もasync defで宣言することになり、プログラム上のreturn値によらず、main()という記述の戻り値はコルーチンになる)を定義し、これを実行プログラムの最後に呼び出すことになりますが、そこでasyncio.run(main())とすると、asyncioモジュール黎明期によく見られたイベントループを意識した低レベルのコーディングをしなくて済む... という意味で便利なAPIです。

ということで、Python 3.7以降で合流した方はこれをおまじないとしてまずは慣れることを優先した方が良いと思いますし、asyncio.runおまじないで十分な場合はこれ以上複雑なことはしないというのも立派な戦略かと思います。

https://docs.python.org/ja/3.8/library/asyncio-task.html#asyncio.run

*1:というほど難しい例は入れていませんが念のため。

*2:こいつ

docs.aiohttp.org

ですね。

文字列の配列に対して、各文字列の前方の共通部分を抜き出すスニペット(Python、JavaScript)

文字列の配列に対して、各文字列の前方の共通部分(のみ)を抜き出すという例です。

f:id:azotar:20201226003123p:plain

Python等であれば、標準ライブラリなどにそのものズバリのものがありそうなという気もしましたが、ぱっと見見つけられませんでしたので、自作しました。 (この程度のものもスクラッチでさらさら書ける腕前ではなかったので、せっかくなので自分メモとして残しておきます。)

特別なことはやっていません。

(1) Python

hoge.py

from functools import reduce
from typing import *

def common_prefix(str_list: List[str]) -> str:
    def common(a: str, b: str) -> str:
        idx = 0
        for i, j, _ in zip(a, b, range(len(a))):
            if i != j:
                break
            idx = _ + 1
        return a[:idx]

    return ''.join(reduce(common, str_list))

上記の確認用コード


ptns = [
    ['abcd', 'abc1234', 'abczf'],
    ['abc1234', 'abcd', 'abzf'],
    ['fbcd', 'abc1234', 'abczf'],
    ['abcd'],
    ['同じです', '同じです', '同じなら', '同じかな', '同じと思われる'],
    ['同じab', '同じxb', '同じby'],  # //途中を挟んで微妙に一致するが、前方部分のみ検出すべき例
    ['同じbbz', '同じxbz', '同じbz'],  # //途中を挟んで微妙に一致するが、前方部分のみ検出すべき例
    ['', '同じbbz', '', '同じxbz', '同じbz']  # //途中を挟んで微妙に一致するが、前方部分のみ検出すべき例
]

for idx, i in enumerate(ptns):
    print(idx, i, "前方共通部分=", common_prefix(i))


↓ 実行結果

0 ['abcd', 'abc1234', 'abczf'] 前方共通部分= abc
1 ['abc1234', 'abcd', 'abzf'] 前方共通部分= ab
2 ['fbcd', 'abc1234', 'abczf'] 前方共通部分= 
3 ['abcd'] 前方共通部分= abcd
4 ['同じです', '同じです', '同じなら', '同じかな', '同じと思われる'] 前方共通部分= 同じ
5 ['同じab', '同じxb', '同じby'] 前方共通部分= 同じ
6 ['同じbbz', '同じxbz', '同じbz'] 前方共通部分= 同じ
7 ['', '同じbbz', '', '同じxbz', '同じbz'] 前方共通部分= 

アルゴリズムを差し替えられるようにする

from functools import reduce
from typing import *

def commonfunc(a: str, b: str) -> str:
    idx = 0
    for i, j, _ in zip(a, b, range(len(a))):
        if i != j:
            break
        idx = _ + 1
    return a[:idx]


def common_prefix2(str_list: List[str], func) -> str:
    return ''.join(reduce(func, str_list))


for idx, i in enumerate(ptns):
    print(idx, i, "前方共通部分=", common_prefix2(i, commonfunc))


# 共通の文字の集合を取得(common_prefix2という名前からは合わなくなるケド...)
def intersectfunc(a, b): return list(set(a) & set(b))

print('intersect版=', common_prefix2(['abcd', 'abc1234', 'abczf'], intersectfunc))

↓ 実行結果

0 ['abcd', 'abc1234', 'abczf'] 前方共通部分= abc
1 ['abc1234', 'abcd', 'abzf'] 前方共通部分= ab
2 ['fbcd', 'abc1234', 'abczf'] 前方共通部分= 
3 ['abcd'] 前方共通部分= abcd
4 ['同じです', '同じです', '同じなら', '同じかな', '同じと思われる'] 前方共通部分= 同じ
5 ['同じab', '同じxb', '同じby'] 前方共通部分= 同じ
6 ['同じbbz', '同じxbz', '同じbz'] 前方共通部分= 同じ
7 ['', '同じbbz', '', '同じxbz', '同じbz'] 前方共通部分= 

intersect版= bac

SequenceMather版

ここで、ふと、difflibのSequenceMatherを思い出した(もともとうろ覚えだったので確かめながらだが...)。

...ので、そちらを活用してみる。

from functools import reduce
from typing import *
from difflib import SequenceMatcher

def common_prefix2(str_list: List[str], func) -> str:
    return ''.join(reduce(func, str_list))


def commonfuncB(a, b):
    [(i, j, n), *_] = SequenceMatcher(
        None, a, b).get_matching_blocks()
    return a[:n] if (i == 0 and n > 0) else ""

for idx, i in enumerate(ptns):
    print(idx, i, "前方共通部分=", common_prefix2(i, commonfuncB))

当初の自作版と同じ結果が得られた。

(2) JavaScript

試作品1

function common_prefix(strArray) {
    const common = (a, b) => {
        let idx = 0;
        for (let i = 0; i < a.length; i++) {
            if (a[i] !== b[i]) {
                break;
            }
            idx = i + 1;
        }
        return a.slice(0, idx);
    }
    return strArray.map((a) => [...a]).reduce(
        (acm, cur) => common(acm, cur)
    ).join('');
}

配列にした方が集合系の標準関数などが使えて良いかなと思ったので、"abcd" -> ["a","b","c","d"]とバラしたのですが、それらしき関数を探しきれず、結局、ちまちまループを回して可読性が高くないような気がするのと、また言うほど早いわけではなさそう。

ということで、startsWithとともに添字を含むロジックを無くして、仕様の意図がわかりやすく、ステップ数も少なそうな次のロジックに落ち着いております。

↓(試作品2)

試作品2

function common_prefix(strArray) {
    return strArray.reduce(
        (a, b) => {
            let wrk = a
            while (true) {
                if (wrk.length == 0 || b.startsWith(wrk)) {
                    return wrk
                }
                wrk = wrk.slice(0, -1)
            }
        }
    )
}

試作品3(2020/12/29)

このレベル&要件(ひとまず動けばよい)であれば、もっと単純にできるかも。

function common(a, b) {
    return (a.length == 0 || b.startsWith(a)) ? a : common(a.slice(0, -1), b);
}

['abc', 'abcd', 'abc1234', 'abczf'].reduce(common);

参考リンク/関連リンク

itdepends.hateblo.jp

最長共通部分列問題 - Wikipedia

Pythonで実装したJSONのdiffトイスクリプト(自分コピペ用)

下記の記事と似た主旨のオレオレメモです。

itdepends.hateblo.jp

JSONファイルのdiffって有名ライブラリなどでは少しリッチすぎるかなという時に、機能が劣っていたり割り切りがあっても、自分で取り回しやすい自作のイディオムが欲しくなったのでコピペ用に書き起こしてみました。


■ツールの仕様・コンセプト

2つのJSON*1ファイルaとbを読み込んで、差異が出たプロパティを出力します。

JSONを比較したい時は、(Pretty形式の行単位の比較ではなく、)まずは差異が出ているプロパティ(ネストされていることが多い)がどこか知りたいよね、というところに注目した分析になっています。

具体的には、

x.y.zというノードで差異が出た場合、

{
   x.y.z : [
  ファイルaの当該プロパティの値,
       ファイルbの当該プロパティの値      
  ]
}

という、それ自体がJSONのレポートを吐き出します。

■jsondiff.py(Pythonです)

from itertools import zip_longest
import json

"""
JSONをdiffする関数
"""

diff = {}


def append(path, a, b):
    diff['.'.join(path)] = [a, b]


def d(a, b, path=[]):
    if type(a) is not type(b):
        append(path, a, b)
        return
    if type(a) is list:
        d4list(a, b, path)
        return
    if type(a) is not dict:
        if a != b:
            append(path, a, b)
        return
    for i in a.keys():
        if i in b.keys():
            d(a[i], b[i], path + [i])
        else:
            append(path + [i], a[i], None)

    for i in b.keys():
        if i not in a.keys():
            append(path + [i], None, b[i])


def make_pathsimbol(path, cnt):
    _path = path[:-2] + [path[-1] + '[' + str(cnt) + ']']
    return _path


def d4list(a, b, path):
    cnt = 0
    for i, j in zip_longest(a, b):
        d(i, j, make_pathsimbol(path, cnt))
        cnt += 1


def jsondiff(a, b):
    d(a, b)
    return diff


if __name__ == "__main__":

    a = {"a": 1, "b": 2, "c": {"d": 3, "x": "ああ"},
         "e": [{"f": 4}, {"g": 5}, {"g": 9, "y": [1, 2]}, {"z": "ない"}], "h": 3}

    b = {"a": 1, "b": 22, "c": {"d": 3, "x": "いい"},
         "e": [{"f": 4}, {"g": 55}, {"g": 99, "y": 9}], "h": "3", "w": "ないない"}

    print(json.dumps(jsondiff(a, b), ensure_ascii=False, indent=4, sort_keys=True))

■jsondiff.pyで食わせているインプットJSON(相当)の例(再掲)

ファイルa = {
    "a": 1,
    "b": 2,
    "c": { "d": 3, "x": "ああ" },
    "e": [
        { "f": 4 }, { "g": 5 }, { "g": 9, "y": [1, 2] }, { "z": "ない" }
    ],
    "h": 3
}

ファイルb = {
    "a": 1,
    "b": 22,
    "c": { "d": 3, "x": "いい" },
    "e": [
        { "f": 4 }, { "g": 55 }, { "g": 99, "y": 9 }
    ],
    "h": "3",
    "w": "ないない"
}

※プロパティbやプロパティc.xがdiff検知されてほしいという例になります。

■jsondiff.pyの実行結果

{
    "b": [
        2,
        22
    ],
    "c.x": [
        "ああ",
        "いい"
    ],
    "e[1].g": [
        5,
        55
    ],
    "e[2].g": [
        9,
        99
    ],
    "e[2].y": [
        [
            1,
            2
        ],
        9
    ],
    "e[3]": [
        {
            "z": "ない"
        },
        null
    ],
    "h": [
        3,
        "3"
    ],
    "w": [
        null,
        "ないない"
    ]
}

今回の目的の範囲ではうまく行っていると思いますが、コレは恥ずかしいッ!!系のバグがあるかもしれません。

ですが、「答案用紙を提出した後に閃めく理論」により、「公開する」と思わぬ不具合に自分で気づけるので、見込みでポストしちゃうこととしました。

*1:見ての通り、ここではエッセンスを示すため、JSONではなく、Pythonのdictで代用しています。実際にはこのコードに加えてjson.loads()などをうまく使うことが前提です。

Pythonのネストされたdictに「a.b.c.d」のようなJavaScript風のアクセスを行う小品のスニペット例 2nd

下記の記事の続きです。自分でも続きがあったのかと思っておりますが、今回は、前回と同じ要領で、あるdictについて、特定のプロパティ(複数可能)を抜き出したdictを得るというミニDSL風の関数を作成しました(以下 hoge2.py)。

itdepends.hateblo.jp

なお、hoge2.pyを作成してて、もう一声加えるとPython 3.9で導入されるネストされたdictのマージ演算子相当に近づけられそうだったので、これも自分メモ・活動の記録としておまけでつけてあります(以下hoge_merge.py)。

■(参考リンク)Python3.9のdictに関するマージ演算子「|」

www.python.jp

抜き出し

■hoge2.py(Pythonです)

selectfieldsというのが該当のもの。 なお、selectfieldsの第2引数で、抜き出したいプロパティを示すリストを指定するが、お行儀の良いものがお行儀の良い順で指定されることとする。例えば、「a.b.c」の後に「a.b」が指定される場合は、「a.b」が抜き出されてしまうことから、「a.b.f」などが成り行き保持されてしまい、「a.b.c」のみ選択したいという指定は機能しない。

dx = {
    "a": {"b": {"c": "あああ", "d": ["いいい"], "f": "えええ"}},
    "x": "ううう"
}


def selectfields(origobj, _paths):
    paths = copy.deepcopy(_paths) #初出時「_pathsを破壊的に動作する挙動」であったが、破壊しないように見直し(2020/12/1)
    obj_ = {}

    def _setobj(obj, path, up=[]):
        c = path.pop(0)
        p = up + [c]
        if len(path) == 0:
            #findfield関数は前回の記事のものと同じ
            if _ := findfield(origobj, p):
                obj[c] = _
            return
        if c not in obj.keys():
            obj[c] = {}
        _setobj(obj[c], path, p)

    for path in paths:
        _setobj(obj_, path)

    return obj_

# dxのa.b.d、a.b.fを抜き出す。ちなみに、「a.x」は存在しないので、空振りする。
dx_ = selectfields(
    dx, ['a.x'.split('.'), 'a.b.d'.split('.'), 'a.b.f'.split('.')])
print(dx_)

■hoge2.pyの実行結果

{'a': {'b': {'d': ['いいい'], 'f': 'えええ'}}}

マージ

getpathsで、あるdictのすべてのプロパティを「a.b.c〜」のリストで取得し、先述の「_setobj」を使って、マージ先のdictにコピーする(merge関数)。

hoge_merge.py



def getpaths(obj):
    pathslist = []

    def _getpaths(obj, up=[]):
        for k in obj.keys():
            p = up + [k]
            if isinstance(obj[k], dict):
                # dictなら掘り下げる
                _getpaths(obj[k], p)
            else:
                # 行き止まりであれば、pathを表すリストを出力する
                pathslist.append(p)

    _getpaths(obj)

    return pathslist


print("getpathsの挙動")
print(getpaths(dx))


def merge(obj1, obj2):
    obj_ = copy.deepcopy(obj1)
    origobj = obj2

    # この_setobjは上述の「hoge2.py」のものと同じ
    def _setobj(obj, path, up=[]):
        c = path.pop(0)
        p = up + [c]
        if len(path) == 0:
            if _ := findfield(origobj, p):
                obj[c] = _
            return
        if c not in obj.keys():

            obj[c] = {}
        _setobj(obj[c], path, p)

    for path in getpaths(obj2):
        _setobj(obj_, path)

    return obj_



dy = {"a": {"y": "わい"}, "z": "ぜっと"}

dx = {
    "a": {"b": {"c": "あああ", "d": ["いいい"], "f": "えええ", "g": {"h": "えいち"}}},
    "x": "ううう"
}

print("merge(dy, dx)の挙動")
print(merge(dy, dx))

hoge_merge.pyの実行結果

getpathsの挙動
[['a', 'b', 'c'], ['a', 'b', 'd'], ['a', 'b', 'f'], ['a', 'b', 'g', 'h'], ['x']]
merge(dy, dx)の挙動
{'a': {'y': 'わい', 'b': {'c': 'あああ', 'd': ['いいい'], 'f': 'えええ', 'g': {'h': 'えいち'}}}, 'z': 'ぜっと', 'x': 'ううう'}

テストパターンが少ないので見逃しがあるかもしれない*1。 また、今回やりたかったことについては、問題をうまく分解できた気がしていますが、一方、効率的かというとそうではないかもしれません。

*1:もともと割り切り仕様になっているところはありますし...