pythonとbashを融合して関数電卓を作る

pythonアドベントカレンダー2017の25日目の記事です。
作業画面は全てshell上なのでshellscriptカレンダーに投稿するか迷いましたが、結局はpythonをワンライナーでどこまで書けるかに行き着くので、優秀なPythonistaの皆様のご意見を頂戴しようかと思い、こちらに投稿致しました。

# 本題のshell function

引数を`python -c`に渡してpython3で数値計算させるshell functionです。
alias指定してppで簡単に呼び出せるようにしています。~/.bashrcにでも書き込んでください。

```bash:~/.bashrc
# pythonで計算
function python_print(){
        OLDIFS=$IFS
        IFS=, # Set derimiter as comma
        python -c "from numpy import *; from scipy.constants import *; import pandas as pd; print($*)"
        IFS=$OLDIFS  # Reset default
}
alias pp='python_print'
```

$IFSの解説は「複数の引数」の節で解説しています。


# 計算

## 四則演算
掛け算`*`やカッコを使うときはクォーテーションで囲うかバックスラッシュでエスケープしましょう。

```shell-session
$ pp 4+4      
8
$ pp 12.42-83.09
-70.67
$ pp 10/1000
0.01
$ pp "4*9"
36
$ pp 4\*9
36
$ pp "(6+3)/6"
1.5
```

## 商、余り
商は`//`, 余りは`%`

```shell-session
$ pp 10//3         
3
$ pp 10%3 
1
```


## 指数、複素数
eは10の*n*乗の意味です。
複素数は`j`、conjugate()は共役です。

```shell-session
$ pp 1e3
1000.0
$ pp "2**10"
1024
$ pp 1+5j+3-9j
(4-4j)
$ pp "(1+5j)**2"
(-24+10j)
$ pp "((1+5j)**2).real"  # 実数 
-24.0
$ pp "((1+5j)**2).imag"  # 虚数
10.0
$ pp "((1+5j)**2).conjugate()"  # 共役
(-24-10j)
$ pp "abs(6-2j)"  # 絶対値
6.324555320336759
```




## n進数

### n進数から10進数への変換
* 2進数→0b<数字>
* 8進数→0o<数字>
* 16進数→0x<数字>

```shell-session
$ pp 0b010  # 2進数 　
2
$ pp 0o010  # 8進数
8
$ pp 0x010  # 16進数
16
```

n=2,8,16以外ならint関数(`int(文字列, 基数)`)を使いましょう。

```shell-session
$ pp "int('010', 11)"
11
$ pp "int('100', 11)"
121
```


### ビット演算

```shell-session
$ pp \~0b1111  #  ビット反転
-16
$ pp "0b1011|0b0100"  # 論理和
15
$ pp "0b1011&0b0100"  # 論理積
0
$ pp '0b110^0b100'  # 排他的論理和
2
```

### シフト

```
$ pp '0b001<<1'  # 1ビット左シフト
2
$ pp '0b001<<2'  # 2ビット左シフト
4
$ pp '0b001<<3'  # 3ビット左シフト
8
$ pp '0b001<<3>>1'  # 3ビット左シフトしてから1ビット右シフト
4
```


### ビット同士の四則演算
戻り値がint型なので四則演算もできます。
int関数の戻り値も同様です。

```shell-session
$ pp 0x10+0o10
24
$ pp "int('011',2) + int('100',2)"   
7
```


### 10進数からn進数への変換

```shell-session
$ pp "bin(2)"   # 2進数
0b10
$ pp "oct(8)"  # 8進数
0o10
$ pp "hex(16)"  # 16進数
0x10
```


# 変数
`$`マークを打たなきゃいけないのが少しやりづらいですね。

上下キーで変数の定義行を呼び出してちゃちゃっと変数の値を変えられるから楽ですね。変数同士の計算もできます。

```shell-session
$ x=121
$ pp $x+23 $x+52
144 173
$ x=1000        
$ pp $x+23 $x+52
1023 1052
$ y=7    
$ pp "$x*$y" 
7000
```

計算結果を変数に格納することもできます。バッククォート\`かダラーマークとカッコ`$()`を使いましょう。

```shell-session
$ x=`pp 6+4`
$ echo $x
10
$ pp "$x**2"
100

$ y=$(pp "2*1e-4")
$ echo $y
0.0002

