ペペロン頭脳

ソフトウェアエンジニアのメモ的なアレ。

pandasのquery()で指定できる@ローカル変数名の謎を追う

3年ぶりだ。

仕事でpandasを使っていて、こんな書き方ができることを知った。
ローカル変数を参照させている。下の例ではfoo列の値が123であるデータをフィルタできる。

bar = 123
df_filtered = df.query('foo == @bar')

公式ドキュメントにもはっきり書かれている。
pandas.DataFrame.query — pandas 2.1.2 documentation

式を渡す時点では単なる文字列なのに、pandasはどうやってbar = 123を参照しているのだろうか?
なんかスタックを辿る仕組みがあるんだろうと見当はつくが、具体的なHowの部分を知るため取材班はアマゾンの奥地へと飛んだ。

結論: inspect.stack()を使えばできる

※この検証はPython 3.9.10で行いました

inspect.stack()を使うことで、コールスタックを辿って各フレームの情報を得ることができる。
ここにローカル変数や関数の引数も含まれている。フレームの f_locals で取得できる。

import inspect


def callee():
    stack = inspect.stack()
    for level, frame in enumerate(stack):
        print(f'\n----- frame level={level} -----')
        print(frame.function)
        print(frame.frame.f_locals)


def caller_lv3():
    callee()


def caller_lv2(arg):
    var2 = arg
    caller_lv3()


def caller_lv1():
    var1 = 1
    caller_lv2(var1)


def main():
    caller_lv1()


main()

実行結果:

----- frame level=0 -----
callee
{'stack': [FrameInfo(frame=<frame at 0x104ded900, ...(省略)...}

----- frame level=1 -----
caller_lv3
{}

----- frame level=2 -----
caller_lv2
{'arg': 1, 'var2': 1}

----- frame level=3 -----
caller_lv1
{'var1': 1}

----- frame level=4 -----
main
{}

----- frame level=5 -----
<module>
{'__name__': '__main__', ...(省略)...}

pandasで上記を実行している箇所

ここでやっている。

github.com

ずっと上の方からスタックレベルを記録してきて、ターゲットの階層を認識した上でそのフレーム情報を取得しているようだ。
これを元に@部分を値へ置換しているということですね。
APIだけ見るとさらっとしてるけど、内部ではちょっとトリッキーなことをしている。

herokuで運用しているGROWIの(1年越しの)アップデートエラー解消

ものすごく今更なんですけど、Herokuで運用しているGROWIのv3.5.18からv3.5.19以上へのアップデートがずっと失敗してたんですよ。去年から。 heroku-postbuildが始まるとMissing list of packages to add to your projectてな。

中身見て調べるのめんどいなーと1年近く放置してたのだが、依存してるプラグインがもうオシマイになるからね!アプリ側更新せいよ!ってHerokuから怒られ始めたので重い腰を上げました。

結論だけ書くと、

  • app.json内のセクションINSTALL_PLUGINSADDITIONAL_PACKAGESに変更されているのだが、これがプラットフォーム側の環境変数に事前登録されずにスクリプトが走るもんでyarn addがコケ続ける事象が発生していた
  • herokuのダッシュボードの"Settings"に行って"Reveal Config Vars"開いて下記追加すればよい
    • ADDITIONAL_PACKAGES
    • growi-plugin-lsx growi-plugin-pukiwiki-like-linker growi-plugin-attachment-refs react-images react-motion

以上で解決。

githubから最新取ってきてpushだけだと絶対ハマると思うんだけど、他に遭遇した人いないのか全然情報出てこなかった。 最初はスタックが古い16なせいかと思って18に上げたけど関係なかった。いずれ上げなきゃいけなかったからいいんだけど。

様子見しながら段階的に上げていって現在最新のv4.1.3まで来たけど、UIがダーク&ネオンな感じで刺激強めだったので設定でmono-blueに変えた。 つかれた。 これからも活用させていただきます。

VueとElectronでアプリを作ってみた

正確には、 Vue CLI 3 & TypeScriptで起こしたプロジェクトにvue add electron-builderしてElement UIはめこんでアプリを作った、になります。

きっかけ

常駐先でちょっとしたツールを作る必要が出てきた。 ユーザーは非IT系の人々なのでGUIはあったほうがよい。 単体アプリで、インストーラーで簡単に導入できるのが望ましい。 さっさと作って次のステップへ進まないといけないので、僕が慣れてる技術で開発できることが望ましい。

→ それElectronでできるよ

Electron触り心地

総合的にはいい感じです。

ElectronはUI側(Chromium)とバックグラウンドプロセス側(Node.js)で構成されています。UI側はSPA書くのと変わりません。

Webではサーバーへのリクエストを行うところが、Electronだとバックグラウンドプロセスとのメッセージングに置き換わる感じです。バックグラウンドプロセス側はNode.jsアプリ書くのと変わりません。UIからのメッセージイベントを受け取って処理を走らせます。

JSに慣れていればサクッと形が作れて、各種プラットフォーム用のインストーラまで生成できて、アップデート検知までできる、というのが素晴らしい。

もちろんデバッグ中はホットリロードでガシガシコード書き換えながら試せる。

…ですが、Electronそのものというかwebpackやnpm由来のモジュールなど、周辺の問題でハマることが多々ある感じでした。

はまったところ

electron-vueは古い。vue add electron-builderを使うべき

Vue+Electronのメジャーな構築方法が新旧2種類あり、古いほうは2年くらいメンテ放置されている!

最初、electron-vueボイラープレートを使う方法が見つかったのでこれに従ってやってたんですね。

Vue CLI + electron-vue でプロジェクト立ち上げ - Qiita

ところが、使えるはずのAPIがNot Foundとか言われたりして何かおかしい。調べてみたらelectron-vueは既にメンテされてなくて色々と古くなっており、Electronはv2(現在最新はv6)だし内蔵Node.jsはv8(現在最新はv12, 安定版でもv10)。

その後ドンピシャな解説が見つかり、無事最新のElectronで構築できました。

qiita.com

そのままだと正常にバンドルできないモジュールがちらほら

npmから入れたモジュールのいくつかが、ビルド時に怒られるという事象が発生しました。 だいたいモジュールのrequire/import絡みで。

webpackの各種loaderがやってる仕事とか、CommonJSのrequire/exportとES6以降のimport/exportの互換性とかちゃんと理解してなくて根本解決はできなかったのですが…試行錯誤ののち、対象モジュールに直接パッチを当てることでなんとかなった。 その場しのぎのパッチは気持ち悪いですが、じっくりやってる時間はありません。

デバッグ時は問題ないのにビルド後はバグるモジュール

node-fetchというわりとメジャーなモジュールなのですが、バグる。 幸いタイムリーにissue情報を見つけたので、リリース版ではなくGitHub上の開発ブランチを直接引っ張ってくることで解決。

https://github.com/yumetodo/vscode-google-photos-uploader/issues/11

それからjson-mergerというモジュールはビルドはできるものの、アプリ起動時の読み込みでコケる。 これはモジュール側のパッケージングに問題がある感じだったので、別ので代替。

ビルドしてしまうと元ソースを追えなくなるので、 どこでコケてるのか切り分けるのに時間が掛かり疲弊しました。

総評

Electron自体はいい感じ。 だけどベースになってるJSのエコシステムにつらみがある。