CUBE SUGAR CONTAINER

技術系のこと書きます。

VirtualBox で仮想マシンが入れ子 (Nested Virtualization) できるようになった

先日リリースされた VirtualBox 6.0 からは AMD の CPU で、6.1 からは Intel の CPU で Nested Virtualization がサポートされた。 Nested Virtualization というのは、仮想マシンの中に仮想マシンを入れ子に作ることを指す。 ようするに、仮想マシンをマトリョーシカのようにする。 この機能は、すでに VMware や KVM といったハイパーバイザではサポートされていたものの、今回それが VirtualBox でも使えるようになったというわけ。 この機能があると、サーバ周りのインフラ系をやっている人たちは、検証環境が作りやすくなってうれしい。 ただし、この機能を実現するには、仮想マシンの中で CPU の仮想化支援機能 (Intel-VT / AMD-V) が有効になっている必要がある 1

VirtualBox 6.1 のリリースノート 2 を見ると、次のような記載がある。

Virtualization core: Support for nested hardware-virtualization on Intel CPUs (starting with 5th generation Core i, codename Broadwell), so far tested only with guest running VirtualBox

どうやら、Intel であれば第 5 世代 Core i 以降の CPU で仮想化支援機能を使った Nested Virtualization ができるようになったらしい。 このニュースは、個人的に感慨深いものだった。 というのも、次のチケットを見てもらいたい。

www.virtualbox.org

このチケットは、VirtualBox に Nested Virtualization の機能を要望したものになっている。 問題は、チケットが作成された日付で、見ると "Opened 11 years ago" とある。 つまり、11 年という歳月をこえて、ユーザに要望されてきた機能がついに実現したというわけ。 ちなみに、これまで開発側の反応はどうだったかというと、チケットには「便利だろうけど実装するの大変だから...」みたいなコメントがあった。 なお、この機能について自分で調べていた頃のブログを調べると、ポストした日付が 8 年前になっていた。

Mac で仮想マシンの入れ子 (Nested Virtualization) をする | CUBE SUGAR STORAGEmomijiame.tumblr.com

今回は、せっかくなので VirtualBox を使った Nested Virtualization を試してみる。 使った環境は次のとおり。

$ sw_vers       
ProductName:    Mac OS X
ProductVersion: 10.14.6
BuildVersion:   18G2022
$ sysctl -a | grep brand_string
machdep.cpu.brand_string: Intel(R) Core(TM) m3-7Y32 CPU @ 1.10GHz
$ vagrant version | head -n 1
Installed Version: 2.2.7
$ vboxmanage | head -n 1
Oracle VM VirtualBox Command Line Management Interface Version 6.1.2

もくじ

下準備

はじめに、Homebrew を使って Vagrant と VirtualBox をインストールしておく。 もちろん、Vagrant を使わずに VirtualBox の GUI フロントエンドを使ってもかまわない。

$ brew cask install vagrant virtualbox

Vagrant + VirtualBox で仮想マシンを用意する (L1)

物理的なハードウェア上で直接動作する仮想化のことを L1 と呼ぶことがあるようだ。 ようするに、一般的な状況としての仮想マシンがこれ。 まずは L1 の仮想マシンとして Vagrant + VirtualBox を使って Ubuntu 18.04 LTS をインストールする。

仮想マシンのイメージをダウンロードしたら、設定ファイルを生成する。

$ vagrant box add ubuntu/bionic64
$ vagrant init ubuntu/bionic64

次のように Vagrant の設定ファイルができる。

$ head Vagrantfile 
# -*- mode: ruby -*-
# vi: set ft=ruby :

# All Vagrant configuration is done below. The "2" in Vagrant.configure
# configures the configuration version (we support older styles for
# backwards compatibility). Please don't change it unless you know what
# you're doing.
Vagrant.configure("2") do |config|
  # The most common configuration options are documented and commented below.
  # For a complete reference, please see the online documentation at

ここで、設定ファイルを編集する必要がある。 ポイントは最後の vb.customize に渡している引数の --nested-hw-virt on で、これがないと L1 の仮想マシンで CPU の仮想化支援機能が有効にならない。 あと、Nested Virtulization をするには、かなり処理のオーバーヘッドがあるので仮想マシンのリソースは多めに確保しておいた方が良い。

  config.vm.provider "virtualbox" do |vb|
    vb.cpus = "2"
    vb.memory = "2048"
    vb.customize ["modifyvm", :id, "--nested-hw-virt", "on"]
  end

なお、Vagrant ではなく VirtualBox の GUI フロントエンドを使って操作しているときは、仮想マシンの設定画面を開いて次の項目にチェックをつければ良い。

f:id:momijiame:20200202080627p:plain
VirtualBox で Nested Virtualization するのに必要な GUI 設定画面のチェック項目

仮想マシンを起動したらログインする。

$ vagrant up
$ vagrant ssh

これで L1 の仮想マシンとして Ubuntu 18.04 LTS が利用できるようになった。

$ cat /etc/lsb-release 
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=18.04
DISTRIB_CODENAME=bionic
DISTRIB_DESCRIPTION="Ubuntu 18.04.3 LTS"
$ uname -r
4.15.0-72-generic

CPU に仮想化支援機能のフラグが立っていることを確認する

それでは、CPU に仮想化支援機能のフラグが立っていることを確認してみよう。 Linux では proc ファイルシステムの /proc/cpuinfo で CPU のフラグが確認できる。 今回使っているのは Intel の CPU なので "vmx" というフラグを探す。

$ grep vmx /proc/cpuinfo
flags    : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 ht syscall nx rdtscp lm constant_tsc rep_good nopl xtopology nonstop_tsc cpuid tsc_known_freq pni pclmulqdq vmx ssse3 cx16 pcid sse4_1 sse4_2 x2apic movbe popcnt aes xsave avx rdrand hypervisor lahf_lm abm 3dnowprefetch invpcid_single pti tpr_shadow flexpriority fsgsbase avx2 invpcid rdseed clflushopt md_clear flush_l1d
flags    : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 ht syscall nx rdtscp lm constant_tsc rep_good nopl xtopology nonstop_tsc cpuid tsc_known_freq pni pclmulqdq vmx ssse3 cx16 pcid sse4_1 sse4_2 x2apic movbe popcnt aes xsave avx rdrand hypervisor lahf_lm abm 3dnowprefetch invpcid_single pti tpr_shadow flexpriority fsgsbase avx2 invpcid rdseed clflushopt md_clear flush_l1d