$ pp $x/$y
50000.0
```


# 複数の引数
カンマで区切れば同時に計算をしてくれます。
空白で区切っても結果は同様です。

```shell-session
$ pp 2+4,3+9,5+1
6 12 6
$ pp 2+4 3+9 5+1
6 12 6
```

`python_print`関数に書かれている`$IFS`変数はデリミタを指定する環境変数です。デフォルトはスペースですが、一時的にカンマに変更しています。
こうすることでshellの変数として複数渡した後、pythonに渡すときにカンマ区切りでprint関数に渡すことができます。

```shell
pp 1 5 7.2  # shellでは空白で複数の引数とする
python -c "print(1, 5, 7.2)"  # pythonではカンマ区切りで渡される
```


# 物理定数
[scipy.constants](https://docs.scipy.org/doc/scipy/reference/constants.html#module-scipy.constants)をスターインポートしていますので、170を超える物理定数が扱えます。詳細はリンク先を参照して下さい。

```shell-session
$ pp e
2.718281828459045
$ pp pi
3.141592653589793
$ pp mu_0             
1.2566370614359173e-06
```

一般的にスターインポートは悪と捉えられていますが、使い捨てのワンライナーコマンドなので、こういったルールは無視していいと思います。
[Why import star is a bad idea](https://pythonconquerstheuniverse.wordpress.com/2011/03/28/why-import-star-is-a-bad-idea/)


# 数学関数
numpyをスターインポートしていますので、sin関数やlog関数がそのまま使えます。


## 対数
logの底はネイピア数、log10の底は10

```shell-session
$ pp "log10(10)"
1.0
$ pp "log(e)"   
1.0
```


## 三角関数
```shell-session
$ pp "sin(pi/2)"
1.0
$ pp "cos(pi/2)"
6.12323399574e-17  # ほぼ0
$ pp "arcsin(sqrt(2)/2)*4"
3.14159265359  # 円周率
```


## 平方根、3乗根
4乗根以上は`**`と分数を使います。

```shell-session

$ pp "sqrt(9)"  # 2乗根
3.0
$ pp "cbrt(27)"  # 3乗根
3.0
$ pp "16**(1/4)"  # 4乗根
2.0
```














# シーケンス


## bashの配列
bashの配列は{最初の数字..終了の数字[..飛ばす数字]}です。
bashの配列を使うときはクォーテーションでくくらないでバックスラッシュによるエスケープにしないといけません。

```shell-session
$ pp {1,2,3}+{10,9,8}
11 10 9 12 11 10 13 12 11
$ pp {0..3}+{10..12}
10 11 12 11 12 13 12 13 14 13 14 15
$ pp {1..3}\*{3..6}
3 4 5 6 6 8 10 12 9 12 15 18
$ pp {100..500..100}\*2  # 100とばし
200 400 600 800 1000
$ pp log10\({100..1000..200}\)  # log10(100) log10(300) log10(500) log10(700) log10(900)
2.0 2.47712125472 2.69897000434 2.84509804001 2.95424250944
$ pp sin\(pi/{1..4}\)  # sin($pi$/1) sin($pi$/2) sin($pi$/3) sin($pi$/4)
1.22464679915e-16 1.0 0.866025403784 0.707106781187
$ ar=(12 98 55)
$ pp $ar+8
12 98 63
$ pp "(lambda f: 1/(f+2))"\({1..5}\)  # lambdaと組み合わせて1/(1+2) 1/(2+2) 1/(2+3) 1/(2+4) 1/(2+5)
0.3333333333333333 0.25 0.2 0.16666666666666666 0.14285714285714285
```

## リスト

```shell-session
$ y=12; pp "[i*$y for i in range(10)]"
[0, 12, 24, 36, 48, 60, 72, 84, 96, 108]
```




## 辞書
shell変数を5にしてlinspaceで5つ数字を発生させて辞書にしています。
スライスもできます。

```shell-session
$ x=5; pp "{k:v for k,v in enumerate(linspace(0.6, -1, $x))}"
{0: 0.59999999999999998, 1: 0.19999999999999996, 2: -0.20000000000000007, 3: -0.6000000000000002, 4: -1.0}
$ x=5; pp "{k:v for k,v in enumerate(linspace(0.6, -1, $x))}[2]"
-0.2
```



## numpy.array
numpyをスターインポートしていますので、`array, arange, linspace, random.randn, random.randint`なんかが使いやすいと思います。

```shell-session
$ pp "arange(0,1,.2)"
[ 0.   0.2  0.4  0.6  0.8]
$ pp "*arange(7)"  # タプル展開
0 1 2 3 4 5 6
$ pp {0..6}  # bashで同じの書ける
0 1 2 3 4 5 6
$ echo {0..6}  # bashで同じの書ける
0 1 2 3 4 5 6
$ pp sin\({0..6}\)  # bash配列を数学関数に食わせる
0.0 0.841470984808 0.909297426826 0.14112000806 -0.756802495308 -0.958924274663 -0.279415498199
$ pp linspace\(1000,10000,10\)
[  1000.   2000.   3000.   4000.   5000.   6000.   7000.   8000.   9000.
  10000.]
