My Fucking Note

Software Engineeringのメモ

PyCon JP 2022のレポート

PyCon JP 2022に参加&初発表してきたのでメモ

1日目

KeyNote (CPythonの高速化について)

CPython以外にも多数のVMが開発され、そこで得られた知見がCPythonの高速化に寄与している。 今まではPyPyくらいしか知らなかった。 インタプリタ言語だと一連の処理をまとめて1つの処理に変換して高速化するSpecializationなる手法が有効と知った。 (アンパック代入をただのスワップ命令として扱う)

詳解 print("Hello, world")

時間内に完全には理解できなかったが、面白そうなので後で詳しく見てみたい。 Pythonの命令コードが気になる。 disモジュールというのはここで初めて聞いた。

Pythonとアスタリスク 🐍🌟💫🐍🌟💫

普段慣れ親しんでいるPythonの文法がテーマなのもあって内容がスラスラ入ってくる。 本題とずれるがPython3.9から辞書を | でマージできるというのは知らなかったので助かった。 インタラクティブな発表だったので聞き手への問いかけとかを参考にさせてもらった。

Python ライブラリ開発における失敗談 〜開発者に選ばれるライブラリを作るために必要なこと〜

個人開発の場合、地味に不便な部分を解消する便利系ライブラリがオススメというのがすごく共感できた。 他のライブラリと比べて1桁少ないとのことだがGitHubの3桁スターは普通にすごいと思う。 github.com

Playwrightを使って簡単に自動テストしてみる

最近良く耳にするがUIのテストは自分で使ったことが無いので聞いてみた。 レコーディング機能とかもあって自分のイメージとだいぶ違った。 今度触ってみたい。

AST(Abstract Syntax Tree)に入門する

自分の発表と内容が重なることもあってスラスラ入ってくる。 NodeTransformerも簡単に使えそうだったのが良い。 pytestのがassertのastを書き換えてることで詳細表示しているというのは完全に知らなかった。

2日目

KeyNote (統計・データ分析)

データ分析するなら意思決定(何かを新しく始めるか何かを辞める)が必要というのにすごく共感。 すごい話が上手くて自分の発表をイメージし緊張した。

Automate the Boring Stuff with Slackbot(ver.2)

自分の発表直前ということもあり内容はあまり入ってきてないが、話し方とかを参考にさせてもらった。 マイク持ったほうが良いかと一瞬思ったが、PCを操作するリモコンは用意してないのでやめた。

コーディング規約自動化: Pylintのカスタムルールを作ろう

自分の発表 何も見ずに話せるほどではないが、流れはだいぶ頭に入っていたので聞き手の方はちょいちょい気にしながら話せた気がする。 結構ノウハウが入っていると思う「モジュール検索パス」とか「inference」の部分を元のスクリプトより丁寧に話したので1分くらい足りなくなる。 最後が結構早口になってしまったが、割とどうでも良いスライドを省略してギリギリ30分ちょっとで終了。 内容がマニアック過ぎて質問ゼロだったらどうしようとか心配してたので、想像以上に質問していただけてありがたい。 トーク終了後はslidoの質問が消えてしまうので落ち着いて中身を見れなかったのが残念。 代わりにTwitterでつぶやいてもらった内容は一通り全部確認した。聞いてくださった方ありがとうございます。

SQL クエリ解析による E2E データリネージの実現

最近はデータエンジニアリングからは離れてソフトウェアエンジニアリングに集中しているが、データリネージが知らない概念だから聞いてみようと参加。 業務で依存関係ばかり気にしているので、データ処理にも同じような概念があって面白い。 SQLから依存関係を抽出するというのは良さそう。

はじめての量子コンピューター

こちらも量子コンピューターをちゃんと調べたことが無いので参加。 基礎からの説明で非常に分かりやすい。 古典コンピューターと違って量子ゲートから出力される結果は確率分布になるのかな。 量子コンピューターのアルゴリズムというのもちゃんと考えると面白そう。

コーヒーブレイクとかパーティーとか

知り合いがいない状態だと中々立ち回りが難しいとは思ったが、発表内容に興味を持ってくれた方々と交流できたので参加して良かった。 他のスピーカーの方達の顔と名前が一致する用にしておこう。 交流させてもらった方達の話を聞くと、良くも悪くも自分がいるアーキテクチャレベルの方針をLinterで徹底しないといけない開発環境は少数派なのだろう。

発表してみて