ちゃんと "vmx" フラグが立っていることがわかった。

仮想マシンの中に Vagrant + Libvirt (KVM) で仮想マシンを作る (L2)

続いては、L1 の仮想マシンの中に、さらに仮想マシンを作る。 先ほどのチケットには L1 / L2 共に VirtualBox を使った検証しかしていない、とあった。 そこで、せっかくなので L2 に KVM を使っても動くのかどうか調べてみることにした。 使う環境としては Libvirt 経由で KVM を Vagrant から扱う。

まずは必要なパッケージをインストールしておく。

$ sudo apt-get update
$ sudo apt-get -y install vagrant-libvirt qemu-kvm libvirt-bin gawk

KVM が使える状態になっていることを kvm-ok コマンドや、カーネルモジュールがロードされていることから確認する。

$ kvm-ok
INFO: /dev/kvm exists
KVM acceleration can be used
$ lsmod | grep kvm
kvm_intel             217088  0
kvm                   610304  1 kvm_intel
irqbypass              16384  1 kvm

現在のユーザを libvirt および kvm グループに参加させる。

$ sudo usermod -aG libvirt,kvm $(who am i | awk '{print $1}')

ここで、いったん L1 の仮想マシンを再起動しておく。

$ exit
$ vagrant reload

そして、もう一度 L1 の仮想マシンにログインする。

$ vagrant ssh

L2 の仮想マシンとしては、違いがわかりやすいように CentOS 7 を使うことにした。 次のようにして仮想マシンを起動する。

$ vagrant box add centos/7 --provider=libvirt
$ vagrant init centos/7
$ vagrant up

ちなみに、前述したとおり Nested Virtualization はオーバーヘッドが大きいので、この作業には大変に時間がかかる。 作業の進捗状況を確認したいときは、次のようにして仮想マシンのコンソールを取って見ると良い。

$ virsh list
$ virsh console <name>

仮想マシンが起動したら、ログインする。

$ vagrant ssh

確認すると、ちゃんと CentOS 7 が動作している。 これで、macOS / Ubuntu 18.04 LTS / CentOS 7 という仮想マシンのマトリョーシカが完成した。

$ cat /etc/redhat-release 
CentOS Linux release 7.6.1810 (Core) 
$ uname -r
3.10.0-957.12.2.el7.x86_64

なんとも感慨深い。

OpenStack 実践ガイド (impress top gear)

OpenStack 実践ガイド (impress top gear)

  • 作者:古賀 政純
  • 出版社/メーカー: インプレス
  • 発売日: 2016/08/25
  • メディア: 単行本(ソフトカバー)


  1. 完全仮想化をサポートしたハイパーバイザ (Xen など) であれば、その限りではないものの遅い

  2. https://www.virtualbox.org/wiki/Changelog-6.1

Ubuntu 18.04 LTS で Sphinx の PDF をビルドする

今回は Ubuntu 18.04 LTS を使って、Sphinx の PDF をビルドする方法について。

使った環境は次のとおり。

$ cat /etc/lsb-release 
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=18.04
DISTRIB_CODENAME=bionic
DISTRIB_DESCRIPTION="Ubuntu 18.04.3 LTS"
$ uname -r
4.15.0-72-generic

下準備

Sphinx は TeX を使って PDF をビルドするんだけど、TeX Live のパッケージはかなり大きい。 そのため、APT でミラーリポジトリを使えるようにしておいた方が良い。

blog.amedama.jp

以下のコマンドを実行すればミラーリポジトリが有効になる。

$ sudo sed -i.bak -e 's%http://[^ ]\+%mirror://mirrors.ubuntu.com/mirrors.txt%g' /etc/apt/sources.list

Sphinx をインストールする

つづいて、Sphinx をインストールする。 最新のバージョンを使いたいので PIP からインストールすることにした。

まずは PIP をインストールする。

$ sudo apt update
$ sudo apt -y install \
      python3-pip \
      python3-setuptools \
      python3-wheel

PIP を使って、最新の Sphinx をインストールする。

$ sudo pip3 install sphinx

もし、バージョンが少し古くても構わないのであれば、次のように APT を使ってインストールすることもできる。

$ sudo apt -y install python3-sphinx

Sphinx のプロジェクトを作成する

sphinx-quickstart コマンドを使ってプロジェクトのテンプレートを作る。

$ sphinx-quickstart

ウィザード形式でプロジェクトの設定を聞かれるので答えていく。

Sphinx のプロジェクトに設定を追加する

日本語を含む PDF をビルドするときは、Sphinx の設定ファイル conf.py に最低限次の設定をした方が良いようだ。

language = 'ja'
latex_docclass = {'manual': 'jsbook'}

TeX Live をインストールする

つづいて、PDF をビルドするのに必要な TeX Live の関連パッケージをインストールする。

$ sudo apt -y install \
   texlive-latex-recommended \
   texlive-latex-extra \
   texlive-fonts-recommended \
   texlive-fonts-extra \
   texlive-lang-japanese \
   texlive-lang-cjk \
   latexmk

あとからパッケージが足りなくてつらい思いをしたくないときは、次のようにしてすべてのパッケージを入れてしまっても良い。 ただし、インストールするのにめちゃくちゃ時間がかかる。

$ sudo apt -y install texlive-full

PDF をビルドする

あとは make コマンドを使って latexpdf ターゲットを実行するだけ。

$ make latexpdf

実行がおわったら、成果物が入るディレクトリに PDF のファイルができているはず。

