CUBE SUGAR CONTAINER

技術系のこと書きます。

Ubuntu 16.04 LTS の NIC に固定 IP アドレスを振る

たまに設定する機会があると、毎回どうやるんだっけとなって調べるので。

今回使った環境は次の通り。

$ cat /etc/lsb-release
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=16.04
DISTRIB_CODENAME=xenial
DISTRIB_DESCRIPTION="Ubuntu 16.04.2 LTS"
$ uname -r
4.4.0-66-generic

NIC は enp9s0 という名前で認識されている。

/etc/network/interfaces を編集する

NIC の設定は /etc/network/interfaces という設定ファイルで行う。 初期設定だと、こんな感じで DHCP を使うようになっていると思う。

$ cat /etc/network/interfaces
# This file describes the network interfaces available on your system
# and how to activate them. For more information, see interfaces(5).

source /etc/network/interfaces.d/*

# The loopback network interface
auto lo
iface lo inet loopback

# The primary network interface
auto enp9s0
iface enp9s0 inet dhcp

これを、こんな感じにする。 設定方法を静的 (static) にした上で、アドレスとネットマスク、ゲートウェイと DNS サーバを指定する。

$ cat /etc/network/interfaces
# This file describes the network interfaces available on your system
# and how to activate them. For more information, see interfaces(5).

source /etc/network/interfaces.d/*

# The loopback network interface
auto lo
iface lo inet loopback

# The primary network interface
auto enp9s0
# DHCP はコメントアウトしておくとか
# iface enp9s0 inet dhcp
# 静的アドレスを設定する
iface enp9s0 inet static
address 192.168.0.10
netmask 255.255.255.0
gateway 192.168.0.1
dns-nameservers 192.168.0.1

編集するときはエディタを使っても良いし、例えば今回の編集内容であればこんな感じのコマンドで書き換えても良い。

$ sudo sed -i "/iface enp9s0 inet dhcp/d" /etc/network/interfaces
$ cat << 'EOF' | sudo tee -a /etc/network/interfaces > /dev/null
iface enp9s0 inet static
address 192.168.0.10
netmask 255.255.255.0
gateway 192.168.0.1
dns-nameservers 192.168.0.1
EOF

後は再起動して設定を反映する。

$ sudo shutdown -r now

いじょう。

Ubuntu 16.04 LTS で ISO ファイルをメディアに書き込む

例えばインストール用の ISO ファイルをダウンロードしてきて、それを DVD-R とかに焼くときにやり方について。

今回使った環境は次の通り。

$ cat /etc/lsb-release
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=16.04
DISTRIB_CODENAME=xenial
DISTRIB_DESCRIPTION="Ubuntu 16.04.2 LTS"
$ uname -r
4.4.0-66-generic

今回使った環境では、メディアに書き込むためのデバイスは /dev/sr0 で認識されている。

$ ls /dev/sr0
/dev/sr0

書き込む ISO ファイルは Ubuntu のインストールメディアにした。

$ file ubuntu-14.04.5-server-amd64.iso
ubuntu-14.04.5-server-amd64.iso: DOS/MBR boot sector ISO 9660 CD-ROM filesystem data (DOS/MBR boot sector) 'Ubuntu-Server 14.04.5 LTS amd64' (bootable); partition 2 : ID=0xef, start-CHS (0x3ff,254,63), end-CHS (0x3ff,254,63), startsector 271196, 4544 sectors

まずは書き込むのに使うツールの入ったパッケージをインストールする。

$ sudo apt-get -y install growisofs

書き込みは growisofs コマンドでやる。 引数として、デバイスと書き込む ISO ファイルを指定してやる。

$ growisofs -Z <dev>=<isofile>

今回の例なら、こんな感じ。 内部的には dd コマンドが使われているみたいだ。

$ growisofs -Z /dev/sr0=ubuntu-14.04.5-server-amd64.iso
Executing 'builtin_dd if=ubuntu-14.04.5-server-amd64.iso of=/dev/sr0 obs=32k seek=0'
/dev/sr0: "Current Write Speed" is 16.4x1352KBps.
   23035904/649068544 ( 3.5%) @4.8x, remaining 2:15 RBU 100.0% UBU   0.2%
   52232192/649068544 ( 8.0%) @6.3x, remaining 1:31 RBU 100.0% UBU  99.4%
   81821696/649068544 (12.6%) @6.4x, remaining 1:16 RBU 100.0% UBU  99.6%
  111935488/649068544 (17.2%) @6.5x, remaining 1:11 RBU 100.0% UBU  99.6%
  142442496/649068544 (21.9%) @6.6x, remaining 1:04 RBU  99.9% UBU  99.6%
  175964160/649068544 (27.1%) @7.3x, remaining 0:56 RBU 100.0% UBU  99.6%
  209977344/649068544 (32.4%) @7.4x, remaining 0:52 RBU 100.0% UBU  99.6%
  244482048/649068544 (37.7%) @7.5x, remaining 0:46 RBU 100.0% UBU  99.6%
  276889600/649068544 (42.7%) @7.0x, remaining 0:41 RBU 100.0% UBU  99.6%
  312344576/649068544 (48.1%) @7.7x, remaining 0:37 RBU 100.0% UBU  99.6%
  348291072/649068544 (53.7%) @7.8x, remaining 0:32 RBU 100.0% UBU  99.6%
  384729088/649068544 (59.3%) @7.9x, remaining 0:28 RBU 100.0% UBU  99.4%
  418971648/649068544 (64.5%) @7.4x, remaining 0:24 RBU 100.0% UBU  99.4%
  456359936/649068544 (70.3%) @8.1x, remaining 0:20 RBU 100.0% UBU  99.6%
  494206976/649068544 (76.1%) @8.2x, remaining 0:15 RBU 100.0% UBU  99.4%
  532545536/649068544 (82.0%) @8.3x, remaining 0:12 RBU 100.0% UBU  99.2%
  571375616/649068544 (88.0%) @8.4x, remaining 0:07 RBU 100.0% UBU  99.4%
  607944704/649068544 (93.7%) @7.9x, remaining 0:04 RBU 100.0% UBU  99.4%
  647725056/649068544 (99.8%) @8.6x, remaining 0:00 RBU   4.0% UBU  99.6%
builtin_dd: 316928*2KB out @ average 7.2x1352KBps
/dev/sr0: flushing cache
/dev/sr0: updating RMA
/dev/sr0: closing session
/dev/sr0: reloading tray

あとは焼きあがったメディアを使ってインストールするだけ。

Ubuntu 16.04 LTS のキーボードを日本語 (JIS) に変更する

2018-08-25 追記: Ubuntu 18.04 LTS でも同じ操作で変更できた

たまにインストールしたとき間違えて英語 (US) に設定して後から直すことになるので。

使った環境は次の通り。

$ cat /etc/lsb-release
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=16.04
DISTRIB_CODENAME=xenial
DISTRIB_DESCRIPTION="Ubuntu 16.04.2 LTS"
$ uname -r
4.4.0-66-generic

キーボードの設定を変更するには dpkg-reconfigure コマンドを keyboard-configuration 引数をつけて実行する。

$ sudo dpkg-reconfigure keyboard-configuration

表示されるウィザードに沿って設定を入力していく。 二番目の設定で Japanese にするのがポイント。

Generic 105-key (Intel) PC > Japanese > Japanese > The default for the keyboard layout > No compose key

いじょう。

Python: Keras/TensorFlow の学習を CPU の拡張命令で高速化する (Mac OS X)

今回のネタは TensorFlow を使っていると、いつも目にしていた警告について。

それは、次のようなもの。

W tensorflow/core/platform/cpu_feature_guard.cc:45] The TensorFlow library wasn't compiled to use SSE4.1 instructions, but these are available on your machine and could speed up CPU computations.
W tensorflow/core/platform/cpu_feature_guard.cc:45] The TensorFlow library wasn't compiled to use SSE4.2 instructions, but these are available on your machine and could speed up CPU computations.
W tensorflow/core/platform/cpu_feature_guard.cc:45] The TensorFlow library wasn't compiled to use AVX instructions, but these are available on your machine and could speed up CPU computations.

どうやら TensorFlow は CPU の拡張命令を使うことができるらしい。 しかし、PyPI 経由で一般に配布されているバイナリパッケージでは、それらは有効になっていない。 もちろん、同じ x86 系 CPU であってもどんな拡張命令セットを持っているかは製品によって異なるので、これは当然の判断だろう。

そこで、今回は CPU の拡張命令を有効にして TensorFlow をソースコードからコンパイルしてみることにした。 そうすることで、どれくらい学習を高速化できるだろうか?というのが主題となる。

先に結論から書いてしまうと、今回使った環境では大体 30% くらい学習が速くなった。 ぶっちゃけ、これくらいであれば素直に GPU を使える環境を用意した方が良いと思う。

blog.amedama.jp

ただし、自分で使う環境向けにチューンするという面において、自前でコンパイルするのは良い選択肢かもしれないと感じた。

今回使った環境は次の通り。

$ sw_vers
ProductName:    Mac OS X
ProductVersion: 10.12.3
BuildVersion:   16D32
$ python --version
Python 3.6.0

あらかじめ、計算速度のベンチマークに使うアプリケーションをダウンロードしておこう。

$ curl -O https://raw.githubusercontent.com/fchollet/keras/master/examples/mnist_cnn.py
$ echo 'K.clear_session()' >> mnist_cnn.py

拡張命令を使わない場合

まずは、CPU の拡張命令を使わない場合にかかる時間を調べておこう。

PyPI から汎用のバイナリパッケージで TensorFlow をインストールする。 ベンチマークにするアプリケーションを実行するために Keras も入れておく。

$ pip install keras tensorflow

インストールできたら Python の REPL を起動する。

$ python

REPL が起動したら MNIST のデータセットをダウンロードしておこう。 これには少し時間がかかる。

>>> from keras.datasets import mnist
Using TensorFlow backend.
>>> mnist.load_data()
Downloading data from https://s3.amazonaws.com/img-datasets/mnist.pkl.gz

終わったら REPL から抜けておく。

>>> exit()

それでは、まずは CPU の拡張命令を使わない場合にかかる時間を測ろう。 time コマンド経由でベンチマーク用のアプリケーションを実行する。

$ time python mnist_cnn.py
Using TensorFlow backend.
X_train shape: (60000, 28, 28, 1)
60000 train samples
10000 test samples
Train on 60000 samples, validate on 10000 samples
Epoch 1/12
W tensorflow/core/platform/cpu_feature_guard.cc:45] The TensorFlow library wasn't compiled to use SSE4.1 instructions, but these are available on your machine and could speed up CPU computations.
W tensorflow/core/platform/cpu_feature_guard.cc:45] The TensorFlow library wasn't compiled to use SSE4.2 instructions, but these are available on your machine and could speed up CPU computations.
W tensorflow/core/platform/cpu_feature_guard.cc:45] The TensorFlow library wasn't compiled to use AVX instructions, but these are available on your machine and could speed up CPU computations.
60000/60000 [==============================] - 92s - loss: 0.3741 - acc: 0.8859 - val_loss: 0.0880 - val_acc: 0.9718
...(省略)...
python mnist_cnn.py  2452.87s user 859.80s system 302% cpu 18:15.11 total

CPU の拡張命令を使わない場合には 2450 秒かかった。

拡張命令を使う場合

次は CPU の拡張命令を使う場合について。 警告メッセージを見る限り、今回使うマシンでは SSE4.1 SSE4.2 AVX という拡張命令が使える。

それらの拡張命令を有効にするためにソースコードから TensorFlow をインストールする。 ソースコードからのインストールには次のドキュメントを参照すると良い。 Download and Setup  |  TensorFlow

まずは、Homebrew を使って依存パッケージをインストールする。

$ brew install bazel swig

次に TensorFlow のソースコードをクローンする。

$ git clone https://github.com/tensorflow/tensorflow.git
$ cd tensorflow

先ほどベンチマークしたのと同じバージョンのタグをチェックアウトしよう。

$ git checkout v1.0.1

次にインストールするための環境を configure で設定していく。 今回は CUDA を使わないので、さほど難しくはない。

$ ./configure
Please specify the location of python. [Default is /Users/amedama/.virtualenvs/py36/bin/python]:
Please specify optimization flags to use during compilation [Default is -march=native]:
Do you wish to use jemalloc as the malloc implementation? (Linux only) [Y/n] n
jemalloc disabled on Linux
Do you wish to build TensorFlow with Google Cloud Platform support? [y/N]
No Google Cloud Platform support will be enabled for TensorFlow
Do you wish to build TensorFlow with Hadoop File System support? [y/N]
No Hadoop File System support will be enabled for TensorFlow
Do you wish to build TensorFlow with the XLA just-in-time compiler (experimental)? [y/N]
No XLA support will be enabled for TensorFlow
Found possible Python library paths:
  /Users/amedama/.virtualenvs/py36/lib/python3.6/site-packages
Please input the desired Python library path to use.  Default is [/Users/amedama/.virtualenvs/py36/lib/python3.6/site-packages]

Using python library path: /Users/amedama/.virtualenvs/py36/lib/python3.6/site-packages
Do you wish to build TensorFlow with OpenCL support? [y/N]
No OpenCL support will be enabled for TensorFlow
Do you wish to build TensorFlow with CUDA support? [y/N]
No CUDA support will be enabled for TensorFlow
Configuration finished
Extracting Bazel installation...
..........................................................
INFO: Starting clean (this may take a while). Consider using --expunge_async if the clean takes more than several minutes.
.........................................................
INFO: All external dependencies fetched successfully.

設定が終わったら次に bazel を使ってビルドする。 ここで使用する CPU の拡張命令を指定する。 --copt=-m の後ろに拡張命令の名前を入力しよう。 この処理には時間がかかるので気長に待つ。

$ bazel build -c opt --copt=-mavx --copt=-msse4.1 --copt=-msse4.2 //tensorflow/tools/pip_package:build_pip_package

もし、ここで指定しなければ CPU 拡張命令を使わない汎用なパッケージになる。

続けて Wheel パッケージをビルドする。 二番目に渡している /tmp/tensorflow_pkg が Wheel パッケージを保存する場所になる。

$ bazel-bin/tensorflow/tools/pip_package/build_pip_package /tmp/tensorflow_pkg

これで CPU の拡張命令が有効になった Wheel パッケージができた。

$ ls /tmp/tensorflow_pkg
tensorflow-1.0.1-cp36-cp36m-macosx_10_12_x86_64.whl

既にインストールされている汎用なパッケージをアンインストールした上で、インストールしよう。

$ pip uninstall -y tensorflow
$ pip install /tmp/tensorflow_pkg/tensorflow-1.0.1-cp36-cp36m-macosx_10_12_x86_64.whl
$ pip list --format=columns | grep -i tensorflow
tensorflow       1.0.1

さて、先ほどと同じようにベンチマークとなるアプリケーションを実行してみよう。 どれくらい速くなるだろうか。

$ time python mnist_cnn.py
Using TensorFlow backend.
X_train shape: (60000, 28, 28, 1)
60000 train samples
10000 test samples
Train on 60000 samples, validate on 10000 samples
Epoch 1/12
60000/60000 [==============================] - 75s - loss: 0.3735 - acc: 0.8859 - val_loss: 0.0888 - val_acc: 0.9724
...(省略)...
python mnist_cnn.py  1871.67s user 901.15s system 296% cpu 15:34.90 total

今度は 1870 秒で終わった。

結果は CPU の拡張命令を使わない場合が 2450 秒で、使った場合が 1870 秒となった。 つまり、処理速度が 30% ほど速くなった。 また、一回のエポックでいうと、だいたい 20% ほど速くなっているようだ。

まとめ

今回は、普段 TensorFlow を使っているときに表示される警告が気になってソースコードからビルドを試してみた。 PyPI で配布されている TensorFlow は CPU に依存しない汎用なバイナリになっている。 もし、自分の CPU に合わせて拡張命令を使ったバイナリを作りたいときは、ソースコードからコンパイルする必要がある。 CPU の拡張命令を有効にすると、今回のケースでは 30% ほど学習が速くなった。 GPU を使った学習の高速化に比べると、これは微々たるもののように感じる。 しかし、自分の環境に向けてチューンされたバイナリを作るという意味では、ソースコードからのコンパイルは有用かもしれない。

いじょう。

macOS Sierra から Ubuntu 16.04 LTS のディスクを NFS でマウントする

普段の開発環境として Mac を使っているものの、一部の作業を別の Linux マシンでやりたい、という場面があった。 そこで Mac から Ubuntu のディスクを NFS でマウントすることにした。 こうすれば開発環境としては Mac を使いつつ、成果物を使った作業は Ubuntu に SSH でログインして実施できる。

今回使った環境は次の通り。 まず、NFS クライアントとなる Mac の OS は macOS Sierra を使っている。

$ sw_vers
ProductName:    Mac OS X
ProductVersion: 10.12.3
BuildVersion:   16D32

そして、NFS サーバとなる Ubuntu については 16.04 LTS を使った。 クライアントからマウントするときに IP アドレスの指定が必要なので 192.168.0.10/24 を割り振ってある。

$ cat /etc/lsb-release
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=16.04
DISTRIB_CODENAME=xenial
DISTRIB_DESCRIPTION="Ubuntu 16.04.2 LTS"
$ uname -r
4.4.0-64-generic

Ubuntu

まずは Ubuntu で NFS サーバを構築していく。 NFS サーバの構築に必要なパッケージをインストールしよう。

$ sudo apt-get install -y nfs-kernel-server

次に NFS サーバで外部に公開するディレクトリを作成する。 ここでは /srv/nfs を使うことにした。 クライアントからファイルの書き込みができるように権限も調整しておく。

$ sudo mkdir -p /srv/nfs
$ sudo chmod a+w /srv/nfs

ディレクトリが用意できたら、次に NFS サーバの設定を行う。 これには /etc/exports という設定ファイルを使う。 公開するディレクトリと、公開先のネットワーク、そしてオプションを指定する。

$ cat << 'EOF' | sudo tee -a /etc/exports > /dev/null
/srv/nfs 192.168.0.0/24(rw,no_subtree_check)
EOF

上記で指定しているオプションの意味は次の通り。 - rw: 書き込み権限を付与する - no_subtree_check: ファイルが見つからないときに子ディレクトリを探さない

設定が終わったら NFS サーバのサービスを起動する。

$ sudo systemctl start nfs-kernel-server
$ sudo systemctl enable nfs-kernel-server

exportfs コマンドを使って状況を確認する。 次のように公開しているディレクトリやネットワーク、オプションが表示されれば上手くいっている。

$ sudo exportfs -v
/srv/nfs        192.168.0.0/24(rw,wdelay,root_squash,no_subtree_check,sec=sys,rw,root_squash,no_all_squash)

手動マウント

これで NFS サーバは構築ができたので、次は NFS クライアントの Mac からそれをマウントしていく。 まずは手動で一時的にマウントするやり方から。

最初に NFS でマウントするための空ディレクトリを用意しておく。

$ sudo mkdir -p /Volumes/NFS

あとは mount_nfs コマンドを使ってマウントするだけ。 NFS でマウントする元となる IP アドレスとディレクトリと、マウントする先のディレクトリを指定する。

$ sudo mount_nfs -P 192.168.0.10:/srv/nfs /Volumes/NFS

あとは、マウントしたディレクトリで色々と作業すれば良い。

$ cat << 'EOF' > /Volumes/NFS/greeting.txt
Hello, World!
EOF

ちゃんとファイルの書き込みもできている。

$ cat /Volumes/NFS/greeting.txt
Hello, World!

Ubuntu の方で確認しても、ちゃんとファイルが書き込まれている。

$ cat /srv/nfs/greeting.txt
Hello, World!

使い終わったらアンマウントしよう。

$ sudo umount /Volumes/NFS

自動マウント

先ほどのやり方では再起動するとアンマウントされてしまうので、主に一時的に使うための方法だった。 しかし、今回のユースケースでは恒常的にマウントしておきたい。 次は再起動してもマウントされ続けるやり方について。

まずは、先ほどと同じようにマウントするための空ディレクトリを用意しておく。 ただし、今回は新しくルートディレクトリ配下にディレクトリを用意することにした。 なぜなら、このやり方では指定したディレクトリを丸ごと使ってしまうため。 /Volumes は外付けハードディスクなどもマウントされる場所なので、専有してしまうのは困る。

$ sudo mkdir -p /mnt

次に /etc/auto_master という設定ファイルに先ほど用意したマウントポイントを書く。 そして、そのマウントポイントの設定ファイルの場所を指定する。 今回の例では設定ファイルを /etc/autofs_nfs という名前にしたけど、これは別になんでも構わない。

$ cat << EOF | sudo tee -a /etc/auto_master > /dev/null
/mnt                    /etc/autofs_nfs
EOF

上記のコマンドを実行すると、設定ファイルは次のようになる。 書式については auto_master(5) を参照のこと。

$ cat /etc/auto_master
#
# Automounter master map
#
+auto_master       # Use directory service
/net            -hosts      -nobrowse,hidefromfinder,nosuid
/home           auto_home   -nobrowse,hidefromfinder
/Network/Servers    -fstab
/-          -static
/mnt                    /etc/autofs_nfs

次に、先ほど指定したマウントポイントに対する設定ファイルを記述する。 ここはなんとなく NFS のオプションと近いので分かりやすいと思う。 最初に指定した NFS というのがマウントするときのディレクトリ名になる。 つまり、/mnt/NFS という形でマウントされる。

$ cat << 'EOF' | sudo tee -a /etc/autofs_nfs > /dev/null
NFS -fstype=nfs,rw,resvport 192.168.0.10:/srv/nfs
EOF

できあがる設定ファイルは、こんな感じ。

$ cat /etc/autofs_nfs
NFS -fstype=nfs,rw,resvport 192.168.0.10:/srv/nfs

あとは autofsd のサービスを起動する。

$ sudo launchctl stop com.apple.autofsd
$ sudo launchctl start com.apple.autofsd

これでマウントされる。

$ mount | grep mnt
map /etc/autofs_nfs on /mnt (autofs, automounted, nobrowse)
192.168.0.10:/srv/nfs on /mnt/NFS (nfs, nodev, nosuid, automounted, nobrowse)
$ df -ah | grep mnt
map /etc/autofs_nfs     0Bi    0Bi    0Bi   100%        0          0  100%   /mnt
192.168.0.10:/srv/nfs  901Gi  7.6Gi  848Gi     1%   169558   59836842    0%   /mnt/NFS

マウントされたディレクトリを見ると、先ほど書き込んだファイルも見えている。

$ cat /mnt/NFS/greeting.txt
Hello, World!

これで、恒常的に NFS をマウントして作業するための準備が整った。

めでたしめでたし。

Ubuntu 16.04 LTS のデフォルトエディタを nano から変更する

Ubuntu 16.04 LTS を使っていたところ、デフォルトのエディタが nano になっていた。 普段 nano は使っていないので vim に変更したい、というのが今回のお話。

使った環境は次の通り。

$ cat /etc/lsb-release
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=16.04
DISTRIB_CODENAME=xenial
DISTRIB_DESCRIPTION="Ubuntu 16.04.2 LTS"
$ uname -r
4.4.0-64-generic

下準備として、ひとまず vim をインストールしておく。

$ sudo apt-get install -y vim

デフォルトのエディタが nano になってしまう

例えば visudo コマンドとかを実行すると nano が起動してしまう。

$ sudo visudo

起動するエディタを指定して実行する

システムが起動するエディタは環境変数 EDITOR で指定できる。 ただし sudo コマンドと組み合わせて使うときは注意が必要で -E オプションもつけなきゃいけない。 このオプションがあると特権ユーザの権限で visudo コマンドを実行するときに環境変数を引き継ぐことができる。

$ EDITOR=vim sudo -E visudo

デフォルトのエディタを変更する

ただ、さっきのように毎回環境変数を指定したり -E オプションをつけて…とするのはめんどくさい。 なのでデフォルトのエディタを変更してしまおう。

デフォルトのエディタを変更するには update-alternatives コマンドに --config editor オプションを指定する。 選ぶことのできるエディタが選択肢の形で表示されるので、どれを使うか番号を入力しよう。

$ sudo update-alternatives --config editor
There are 4 choices for the alternative editor (providing /usr/bin/editor).

  Selection    Path                Priority   Status
------------------------------------------------------------
* 0            /bin/nano            40        auto mode
  1            /bin/ed             -100       manual mode
  2            /bin/nano            40        manual mode
  3            /usr/bin/vim.basic   30        manual mode
  4            /usr/bin/vim.tiny    10        manual mode

Press <enter> to keep the current choice[*], or type selection number: 3
update-alternatives: using /usr/bin/vim.basic to provide /usr/bin/editor (editor) in manual mode

これで、特に環境変数などを指定することなく vim が起動するようになる。

$ sudo visudo

めでたしめでたし。

Python: python-fire の CLI 自動生成を試す

今回は Google が公開した python-fire というパッケージを試してみた。 python-fire では、クラスやモジュールを渡すことで、定義されている関数やメソッドを元に CLI を自動で生成してくれる。

ただし、一つ注意すべきなのは、できあがる CLI はそこまで親切な作りではない、という点だ。 実際にユーザに提供するような CLI を実装するときは、従来通り Click のようなフレームワークを使うことになるだろう。 では python-fire はどういったときに活躍するかというと、これは開発時のテストだと思う。 実装した内容をトライアンドエラーするための CLI という用途であれば python-fire は非常に強力なパッケージだと感じた。

今回使った環境は次の通り。

$ sw_vers
ProductName:    Mac OS X
ProductVersion: 10.12.3
BuildVersion:   16D32
$ python --version
Python 3.6.0

インストール

インストールは Python のパッケージツールの pip を使ってできる。

$ pip install fire

もし pip がインストールされていないときは、あらかじめ入れておこう。

$ curl https://bootstrap.pypa.io/get-pip.py | sudo python

あと、これは完全に蛇足だけどシステムの Python 実行環境にそのまま入れるのはおすすめしない。 色んなパッケージを試すときは、システムからは独立した Python 仮想環境を作って入れた方が良い。 以下は、例えば virtualenv を使ったやり方。

$ sudo pip install virtualenv
$ mkdir -p venv
$ virtualenv venv

これで、最低限のパッケージの入った Python 仮想環境ができる。

(venv) $ pip list --format=columns
Package    Version
---------- -------
appdirs    1.4.3
packaging  16.8
pip        9.0.1
pyparsing  2.2.0
setuptools 34.3.1
six        1.10.0
wheel      0.29.0

python-fire をインストールすると、次のようなパッケージが入る。 意外と依存パッケージが多い。

(venv) $ pip install fire
(venv) $ pip list --format=columns
Package          Version
---------------- -------
appdirs          1.4.2
appnope          0.1.0
decorator        4.0.11
fire             0.1.0
ipython          5.3.0
ipython-genutils 0.1.0
packaging        16.8
pexpect          4.2.1
pickleshare      0.7.4
pip              9.0.1
prompt-toolkit   1.0.13
ptyprocess       0.5.1
Pygments         2.2.0
pyparsing        2.2.0
setuptools       34.3.1
simplegeneric    0.8.1
six              1.10.0
traitlets        4.3.2
wcwidth          0.1.7
wheel            0.29.0

基本的な使い方

まずは python-fire の基本的な使い方から見ていく。 とはいっても、その使い方は至ってシンプル。 例えば CLI を自動生成したいクラスがあるなら、それを fire.Fire() コマンドに渡すだけ。

次のコマンドを実行すると helloworld.py というファイルでサンプルコードが保存される。

$ cat << 'EOF' > helloworld.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-


class MyClass(object):

    def greet(self):
        print('Hello, World!')


if __name__ == '__main__':
    # fire をインポートする
    import fire
    # コマンドラインで呼び出したいクラスを指定する
    fire.Fire(MyClass)
EOF

上記のサンプルコードでは MyClass というクラスを python-fire に渡している。 このクラスには greet() というメソッドがあって、文字列をプリントする内容になっている。

上記を何の引数も渡さずに実行してみよう。

$ python helloworld.py
Type:        MyClass
String form: <__main__.MyClass object at 0x10bb64f28>
File:        ~/Documents/temporary/helloworld.py

Usage:       helloworld.py
             helloworld.py greet

すると Usage が出力されることがわかる。

上記の出力に沿ってオプションを渡してみよう。 具体的には、MyClass の持つメソッド名である greet を指定する。

$ python helloworld.py greet
Hello, World!

これだけで MyClass#greet() の処理が呼び出された。

関数・メソッドに引数を渡す

先ほどの例ではメソッドの呼び出しを試した。 ただ、そのメソッドには引数がなかった。 次は引数のあるメソッドを呼び出すときについて見てみる。

以下のコマンドを実行すると、引数のあるメソッドを定義したサンプルコードができる。

$ cat << 'EOF' > calculate.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-


class MyClass(object):

    def add(self, a, b=1):
        return a + b


if __name__ == '__main__':
    import fire
    fire.Fire(MyClass)
EOF

上記では MyClass#add() メソッドが引数として ab を受け取る。 そのうち b に関してはデフォルト引数が指定されている。

上記で用意したサンプルコードを先ほどと同じようにメソッド名だけ入れて実行してみよう。 この場合、必要な引数 a が入力されていないためトレースが表示される。

$ python calculate.py add
Fire trace:
1. Initial component
2. Instantiated class "MyClass" (calculate.py:5)
3. Accessed property "add" (calculate.py:7)
4. ('The function received no value for the required argument:', 'a')

Type:        method
String form: <bound method MyClass.add of <__main__.MyClass object at 0x109fba518>>
File:        ~/Documents/temporary/calculate.py
Line:        7

Usage:       calculate.py add A [B]
             calculate.py add --a A [--b B]

Usage を見ると、メソッド名である add に続いて引数を渡せば良いことがわかる。

上記にもとづいてメソッド名に続いて引数を渡してみよう。 それぞれ引数の ab に対応する。

$ python calculate.py add 1 2
3

引数の b はデフォルト引数が指定されているので省略できる。

$ python calculate.py add 1
2

引数に渡す内容の指定を順不定にしたいときは -- を使って引数名を指定してやる。

$ python calculate.py add --a=1 --b=2
3

コンストラクタに引数を渡す

先ほどまでの例では CLI を自動生成したいクラスのコンストラクタに引数がなかった。 現実には、何も引数を取らないコンストラクタの方が珍しいと思う。 そこで、次はコンストラクタに引数を取る場合を試してみる。

次のコマンドを実行するとコンストラクタに引数を取るサンプルコードができる。

$ cat << 'EOF' > constructor.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-


class MyClass(object):

    def __init__(self, msg):
        """コンストラクタに引数 msg を取る"""
        self.msg = msg

    def greet(self):
        print(self.msg)


if __name__ == '__main__':
    import fire
    fire.Fire(MyClass)
EOF

先ほどの例と同じように引数を何も指定せず実行してみよう。 するとコンストラクタの引数が指定されていないという内容でトレースが表示される。

$ python constructor.py
Fire trace:
1. Initial component
2. ('The function received no value for the required argument:', 'msg')

Type:        type
String form: <class '__main__.MyClass'>
File:        ~/Documents/temporary/constructor.py
Line:        5

Usage:       constructor.py MSG
             constructor.py --msg MSG

どうやらコンストラクタの引数をまずは指定する必要があるらしい。

上記の表示に沿って、コンストラクタの --msg 引数を指定してみよう。

$ python constructor.py --msg="Hello World"
Type:           MyClass
String form:    <__main__.MyClass object at 0x102b303c8>
File:           ~/Documents/temporary/constructor.py
Init docstring: コンストラクタに引数 msg を取る

Usage:          constructor.py --msg='Hello World' 
                constructor.py --msg='Hello World' greet
                constructor.py --msg='Hello World' msg

すると、今度は実行できるメソッド名が表示されるようになった。 メソッド greet() の下にある msg はインスタンスのメンバとして引数を保存しているため表示されているんだろう。

上記の表示に沿ってコンストラクタの引数を入力しながらメソッドを指定する。

$ python constructor.py --msg='Hello World' greet
Hello World

これでコンストラクタに引数を取るクラスであっても実行できるようになった。

関数から自動生成する

これまでの例ではクラスに対して CLI を自動生成する例だった。 次は関数に対して試してみることにしよう。

次のコマンドを実行すると関数を使うパターンのサンプルコードができる。

$ cat << 'EOF' > function.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-


def greet(msg='Hello, World!'):
    print(msg)


if __name__ == '__main__':
    import fire
    fire.Fire(greet)
EOF

とはいえ、やっていることはこれまでと変わらない。 渡すものがクラスの代わりに関数になっただけ。

今度も、特に何も指定せずまずは実行してみよう。

$ python function.py
Hello, World!

今回に関しては、これだけで事足りてしまった。

ちなみに、上記の場合はデフォルト引数が指定されていたので動いた。 もちろん、次のようにして引数を指定することもできる。

$ python function.py --msg="Hajimemashite Sekai"
Hajimemashite Sekai

ちなみに、ここで面白い挙動に気づいた。 引数を指定するときに内容にカンマが含まれていると、型が自動的にタプルになるようだ。

$ python function.py --msg="Hajimemashite, Sekai"
('Hajimemashite', 'Sekai')

型の自動判別

先ほどの例を元に、どういった内容を引数に指定すると、どの型と判別するのか調べてみた。

次のサンプルコードでは受け取った引数の型を出力する。

$ cat << 'EOF' > argtype.py 
#!/usr/bin/env python
# -*- coding: utf-8 -*-


def print_type(obj):
    print(type(obj))


if __name__ == '__main__':
    import fire
    fire.Fire(print_type)
EOF

色んな内容を渡してみた結果が次の通り。 いきなりタプルになったのは驚いたけど、文字列を指定したいなら '' で囲むのがセオリーなようだ。

$ python argtype.py --obj="foo"
<class 'str'>

$ python argtype.py --obj="1"  
<class 'int'>
$ python argtype.py --obj="1.0"
<class 'float'>
$ python argtype.py --obj="'1'"
<class 'str'>

$ python argtype.py --obj="1,2"
<class 'tuple'>
$ python argtype.py --obj="'1,2'"
<class 'str'>

$ python argtype.py --obj="[1, 2]"
<class 'list'>
$ python argtype.py --obj="{1, 2}"
<class 'set'>
$ python argtype.py --obj="{1: 2}"
<class 'dict'>

モジュールから自動生成する

次は、おそらく一番使う場面が多そうなやり方。 モジュールをそのまま指定して CLI を自動生成してしまうやり方。

次のコマンドを実行するとモジュールをそのまま指定するサンプルコードができる。 この場合は、特に何も指定せず fire.Fire() をインスタンス化すれば良い。

$ cat << 'EOF' > module.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-

class MyClass(object):

    def greet(self):
        print('Hello, World!')


def greet(msg='Hello, World!'):
    print(msg)


def add(a, b):
    return a + b


if __name__ == '__main__':
    import fire
    fire.Fire()
EOF

モジュールには MyClass クラスや greet() および add() 関数が定義されている。

次も、特に何も指定せずまずは実行してみよう。 ただ、これだけだと何が指定できるのかよく分からない出力になる。

$ python module.py
MyClass: <class '__main__.MyClass'>
greet:   <function greet at 0x10e53ee18>
add:     <function add at 0x10e67d378>
fire:    <module 'fire' from '/Users/amedama/.virtualenvs/fire/lib/python3.6/site-packages/fire/__init__.py'>

そこで --help オプションを指定してみよう。 間にある -- は python-fire 自体に渡す引数と、自動生成した CLI に渡す引数を区別するために使う。

$ python module.py -- --help
Type:        dict
String form: {'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <_frozen_importlib_e <...> ule 'fire' from '/Users/amedama/.virtualenvs/fire/lib/python3.6/site-packages/fire/__init__.py'>}
Length:      13

Usage:       module.py 
             module.py MyClass
             module.py greet
             module.py add
             module.py fire

すると、実行できる内容が出力されるようになった。

関数に関しては、これまでと特に変わらないやり方で実行できる。

$ python module.py greet
Hello, World!
$ python module.py add 1 2
3

インスタンスメソッドについては、クラス名とメソッド名を続けて入力すれば良い。

$ python module.py MyClass greet
Hello, World!

インタラクティブシェルに入る

最初に依存パッケージの中に ipython があったことに気づいていたかもしれない。 ipython は高機能な Python の REPL で python-fire の引数に --interactive を指定すると、それが起動するようになっている。 起動した ipython では特にインポートなどをすることなくモジュールの中で定義されているクラスなどを使うことができる。

$ python helloworld.py -- --interactive
Fire is starting a Python REPL with the following objects:
Modules: fire
Objects: MyClass, component, helloworld.py, result, trace

Python 3.6.0 (default, Feb 25 2017, 20:17:10)
Type "copyright", "credits" or "license" for more information.

IPython 5.3.0 -- An enhanced Interactive Python.
?         -> Introduction and overview of IPython's features.
%quickref -> Quick reference.
help      -> Python's own help system.
object?   -> Details about 'object', use 'object??' for extra details.

In [1]: MyClass
Out[1]: __main__.MyClass

In [2]: MyClass().greet()
Hello, World!

In [3]: exit()

通常通り ipython を指定しただけでは、こうはならない。 インポートしていないオブジェクトを使おうとするとエラーになる。

$ ipython
Python 3.6.0 (default, Feb 25 2017, 20:17:10)
Type "copyright", "credits" or "license" for more information.

IPython 5.3.0 -- An enhanced Interactive Python.
?         -> Introduction and overview of IPython's features.
%quickref -> Quick reference.
help      -> Python's own help system.
object?   -> Details about 'object', use 'object??' for extra details.

In [1]: MyClass
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-1-d1eb688265f8> in <module>()
----> 1 MyClass

NameError: name 'MyClass' is not defined

本来であれば次のようにしてインポートしてやる必要がある。 python-fire 経由で起動すると、この手間をはぶくことができるようだ。

In [2]: from helloworld import MyClass

In [3]: MyClass
Out[3]: helloworld.MyClass

呼び出しをトレースする

これまでの例でもエラーになったときに何度か表示されていたけど、呼び出し内容をトレースすることもできる。 これには python-fire の引数として --trace オプションを指定する。

$ python helloworld.py greet -- --trace
Fire trace:
1. Initial component
2. Instantiated class "MyClass" (helloworld.py:5)
3. Accessed property "greet" (helloworld.py:7)

上記のように各ステップでどういったことが実行されているのかが表示される。

補完スクリプトを出力する

python-fire では bash 用の補完スクリプトを出力することもできる。 それには、次のようにして --completion を python-fire の引数に渡す。

$ python helloworld.py -- --completion
# bash completion support for helloworld.py
# DO NOT EDIT.
# This script is autogenerated by fire/completion.py.

_complete-helloworldpy()
{
  local start cur opts
  COMPREPLY=()
  start="${COMP_WORDS[@]:0:COMP_CWORD}"
  cur="${COMP_WORDS[COMP_CWORD]}"

  opts=""


  if [[ "$start" == "helloworld.py" ]] ; then
    opts="greet"
  fi

  if [[ "$start" == "helloworld.py greet" ]] ; then
    opts="--self"
  fi

  COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
  return 0
}

complete -F _complete-helloworldpy helloworld.py

ここで出力される内容を bash の設定ファイルに入れて読み込んでやるとシェルで補完が効くようになる。

$ python helloworld.py -- --completion >> ~/.bashrc
$ source ~/.bashrc

ただし、出力される補完スクリプトはパスが通っている場所で、かつスクリプトに実行権限がついていることを前提になっている。 そのため、上記のように python コマンド経由で実行する限りでは補完が効かない。 もし使うとしたら、こんな感じにしないとダメそう。

$ chmod +x helloworld.py
$ sudo mv helloworld.py /usr/local/bin/

ちなみに bashcompinit を有効にして試してみたけど zsh では動かなかった。

詳細を出力する

これまで見てきたように --help オプションを指定すると詳しい使い方が出力された。 次のように --verbose オプションを指定すると、特殊メソッドやアトリビュートを含むさらに詳しい内容が出力される。

$ python helloworld.py -- --verbose
Type:        MyClass
String form: <__main__.MyClass object at 0x1055cc4a8>
File:        ~/Documents/temporary/helloworld.py

Usage:       helloworld.py
             helloworld.py __class__
             helloworld.py __delattr__
             helloworld.py __dict__
             helloworld.py __dir__
             helloworld.py __doc__
             helloworld.py __eq__
             helloworld.py __format__
             helloworld.py __ge__
             helloworld.py __getattribute__
             helloworld.py __gt__
             helloworld.py __hash__
             helloworld.py __init__
             helloworld.py __init_subclass__
             helloworld.py __le__
             helloworld.py __lt__
             helloworld.py __module__
             helloworld.py __ne__
             helloworld.py __new__
             helloworld.py __reduce__
             helloworld.py __reduce_ex__
             helloworld.py __repr__
             helloworld.py __setattr__
             helloworld.py __sizeof__
             helloworld.py __str__
             helloworld.py __subclasshook__
             helloworld.py __weakref__
             helloworld.py greet

まとめ

今回は Google の作った CLI を自動生成するパッケージである python-fire を試してみた。 生成される CLI は割りと雑だけど、開発者が使う分には全く問題ないレベルだと思う。 これまでは挙動を軽く試すのにもスクリプトを毎回書き換えたり ipython を起動したりと面倒が多かった。 一つ一つの作業にかかる時間は短くとも、長い目で見れば多くの時間を浪費していることだろう。 そんなとき python-fire を使えば、スクリプトに数行を追加するだけでその手間をはぶくことができるのは大きいと感じた。

いじょう。