_ :: Program (original) (raw)
Hy言語でS式をそのまま表示する方法
Hy言語でS式をJSONから生成して表示しようしていたのだが、Pythonコードに直せられてしまったものが表示され困った。
(print '(+ 1 1))
そのままS式として表示して欲しかったのでやり方を調べた。
hy.repr
を使用するとそのまま表示されるようだ。
(print (hy.repr '(+ 1 1)))
Hy言語で文字列をシンボルに変換する方法
この方法もなかなか見つけられなかったのでここに記しておく。
(import hy.models [Symbol])
(let [ text "hoge" sym (Symbol text)] (print (type text) text) (print (type sym) sym) (print (type 'hoge) 'hoge))
<class 'str'> hoge <class 'hy.models.Symbol'> hoge <class 'hy.models.Symbol'> hoge
Hy言語でタイプヒントを付ける方法
(require hyrule [of]) (import dataclasses [dataclass])
(setv #^ int n 0)
(defclass [dataclass] Hoge []
#^ str name
(setv #^ (of Optional str) comment None)
#^ (of set str) attrs
)
余談
Hyruleってハイラルって読むらしい。
背景
プログラミング言語の中でも特によく使われているのは、Java・JavaScript・Pythonです(恣意的)。
これらの言語をClojureでまとめて書くことができれば楽ちんです。
Pythonはlibpython-cljというライブラリを使えば、ClojureからPythonを使うことができてしまいます。
libpython-clj
開発環境として、以下を使用しています。
- macOS(Sonoma)
- pyenv
Clojureからlibpython-cljによってPythonを使うにはPythonの動的ライブラリ(libpython.dylib)とバイナリ(python)が必要なようです。
libpython-cljはそれらの場所を明示しなくてもある程度自動で探してくれるようなので開発環境によっては、
- インストールした終わり!
- ブログ記事終わり!
にできるのですが自分の環境では問題が発生しました。
問題と解決
ClojureのREPLで以下のコードを試すとエラーが発生してしまいました
user=> user=> (require '[libpython-clj2.require :refer [require-python]]) Execution error at libpython-clj2.python/initialize! (python.clj:129). Failed to find a valid python library!
user=> user=> (py/initialize! :python-executable "/Users/watanany/.pyenv/versions/3.11.5/bin/python3.11" :library-path "/Users/watanany/.pyenv/versions/3.11.5/Python") Syntax error compiling at (insn/util.clj:105:1). Unable to find static field: ACC_OPEN in interface org.objectweb.asm.Opcodes
問題1: 自動で動的ライブラリやバイナリを見つけてくれない
こちらの方は、Python動的ライブラリ(libpython.dylib)が見つからないという意味でした。
libpython.dylibが単に見つけられないのか、そもそもインストールされていないのか、自分が使っているPythonの環境で調査する必要がありました。
macos - How do I find where Python's libpython is located? - Stack Overflow
$ ls -l $(python3 -c 'from distutils.sysconfig import get_config_var; print(get_config_var("LIBDIR"))')
でlibpython.dylibがありそうなディレクトリを調べたところ、Pythonの静的ライブラリ(libpython.a)はあったのですが、動的ライブラリ(libpython.dylib)は存在しませんでした。
pyenvでlibpython.dylibをインストールする方法を調べると、関連してそうなISSUEを見つけることができました。
ISSUEにあるコマンド通り叩くとビルド中にtkinterがない旨のエラーが出てきたので、tkinterをbrewでインストールしつつ、Pythonをインストールしました。
$ brew install python-tk@3.11 $ env PYTHON_CONFIGURE_OPTS="--enable-framework" pyenv install -v 3.11.5
もう一度Pythonをインストールすると無事libpython.dylibが作られていました。
問題2: Javaのエラーが出る
問題2の方も動的ライブラリが見つけられないことが原因のエラーかと思ったのですが、いつも使っているleiningenのprofileの:dependenciesに古い依存ライブラリが含まれていたためでした。 (ChatGPTからヒントをいただいた)
どの依存ライブラリがエラーを出しているかわからなかったので、一旦、:dependenciesをlibpython-cljのみにしたところ解決することができました。
{:py {:plugins [[lein-ancient "0.6.15"] [lein-exec "0.3.7"] [lein-pprint "1.3.2"] [lein-localrepo "0.5.4"]] :dependencies [[clj-python/libpython-clj "2.025"]] :injections [(require '[clojure.repl :refer :all]) (require '[clojure.pprint :refer :all]) (require '[libpython-clj2.python :refer [py. py.. py.-] :as py]) (require '[libpython-clj2.require :refer [require-python]]) ]}}
ClojureからPythonを実行
(require '[libpython-clj2.require :refer [require-python]]) (require '[libpython-clj2.python :refer [py. py.. py.-] :as py]) (require-python '[pandas :as pd])
(def df (pd/DataFrame {:A [0 1 2] :B [3 4 5] :C [6 7 8]}))
(println df)
(println (:A (py. df to_dict)))
実行すると無事動いてくれました!
$ PYENV_VERSION=3.11.5 lein with-profile +py repl user=> (load-file "/tmp/clj.clj") A B C 0 0 3 6 1 1 4 7 2 2 5 8 {0: 0, 1: 1, 2: 2} nil
RedashをGoogle Cloud Platformにホストする上で、Redashの公式ドキュメントの通り行かなかったことがあるので備忘録として残します。
Redashインスタンスの生成
Redashイメージのインストール
Setting up a Redash Instance | Redashにある通り、google cloud shellにアクセスして以下のコマンドを実行する。
$ gcloud compute images create "redash-5-0-2" --source-uri gs://redash-images/redash.5.0.2-b5486-build2.tar.gz
これで、GCEにRedashのイメージが作成される。
Redashインスタンスの生成
続いて、google cloud shellから同様に以下のコマンドを実行する。
$ gcloud compute instances create redash --image redash-5-0-2 --zone asia-northeast1-a
今回は東京リージョンにインスタンスを作りたかったので、--zone asia-northeast1-a
をオプションとしてつけた。
Redashの公式ドキュメントではこの後すぐにRedashの管理者作成を行なっているが、
インスタンスのファイアウォールの編集から「HTTP トラフィックを許可する」にチェックを入れる必要があった。
Google CloudからSSHしてみると、/opt/redash
以下にdocker-compose.yml
とPostgreSQLのデータディレクトリが存在していた。
IPアドレスの固定
エフェメラルは「束の間の」「儚い」という意味らしい。
その名の通り、インスタンス実行中のみ割り当てられるIPアドレスで、インスタンス終了と同時にインスタンスから解放されてしまう。
SSL化したいのでドメイン名を設定するつもりだし、ドメインを設定するには変化しない静的外部IPアドレスが適している。
GCPの公式ドキュメントに沿ってエフェメラル外部IPアドレスを静的外部IPアドレスに変更した。
ドメイン名は諸々の事情でそのIPアドレスに対して、AWSのRoute 53で設定した。
Let's Encrypt(Certbot)
Redashはアカウント情報があり、ユーザーはログインのために、ユーザー名とパスワードを入力する必要がある。
暗号化せずにその情報を垂れ流すのは嫌だったので、Let's Encryptを使ってSSL化することにした。
インストール
RedashのインスタンスにSSHして(今度はgoogle cloud shellではない)、
Let's Encryptの公式ドキュメント通りに以下のコマンドを叩いた。
$ sudo apt-get update $ sudo apt-get install software-properties-common $ sudo add-apt-repository universe $ sudo add-apt-repository ppa:certbot/certbot $ sudo apt-get update $ sudo apt-get install certbot
SSL証明書の発行
Let's Encryptのcertbotコマンドには色々なオプションが存在して、迷ってしまった。
今回は、設定ファイルの自動生成などには頼りたくなかったので、certbotをスタンドアローンモードで起動することにした。
certbotをスタンドアローンモードで起動するには、ドメイン検証のために80番ポートを使う必要があるので、もしRedashが80番ポートを使っているなら一旦サーバーを落とす必要がある。
$ cd /opt/redash $ sudo docker-compose down
RedashのNGINXが落ちたのを確認して、certbotにオプションをつけてSSL関連のファイルを取得した。
$ sudo certbot certonly --standalone -d redash.example.com -m email@example.com --agree-tos -n
以上のコマンドを実行すると、/etc/letsencrypt
以下にSSLの関連ファイルが生成される。
SSL化
certbotで生成した鍵ファイルを使ってnginxをSSLに対応させるため、設定ファイルを変更する必要がある。
今回は、設定ファイルを/opt/redash/nginx/nginx.conf
に作って、Dockerでその設定ファイルをマウントすることにした。
NGINXの設定
/opt/redash/nginx/nginx.conf
upstream redash { server redash:5000; }
server { listen 80 default; return 301 https://$host$request_uri; }
server { listen 443 ssl;
ssl_certificate /etc/nginx/conf.d/redash.example.com/fullchain.pem; ssl_certificate_key /etc/nginx/conf.d/redash.example.com/privkey.pem;
gzip on; gzip_types *; gzip_proxied any;
location / { proxy_set_header Host $http_host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto; proxy_pass http://redash; } }
docker-composeの設定
/opt/redash/docker-compose.yml
version: '2'
x-redash-service: &redash-service
image: redash/redash:5.0.2.b5486
depends_on:
- postgres
- redis
env_file: /opt/redash/env
restart: always
services:
server:
<<: *redash-service
command: server
ports:
- "5000:5000"
environment:
REDASH_WEB_WORKERS: 4
scheduler:
<<: *redash-service
command: scheduler
environment:
QUEUES: "celery"
WORKERS_COUNT: 1
scheduled_worker:
<<: *redash-service
command: worker
environment:
QUEUES: "scheduled_queries"
WORKERS_COUNT: 1
adhoc_worker:
<<: *redash-service
command: worker
environment:
QUEUES: "queries"
WORKERS_COUNT: 2
redis:
image: redis:3.0-alpine
restart: always
postgres:
image: postgres:9.5.6-alpine
env_file: /opt/redash/env
volumes:
- /opt/redash/postgres-data:/var/lib/postgresql/data
restart: always
nginx:
image: redash/nginx:latest
ports:
- "80:80"
- "443:443"
depends_on:
- server
links:
- server:redash
volumes:
- /opt/redash/nginx/nginx.conf:/etc/nginx/conf.d/default.conf
- /etc/letsencrypt/live/redash.example.com/fullchain.pem:/etc/nginx/conf.d/redash.example.com/fullchain.pem
- /etc/letsencrypt/live/redash.example.com/privkey.pem:/etc/nginx/conf.d/redash.example.com/privkey.pem
restart: always
Redashの起動
以上で、SSL化用の鍵ファイルと各種設定ファイルが完成した。
GCPでファイアウォールの編集で「HTTP トラフィックを許可する」にチェックを入れて、
$ sudo docker-compose up -d
でRedashを起動。
無事、https://redash.example.com
にアクセスできることが確認できた。
Pythonプロジェクトの2つのパターン
- 使い捨てのスクリプトを書く
- パッケージとして整備しつつ外部のプログラムに使われるのを意識しつつ書く(メンテナンス性高い)
2.の用途として、自分が直面しているのはPyramidでWEBアプリを作るときです。
Pyramidは設定ファイルとして、PasteDeployを利用しています。
この設定ファイルから自パッケージ内のエントリポイントとなる関数名を取り出し、WSGIサーバーであるwaitress
やgunicorn
が利用することになります。
今回は2.のような場合について、どのようなディレクトリ構造がベストプラクティスなのかを調査しました。
ディレクトリ構造の研究
ディレクトリ構造を研究するために以下を参考にしました。
- プロジェクトの構造化 — The Hitchhiker's Guide to Python …①
- Quick Tutorial for Pyramid — The Pyramid Web Framework v1.9.2 …②
- requests/requests: Python HTTP Requests for Humans™ ✨🍰✨ …③
Pipenvは比較的新しいツールあまり言及している記事がありません。
実際、①と②にはPipenvは登場しません。
③には①と②に即したディレクトリ構造でありながら、かつPipenvを使って構成されていたので参考にしました。
基本的な構造
Pipenvを使わない場合
①を見るとディレクトリ構造は以下のようになっています。
- README.rst
- LICENSE
- setup.py
- requirements.txt
- sample/init.py
- sample/core.py
- sample/helpers.py
- docs/conf.py
- docs/index.rst
- tests/test_basic.py
- tests/test_advanced.py
sample
モジュールパッケージが自分が書くソースコードの中心となり、自分で書いたコードはこのディレクトリ以下に配置していくことになります。
setup.py
はsample
モジュールパッケージの依存関係を処理するためのもので、sample
でサードパーティのライブラリが必要な場合などはここで処理していきます。
編集可能モード
(②を順に見ていくと登場しますが)
pip
にはモジュールパッケージを編集可能モードとしてインストールできる機能があります。
例えば、上記のsample
モジュールパッケージでは
$ git clone git@github.com:kennethreitz/samplemod.git $ cd samplemod/ $ pip install -e .
とすることでsample
モジュールパッケージを編集可能モードでインストールできます。
編集可能モードでのインストールのメリットとしては:
- インストールで必要な工程を踏んでいるので、
sample
が必要としているサードパーティのライブラリを一遍にインストールできる - 編集可能モードの名前の通りいつでもモジュールパッケージに修正を加えることができる
- このモジュールパッケージを使うときは標準ライブラリのように扱える(どこでも
import sample
でインポートできる)
Pipenvも使う場合
「依存関係の処理はsetup.py
で行われるが、Pipenv
を使うメリットは何だろう?」と考えると、2つあると思います。
- 仮想環境(virtualenv)を作ってくれるので開発環境をシステム環境から隔離する
sample
モジュールパッケージが直接的に依存していないライブラリを管理する(例えば、デバッグツールとテストツールなど)
1.に関して、Pythonの標準ライブラリvenvを使えばできてしまえますが、モジュールパッケージを使う準備するときについでに仮想環境を作ってくれるので便利です。
2.に関して、実際にrequests
ライブラリ(③)を見てみると、Pipfile
にはほとんどテストツールやリンカ用のツールが記述されています。
Pipenvで編集可能モード
また、requests
ライブラリのPipfile
には以下のような記述があります。
[packages] "e1839a8" = {path = ".", editable = true, extras = ["socks"]}
これは、requests
ライブラリを編集可能モードとしてインストールした痕跡ですね。
Pipenv
ではpip
と同様に編集可能モードとしてモジュールパッケージをインストールすることができます。
$ git clone git@github.com:requests/requests.git $ cd requests/ $ pipenv install -e .
pip
と同様の使用感ですね。
一度、編集可能モジュールとしてインストールしてしまえばPipfileに記述されるので、別なシステムに移動した際でもPipenv
を使っていれば勝手に処理してくれるようになります。
実際、requests
のMakefile
には仮想環境(virtualenv)初期化用のコマンドが記述されています。
上記のpipenv install -e .
の代わりに
$ git clone git@github.com:requests/requests.git $ cd requests/ $ make init # pip install --upgrade pipenv && pipenv install --dev --skip-lock と同じ
とすれば、
- 仮想環境(virtualenv)を作る
- requestsモジュールパッケージを編集可能モードでインストールする(その依存関係にあるものも)
- テストツールやリンカ用のツールをインストールする
というのを一挙に終わらせてくれます。
あとは、pipenv shell
として仮想環境に入り開発に移ったり、pipenv run ほにゃらら
とコマンドを叩くだけです。楽ですね。
Docker
PipenvはPipfileの依存パッケージをシステムのPythonにインストールすることも容易です。
例えば、Pipenvが作る仮想環境の代わりにDockerで包んでしまう場合でも、
$ pipenv install --system
とすれば、Docker内ではmyprojectパッケージを標準ライブラリのように自由に使うことができそうです。
(やったことないけど)
感想
Pipfile
やsetup.py
とrequirements.txt
など同じことを目的にしてるように見えるファイルがたくさんあるので混乱しますが、
目的を適切に分けて考えてツールを使えば便利に使えそうでした。
困ったときはrequests
ライブラリのディクレトリ構造を参考にしていこうと思います。
お仕事でデータ分析をすることになって1ヶ月ほど。
データ分析をする上でPythonはとても便利です。
モジュールも整備されており、ドキュメントも充実しています(ほとんど英語だけど)。
データサイエンスを利用したサーバーを組むなら、同じくPythonでやってしまった方が今までの方法を利用できて便利です。
今回は、PythonでWEBサーバーを組んだのでそれをデプロイする方法を整備しました。
Fabricとは?
FabricはSSH越しでリモートPCのコマンドを実行できるモジュールです。
Railsで良く使われるデプロイツールのCapistranoに対して、PythonではFabricな感じみたいですね。
Capistranoよりもお手軽な感じらしく、タスクなどを覚える必要がないということでした。
逆にいうと必要最低限のものしか準備されておらず、デプロイツールというよりは公式ドキュメントが言うとおり、
本当にリモートPCのコマンドを叩くためのライブラリのようですね。
「PythonでCapistranoを使うのもなんかあれだし、Ansibleを使うのも面倒臭いし手軽にリモートPCの操作できないかな?」って時に便利そうです。
Fabricのバージョン?
注意すべきなのは、Fabric 1.x、Fabric2、Fabric3とあって全部別物レベルに違うことです。
結論として使うべきはFabric2です。
Fabric 1.xはFabric2の旧バージョンであり、公式ドキュメントではFabric2のインストールを強く推奨しています。互換性もありません。
Fabric3はFabric 1.xがPython3に対応していない頃にforkされたもので現在はあまり活発に開発されていないようです。
現行の開発が進んでいる、Fabric2ではきちんとPython3にも対応しており、色々と改善されているようです。
Googleで検索して出てくる日本語化された公式ドキュメントはFabric 1.xのドキュメントなので注意が必要です。
コード
やることはfabfile.py
を置いてfab
コマンドを叩くだけです。
fabfile.py
from fabric import task
PROJECT_ROOT = '/path/to/project' REPO_URL = 'Gitレポジトリ のURL' PIDFILE = 'run/gunicorn.pid'
@task def init(c): c.run(f'[ ! -e {PROJECT_ROOT} ] && git clone {REPO_URL} {PROJECT_ROOT}', hide=True)
@task def deploy(c, version): c.run(f'cd {d.project_root} && git fetch origin && git checkout {version}', hide=True) c.run(f'cd {d.project_root} && PYENV_VERSION={PYENV_VERSION} pyenv exec pipenv clean', hide=True) c.run(f'cd {d.project_root} && PYENV_VERSION={PYENV_VERSION} pyenv exec pipenv install --dev --skip-lock', hide=True) c.run(f'cd {d.project_root} && kill -HUP $(cat {PIDFILE})', hide=True)
あとは、リモートPCにSSHできる環境から、
$ pipenv run fab -i 秘密鍵 -H ユーザー名@IPアドレス init deploy --version=Gitのタグ
とするだけです。
色々試行錯誤の末に、シンプルにgit checkout
して、Gunicornをリロードするというものになりました。
今回はPyenvとPipenv両方を使っているのそれを考慮したものになりました。
どっちも使っていなければ、上記のコードから関連する記述を消せば動くと思います。
またWSGIサーバーとしてGunicornを使っているので、更新分をリロードするためにHUPシグナルを送っています。
Gitの理由
最初はCapistranoと同じくreleasesディレクトリを作って、currentへシンボリックリンクを貼るみたいなのをやっていたのですが、
Pipenvとの共存を考えると難しそうだなとなって辞めました。
RubyのBundlerとPythonのPipenvは似ているようで、Pipenvはインタプリタまで隔離環境としてコピーするのが異なりますね。
感想
データ分析のために整備していたコードをそのままサーバー化しちゃえ、というのは自然な発想な気がします。
2018年、Javaを抜いてPythonが人気の理由もなんとなくわかった気がします。
Sessionとsession
SQLAlchemyでscoped_session
を使った方法でわからなかったことの一つに、
Session.query(ほにゃらら)
と
session = Session() session.query(ほにゃらら)
にどんな違いがあるのか気になっていました。
Session.remove
を行うまでずっと同じセッションを返し続けるという仕様ならなんで2通りあるのだろうと疑問でした。
答えは、公式ドキュメントにある通り、
from sqlalchemy.orm import scoped_session from sqlalchemy.orm import sessionmaker
session_factory = sessionmaker(bind=some_engine) Session = scoped_session(session_factory)
print Session.query(MyClass).all()
と違いはありませんでした。
SQLAlchemyを使っていてセッションとコネクションがわからなかったのでまとめました。
(記事中に間違いなどあれば是非コメントでお知らせ願えたらありがたいです)
定義の再確認
セッション(Session)
データベースとの論理的な接続
データベースとの通信のための情報(ログイン情報や現在のトランザクションレベル)を保持している
コネクション(Connection)
データベースとの低レベルプロトコルを用いた物理的な接続
備考
- コネクションは複数のセッションを持つことができる
- セッションは複数のトランザクションにまたいで寿命を存続できる
普通はコネクションで一つのセッションを持つようにするらしい。
セッションをいつ開始・終了するか
セッションをいつ開始・終了するかについてはいくつかのパターンがある。
WEBアプリでの場合
典型的なWEBアプリでは、WEBクライアントからのリクエストごとにセッションの開始・終了を行う。
つまり、リクエストの寿命 = セッションの寿命となる。
いくつかのPythonのWAFではセッションの寿命を制御してくれる統合ライブラリを提供していて、可能ならばそれらの使用を推奨するとのこと。
(FlaskならばFlask-SQLAlchemy、PyramidならばZope-SQLAlchemyなど)
それ以外の場合
Pythonを利用するのはWEBアプリを作る場合だけではないと思う。
その場合は、上記の統合ライブラリを使えない。
SQLAlchemyはそのような場合に備えて自前でヘルパー関数を持っている。
それがscoped_session
だ。
自分のユースケースでは、データ分析するためにデータをMySQLから取り出したいので、scoped_session
を使う必要がありそうだ。
scoped_session
今まで、『scoped_session
って何だろう?』という状態で使っていたので簡単にまとめておく。
scoped_session
は以下のように使ってSession
オブジェクトを生成することができる。
from sqlalchemy.orm import scoped_session from sqlalchemy.orm import sessionmaker
session_factory = sessionmaker(bind=some_engine) Session = scoped_session(session_factory)
生成されたSession
オブジェクトは以下のようにして使うことができ、このsome_session
を使えばデータベースと通信ができる。
some_session = Session()
Sessionは何回呼び出しても同じオブジェクトを返し続ける。
some_other_session = Session() some_session is some_other_session True
これは同じセッションを異なった場所で簡単に使えるようにするためにあるものらしい。
一見して、クラスをインスタンス化しているように見えるので新しいセッションが生成されるのかと思いきや違うらしい。
とても紛らわしい。 ← 勉強不足なだけ。
新しいセッションを開始するには、以下のコードのように、
現在のSessionを削除し、もう一度Sessionを呼び出せばDBとの新しいセッションが開始される。
Session.remove() new_session = Session() new_session is some_session False
WEBアプリの場合はこの一連の流れを統合ライブラリが行ってくれるが、
それ以外の場合はセッションの寿命を管理したいならば、自前でこのコードを書く必要がある。
感想
今までなんとなく使っていたscoped_session
について知ることが出来て良かったです。
今回は、プログラムで重いクエリを叩くとプログラムがハングすることがあり、セッション周りが原因で起きているのではないかと思い調査しました。
調査後にMySQLのセッションタイムアウト時間を見てみると8時間に設定されており、
結局のところハングの原因はセッション周りではなく、単に自分のPythonコードが重くて止まっているように見えていただけという。。
参考
セッションとコネクションの定義について
SQLAlchemyのSession全般について
Session Basics — SQLAlchemy 0.9 Documentation
scoped_sessionヘルパーについて
Contextual/Thread-local Sessions — SQLAlchemy 0.9 Documentation