ちょうどLinterについて色々調べた後にCfPがあったので、応募したら採択。 1ヶ月前くらいから本格的に発表の準備を始める。 当初考えていたPylintのチュートリアルをなぞる流れだと面白く無いなーとか思いながらストーリーを考える。 スライドにしてみる、通しで説明してみる、説明の流れが悪いところを再構成、を5回くらい繰り返してスライド完成。 説明のスクリプトを考える、喋ってみる、上手くしゃべれないところを再考とスライド修正、も5回くらい繰り返してちょうど30分安定して話せるくらい。 最終的に元々考えていた発表の流れからは結構変わった。 ちょうど1ヶ月くらい準備にかかって、直近の2週間は仕事と準備でかなり寝不足だった。

初めてだったのもあり準備がかなり大変だったが、Linterに興味を持ってくれた方から好意的なFBをもらえたので報われた。 JSのESLintはプラグイン化の方法が簡単に出てくるのに、PythonのLinterはどうすればよいの?が少しは解消されたかな。 アウトプット用に調べて自分の中で整理できたことが多かったのも収穫。

  • AST
  • Visitorパターン
  • Pylintのアーキテクチャ
  • モジュール検索パス
  • inference
  • スコープの管理

改めて、ドキュメンテーションされないような内側の仕組みを追って図解する的なことが好きだなーと思った。 発表形式でなくても良いのでできるだけアウトプットを継続していきたい。

最後に

普段自分では使わないエリアの話を色々と聞けてすごく面白いイベントだった。スタッフさんありがとうございます。 Pythonは比較的長く使っているが、初めてPythonコミュニティでちゃんとアウトプットできたのは良かった。 当日見きれなかったトークも多いのでYouTubeで確認していく。

Zappaのexcludeでパス指定はできない

Zappaとは

ZappaAWS用サーバーレスPythonのパッケージだ。 ZappaはLambda用のパッケージングやAPI GatewayやIAMの設定をしてくれて、Pythonアプリケーションを簡単にデプロイすることができる。

excludeオプション