$ file _build/latex/*.pdf
_build/latex/example.pdf: PDF document, version 1.5

いじょう。

参考文献

sphinx-users.jp

sphinx-users.jp

Ubuntu 18.04 LTS で利用できるフォントの一覧を得る

今回は、Ubuntu 18.04 LTS で利用できるフォントの一覧を得る方法について。 結論から先に述べると fc-list コマンドを使えば良い。

使った環境は次のとおり。 ちなみに、相当古い Ubuntu でも同じ方法が使えるみたい。

$ cat /etc/lsb-release 
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=18.04
DISTRIB_CODENAME=bionic
DISTRIB_DESCRIPTION="Ubuntu 18.04.3 LTS"
$ uname -r
4.15.0-74-generic

もくじ

下準備

下準備として fontconfig パッケージをインストールしておく。

$ sudo apt -y install fontconfig

利用できるフォントの一覧を得る

準備ができたら fc-list コマンドを実行する。 すると、利用できるフォントと、そのパスが一覧で得られる。

$ fc-list
/usr/share/fonts/truetype/dejavu/DejaVuSerif-Bold.ttf: DejaVu Serif:style=Bold
/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf: DejaVu Sans Mono:style=Book
/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf: DejaVu Sans:style=Book
/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf: DejaVu Sans:style=Bold
/usr/share/fonts/truetype/dejavu/DejaVuSansMono-Bold.ttf: DejaVu Sans Mono:style=Bold
/usr/share/fonts/truetype/dejavu/DejaVuSerif.ttf: DejaVu Serif:style=Book

フォントを追加してみる

試しに IPAex フォントを追加でインストールして増えることを確認してみよう。

$ sudo apt -y install fonts-ipaexfont

もう一度 fc-list コマンドを実行すると、ちゃんと IPAex フォントが増えていることがわかる。

$ fc-list
/usr/share/fonts/truetype/dejavu/DejaVuSerif-Bold.ttf: DejaVu Serif:style=Bold
/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf: DejaVu Sans Mono:style=Book
/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf: DejaVu Sans:style=Book
/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf: DejaVu Sans:style=Bold
/usr/share/fonts/opentype/ipaexfont-gothic/ipaexg.ttf: IPAexGothic,IPAexゴシック:style=Regular
/usr/share/fonts/truetype/dejavu/DejaVuSansMono-Bold.ttf: DejaVu Sans Mono:style=Bold
/usr/share/fonts/opentype/ipaexfont-mincho/ipaexm.ttf: IPAexMincho,IPAex明朝:style=Regular
/usr/share/fonts/truetype/fonts-japanese-mincho.ttf: IPAexMincho,IPAex明朝:style=Regular
/usr/share/fonts/truetype/fonts-japanese-gothic.ttf: IPAexGothic,IPAexゴシック:style=Regular
/usr/share/fonts/truetype/dejavu/DejaVuSerif.ttf: DejaVu Serif:style=Book

いじょう。

Python: Optuna で決められた時間内で最適化する

今回は Optuna の便利な使い方について。 現行の Optuna (v0.19.0) には決められた時間内で可能な限り最適化したい、というニーズを満たす API が実装されている。

使った環境は次の通り。

$ sw_vers 
ProductName:    Mac OS X
ProductVersion: 10.14.6
BuildVersion:   18G1012
$ python -V          
Python 3.7.5
$ pip list | grep -i optuna
optuna            0.19.0

下準備

まずは Optuna と Scikit-learn をインストールしておく。

$ pip install optuna scikit-learn

決められた時間内で最適化するサンプルコード

以下が決められた時間内で可能な限り最適化するサンプルコード。 実現するには Study#optimize()n_trials の代わりに timeout オプションを指定する。 渡す値は最適化に使う秒数になっており、以下では 60 秒を指定している。 サンプルコードでは、RandomForest で乳がんデータセットを 5-Fold Stratified CV するときのハイパーパラメータを探索している。

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import optuna
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import StratifiedKFold
from sklearn.model_selection import cross_validate
from sklearn import datasets


class Objective:
    """目的関数に相当するクラス"""

    def __init__(self, X, y):
        self.X = X
        self.y = y

    def __call__(self, trial):
        """オブジェクトが呼び出されたときに呼ばれる特殊メソッド"""
        # RandomForest のパラメータを最適化してみる
        params = {
            'n_estimators': 100,
            'max_depth': trial.suggest_int('max_depth', 2, 32),
            'min_samples_leaf': trial.suggest_int('min_samples_leaf', 1, 16),
        }
        model = RandomForestClassifier(**params)
        # 5-Fold Stratified CV
        kf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
        scores = cross_validate(model,
                                X=self.X, y=self.y,
                                cv=kf,
                                # メトリックは符号を反転したロジスティック損失
                                scoring='neg_log_loss',
                                n_jobs=-1)
        return scores['test_score'].mean()


def main():
    dataset = datasets.load_breast_cancer()
    X, y = dataset.data, dataset.target
    objective = Objective(X, y)
    # 関数を最大化するように最適化する
    study = optuna.create_study(direction='maximize')
    # 試行回数ではなく特定の時間内で最適化する
    study.optimize(objective, timeout=60)  # この例では 60 秒
    print('params:', study.best_params)


if __name__ == '__main__':
    main()

実行すると、次のようになる。

$ python optimeout.py
[I 2019-12-02 18:45:41,029] Finished trial#0 resulted in value: -0.1420495901047513. Current best value is -0.1420495901047513 with parameters: {'max_depth': 28, 'min_samples_leaf': 9}.
...
[I 2019-12-02 18:46:39,488] Finished trial#25 resulted in value: -0.11825965818535904. Current best value is -0.11397258384370261 with parameters: {'max_depth': 6, 'min_samples_leaf': 1}.
params: {'max_depth': 6, 'min_samples_leaf': 1}

上記を見ると、約 1 分で最適化が終了していることがわかる。

ぶっちゃけやってみるまで 1 回の試行にどれだけ時間がかかるかなんてわからないし、試行回数を指定するより便利だと思う。

Kaggleで勝つデータ分析の技術

Kaggleで勝つデータ分析の技術

  • 作者: 門脇大輔,阪田隆司,保坂桂佑,平松雄司
  • 出版社/メーカー: 技術評論社
  • 発売日: 2019/10/09
  • メディア: 単行本(ソフトカバー)
  • この商品を含むブログを見る

Python: featuretools ではじめる総当り特徴量エンジニアリング

今回は featuretools というパッケージを用いた総当り特徴量エンジニアリング (brute force feature engineering) について書いてみる。 総当り特徴量エンジニアリングは、実際に効くか効かないかに関係なく、考えられるさまざまな処理を片っ端から説明変数に施して特徴量を作るというもの。 一般的にイメージする、探索的データ分析などにもとづいて特徴量を手動で作っていくやり方とはだいぶアプローチが異なる。 そして、featuretools は総当り特徴量エンジニアリングをするためのフレームワークとなるパッケージ。

使った環境は次の通り。

$ sw_vers
ProductName:    Mac OS X
ProductVersion: 10.14.6
BuildVersion:   18G1012
$ python -V
Python 3.7.5

もくじ

下準備

まずは featuretools をインストールしておく。

$ pip install featuretools

そして、Python のインタプリタを起動する。

$ python

単独のデータフレームで試してみる

まずは、サンプルとなるデータフレームを用意する。

>>> import pandas as pd
>>> data = {
...     'name': ['a', 'b', 'c'],
...     'x': [1, 2, 3],
...     'y': [2, 4, 6],
...     'z': [3, 6, 9],
... }
>>> df = pd.DataFrame(data)

このデータフレームには、名前に加えて三次元の座標を表すような特徴量が含まれている。 ここから、いくつかの特徴量を抽出してみよう。

>>> df
  name  x  y  z
0    a  1  2  3
1    b  2  4  6
2    c  3  6  9

まずは featuretools をインポートする。

>>> import featuretools as ft

featuretools では EntitySet というオブジェクトが処理の起点になる。 このオブジェクトを使うことで複数のデータフレームをまとめて扱うことができる。 ただし、現在のサンプルはデータフレームを 1 つしか使わないのであまり意味はない。

>>> es = ft.EntitySet(id='example')

EntitySet にデータフレームを追加する。 featuretools 的には、EntitySet に Entity を追加することになる。

>>> es = es.entity_from_dataframe(entity_id='locations',
...                               dataframe=df,
...                               index='name',  # 便宜上、名前をインデックス代わりにする
...                               )

これで EntitySet に Entity が登録された。

>>> es
Entityset: example
  Entities:
    locations [Rows: 3, Columns: 4]
  Relationships:
    No relationships

それぞれの Entity は辞書ライクに参照できる。

>>> es['locations']
Entity: locations
  Variables:
    name (dtype: index)
    x (dtype: numeric)
    y (dtype: numeric)
    z (dtype: numeric)
  Shape:
    (Rows: 3, Columns: 4)

上記において dtype という部分に index や numeric といった、見慣れない表示があることに注目してもらいたい。 詳しくは後述するものの、featuretools ではカラムの型を pandas よりも細分化して扱う。 これは、そのカラムに対してどのような処理を適用するのが適切なのかを判断するのに用いられる。

また、内部に格納されているデータフレームも次のようにして参照できる。

>>> es['locations'].df
  name  x  y  z
a    a  1  2  3
b    b  2  4  6
c    c  3  6  9

特徴量を作る

これで準備ができたので、実際に特徴量を作ってみよう。 特徴量の生成には featuretools.dfs() という API を用いる。 dfs は Deep Feature Synthesis の略語となっている。 featuretools.dfs() には、起点となる EntitySet と Entity および適用する処理内容を指定する。 以下では es['locations'] を起点として、add_numeric と subtract_numeric という処理を適用している。

>>> feature_matrix, feature_defs = ft.dfs(entityset=es,
...                                       target_entity='locations',
...                                       trans_primitives=['add_numeric', 'subtract_numeric'],
...                                       agg_primitives=[],
...                                       max_depth=1,
...                                       )

生成された特徴量を確認してみよう。 元は 4 次元だった特徴量が 9 次元まで増えていることがわかる。 カラム名と内容を見るとわかるとおり、増えた分はそれぞれのカラムを足すか引くかして作られている。

>>> feature_matrix
      x  y  z  x + y  y + z  x + z  x - y  y - z  x - z
name                                                   
a     1  2  3      3      5      4     -1     -1     -2
b     2  4  6      6     10      8     -2     -2     -4
c     3  6  9      9     15     12     -3     -3     -6
>>> feature_matrix.shape
(3, 9)

もう一方の返り値には特徴量の定義に関する情報が入っている。

>>> feature_defs
[<Feature: x>, <Feature: y>, <Feature: z>, <Feature: x + y>, <Feature: y + z>, <Feature: x + z>, <Feature: x - y>, <Feature: y - z>, <Feature: x - z>]

さらに組み合わせた特徴量を作る

続いて、先ほどは 1 を指定した max_depth オプションに 2 を指定してみよう。 これは DFS の深さを表すもので、ようするに一度作った特徴量同士でさらに同じ処理を繰り返すことになる。

>>> feature_matrix, feature_defs = ft.dfs(entityset=es,
...                                       target_entity='locations',
...                                       trans_primitives=['add_numeric', 'subtract_numeric'],
...                                       agg_primitives=[],
...                                       max_depth=2,
...                                       )

生成された特徴量を確認すると 21 次元まで増えている。 中身を見ると、最初の段階で作られた特徴量同士をさらに組み合わせて特徴量が作られている。

>>> feature_matrix
      x  y  z  x + y  y + z  x + z  ...  x + y - y  x + z - y + z  x + y - z  y - y + z  x - x + y  x - y + z
name                                ...                                                                      
a     1  2  3      3      5      4  ...          1             -1          0         -3         -2         -4
b     2  4  6      6     10      8  ...          2             -2          0         -6         -4         -8
c     3  6  9      9     15     12  ...          3             -3          0         -9         -6        -12

[3 rows x 21 columns]

特徴量の加工に用いる処理 (Primitive) について

先ほどの DFS では add_numeric と subtract_numeric という 2 種類の加工方法を指定した。 featuretools では特徴量の加工方法に Primitive という名前がついている。

Primitive は、大まかに Transform と Aggregation に分けられる。 Transform は名前からも推測できるように元の shape のまま、足したり引いたりするような処理を指している。 それに対して Aggregation は何らかのカラムで GroupBy するような集計にもとづく。

デフォルトで扱える Primitive の一覧は以下のようにして得られる。

>>> primitives = ft.list_primitives()
>>> primitives.head()
               name         type                                        description
0  time_since_first  aggregation  Calculates the time elapsed since the first da...
1          num_true  aggregation                Counts the number of `True` values.
2               all  aggregation     Calculates if all values are 'True' in a list.
3              last  aggregation               Determines the last value in a list.
4               std  aggregation  Computes the dispersion relative to the mean v...

次のように、Primitive は Transform と Transform に分けられることが確認できる。

>>> primitives.type.unique()
array(['aggregation', 'transform'], dtype=object)

複数のデータフレームで試してみる

続いては複数のデータフレームから成るパターンを試してみよう。 これは、SQL でいえば JOIN して使うようなテーブル設計のデータが与えられるときをイメージするとわかりやすい。

次のように、item_id というカラムを使って JOIN して使いそうなサンプルデータを用意する。 商品のマスターデータと売買のトランザクションデータみたいな感じ。

>>> data = {
...     'item_id': [1, 2, 3],
...     'name': ['apple', 'banana', 'cherry'],
...     'price': [100, 200, 300],
... }
>>> item_df = pd.DataFrame(data)
>>> 
>>> from datetime import datetime
>>> data = {
...     'transaction_id': [10, 20, 30, 40],
...     'time': [
...         datetime(2016, 1, 2, 3, 4, 5),
...         datetime(2017, 2, 3, 4, 5, 6),
...         datetime(2018, 3, 4, 5, 6, 7),
...         datetime(2019, 4, 5, 6, 7, 8),
...     ],
...     'item_id': [1, 2, 3, 1],
...     'amount': [1, 2, 3, 4],
... }
>>> tx_df = pd.DataFrame(data)

上記を、新しく用意した EntitySet に登録していく。

>>> es = ft.EntitySet(id='example')
>>> es = es.entity_from_dataframe(entity_id='items',
...                               dataframe=item_df,
...                               index='item_id',
...                               )
>>> es = es.entity_from_dataframe(entity_id='transactions',
...                               dataframe=tx_df,
...                               index='transaction_id',
...                               time_index='time',
...                               )

次のように Entity が登録された。

>>> es
Entityset: example
  Entities:
    items [Rows: 3, Columns: 3]
    transactions [Rows: 4, Columns: 4]
  Relationships:
    No relationships

次に Entity 同士に Relationship を張ることで結合方法を featuretools に教えてやる。

>>> relationship = ft.Relationship(es['items']['item_id'], es['transactions']['item_id'])
>>> es = es.add_relationship(relationship)

これで、EntitySet に Relationship が登録された。

>>> es
Entityset: example
  Entities:
    items [Rows: 3, Columns: 3]
    transactions [Rows: 4, Columns: 4]
  Relationships:
    transactions.item_id -> items.item_id

Aggregation 特徴を作ってみる

それでは、この状態で DFS を実行してみよう。 今度は Primitive として Aggregation の count, sum, mean を指定してみる。 なお、Aggregation は Entity に Relationship がないと動作しない。

>>> feature_matrix, feature_defs = ft.dfs(entityset=es,
...                                       target_entity='items',
...                                       trans_primitives=[],
...                                       agg_primitives=['count', 'sum', 'mean'],
...                                       max_depth=1,
...                                       )

作られた特徴を確認すると、トランザクションを商品ごとに集計した情報になっていることがわかる。

>>> feature_matrix
           name  price  COUNT(transactions)  SUM(transactions.amount)  MEAN(transactions.amount)
item_id                                                                                         
1         apple    100                    2                         5                        2.5
2        banana    200                    1                         2                        2.0
3        cherry    300                    1                         3                        3.0

Aggregation と Transform の組み合わせ

続いては Aggregation と Transform を両方指定してやってみよう。

>>> feature_matrix, feature_defs = ft.dfs(entityset=es,
...                                       target_entity='items',
...                                       trans_primitives=['add_numeric', 'subtract_numeric'],
...                                       agg_primitives=['count', 'sum', 'mean'],
...                                       max_depth=1,
...                                       )

しかし、先ほどと結果が変わらない。 この理由は max_depth に 1 を指定しているためで、最初の段階では Transform を適用する先がない。

>>> feature_matrix
           name  price  COUNT(transactions)  SUM(transactions.amount)  MEAN(transactions.amount)
item_id                                                                                         
1         apple    100                    2                         5                        2.5
2        banana    200                    1                         2                        2.0
3        cherry    300                    1                         3                        3.0

試しに max_depth を 2 に増やしてみよう。

>>> feature_matrix, feature_defs = ft.dfs(entityset=es,
...                                       target_entity='items',
...                                       trans_primitives=['add_numeric', 'subtract_numeric'],
...                                       agg_primitives=['count', 'sum', 'mean'],
...                                       max_depth=2,
...                                       )

すると、今度は Aggregation で作られた特徴に対して、さらに Transform の処理が適用されていることがわかる。

>>> feature_matrix
           name  price  ...  COUNT(transactions) - MEAN(transactions.amount)  price - SUM(transactions.amount)
item_id                 ...                                                                                   
1         apple    100  ...                                             -0.5                                95
2        banana    200  ...                                             -1.0                               198
3        cherry    300  ...                                             -2.0                               297

[3 rows x 17 columns]

カラムが省略されてしまっているので、定義の方を確認すると次の通り。

>>> from pprint import pprint
>>> pprint(feature_defs)
[<Feature: name>,
 <Feature: price>,
 <Feature: COUNT(transactions)>,
 <Feature: SUM(transactions.amount)>,
 <Feature: MEAN(transactions.amount)>,
 <Feature: COUNT(transactions) + price>,
 <Feature: COUNT(transactions) + SUM(transactions.amount)>,
 <Feature: MEAN(transactions.amount) + price>,
 <Feature: MEAN(transactions.amount) + SUM(transactions.amount)>,
 <Feature: COUNT(transactions) + MEAN(transactions.amount)>,
 <Feature: price + SUM(transactions.amount)>,
 <Feature: COUNT(transactions) - price>,
 <Feature: COUNT(transactions) - SUM(transactions.amount)>,
 <Feature: MEAN(transactions.amount) - price>,
 <Feature: MEAN(transactions.amount) - SUM(transactions.amount)>,
 <Feature: COUNT(transactions) - MEAN(transactions.amount)>,
 <Feature: price - SUM(transactions.amount)>]

単独のデータフレームで Aggregation する

ここまでの例だけ見ると、単独のデータフレームが与えられたときは Aggregation の特徴は使えないのか?という印象を持つと思う。 しかし、そんなことはない。 試しに以下のようなデータフレームを用意する。

>>> data = {
...     'item_id': [1, 2, 3, 4, 5],
...     'name': ['apple', 'broccoli', 'cabbage', 'dorian', 'eggplant'],
...     'category': ['fruit', 'vegetable', 'vegetable', 'fruit', 'vegetable'],
...     'price': [100, 200, 300, 4000, 500],
... }
>>> item_df = pd.DataFrame(data)

上記を元に EntitySet を作る。

>>> es = ft.EntitySet(id='example')
>>> es = es.entity_from_dataframe(entity_id='items',
...                               dataframe=item_df,
...                               index='item_id',
...                               )

作れたら EntitySet#normalize_entity() を使って新しいエンティティを作る。

>>> es = es.normalize_entity(base_entity_id='items',
...                          new_entity_id='category',
...                          index='category',
...                          )

EntitySet は以下のような状態になる。

>>> es
Entityset: example
  Entities:
    items [Rows: 5, Columns: 4]
    category [Rows: 2, Columns: 1]
  Relationships:
    items.category -> category.category
>>> es['category']
Entity: category
  Variables:
    category (dtype: index)
  Shape:
    (Rows: 2, Columns: 1)
>>> es['category'].df
            category
fruit          fruit
vegetable  vegetable

category カラムに入る値だけから成る Entity ができて Relationship が張られている。 これは SQL でいえば外部キー制約用のテーブルをマスターとは別に作っているようなイメージ。

上記に対して Aggregation を適用してみよう。

>>> feature_matrix, feature_defs = ft.dfs(entityset=es,
...                                       target_entity='items',
...                                       trans_primitives=[],
...                                       agg_primitives=['count', 'sum', 'mean'],
...                                       max_depth=2,
...                                       )

すると、category カラムの内容ごとに集計された特徴量が作られていることがわかる。

>>> feature_matrix
             name   category  price  category.COUNT(items)  category.SUM(items.price)  category.MEAN(items.price)
item_id                                                                                                          
1           apple      fruit    100                      2                       4100                 2050.000000
2        broccoli  vegetable    200                      3                       1000                  333.333333
3         cabbage  vegetable    300                      3                       1000                  333.333333
4          dorian      fruit   4000                      2                       4100                 2050.000000
5        eggplant  vegetable    500                      3                       1000                  333.333333

featuretools で取り扱うデータ型について

前述した通り、featuretools では適用する処理を選別するために pandas よりも細かい粒度でデータ型を取り扱う。 もし、適切なデータ型になっていないと意図しない処理が適用されて無駄やリークを起こす原因となる。 データ型の定義は現行バージョンであれば以下のモジュールにある。

github.com

ざっくり調べた感じ以下の通り。

説明
Variable - 全ての型のベース
Unknown Variable 不明なもの
Discrete Variable 名義尺度・順序尺度のベース
Boolean Variable 真偽値
Categorical Discrete 順序なしカテゴリ変数
Id Categorical 識別子
Ordinal Discrete 順序ありカテゴリ変数
Numeric Variable 数値
Index Variable インデックス
Datetime Variable 時刻
TimeIndex Variable 時刻インデックス
NumericTimeIndex TimeIndex, Numeric 数値表現の時刻インデックス
DatetimeTimeIndex TimeIndex, Datetime 時刻表現の時刻インデックス
Timedelta Variable 時間差
Text Variable 文字列
LatLong Variable 座標 (緯度経度)
ZIPCode Categorical 郵便番号
IPAddress Variable IP アドレス
FullName Variable 名前
EmailAddress Variable メールアドレス
URL Variable URL
PhoneNumber Variable 電話番号
DateOfBirth Datetime 誕生日
CountryCode Categorical 国コード
SubRegionCode Categorical 地域コード
FilePath Variable ファイルパス

組み込みの Primitive について

続いて、featuretools にデフォルトで組み込まれている Primitive について勉強がてらざっくり調べた。 現行バージョンに組み込まれているものは以下で確認できる。

github.com

なお、動作する上で特定のパラメータを必要とするものもある。

Transform

まずは Transform から。

名前 入力型 出力型 説明
is_null Variable Boolean Null か (pandas.isnull)
absolute Numeric Numeric 絶対値 (np.absolute)
time_since_previous DatetimeTimeIndex Numeric 時刻の最小値からの差分
time_since DatetimeTimeIndex, Datetime Numeric 特定時刻からの差分
year Datetime Ordinal
month Datetime Ordinal
day Datetime Ordinal
hour Datetime Ordinal
minute Datetime Numeric
second Datetime Numeric
week Datetime Ordinal
is_weekend Datetime Boolean 平日か
weekday Datetime Ordinal 週の日付 (月:0 ~ 日:6)
num_characters Text Numeric 文字数
num_words Text Numeric 単語数
diff Numeric Numeric 値の差
negate Numeric Numeric -1 をかける
percentile Numeric Numeric パーセンタイルに変換
latitude LatLong Numeric 緯度
longitude LatLong Numeric 経度
haversine (LatLong, LatLong) Numeric 2 点間の距離
not Boolean Boolean 否定
isin Variable Boolean リストに含まれるか
greater_than (Numeric, Numeric), (Datetime, Datetime), (Ordinal, Ordinal) Boolean より大きいか (np.greater)
greater_than_equal_to (Numeric, Numeric), (Datetime, Datetime), (Ordinal, Ordinal) Boolean 同じかより大きいか (np.greater_equal)
less_than (Numeric, Numeric), (Datetime, Datetime), (Ordinal, Ordinal) Boolean より小さいか (np.less)
less_than_equal_to (Numeric, Numeric), (Datetime, Datetime), (Ordinal, Ordinal) Boolean 同じかより小さいか (np.less_equal)
greater_than_scalar Numeric, Datetime, Ordinal Boolean 特定の値より大きいか
greater_than_equal_to_scalar Numeric, Datetime, Ordinal Boolean 特定の値より大きいか (値を含む)
less_than_scalar Numeric, Datetime, Ordinal Boolean 特定の値より小さいか
less_than_equal_to_scalar Numeric, Datetime, Ordinal Boolean 特定の値より小さいか (値を含む)
equal (Variable, Variable) Boolean 同じか (np.equal)
not_equal (Variable, Variable) Boolean 同じでないか (np.not_equal)
equal_scalar (Variable, Variable) Boolean 特定の値と等しいか
not_equal_scalar (Variable, Variable) Boolean 特定の値と等しくないか
add_numeric (Numeric, Numeric) Numeric 加算 (np.add)
subtract_numeric (Numeric, Numeric) Numeric 減算 (np.subtract)
multiply_numeric (Numeric, Numeric) Numeric 乗算 (np.multiply)
divide_numeric (Numeric, Numeric) Numeric 除算 (np.divide)
modulo_numeric (Numeric, Numeric) Numeric 余算 (np.mod)
add_numeric_scalar Numeric Numeric 特定の値を足す
subtract_numeric_scalar Numeric Numeric 特定の値を引く
scalar_subtract_numeric_feature Numeric Numeric 特定の値から引く
multiply_numeric_scalar Numeric Numeric 特定の値を掛ける
divide_numeric_scalar Numeric Numeric 特定の値で割る
divide_by_feature Numeric Numeric 特定の値を割る
modulo_numeric_scalar Numeric Numeric 特定の値で割った余り
modulo_by_feature Numeric Numeric 特定の値を割った余り
multiply_boolean (Boolean, Boolean) Boolean ビット同士を比べた AND (np.bitwise_and)
and (Boolean, Boolean) Boolean 論理積 (np.logical_and)
or (Boolean, Boolean) Boolean 論理和 (np.logical_or)

Aggregation

続いて Aggregation を。

名前 入力型 出力型 説明
count Index Numeric 要素数
num_unique Numeric Numeric ユニークな要素数
sum Numeric Numeric
mean Numeric Numeric 平均
std Numeric Numeric 標準偏差
median Numeric Numeric 中央値
mode Numeric Numeric 最頻値
min Numeric Numeric 最小値
max Numeric Numeric 最大値
first Variable - 最初の要素
last Variable - 最後の要素
skew Numeric Numeric 歪度
num_true Boolean Numeric 真の要素数
percent_true Boolean Numeric 真の比率
n_most_common Discrete Discrete 出現頻度の高い要素 TOP n
avg_time_between DatetimeTimeIndex Numeric 平均間隔
any Boolean Boolean いずれかが真であるか
all Boolean Boolean 全て真であるか
time_since_last DatetimeTimeIndex Numeric 最後の要素からの時間差
time_since_first DatetimeTimeIndex Numeric 最初の要素からの時間差
trend (Numeric, DatetimeTimeIndex) Numeric 線形回帰した際の傾き
entropy Categorical Numeric エントロピー

いじょう。 計算する種類が多かったり max_depth が深いとデータによっては現実的な時間・空間計算量におさまらなくなるので気をつけよう。 個人的には、空間計算量を節約するために作った特徴量をジェネレータとかでどんどんほしいところだけど、そういう API はざっと読んだ感じなさそう。 順番にデータフレームを結合して最終的な成果物をどんと渡す作りになっている。 再帰的に計算をするために、これは仕方ないのかなー、うーん。

Kaggleで勝つデータ分析の技術

Kaggleで勝つデータ分析の技術

  • 作者: 門脇大輔,阪田隆司,保坂桂佑,平松雄司
  • 出版社/メーカー: 技術評論社
  • 発売日: 2019/10/09
  • メディア: 単行本(ソフトカバー)
  • この商品を含むブログを見る

Python: 未処理の例外が上がったときの処理をオーバーライドする

今回はだいぶダーティーな手法に関する話。 未処理の例外が上がったときに走るデフォルトの処理をオーバーライドしてしまう方法について。 あらかじめ断っておくと、どうしても必要でない限り、こんなことはやらない方が望ましい。 とはいえ、これによって助けられることもあるかも。

使った環境は次の通り。

$ sw_vers                               
ProductName:    Mac OS X
ProductVersion: 10.14.6
BuildVersion:   18G1012
$ python -V                    
Python 3.7.5

もくじ

下準備

下準備として、Python のインタプリタを起動しておく。

$ python

デフォルトの挙動をオーバーライドする

try ~ except で捕捉されない例外があると、次のように例外の詳細とトレースバックが出力される。

>>> raise Exception('Oops!')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
Exception: Oops!

このときの挙動は sys.excepthook でフックされているので、このオブジェクトを上書きすることでオーバーライドできる。 例えば、実用性は皆無だけどただメッセージを出力するだけの処理に置き換えてみよう。

>>> import sys
>>> def myhook(type, value, traceback):
...     print('Hello, World!', file=sys.stderr)
... 
>>> sys.excepthook = myhook

例外を上げてみると、次のようにメッセージが表示されるだけになる。

>>> raise Exception('Oops!')
Hello, World!

関数のシグネチャについて

フックの関数のシグネチャについて、もうちょっと詳しく見てみよう。 以下のようにデバッグ用の関数をフックに指定する。

>>> def debughook(type, value, traceback):
...     print(type, value, traceback, file=sys.stderr)
... 
>>> sys.excepthook = debughook

試しに例外を上げてみると、次のようになった。 例外クラスの型、引数、トレースバックのオブジェクトが渡されるようだ。

>>> raise Exception('Oops!')
<class 'Exception'> Oops! <traceback object at 0x1024a6910>

スレッドを使うときの問題点について

なお、このフックはスレッドを使っているときに有効にならないという問題がある。

実際に試してみよう。 先ほどのデバッグ用のフックが有効な状態で、別のスレッドを起動する。 そして、スレッドの中で例外を上げるように細工してやろう。 すると、次のように普通のトレースバックが表示されてしまう。

>>> import threading
>>> def f():
...     raise Exception('Oops!')
... 
>>> threading.Thread(target=f).start()
>>> Exception in thread Thread-1:
Traceback (most recent call last):
  File "/usr/local/Cellar/python/3.7.5/Frameworks/Python.framework/Versions/3.7/lib/python3.7/threading.py", line 926, in _bootstrap_inner
    self.run()
  File "/usr/local/Cellar/python/3.7.5/Frameworks/Python.framework/Versions/3.7/lib/python3.7/threading.py", line 870, in run
    self._target(*self._args, **self._kwargs)
  File "<stdin>", line 2, in f
Exception: Oops!

上記のように、スレッドを使った場合にはフックが有効にならない。 この問題は Python 3.8 で追加された API によって解決できる。

$ python -V
Python 3.8.0

Python 3.8 では threading モジュールに excepthook というオブジェクトが追加されている。 このオブジェクトを上書きすることで処理をオーバーライドできるようになった。

>>> def threading_hook(args):
...     print('Hello, World!', args)
... 
>>> threading.excepthook = threading_hook
>>> 
>>> threading.Thread(target=f).start()
Hello, World! _thread.ExceptHookArgs(exc_type=<class 'Exception'>, exc_value=Exception('Oops!'), exc_traceback=<traceback object at 0x1033f4900>, thread=<Thread(Thread-2, started 123145518649344)>)

デフォルトの挙動に戻す

デフォルトのフックへの参照は sys.__excepthook__ にあるため、これを使えば挙動を元に戻せる。 なお、sys.__excepthook__ の方は絶対に変更しないこと。

>>> sys.excepthook = sys.__excepthook__
>>> raise Exception('Oops!')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
Exception: Oops!

試してないけど Jupyter とかでエラーになったときチャットに通知を送る、なんて用途に使えるかもね。

Python: 関数合成できる API を作ってみる

今回は普通の Python では満足できなくなってしまった人向けの話題。 dfplypipe といった一部のパッケージで採用されているパイプ処理や関数合成できる API を作る一つのやり方について。

使った環境は次の通り。

$ sw_vers                  
ProductName:    Mac OS X
ProductVersion: 10.14.6
BuildVersion:   18G1012
$ python -V
Python 3.7.5

もくじ

カッコ以外で評価されるオブジェクトを作る

通常の Python では、関数やメソッドはカッコを使ってオブジェクトを評価する。 しかし、クラスを定義するとき特殊メソッドを使って演算子オーバーライドすることで、その枠に収まらないオブジェクトが作れる。

例えば、以下のように関数をラップするクラスを定義する。 特殊メソッドの __rrshift__() は、自身の「左辺」にある右ビットシフト演算子が評価されるときに呼び出される。 なお、別に右ビットシフト演算子を使う必然性はないので、別の演算子をオーバーライドしても構わない。

>>> class Pipe:
...     """関数をラップするクラス"""
...     def __init__(self, f):
...         # インスタンス化するとき関数を受け取る
...         self.f = f
...     def __rrshift__(self, other):
...         # 自身の左辺にある右ビットシフト演算子を評価するとき関数を実行する
...         return self.f(other)
... 

これを使って、例えば値を二乗するオブジェクトを作ってみよう。

>>> pow = Pipe(lambda x: x ** 2)

このオブジェクトに右ビットシフト演算子を使って値を渡すと、その内容が二乗される。

>>> 10 >> pow
100

他の関数も定義してつなげるとメソッドチェーンっぽいことができる。

>>> double = Pipe(lambda x: x * 2)
>>> 10 >> pow >> double
200

ただ、このままだと関数と関数だけをつないだときに例外になってしまう。 この場合は、左辺にあるオブジェクトの右辺にある右ビットシフト演算子が先に評価されているため。

>>> pow >> double
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unsupported operand type(s) for >>: 'Pipe' and 'Pipe'

関数合成できるオブジェクトを作る

そこで、先ほどのクラスに手を加える。 以下のように、自身の右辺に渡された処理を自身のリストにキューイングしておけるようにする。

>>> import copy
>>> class Pipe:
...     """関数合成に対応したクラス"""
...     def __init__(self, f):
...         self.f = f
...         # 適用したい一連の関数を記録しておくリスト
...         self.pipes = []
...     def __rshift__(self, other):
...         """自身の右辺にある右ビットシフト演算子を評価したときに呼ばれる特殊メソッド"""
...         # 自身をコピーしておく
...         copied_self = copy.deepcopy(self)
...         # コピーした内容のリストに適用したい処理をキューイングする
...         copied_self.pipes.append(other)
...         # コピーした自身を返す
...         return copied_self
...     def __rrshift__(self, other):
...         """自身の左辺にある右ビットシフト演算子を評価したときに呼ばれる特殊メソッド"""
...         # まずは自身の関数を適用する
...         result = self.f(other)
...         # キューイングされていた関数を順番に適用していく
...         for pipe in self.pipes:
...             result = pipe.__rrshift__(result)
...         # 最終的な結果を返す
...         return result
... 

こうすると、関数同士をつないだ場合にもオブジェクトが返るようになる。

>>> pow = Pipe(lambda x: x ** 2)
>>> double = Pipe(lambda x: x * 2)
>>> pow >> double
<__main__.Pipe object at 0x102090950>

上記を変数に保存しておいて値を適用すると、ちゃんと本来の意図通りにチェーンされた結果が返ってくる。

>>> pow_double = pow >> double
>>> 10 >> pow_double
200

自身をディープコピーする理由について

ちなみに、先ほど右辺にある右ビットシフト演算子が評価されるときに自身のオブジェクトをディープコピーしていた。 もし、ディープコピーしないとどうなるだろうか。 実際にやってみよう。

>>> class Pipe:
...     def __init__(self, f):
...         self.f = f
...         self.pipes = []
...     def __rshift__(self, other):
...         # 自身をコピーせずに関数をキューイングする場合
...         self.pipes.append(other)
...         return self
...     def __rrshift__(self, other):
...         result = self.f(other)
...         for pipe in self.pipes:
...             result = pipe.__rrshift__(result)
...         return result
... 

コピーしない場合でも、ちゃんと Pipe オブジェクトは返ってくる。

>>> pow = Pipe(lambda x: x ** 2)
>>> double = Pipe(lambda x: x * 2)
>>> pow >> double
<__main__.Pipe object at 0x102090a50>

しかし、右辺の右ビットシフト演算子が評価された時点で適用する処理のリストにキューイングされてしまう。

>>> pow.pipes
[<__main__.Pipe object at 0x102090b50>]

つまり、元のオブジェクトを変更してしまう。 本来なら二乗してほしいだけのオブジェクトで二倍も同時にされてしまうことになる。

>>> 10 >> pow
200

デコレータとして使う

ちなみに、ここまで作ってきたクラスはクラスデコレータとして使うこともできる。

ようするに、次のように関数をクラスでデコレートできる。

>>> @Pipe
... def triple(x):
...     return x * 3
... 
>>> @Pipe
... def half(x):
...     return x // 2
... 
>>> 10 >> triple >> half
15

デコレータの詳細については以下を参照のこと。

blog.amedama.jp

引数を受け取れるようにする

次に、適用される関数に引数を渡したくなる。 この場合、__call__() メソッドを実装して関数に引数を渡す形でオブジェクトを作り直すようにすると良い。

>>> class Pipe:
...     def __init__(self, f):
...         self.f = f
...         self.pipes = []
...     def __rshift__(self, other):
...         copied_self = copy.deepcopy(self)
...         copied_self.pipes.append(other)
...         return copied_self
...     def __rrshift__(self, other):
...         result = self.f(other)
...         for pipe in self.pipes:
...             result = pipe.__rrshift__(result)
...         return result
...     def __call__(self, *args, **kwargs):
...         """オブジェクトが実行されたときに呼ばれる特殊メソッド"""
...         # 実行されたときの引数を関数に渡すようにしたオブジェクトを返す
...         return Pipe(lambda x: self.f(x, *args, **kwargs))
... 

例えば、掛ける数を引数にした掛け算を実装してみよう。

>>> @Pipe
... def multiply(x, n):
...     return x * n
... 

これは、次のように使うことができる。

>>> 10 >> multiply(2) >> multiply(3)
60

おもしろいね。

参考プロジェクト

github.com

github.com