$ pp "random.randint(-100,100,10).reshape(5,-1)"
[[ -5 -74]
 [ 88  53]
 [ 68  64]
 [ 78  67]
 [-81   2]]
```

shell変数をarray化できます。

```shell-session
$ ar=[[4,6,8],[1,3,5]]  
$ pp "array($ar)"
[[4 6 8]
 [1 3 5]]
```

全部を合計sum
第一引数であるaxisをいじると縦方向横方向のみの計算ができます。

```shell-session
$ pp "array($ar).sum()"  # すべての合計
27
$ pp "array($ar).sum(0)"  # [4+1, 6+3, 8+5]
[ 5  9 13]
$ pp "array($ar).sum(1)"  # [4+6+8, 1+3+5]
[18  9]
```

全部を掛け算prod

```shell-session
$ pp "array($ar).prod()"
2880
$ pp "array($ar).prod(0)"
[ 4 18 40]
$ pp "array($ar).prod(1)"
[192  15]
```








# 行列計算

```shell-session
$ pp 'eye(4)'  # 単位行列
[[ 1.  0.  0.  0.]
 [ 0.  1.  0.  0.]
 [ 0.  0.  1.  0.]
 [ 0.  0.  0.  1.]]
$ pp 'tri(4)'  # 三角行列
[[ 1.  0.  0.  0.]
 [ 1.  1.  0.  0.]
 [ 1.  1.  1.  0.]
 [ 1.  1.  1.  1.]]
$ pp 'zeros([2,3])'  # ゼロ行列
[[ 0.  0.  0.]
 [ 0.  0.  0.]]
$ pp 'ones([2,3])'  # 要素がすべて1の行列
[[ 1.  1.  1.]
 [ 1.  1.  1.]]
$ pp '2*eye(4)'  # intを掛け算すればすべての要素に掛け算される     
[[ 2.  0.  0.  0.]
 [ 0.  2.  0.  0.]
 [ 0.  0.  2.  0.]
 [ 0.  0.  0.  2.]]
```


必要に応じてlinalgモジュールを使います。

```shell-session
$ z=[[2,3],[1,4]]  
$ pp "array($z)" 
[[2 3]
 [1 4]]
$ pp "array($z).T"  # 転置行列
[[2 1]
 [3 4]]
$ pp "linalg.inv(array($z))"  # 逆行列
[[ 0.8 -0.6]
 [-0.2  0.4]]
$ pp "linalg.inv(array($z)).dot($z)"  # 内積
[[ 1.  0.]
 [ 0.  1.]]
```







# 表計算
pandasをインポートしているので、表形式の出力ができます。

```shell-session
$ pp "pd.DataFrame(random.randn(4,5))"                                        
          0         1         2         3         4
0 -0.404818  1.418255 -0.955262  0.420550 -0.675649
1 -1.127183  1.332479 -1.387252 -0.521835  0.665057
2  0.318531 -0.548281 -1.277276  0.072488  0.546565
3 -0.431743 -0.042868 -1.186896  1.685872  0.483884
```

ipythonとか電卓呼びだせばいいんですけど、呼び出すまでもない使い捨て計算をさらっとしたい時に便利です。
bashの配列とnumpyの関数が組み合わせられるのが便利だと思いました。
...というまとめにしようと思っていましたが、ここまでやるならipython呼べし！



# 参考
こういうところで勉強すればさらなる可能性を模索できそうです。

* [Qiita - @akinomyoga/Bash $((算術式)) のすべて - A 基本編](https://qiita.com/akinomyoga/items/9761031c551d43307374#a338-%E7%B4%AF%E4%B9%97)
* [Qiita - @cocuh/闇Pythonista入門(Pythonワンライナーのテクニック集)](https://qiita.com/cocuh/items/37bfdc0f24750127e7d3)

パイプ`|`を使えないのが難儀しているところです。
[xargsにbashのfunctionを渡す方法](http://d.hatena.ne.jp/wristed/20120603/1338691396)を見ましたがうまくできませんでした。

csvとかをcatしてパイプで食わせたりlsをパイプしたくなりますね。

奥が深いので、他にも良い使い方を考えついた方はコメントで教えて下さい。