Zappaにはパッケージングから除外したいファイルを指定するexcludeというオプションがある。このオプションはテストコードやドキュメントなどの実行時に不要なファイルをパッケージから除外するためのものであると思われる。このexcludeでgitの.gitignoreで使われるようなパスとファイル名を組み合わせた指定ができないことが分かった。 例えば doc/*.txt の様な指定はできない。

なぜパス指定ができないか

Zappaのコードを読んでみると、excludeオプションは zappa.core.create_lambda_zip 関数に渡されている。

def create_lambda_zip(
    self,
    prefix="lambda_package",
    handler_file=None,
    slim_handler=False,
    minify=True,
    exclude=None,
    exclude_glob=None,
    use_precompiled_packages=True,
    include=None,
    venv=None,
    output=None,
    disable_progress=False,
    archive_format="zip",
):

create_lambda_zip 関数を読み進めていくと、Python標準ライブラリの shutil.copytreeshutil.ignore_patterns使われている

copytree(
    cwd,
    temp_project_path,
    metadata=False,
    symlinks=False,
    ignore=shutil.ignore_patterns(*excludes),
)

copytreeのドキュメントには次のように書いてある。

ignore は copytree() が走査しているディレクトリと os.listdir() が返すその内容のリストを引数として受け取ることのできる呼び出し可能オブジェクトでなければなりません。 copytree() は再帰的に呼び出されるので、 ignore はコピーされる各ディレクトリ毎に呼び出されます。 ignore の戻り値はカレントディレクトリに相対的なディレクトリ名およびファイル名のシーケンス(すなわち第二引数の項目のサブセット)でなければなりません。それらの名前はコピー中に無視されます。 ignore_patterns() を用いて glob 形式のパターンによって無視する呼び出し可能オブジェクトを作成することが出来ます。

Python 3.10の実装だと

まずos.scandirで各ディレクトリ内のファイルを列挙している。 https://github.com/python/cpython/blob/v3.10.4/Lib/shutil.py#L554~L555

with os.scandir(src) as itr:
    entries = list(itr)

次にscandir()がyieldするos.DirEntryのname属性に対してignoreを実行している。

https://github.com/python/cpython/blob/v3.10.4/Lib/shutil.py#L453

ignored_names = ignore(os.fspath(src), [x.name for x in entries])

ignore_patternsが生成するignore関数は第一引数でパスを受け取るようになっているが、中をよく見ると第一引数は全く使われていない。

https://github.com/python/cpython/blob/v3.10.4/Lib/shutil.py#L443~L447

def _ignore_patterns(path, names):
    ignored_names = []
    for pattern in patterns:
        ignored_names.extend(fnmatch.filter(names, pattern))
    return set(ignored_names)

os.DirEntryのname属性にもパス情報は含まれていない。 そのためZappaのexcludeオプションにスラッシュ区切りのパスでパターンを指定しても、比較対象のos.DirEntry.nameにはマッチせずただ無視されるだけとなる。

Python3.7くらいまでの実装も確認したが、os.scandirの代わりにos.listdirが使われていたり、copytreeの実装が若干違うだけで振る舞いは変わらなそうだった。

psycopg2のインストールエラー ld: library not found for -lssl

Python3.8にpsycopg2をインストールしようとしたところ、エラーが出て失敗した。 インストールしようとしたのはpsycopg2の2.8で、最新の2.9は普通にインストールできた。

大量のエラーメッセージが表示されるが重要なのは以下の部分。

ld: library not found for -lssl
clang: error: linker command failed with exit code 1 (use -v to see invocation)
error: command 'clang' failed with exit status 1

どうやらビルド時にリンカ(ld)がOpenSSLのライブラリ(lssl)を見つけられないらしい。

自分の環境だとHomebrewでインストールしたOpenSSLが /usr/local/opt/openssl に存在するのでldがこの場所も探すように設定すれば良さそう。

環境変数 LIBRARY_PATH/usr/local/opt/openssl/lib を追加するか環境変数 LDFLAGS-L/usr/local/opt/openssl/lib を設定したらビルド及びインストールに成功した。

set -x LIBRARY_PATH $LIBRARY_PATH:/usr/local/opt/openssl/lib

# for bash users
# export LIBRARY_PATH=$LIBRARY_PATH:/usr/local/opt/openssl/lib
set -x LDFLAGS -L/usr/local/opt/openssl/lib

# for bash users
# export LDFLAGS=-L/usr/local/opt/openssl/lib

自分はfishを使っているので最終的にはユニバーサルな環境変数として設定して永続化した。(追記にしていないのは他に設定しているパスがなかったので)

set -Ux LIBRARY_PATH /usr/local/opt/openssl/lib

ちなみにpsycopg2にはビルドが不要で互換性のあるpsycopg2-binaryというパッケージがある。しかし、公式のドキュメントを読むと開発やテスト向けには良いがプロダクション向けではないので注意が必要。

psycopg vs psycopg-binary The psycopg2-binary package is meant for beginners to start playing with Python and PostgreSQL without the need to meet the build requirements.

If you are the maintainer of a published package depending on psycopg2 you shouldn’t use psycopg2-binary as a module dependency. For production use you are advised to use the > source distribution.

The binary packages come with their own versions of a few C libraries, among which libpq and libssl, which will be used regardless of other libraries available on the client: upgrading > the system libraries will not upgrade the libraries used by psycopg2. Please build psycopg2 from source if you want to maintain binary upgradeability.

テンプレートエンジンpug

Vue.jsのテンプレートが見慣れない記法で書かれていた。

<template lang="pug"> となっていて、pugというテンプレートエンジンを使っているらしい。

基本文法

タグ名のあとにスペースを開けてタグの中身を書く。

p 中身

<p>中身</p>

タグの属性は関数呼び出しのように書く。

input(
  type='checkbox'
  name='agreement'
  checked
)

<input type="checkbox" name="agreement" checked="checked" />

コード量が減ることが基本的なメリットらしい。 普段からタグの記述に不便さを感じていなかったので、あまり良さを実感できていない。

PyPIにパッケージを公開する手順の整理

はじめに

この記事ではPyPIにパッケージを公開するための手順について理解が曖昧だった部分を中心に整理してまとめます。 内容はPython公式のチュートリアル Packaging Python Projects を参考にしています。

PyPIについて

PyPIPython Package Indexの略でPythonのパッケージを管理しているリポジトリです。 普段pipを使ってインストールするパッケージはPyPIに登録されています。 PyPIにパッケージを登録する場合はDjangorequestsのような他のパッケージと重複しない一意なパッケージ名が必要になります。

パッケージの公開方法

パッケージを公開するための大まかな流れは次のようになります。

  1. パッケージ公開に必要なファイルの準備
  2. パッケージのビルド
  3. パッケージのアップロード

ディレクトリ構成

チュートリアルでは次のようなディレクトリ構成を採用していて、これがほぼ最小構成になります。

packaging_tutorial/
├── LICENSE
├── pyproject.toml
├── README.md
├── setup.cfg
├── src/
│   └── example_package/
│       ├── __init__.py
│       └── example.py
└── tests/
  • LICENSE: パッケージのライセンス
  • pyproject.toml: パッケージをビルドするための情報を書く設定ファイル
  • README.md: パッケージの利用者向けのドキュメント
  • setup.cfg: パッケージ名やバージョンを含むパッケージ自体の情報を書く設定ファイル
  • src/: パッケージを配置するディレクト
  • src/example_package/: パッケージ本体
  • tests/: テストコードを配置するディレクト

設定ファイルについて

Pythonの著名なパッケージではsetup.cfgの他にsetup.pyというファイルを利用していたり、pyproject.tomlがなかったりして闇雲に参考にすると混乱します。 そこでここでは各ファイルの役割を整理して説明します。

setup.cfg

setup.cfgsetuptools用の静的なメタデータconfigparserの形式で記述します。 setup.pyが実行可能なPythonコードであるのに対してsetup.cfgは静的な設定ファイルなのでバグが入り込む余地が少ないです。 そのため、基本的にはsetup.cfgに必要な設定を書くことが推奨されています。 チュートリアルでは次のような設定を使っています。

[metadata]
name = example-pkg-YOUR-USERNAME-HERE
version = 0.0.1
author = Example Author
author_email = author@example.com
description = A small example package
long_description = file: README.md
long_description_content_type = text/markdown
url = https://github.com/pypa/sampleproject
project_urls =
    Bug Tracker = https://github.com/pypa/sampleproject/issues
classifiers =
    Programming Language :: Python :: 3
    License :: OSI Approved :: MIT License
    Operating System :: OS Independent

[options]
package_dir =
    = src
packages = find:
python_requires = >=3.6

[options.packages.find]
where = src

利用可能なメタデータsetuptoolsのドキュメントにまとまっています。

setup.py

setup.pysetup.cfg同様にsetuptools用のメタデータですが、実行可能なPythonファイルであり、内容もインストール時に動的に決定します。 setup.pyではパッケージに関する必要な情報を引数にsetuptools.setup()関数を実行します。 上記のsetup.cfgsetup.pyで書く場合は次のようになります。

import setuptools

with open("README.md", "r", encoding="utf-8") as fh:
    long_description = fh.read()

setuptools.setup(
    name="example-pkg-YOUR-USERNAME-HERE",
    version="0.0.1",
    author="Example Author",
    author_email="author@example.com",
    description="A small example package",
    long_description=long_description,
    long_description_content_type="text/markdown",
    url="https://github.com/pypa/sampleproject",
    project_urls={
        "Bug Tracker": "https://github.com/pypa/sampleproject/issues",
    },
    classifiers=[
        "Programming Language :: Python :: 3",
        "License :: OSI Approved :: MIT License",
        "Operating System :: OS Independent",
    ],
    package_dir={"": "src"},
    packages=setuptools.find_packages(where="src"),
    python_requires=">=3.6",
)

昔からあるパッケージや複雑な処理が必要な大規模なパッケージではsetup.pyをよく利用していますが、最初にパッケージを公開する段階ではsetup.cfgで十分でしょう。 ただし、後述するpipのeditableインストールのために次のようなsetuptools.setup()関数を実行するだけのsetup.pyをプロジェクトに含めておくと良いです。

import setuptools


setuptools.setup()

pyproject.toml

pyproject.tomlはパッケージをビルドする手順を記載する設定ファイルになります。 ビルドに関する情報はbuild-systemテーブルに記載します。

[build-system]
requires = [
    "setuptools>=42",
    "wheel"
]
build-backend = "setuptools.build_meta"

build-system.requiresには公開したいパッケージをビルドするために必要なビルド用のパッケージを列挙します。 チュートリアルではsetuptoolswheelを採用しています。

build-system.build-backendにはパッケージをビルドするためのPythonオブジェクトを指定します。 チュートリアルではsetuptools.build_metaを採用しています。

setuptoolswheelが最もスタンダードなパッケージになりますが、他にも選択肢*1はあり、それらを採用する場合はbuild-systemを書き換えます。

pyproject.tomlがなくともパッケージのビルドは可能で、その場合は必要なパッケージを手動でインストールし、各パッケージのビルド用のコマンドを実行することになります。 例えばよく紹介されている次の手順は手動でsetuptoolswheelをインストールしてビルドする方法になります。

# ビルド用のパッケージをインストール
pip install --upgrade setuptools wheel

# ビルド用のコマンドを実行
python setup.py sdist bdist_wheel

pyproject.tomlはパッケージのビルドに関する情報以外も記載することができ、各パッケージはtool.パッケージ名テーブルの中を自由に使うことができます。*2例えばPythonのLinter/Formatterであるblackisortがオプションを設定する目的で使っています。

pipのeditableインストールについて

開発中のパッケージを開発環境にインストールしてインポートしたり実行可能なコマンドを利用可能にする場合にpipのeditable modeを有効にしてインストールすると、ソースコードを変更しても再インストール不要で変更が反映されます。 具体的には開発中のパッケージのプロジェクトルート(setup.cfgなどが置いてあるディレクトリ)で次のコマンドを実行します。

pip install -e .

pipのeditableインストールには以下のような変更の歴史があります。

  1. pip21.1より前のバージョンではeditableインストールをするにはsetup.pyが必須だった
  2. pip21.1の機能でsetup.py無しでsetup.cfgだけでもeditableインストールがサポートされた
  3. pip21.1.3setup.pyが無い場合はpyproject.tomlbuild-systemの指定が必須に変更された

そのため、editableインストールを利用する場合は次の3ファイルを揃えておくと古いバージョンのpipに対しても互換性が確保されて不具合が起きにくいでしょう。

  • パッケージのメタデータを記載したsetup.cfg
  • setuptools.setup()を実行するだけのsetup.py
  • build-systemを定義したpyproject.toml

ビルド方法

pyproject.tomlにビルド用のメタデータを記載している場合、buildパッケージだけでビルドができます。 buildパッケージを使う場合、ビルドに必要なsetuptoolsなどのパッケージはビルド用の一時的な仮想環境にインストールされるので、開発環境に影響を及ぼすこともありません。

# buildパッケージのインストール
pip install --upgrade build

# パッケージのビルド
python -m build

ビルドが完了するとdistディレクトリの中にビルド済みのファイルが作成されます。

アップロード方法

事前にアカウント登録が必要になります。

アップロードにはtwineというパッケージを使います。 最初は本番のPyPIではなくテスト用のTestPyPI にアップロードすることを推奨します。

twineのインストール

pip install --upgrade twine

パッケージのチェック

アップロード前に内容に問題が無いかチェックします。 何もエラーが出なければアップロードに進みます。

twine check dist/*

TestPyPI へのアップロード

アップロード

twine upload --repository testpypi dist/*

TestPyPIからのインストール

pipインストール時に--index-urlオプションを付けると本番のPyPI以外からもインストールできます。

pip install --index-url https://test.pypi.org/simple/ パッケージ名

本番のPyPIへのアップロード

twine upload dist/*

本番のPyPIからのインストール

通常のpipインストールと同様です。

pip install パッケージ名

DjangoのFormのレンダリング方法

動作確認用のFormとViewを準備する

forms.py

from django import forms


class MyForm(forms.Form):
    name = forms.CharField(max_length=10)
    age = forms.IntegerField(min_value=0, max_value=120)

views.py

from django.views.generic import FormView

from .forms import MyForm


class MyFormView(FormView):
    form_class = MyForm
    template_name = 'my_form.html'

    def form_valid(self, form):
        print(form.cleaned_data)
        return super().form_valid(form)

レンダリングを全てFormクラスに任せる

templates/my_form.html

{{ form }}

フィールドを個別にレンダリングする templates/my_form.html

<form action="" method="POST">
  {% csrf_token %}
  {{ form.non_field_errors }}
  {{ form.name.label_tag }}
  {{ form.name }}
  {{ form.name.errors }}
  {{ form.age.label_tag }}
  {{ form.age }}
  {{ form.age.errors }}
  <button type="submit">作成</button>
</form>

django-widget-tweaksでフィールドの属性を細かく制御する templates/my_form.html

{% load widget_tweaks %}

<form action="" method="POST">
  {% csrf_token %}
  {{ form.non_field_errors }}
  {{ form.name.label_tag }}
  {% render_field form.name class+="class1 class2" placeholder="名前を入力してください" %}
  {{ form.name.errors }}
  {{ form.age.label_tag }}
  {{ form.age }}
  {{ form.age.errors }}
  <button type="submit">作成</button>
</form>