_ :: 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

開発環境として、以下を使用しています。

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.ymlPostgreSQLのデータディレクトリが存在していた。

IPアドレスの固定

Google CloudではIPアドレスが2種類存在する。

エフェメラルは「束の間の」「儚い」という意味らしい。

その名の通り、インスタンス実行中のみ割り当てられる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つのパターン

  1. 使い捨てのスクリプトを書く
  2. パッケージとして整備しつつ外部のプログラムに使われるのを意識しつつ書く(メンテナンス性高い)

2.の用途として、自分が直面しているのはPyramidでWEBアプリを作るときです。

Pyramidは設定ファイルとして、PasteDeployを利用しています。

この設定ファイルから自パッケージ内のエントリポイントとなる関数名を取り出し、WSGIサーバーであるwaitressgunicornが利用することになります。

今回は2.のような場合について、どのようなディレクトリ構造がベストプラクティスなのかを調査しました。

ディレクトリ構造の研究

ディレクトリ構造を研究するために以下を参考にしました。

Pipenvは比較的新しいツールあまり言及している記事がありません。

実際、①と②にはPipenvは登場しません。

③には①と②に即したディレクトリ構造でありながら、かつPipenvを使って構成されていたので参考にしました。

基本的な構造

Pipenvを使わない場合

①を見るとディレクトリ構造は以下のようになっています。

sampleモジュールパッケージが自分が書くソースコードの中心となり、自分で書いたコードはこのディレクトリ以下に配置していくことになります。

setup.pysampleモジュールパッケージの依存関係を処理するためのもので、sampleサードパーティのライブラリが必要な場合などはここで処理していきます。

編集可能モード

(②を順に見ていくと登場しますが)

pipにはモジュールパッケージを編集可能モードとしてインストールできる機能があります。

例えば、上記のsampleモジュールパッケージでは

$ git clone git@github.com:kennethreitz/samplemod.git $ cd samplemod/ $ pip install -e .

とすることでsampleモジュールパッケージを編集可能モードでインストールできます。

編集可能モードでのインストールのメリットとしては:

  1. インストールで必要な工程を踏んでいるので、sampleが必要としているサードパーティのライブラリを一遍にインストールできる
  2. 編集可能モードの名前の通りいつでもモジュールパッケージに修正を加えることができる
  3. このモジュールパッケージを使うときは標準ライブラリのように扱える(どこでもimport sampleでインポートできる)

Pipenvも使う場合

「依存関係の処理はsetup.pyで行われるが、Pipenvを使うメリットは何だろう?」と考えると、2つあると思います。

  1. 仮想環境(virtualenv)を作ってくれるので開発環境をシステム環境から隔離する
  2. 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を使っていれば勝手に処理してくれるようになります。

実際、requestsMakefileには仮想環境(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 と同じ

とすれば、

  1. 仮想環境(virtualenv)を作る
  2. requestsモジュールパッケージを編集可能モードでインストールする(その依存関係にあるものも)
  3. テストツールやリンカ用のツールをインストールする

というのを一挙に終わらせてくれます。

あとは、pipenv shellとして仮想環境に入り開発に移ったり、pipenv run ほにゃららとコマンドを叩くだけです。楽ですね。

Docker

PipenvはPipfileの依存パッケージをシステムのPythonにインストールすることも容易です。

例えば、Pipenvが作る仮想環境の代わりにDockerで包んでしまう場合でも、

$ pipenv install --system

とすれば、Docker内ではmyprojectパッケージを標準ライブラリのように自由に使うことができそうです。

(やったことないけど)

感想

Pipfilesetup.pyrequirements.txtなど同じことを目的にしてるように見えるファイルがたくさんあるので混乱しますが、

目的を適切に分けて考えてツールを使えば便利に使えそうでした。

困ったときはrequestsライブラリのディクレトリ構造を参考にしていこうと思います。

お仕事でデータ分析をすることになって1ヶ月ほど。

データ分析をする上でPythonはとても便利です。

モジュールも整備されており、ドキュメントも充実しています(ほとんど英語だけど)。

データサイエンスを利用したサーバーを組むなら、同じくPythonでやってしまった方が今までの方法を利用できて便利です。

今回は、PythonでWEBサーバーを組んだのでそれをデプロイする方法を整備しました。

Fabricとは?

FabricはSSH越しでリモートPCのコマンドを実行できるモジュールです。

Railsで良く使われるデプロイツールのCapistranoに対して、PythonではFabricな感じみたいですね。

Capistranoよりもお手軽な感じらしく、タスクなどを覚える必要がないということでした。

逆にいうと必要最低限のものしか準備されておらず、デプロイツールというよりは公式ドキュメントが言うとおり、

本当にリモートPCのコマンドを叩くためのライブラリのようですね。

PythonCapistranoを使うのもなんかあれだし、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コードが重くて止まっているように見えていただけという。。

参考

セッションとコネクションの定義について

sql server - What is the difference between a connection and a session? - Database Administrators Stack Exchange

SQLAlchemyのSession全般について

Session Basics — SQLAlchemy 0.9 Documentation

scoped_sessionヘルパーについて

Contextual/Thread-local Sessions — SQLAlchemy 0.9 Documentation