Raspberry Pi(jessie)に繋いだスピーカーがOS再起動時にボンというのを抑止したい（Pythonスクリプト）

#前置き
この記事は[Raspberry Pi(jessie)に繋いだスピーカーがOS再起動時にボンというのを抑止したい（Perlスクリプト） ](http://qiita.com/kouichirou/items/76dadc7cab6ef694fe18)のスクリプトのみをPythonでも書いてみた、という内容です。
ですので、スクリプト以外は特に目新しい物は無いのでご承知おき下さい。

#前提
[Raspberry Pi(jessie)に繋いだスピーカーがOS再起動時にボンというのを抑止したい（Perlスクリプト） ](http://qiita.com/kouichirou/items/76dadc7cab6ef694fe18)をご確認下さい。


#解説
では早速まいりましょう。

###スクリプト全体
とりあえず貼っておきます。下で解説します。

```lang:get_dac_port_num.py
#!/usr/bin/python
# coding: utf-8

import sys
import re

HUB = {}

reBUS  = re.compile(r'Bus (\d*)\.Port')
reHUB  = re.compile(r'^(\s*)\|__.*Port (\d*): .*Class=Hub')
rePort = re.compile(r'^(\s*)\|__.*Port (\d*): .*Driver=snd-usb-audio')

for line in sys.stdin:
        tmp = reHUB.search( line )
        if tmp:
                HUB[len(tmp.group(1))] = str(int(tmp.group(2))) + "."
                continue

        tmp = rePort.search( line )
        if tmp:
                HUB[len(tmp.group(1))] = str(int(tmp.group(2)))
                for v in range(0,len(tmp.group(1))+1):
                        if HUB.has_key(v):
                                sys.stdout.write(HUB.get(v))
                exit()

        tmp = reBUS.search( line )
        if tmp:
                HUB[0] = str(int(tmp.group(1))) + "-"
                continue
```


では、細かく見ていきましょう。

###インポート
標準入力と、正規表現を使うためsysとreをインポートしました。

```
import sys
import re
```


###変数定義
各番号を入れておくためのディクショナリを定義します。別に名前はHUBでなくても良いんですが、たまたまそうなってましたね。

```
HUB = {}
```
次は、正規表現を早く回すためのおまじないとして、事前にコンパイルしています。
上から順に、バス番号、ハブ番号、ポート番号の為の正規表現です。正規表現そのものは[Perlの時](http://qiita.com/kouichirou/items/76dadc7cab6ef694fe18#%E5%AD%90%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%97%E3%83%88)と全く同じです。

```
reBUS  = re.compile(r'Bus (\d*)\.Port')
reHUB  = re.compile(r'^(\s*)\|__.*Port (\d*): .*Class=Hub')
rePort = re.compile(r'^(\s*)\|__.*Port (\d*): .*Driver=snd-usb-audio')
```

細かいことは説明しませんが、後方参照用としてのグループ（カッコでくるんだ部分）がそれぞれ入っています。くわしくは[こちら](http://www.geocities.jp/m_hiroi/light/python04.html)を参照下さい。


###メインルーチン
####for文
標準入力から１行ずつ処理しますよー、という事です。

```
for line in sys.stdin:
```
一応、[こちら](http://webdev.bulkitem.com/python%E3%81%A7%E6%A8%99%E6%BA%96%E5%85%A5%E5%8A%9B%E3%82%92%E8%AA%AD%E3%82%80)あたりに書いてある様な、`buffer = sys.stdin.readline.splitlines()`の様に最初に全体読み込んでしまうやり方も考慮したんですが、スピード的に遅かったようなのでやめました。（と言っても20万行位の（lsusb -tではまずありえない）量にテストデータを水増しした際の結果で10%程度の差でしたので、実用としてはどっちでもOK牧場でしょう。）
 　

####forの中身1(HUB)
まずHUBの値をチェックします。
事前にコンパイル済みのオブジェクト`reHUB`で、読み取り行を検索します。
`tmp`は、検索結果が入るオブジェクトです。検索時にグループとして取得しておいた値を`tmp.group(1)`の様にして後方参照する為に使います。
検索結果のオブジェクトがあった場合（＝正規表現がマッチした場合）、HUBディクショナリに対して値を追加します。キーは例の如く該当行の頭の空白文字数、値はPortの後に続く数字を文字列として取得してからint型に変換後に再度文字列として"."（ドット）を添えて追加します。

```
        tmp = reHUB.search( line )
        if tmp:
                HUB[len(tmp.group(1))] = str(int(tmp.group(2))) + "."
                continue
```
例えば、入力文字列が以下のように

```
    |__ Port 1: Dev 2, If 0, Class=Hub, Driver=hub/5p, 480M
```
となっていた場合であれば、空白が4桁ありますので

```
HUB[len("    ")] = str(int("1")) + "."
```
となり、つまり`HUB[4] = "1" + "."`という事になります。
ちなみに、なぜ数字をintにしてstrしているかというと、もし数字部分が`01`だったら最終的にunbindに渡す為に欲しい結果が例えば`01-01.01`のように変な事になっちゃうので、困ります:pensive:。
その為、`01`という文字列だったとしてもそれをintしておけば`1`になるので後々都合が良いのです。

値の代入が終わったら、コンテニューしてforの頭に戻ります。

```
                continue
```
これを入れておかないと、後続のrePortやreBUSの検索まで行ってしまうので、大変無駄です。トータルで倍近い処理となってしまいます。と言っても20万行位で1.2秒か0.6秒か程度なので、、、（以下略）
　


####forの中身2(Port)
殆ど上記Hubと同じ処理内容なんですが、違うのは出力処理が有ることです。

```
        tmp = rePort.search( line )
        if tmp:
                HUB[len(tmp.group(1))] = str(int(tmp.group(2)))
                for v in range(0,len(tmp.group(1))+1):
                        if HUB.has_key(v):
                                sys.stdout.write(HUB.get(v))
                exit()
```

Portの検索でマッチする＝BUSもHUBも既に値が入っている筈です。
ですので、HUBディクショナリに入った値を必要な分だけ出力します。（必要な分だけ、というのは、途中にHUBを何個もぶら下げた長い枝があったりすると、HUBはそのHUBの個数分だけ深くなっています。

例えば、途中に長い枝があって、HUBディクショナリが以下のような状態になったとします。（ディクショナリのキーは本来ソートされていない筈ですが、簡単の為。）

```
['0':'1-' , '4':'2.' , '8':'1.' , '12':'3.' , '16':'2.' , '20':'1.' ]
#値に-が付くのはBUS、.が付くのはHUBです。
```

その後何ステップか進み、

```
['0':'1-' , '4':'3.' , '8':'3' , '12':'3.' , '16':'2.' , '20':'1.' ]
#値に-が付くのはBUS、.が付くのはHUB、何もつかない数字のみはPortです。
```
という状態になったら、`1-3.3`と出力されるのが正しいのですが、全部出しちゃうと`1-3.33.2.1.`と意味不明となるのです。

なので、HUBディクショナリのキー用として0から必要な桁数分だけ（上記だと8まで）をrangeで値を作りますが、PythonのRangeは0からスタートだそうなので+1しないといけません。

```
                for v in range(0,len(tmp.group(1))+1):
                        if HUB.has_key(v):
                                sys.stdout.write(HUB.get(v))
```
また、キーは飛び飛びの値をとりますので、値が入ってない時のエラー防止として`has_key`でチェックしています。
 　

####forの中身3(BUS)
いよいよ最後、BUSです。
と言っても、殆どHUBと同じです。異なるのはキーが0と決め打ちな点です。BUSは最上位ですので、一々計算する必要がありません。

```
        tmp = reBUS.search( line )
        if tmp:
                HUB[0] = str(int(tmp.group(1))) + "-"
                continue
```


####説明おわり

#PerlとPythonの速度的な話
lsusb -tの結果は高々十数行ですので、実用的にはまぁホントにどっちでも良いんですが :sweat_smile: 、パフォーマンスどのくらい違うんだろうなー、と上述の如く、無駄に対象の行数を増やしたケースで同じLinux上でテストしてみました。
概ね20万行に水増ししましたが、余り行数自体に言及しても意味が無いので、あくまで同じ行数でどの程度差があるかという点だけをとりあえず気にすることにしましょう。

Perlのスクリプト（該当の処理だけ抜粋版）の時間をtimeで何回か計測した所、概ね

```lang:Perl
real    0m0.450s
user    0m0.433s
sys     0m0.031s
```
というような数字感です。翻ってPythonだと、

```lang:Python
real    0m0.720s
user    0m0.710s
sys     0m0.019s
```
といった感じ。userの値としてはPythonの方が1.6倍程度。結構差がでますね。

対象の行数を、実際の数行程度にした場合では

```lang:Perl
real    0m0.011s
user    0m0.004s
sys     0m0.006s
```

```lang:Python
real    0m0.021s
user    0m0.012s
sys     0m0.009s
```
と言った感じ。ふーん。Perlも結構重たいと思ってたけど、Pythonよりはちょっと早いのか。（あくまで今回の処理において、ですが。）

#Pythonだけでの速度的な話
一応、速さを気にしてしまったので、それなりにPythonのアルゴリズムも色々考えました。

##Continueを入れる　→　早くなった
上でも既に説明してますが、普通に考えて早くなりますよね。何か処理したらあとは余計な処理しないで次の行に行くので。
ちなみに、Perl版でもこれやったほうが良いんでしょうが、折角短く書けたのを変えるのも嫌だったので考えるのをやめました。

##BUS,HUB,Portの順ではなく、HUB,Port,BUSの順とする　→　早くなった
これも、色々試しましたが、

- BUSは普通に考えて絶対的に数が少ないので、最後で良い。
- HUBについては、今回の水増しテストデータにわざとHUBを多数入れてあるので最初に持ってきたほうが良かったと思われる。
- なのでPortが真ん中。HUBが少なくてPortがやたら多いデータだとHUBとPortの順番変えた方が良いのかも

という事で落ち着きました。
大学のアルゴリズムの授業とかなら数を数えたりして評価するのでしょうが、まぁ、割愛です :stuck_out_tongue: 。


##関数を使ってみる　→　遅くなった
BUSもHUBもPortもforの中の処理は似てる為、つい関数化したくなりました。
ただ、関数化するためにreBUSの正規表現を変えなくてはならず、どうもその新reBUSが足を引っ張った様で、1割程度遅くなりました。（なお、以下ソースでは手抜きだったので出力部分が未完成なのと、HUBディクショナリへの代入時の"-"とか"."が無いというあたりはご愛嬌。そんなに影響しないでしょ。）

```
reBUS  = re.compile(r'^\S*(\s*)Bus (\d*)\.Port')
#reBUS  = re.compile(r'Bus (\d*)\.Port')
reHUB  = re.compile(r'^(\s*)\|__.*Port (\d*): .*Class=Hub')
rePort = re.compile(r'^(\s*)\|__.*Port (\d*): .*Driver=snd-usb-audio')

def calc(re,line):
        tmp = re.search( line )
        if tmp:
                HUB[len(tmp.group(1))] = int(tmp.group(2))
                return True
        else:
                return False

for line in sys.stdin:
        if calc(reHUB,line):
                continue

        if calc(rePort,line):
                print HUB
                exit()

        if calc(reBUS,line):
                continue
```



##関数は辞めたけどシンプルに書いてみる　→　遅くなった
事前コンパイルのオブジェクトをreSというディクショナリに入れてforしました。
見た目が綺麗になったような、ならないような。。
速度的にも倍近くと遅くなってしまったです。新reBUSとreSと、HUBディクショナリへの代入後に毎回`RE == rePort`の確認を入れてるあたりが余計なのでしょう。

```
reS = [reBUS , rePort , reHUB ]

for line in sys.stdin:
        for RE in reS:
                tmp = RE.search( line )
                if tmp:
                        HUB[len(tmp.group(1))] = int(tmp.group(2))
                        if RE == rePort:
                                print HUB
                                exit()
                        continue
```

#改めてのソース全体
ということで、私なりに色々試行錯誤してみての、改めての以下のソースです。（最初と同じです）

```lang:get_dac_port_num.py
#!/usr/bin/python
# coding: utf-8

import sys
import re

HUB = {}

reBUS  = re.compile(r'Bus (\d*)\.Port')
reHUB  = re.compile(r'^(\s*)\|__.*Port (\d*): .*Class=Hub')
rePort = re.compile(r'^(\s*)\|__.*Port (\d*): .*Driver=snd-usb-audio')

for line in sys.stdin:
        tmp = reHUB.search( line )
        if tmp:
                HUB[len(tmp.group(1))] = str(int(tmp.group(2))) + "."
                continue

        tmp = rePort.search( line )
        if tmp:
                HUB[len(tmp.group(1))] = str(int(tmp.group(2)))
                for v in range(0,len(tmp.group(1))+1):
                        if HUB.has_key(v):
                                sys.stdout.write(HUB.get(v))
                exit()

        tmp = reBUS.search( line )
        if tmp:
                HUB[0] = str(int(tmp.group(1))) + "-"
                continue
```


#初Pythonの所感
最近、PHPやShell、Perlなどの言語ばかりでコード書いていた為、Pythonの噂通りの礼儀正しさに若干煩わしさを覚えました。
しかし、一方で、型変換とかちゃんとやんないとな、とか、あんまりマニアックに省略して書いちゃうと初心者サッパリわからんよなー、とかいう意識も最近出ていたのでJavaを書くときのような背筋をピンとした気持ち（？）で取り組めました。

まだPythonのごく基本的な機能しか使っていないと思うので、今後勉強して面白い事あったらまた記事にします。

#最後に
このPythonスクリプトは、[Perlの時の子スクリプト](http://qiita.com/kouichirou/items/76dadc7cab6ef694fe18#%E5%AD%90%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%97%E3%83%88)と違って内部でlsusb -tを叩いていない為、もし実装してみようと思ったら、`chmod ugo+x [Pythonスクリプトフルパス]`しておいて、

```
/usb/bin/lsusb -t | [Pythonスクリプトフルパス]　> $UNBIND
```
みたいに親スクリプト中に記載下さい。

では。