Cloud Dataflow触ってみた

Cloud Dataflow(以下、Dataflow)を触ってみたので、忘れないようにメモです。

Dataflowの概要

  • GoogleGCP上で提供している、フルマネージドなApache Beamの実行環境
    • Batch と Streaming をあわせて Beam
  • 自分で処理を記述する他に、予め準備されているテンプレートを利用することができる
  • JavaPythonをサポートしている(一部の機能はJavaだけ)
    • GoはApache BeamのSDKとしてはあるが、Dataflowとしてはまだサポートしていないぽい

Apache Beamとは

チュートリアル

最終的にはKotlinで書きたいですが、とりあえずJavaチュートリアルをやってみました。
https://cloud.google.com/dataflow/docs/quickstarts/quickstart-java-maven?hl=ja
事前にJavaMavenのインストールは済ませた状態です。
(インストールしたJavaのバージョンは1.8です)

チュートリアルはBatch的な処理を行い、GCS上に置かれている、シェイクスピアリア王に出てくる英単語の回数をカウントするものです。

$ gsutil ls "gs://apache-beam-samples/shakespeare/kinglear.txt"  
gs://apache-beam-samples/shakespeare/kinglear.txt

ローカルでの実行

チュートリアルに従って、ローカル実行するとoutput-*というファイルがいくつかでき、その中に英単語ごとに出現回数がカウントされた結果が格納されてます。

$ mvn compile exec:java -Dexec.mainClass=org.apache.beam.examples.WordCount -Dexec.args="--output=./output/"
$ ls 
output-00000-of-00005 output-00001-of-00005 output-00002-of-00005 output-00003-of-00005 output-00004-of-00005 pom.xml               src                   target
$ head output-00000-of-00005     
O: 82
hang: 3
hall: 1
ten: 1
turn: 13
frowning: 1
approves: 2
razed: 1
empty: 1
serious: 2

GCP上での実行

PROJECT_IDSTORAGE_BUCKERを環境に合わせて下記のコマンドを実行することで、GCP上で実行されます。

$ mvn -Pdataflow-runner compile exec:java \
      -Dexec.mainClass=org.apache.beam.examples.WordCount \
      -Dexec.args="--project=<PROJECT_ID> \
      --stagingLocation=gs://<STORAGE_BUCKET>/staging/ \
      --output=gs://<STORAGE_BUCKET>/output \
      --runner=DataflowRunner"

dataflowのUIから実行中の状態を下記のように確認できます。
f:id:replicity:20190505124916p:plain

正しく実行できていれば、STORAGE_BUCKERで指定した箇所にoutput-*のフォーマット名で英単語ごとに出現回数がカウントされた結果が格納されてます。

何をしているのか

dataflowではpipelineを作成し、そのパイプラインをdataflow上で動かすことで処理を行います。
pipelineでは以下の処理を実施している。

入力(データ読み込み) -> データに対する処理(変換・集計) -> 出力(データ書き出し)  

pipelineの中で、扱うデータセットPCollectionといい、このPCollectionを受け取り、要素に対して処理を行いPCollectionを返すのをTransformという。
よくあるpipelineパターンについては公式ページに書かれていて、今回のチュートリアルは「basic pipeline」に該当する。
https://beam.apache.org/documentation/pipelines/design-your-pipeline/

チュートリアルのプログラムでは、以下の流れになっている。

  1. pipelineのオプションの作成(ランナー・入力ファイル・出力ファイルのプレフィックス指定等)
  2. データ読み込み(PCollectionを返す)
  3. 行を単語単位に分割した、PCollectionを作成
  4. 単語の出現回数をカウントした、PCollectionを作成
  5. PCollectionをファイルに出力

今後

次はkotlinでpipelineを書いてdataflowを動かしてみたいと思います。

参考

ゴリラ.vim #1 参加記

ウホ、ウホホ、ウホホホ
ゴリラ.vimに参加してきました。
gorillavim.connpass.com 懇親会で3缶ほど飲んだ勢いで帰りの電車内で書いてます。

会場はDeNAさん提供。
https://twitter.com/MarcyWorkOut/status/1097431483181019137

ハッシュタグは #gorilliavim
[https://search.yahoo.co.jp/realtime/search;ylt=A2RCCzR4v2pcF1wAeARnl_p7;ylu=X3oDMTBiNzloa3JsBHZ0aWQDanBjMDAx?p=+gorillavim&ei=UTF-8]

感想

発表は素晴らしいものがたくさんあって、vim熱の高まりを感じました。
それぞれの発表については発表者の方が #gorillavim で資料を上げてくださっているので、全体の感想だけ書いておきます。

ゴリラ.vimという名前ですが、ゴリラ要素は驚くほどなかったです。(Twitterでウホウホいっていたぐらい)
初心者向けの発表から、言葉の節々から技術力の高さを感じさせられる発表があったりととても楽しい時間でした。
ゴリラさんVim歴半年ぐらいだったの!?
Vimの中毒性わかる!
r! 便利そう!
vimが歌った!
reply.vim 便利そう、REPL多様してるし発表聞きながら入れた!
Twitterのトレンドにも入ったようです。
https://twitter.com/gorilla0513/status/1097453791186579456
素晴らしい会を主催してくださった ゴリラ (@gorilla0513) | Twitter さん及び発表者の方・会場を提供してくださったDeNAさんありがとうございます!!!
懇親会で参加者の方と話していたら、学生さんが結構いて驚きました。(さすがVim!)
またライブコーディングならぬライブvimrc設定が行われてたりと非常に濃い懇親会で最高でした。

Goのjson.Unmarshalについてのメモ

jsonを文字列をgoの構造体にするときにお世話になるjson.Unmaarshalを使い、json文字列ではネストした構造のデータを基本型だけをフィールドとしてもつ構造体にunmarshalする方法についてのメモ。

結論

先に結論を書いておくと、以下の例のように変換先の構造体でUnmarshalメソッドを実装すればいいです。

package main

import (
    "encoding/json"
    "fmt"
    "log"
)

type Person struct {
    FirstName string `json:"firstname"`
    LastName  string `json:"lastname"`
    Age       int64  `json:"age"`
}

func (s *Person) UnmarshalJSON(data []byte) error {
    type Name struct {
        FirstName string `json:"firstname"`
        LastName  string `json:"lastname"`
    }

    type person struct {
        Name Name  `json:"name"`
        Age  int64 `json:"age"`
    }

    var tmp person
    if err := json.Unmarshal(data, &tmp); err != nil {
        log.Fatal(err)
    }

    *s = Person{
        FirstName: tmp.Name.FirstName,
        LastName:  tmp.Name.LastName,
        Age:       tmp.Age,
    }
    return nil
}

func main() {
    data := []byte(`{
  "name": {
      "firstname" : "first",
      "lastname" : "last"
  },
  "age": 20
}`)
    var p Person
    json.Unmarshal(data, &p)
    bytes, _ := json.Marshal(p)
    fmt.Printf("%s", bytes)

}

https://play.golang.org/p/m5dH2gp8OGU

実行結果はこちら。

{"firstname":"first","lastname":"last","age":20}

Unmarshalの仕組み

Unmarshal関数でJSONをGoの構造体に変換するときは、以下のようなルールで変換が行われます。
詳しいルールはドキュメントを参照してください。
https://golang.org/pkg/encoding/json/#Unmarshal

  • Unmarshalに引数として渡した構造体のフィールド名またはjsonタグがJSONのキー名と一致した場合に、フィールドの値としてJSONの値を格納する。
  • 構造体のフィールド名またはjsonタグに一致しないフィールドは無視される。

シンプルなJSONをunmarshalする

ネストがないシンプルなJSONをUnmarshlするのはとっても簡単。
JSONのキー名に対応するフィールド名を持つ構造体を定義してあげて、構造体のポインターjson.Unmarshalに渡して上げればいいです。

import (
    "encoding/json"
    "fmt"
)

type Person struct {
    Name string `json:"name"`
    Age  int64
}

func main() {
    data := []byte(`{
      "name" : "first last",
      "age" : 20
  }`)

    var person *Person
    if err := json.Unmarshal(data, &person); err != nil {
        panic(err)
    }
    fmt.Printf("%+v", person)
}

実行結果

&{Name:first last Age:20}

すごく簡単!!

ネストがあるJSONをunmarshlする

JSONの中にネストがある場合には少しだけ複雑で、jsonタグやフィールド名ではネストが表現できないので、
ネストの中身に対応した構造体を定義して(下記の例だとName型)、その型をフィールドとしてもつ構造体のポインターjson.unmarshalに渡す必要があります。

import (
    "encoding/json"
    "fmt"
)

type (
    Name struct {
        FirstName string `json:"firstname"`
        LastName  string `json:"lastname"`
    }

    Person struct {
        Name `json:"name"`
        Age  int64 `json:"age"`
    }
)

func main() {
    data := []byte(`{
  "name": {
      "firstname" : "first",
      "lastname" : "last"
  },
  "age": 27
}`)
    var p Person
    json.Unmarshal(data, &p)
    bytes, _ := json.Marshal(p)
    fmt.Printf("%s", bytes)

}

実行結果

{"name":{"firstname":"first","lastname":"last"},"age":27}

ネストされたデータがあるJSONをネストがないようにunmarshalする(flatten)

unmarshalを行うだけなら上記の方法で特に困ることはないのですが、unmarshalした後の処理でデータにネストがあると扱いづらい時もあると思います。
要はこれを

{
    "name": {
        "firstname" : "first",
        "lastname" : "last"
    },
    "age": 27
}

この構造体にしたい。

type Person struct {
    FirstName string `json:"firstname"`
    LastName  string `json:"lastname"`
    Age       int64  `json:"age"`
}

JSONの中にネストがある場合には、ネストしたデータに対応する構造体を定義する必要があるので、先程の例のような変換を行ったあとに、基本型だけをフィールドとして持つ構造体にマッピングさせます。

import (
    "encoding/json"
    "fmt"
)

type (
    Name struct {
        FirstName string `json:"firstname"`
        LastName  string `json:"lastname"`
    }

    Person struct {
        Name `json:"name"`
        Age  int64 `json:"age"`
    }

    FlatPerson struct {
        FirstName string `json:"firstname"`
        LastName  string `json:"lastname"`
        Age       int64  `json:"age"`
    }
)

func main() {
    data := []byte(`{
    "name": {
        "firstname" : "first",
        "lastname" : "last"
    },
    "age": 27
}`)
    var p Person
    json.Unmarshal(data, &p)
    // unmarshal後に別の構造体にマッピングしている
    f := FlatPerson{
        FirstName: p.FirstName,
        LastName:  p.LastName,
        Age:       p.Age,
    }
    bytes, _ := json.Marshal(f)
    fmt.Printf("%s", bytes)
}

実行結果

{"firstname":"first","lastname":"last","age":27}

やりたかったことはできたが、unmarshal後に毎回マッピングするのは面倒、unmarshal時にやってしまいたいです。
json.Unmarshal関数は第二引数として渡された型がUnmarshalerインターフェイスを実装している場合には、その型のUnmarshalJSONを呼び出します。
UnmarshalerインターフェイスUnmarshalJSONだけを持つシンプルなインターフェイスなので、json.Unmarshalの第二引数として渡したい型にUnmarshalJSONを実装してあげるだけでいいです。

type Unmarshaler interface {
    UnmarshalJSON([]byte) error
}

ということで、第二引数に渡している型にUnmarshalJSONを実装したものが、一番最初に結論として記載しているコードになります。

参考にさせて頂いたサイト

docker-composeで開発用にelasticsearchとkibanaを立てる

利用するimage

elasticsearchの公式imageは3種類ある。

The images are available in three different configurations or "flavors".
The basic flavor, which is the default, ships with X-Pack Basic features pre-installed and automatically activated with a free licence.
The platinum flavor features all X-Pack functionally under a 30-day trial licence. The oss flavor does not include X-Pack, and contains only open-source Elasticsearch.

https://www.elastic.co/guide/en/elasticsearch/reference/6.2/docker.html

  • basic : フリーライセンス、X-Pack Basicがインストールされている
  • platinum : X-Packの全ての機能が入っていて、30日のトライアルライセンス
  • oss X-Packがインストールされておらず、OSSのelasticsearchが入っている。

kibanaの公式imageは2種類ある。

The images are available in two different configurations or "flavors".
The x-pack flavor, which is the default, ships with X-Pack features pre-installed. The oss flavor does not include X-Pack, and contains only open source Kibana.

https://www.elastic.co/guide/en/kibana/6.2/docker.html

  • basic : X-Pack Basicがインストールされている
  • oss X-Packがインストールされておらず、OSSのelasticsearchが入っている。

今回は開発用としてとりあえず立てたいだけなので、X-Packが入っていないOSS版を利用する。

docker-compose

公式サイトにかかれているdocker-composeの設定を参考に作成したものがこちら。

version: "3"
services:
  elasticsearch:
    image: docker.elastic.co/elasticsearch/elasticsearch-oss:6.2.4
    container_name: elasticsearch
    environment:
      - cluster.name=docker-cluster
      - "ES_JAVA_OPTS=-Xms512m -Xmx512m"
    ulimits:
      memlock:
        soft: -1
        hard: -1
    volumes:
          - esdata1:/usr/share/elasticsearch/data
    ports:
          - 9200:9200
  kibana:
    image: docker.elastic.co/kibana/kibana-oss:6.2.4
    container_name: kibana
    links:
      - elasticsearch:elasticsearch
    ports:
      - 5601:5601

volumes:
  esdata1:
    driver: local

設定していることは下記。

  • kibanaからelasticsearchに接続
  • 9200ポートでelasticsearch、5601ポートでkibanaにアクセス
  • elasticsearchのデータを残すためにvolumeをマウント

この状態でdocker-compose upすればelasticsearchとkibanaが立ち上がる。

$docker-compost up 

# 別ウィンドウでシェルを立ち上げて起動していることを確認
$ curl '127.0.0.1:9200'         

{
  "name" : "6bfm0uu",
  "cluster_name" : "docker-cluster",
  "cluster_uuid" : "Wv9oXM4mS9O66WhI4d4DuA",
  "version" : {
    "number" : "6.2.4",
    "build_hash" : "ccec39f",
    "build_date" : "2018-04-12T20:37:28.497551Z",
    "build_snapshot" : false,
    "lucene_version" : "7.2.1",
    "minimum_wire_compatibility_version" : "5.6.0",
    "minimum_index_compatibility_version" : "5.0.0"
  },
  "tagline" : "You Know, for Search"
}

$ curl  '127.0.0.1:9200/_cat/indices?v'
health status index                           uuid                   pri rep docs.count docs.deleted store.size pri.store.size
green  open   .monitoring-es-6-2019.01.27     wJHPWNTlTmakw24pR1oLzA   1   0       5623            6      5.3mb          5.3mb
green  open   .monitoring-kibana-6-2019.01.27 8kPG3bE6Rr2bykskvBaESA   1   0        800            0      649kb          649kb
green  open   .kibana                         bf2Q6gT-SI67CkvQS_8-mA   1   0          1            0        4kb            4kb

この状態でlocalhost:9200にアクセスでelasticsearchにつながる。
ブラウザでlocalhost:5601にアクセスするとkibanaが表示される。
f:id:replicity:20190127183400p:plain

おまけ

上で立てたelasticsearchとkibanaをしばらく使っていたのですが、kibanaでfilterした結果をCSVファイルとしてダウンロードしようと思ったら、X-Packが必要とのことでX-Packを入れた版のdcoker-compose.yamlも作成。

X-Packを入れる場合

ライセンス登録(6.2以下を使用して、1ヶ月以上使う予定の場合)

何も登録しない場合には1ヶ月でライセンスが切れるので、それ以上使用する場合にはライセンスの登録が必要。
ちなみに、X-Packのbasicライセンスは6.3からデフォルトで入っているようになったので、6.3以降は使用する場合はライセンス登録する必要はない。
ライセンスの登録は下記にサイトにアクセスして、指示に従って適当に進めればOK。
登録が完了して、ダウンロードしたライセンス情報のjsonは後で使うので、場所がわかるようにしておく。
https://register.elastic.co/registration

docker-compose

X-Packを入れた以外は、特に変更点はないです。(imageが変わってる)

version: "3"
services:
  elasticsearch:
    image: docker.elastic.co/elasticsearch/elasticsearch:6.2.4
    container_name: elasticsearch
    environment:
      - cluster.name=docker-cluster
      - "ES_JAVA_OPTS=-Xms512m -Xmx512m"
    ulimits:
      memlock:
        soft: -1
        hard: -1
    volumes:
          - esdata1:/usr/share/elasticsearch/data
    ports:
          - 9200:9200
  kibana:
    image: docker.elastic.co/kibana/kibana:6.2.4
    container_name: kibana
    links:
      - elasticsearch:elasticsearch
    ports:
      - 5601:5601

volumes:
  esdata1:
    driver: local

X-Packを入れたので、メニューが増えている。 f:id:replicity:20190127183601p:plain

ライセンスの適用 (6.2以下を使用して、1ヶ月以上使う予定の場合)

elasticsearchに対して、ダウンロードしたライセンスファイルを指定することで、ライセンスを適用します。

# ライセンスの確認
$ curl -XGET 'http://localhost:9200/_xpack/license'    
{
  "license" : {
    "status" : "active",
    "uid" : "65e1b347-ed45-4f36-8a1f-5233ef4608a7",
    "type" : "basic",
    "issue_date" : "2019-01-27T08:58:11.429Z",
    "issue_date_in_millis" : 1548579491429,
    "expiry_date" : "2019-02-26T08:58:11.429Z",
    "expiry_date_in_millis" : 1551171491429
    "max_nodes" : 1000,
    "issued_to" : "docker-cluster",
    "issuer" : "elasticsearch",
    "start_date_in_millis" : -1
  }
}

# 適用
$ curl -XPUT -u elastic:changeme 'http://localhost:9200/_xpack/license?acknowledge=true' -H "Content-Type: application/json" -d @license.json 
{"acknowledged":true,"license_status":"valid"}%                                                     

# 確認
$ curl -XGET 'http://localhost:9200/_xpack/license'    
{
  "license" : {
    "status" : "active",
    "uid" : "c5e7b07a-9630-4712-bbef-3e6f37df29ee",
    "type" : "basic",
    "issue_date" : "2019-01-27T00:00:00.000Z",
    "issue_date_in_millis" : 1548547200000,
    "expiry_date" : "2020-01-27T23:59:59.999Z",
    "expiry_date_in_millis" : 1580169599999,
    "max_nodes" : 100,
    "issued_to" : "##########",
    "issuer" : "Web Form",
    "start_date_in_millis" : 1548547200000
  }
}

参考

goのWaitGroupでハマった話

goで並列処理をするときにsync.WautGroupを使って、Goroutineが終わるのを待つ処理を書いて、意図した通りに動かず30分ほど無駄にした話です。

結論

先に結論を書いておくと、Goroutineの中でAddするのではなく、Goroutineを呼び出す前にAddをしておけば問題ないです。

func something(wg *sync.WaitGroup) {
    fmt.Println("hello")
    defer wg.Done()
}

func main() {
    var wg sync.WaitGroup

    for i := 0; i < 2; i++ {
        // これをGorutineの中でやらない
        wg.Add(1) 
        go something(&wg)
    }
    fmt.Println("wait")
    wg.Wait()
    fmt.Println("end")
}

なにでハマったのか

最初に書いていたコードはこんな感じ。

func something(){
    fmt.Println("hello")
}

func main() {

    for i := 0; i < 2; i++ {
        something()
    }
    fmt.Println("end")
}

これはhelloが2回表示されたあとにendが表示されるだけの、なんの問題もないコード。

hello
hello
end

mainから呼んでいるsomethingをGoroutineとして実行するように、somethingの呼び出し時にgoを追加。

func something(){
    fmt.Println("hello")
}

func main() {

    for i := 0; i < 2; i++ {
        go something()
    }
    fmt.Println("end")
}

この状態だとmainはGoroutineの終了をまたずに終了してしまうので、実行すると下記のように出力された。

end 

このままだとGoroutineの処理が最後まで終わらないので、mainsomethingが終わってから終わるようにsync.WautGroupを使うように変更しました。

func something(wg *sync.WaitGroup) {
    fmt.Println("hello")
    wg.Add(1)
    defer wg.Done()
}

func main() {
    var wg sync.WaitGroup

    for i := 0; i < 2; i++ {
        go something(&wg)
    }
    fmt.Println("wait")
    wg.Wait()
    fmt.Println("end")
}

これで以下のような出力になることを想定してました。

wait
hello
hello
end

が、実際に出力されたのは下記。

wait
end

helloが出力されないままmainが終わってしまってるようです。

試しにforの中でsleepするように変更する。

func something(wg *sync.WaitGroup) {
    fmt.Println("hello")
    wg.Add(1)
    defer wg.Done()
}

func main() {
    var wg sync.WaitGroup

    for i := 0; i < 2; i++ {
        go something(&wg)
        time.Sleep(1 * time.Millisecond)
    }
    fmt.Println("wait")
    wg.Wait()
    fmt.Println("end")
}

helloが出力されるようになりました。
これで一応はGoroutineが終わるまでまってmainが終わるようになりました…
(sleepさせる時間は、少し試してみたところ1msほどsleepさせれば安定してhelloが出力された)

hello
hello
wait
end

sleepをforループの中で毎回するのは、ループの回数が増えてきた単純に遅くなっていくのでなるべく避けたいので、調べてみたら下記の記事でわかりやすく説明してくれてました。
https://qiita.com/ruiu/items/dba58f7b03a9a2ffad65
Goroutineがスケジューリングされるタイミングは即時ではなく、任意なのでGoroutineが動いてAddされる前にWaitまで到達することがあるということで、Goroutineの中ではなく、Goroutineの呼び出し前にAddをしておく必要があるということでした。
最初にも載せてありますが、直したコードがこちら。

func something(wg *sync.WaitGroup) {
    fmt.Println("hello")
    defer wg.Done()
}

func main() {
    var wg sync.WaitGroup

    for i := 0; i < 2; i++ {
        wg.Add(1)
        go something(&wg)
    }
    fmt.Println("wait")
    wg.Wait()
    fmt.Println("end")
}

今までは、Goroutineを生成する個数自体が多かったり、Goroutineを呼び出してからすぐにWaitをすることがなかったので、気づくことがなかっただけで、何回かGoroutineの中でAddをしているコード書いてしまっていました。 しかもそれがなんの問題もなく動いていたので、原因に気づくのにずいぶん遠回りすることになりました。
今後は気をつけて書いていこうと思います。

pyconjp2018に参加して来ました

pyconに参加したあと、私生活がごたごたしていたずいぶん遅くなってしまいましたが、blog書きました。

概要

9月15,16,17,18で開催されたpyconjp 2018に参加してきました。
15日はスプリント、16日がチュートリアル、17,18日がカンファレンスといったスケジュールで、自分はスプリントとカンファレンスに参加してきました。
今年のテーマは「ひろがる Python
公式サイト : https://pycon.jp/2018/

スプリント

自分はQuantXというサービスでpythonを使用して株価の予想を行うプログラムを作って見るスプリントに参加しました。
Quantは作ったアルゴリズムを売ることができるので、いいアルゴリズムが実装できればお小遣い稼ぎができるといったサービスです。
スプリントでは既にこのサービスでアルゴリズムを売っている人や、QuantXを運営しているSmart Trade社の人がいたためとてもスムーズに開発を進めることができました。
金融の知識も簡単なことならば、その場にいる人に質問したら教えてくれたりと非常に助かりました。
加えて、talibめっちゃ便利。 これなかったら時間内に何もできなかったと思う。

自分がやったこととしては下記のような感じです。
- チュートリアルを実施して2つのアルゴリズムを実装 - この2つのアルゴリズムを組み合わせてみた - jupyter環境でQuantX FactoryのAPIを叩ける環境をさわってみた

user slack : https://smarttradeusers.slack.com/messages/C5MMXBTRC/? 公式サイト : https://factory.quantx.io/ SDK Doc : https://factory.quantx.io/quantx-sdk/index.html#

1日目

keynote

アルゼンチンでpythonを広げる活動をやっている人。
pythonを広げる活動をするようになった話をその活動の話だった。
活動の中で彼女に振られたり、ヤギを交通事故を起こしたり、ボリビアの内戦に遭遇しながらも活動を続けており、pythonへの情熱が凄まじかった。
print "Hello world" から始まって、現実でもいろんな世界に行くことができた」というまとめが最高にエモかった。

招待講演 東大松尾研流 実践的AI人材育成法

東大 松尾研のリサーチディレクターの中山さん
最初の15分pythonの話がまったくでてこなかったので、途中退出してブースを回っていたのでわからない。
「numpy力」というワードがtwitterで急に流れてきてた。

Webアプリケーションの仕組み

https://scrapbox.io/shimizukawa/PyCon_JP_2018:_Webアプリケーションの仕組み すごく丁寧なWebの裏側の話だった。
web開発は上から下まで幅広い知識が必要だが、フレームワークを使えば全体を知らなくてもOK。
フレームワークがなぜ生まれたのかをゼロから作って追体験していく話。
会場で使っているフレームワークを聞いていたが、django派が多かった。
実際にコードを動かしながら説明しており、非常にわかりやすかった。
発表内容を参考にして、自分でも一度ゼロから追体験してみるべきだと思った。

あなたと私いますぐパッケージング

ここ1年のパッケージングまわりのライブラリの状況とパッケージ管理のベストプラクティスについて。
パッケジング周りについて、ざっくりと理解できるセッションだった。
パッケージングがsetuptoolsに依存しなくなり、他のパッケージング方法がでてくるかもしれないとのこと。
pipenvでパッケージ管理は問題なさそう。

Pythonで時系列のデータを分析してみよう

https://www.slideshare.net/ssuser49f8441/python-114923273 時系列データにstatsmodelsを適用するだけで、わりと簡単にデータ分析ができるといった内容だった。
もう少し発展的な内容を期待していたので、若干物足りなさはあったが、statsmodelsの使用感はなんとなくわかったのでよかった。

Pythonistaの選球眼(せんきゅうがん) - エンジニアリングと野球の目利きになる技術

野球のデータ分析のPFを作ってきた話を過去のpyconでの発表を振り返りながら、最近作った環境の説明。
選球眼とは「毎日発生する【選択と決断】の繰り返しの中で【選択と集中】ができること」
選択するためには「技術力」を磨くこと「決断」を恐れないこと。
集中するためには、実現したいことを「言語化」し、言語化したものから決断する「ルール」を作り、「時間」・「お金」といったリソースを集中することが大事とのこと。

2日目

2日目は若干体調が悪かったので、午後からyoutube liveで視聴しながらtwitterでつぶやいて参加してました。

REST APIに疲れたあなたに送るGraphQL 入門

RESTの問題点を上げながらGraphQLでそれをどうやって解決しているのかの説明。
後半はAWSのApp SyncでGraphQLを使ってみるデモ。
pythonでGraphQLを使用する話を期待していてので、少し期待外れだったが、GraphQLの概要を理解する意味ではよかった。

複数アプリケーションのプロセスとログを管理するための新しいツールと手法

Jaffleというツール説明 : 複数のpythonアプリケーションと外部プロセスをまとめて管理
https://jaffle.readthedocs.io/en/latest/
開発環境でログの管理を手軽にできるツールがあると確かに便利そうだった。
あとで触ってみる。

Pythonによる異常検知入門

異常検知自体の説明から入り、異常検知の各アルゴリズムの説明とpythonでの実装方法の話だった。
スライドが非常にわかりやすく、異常検知についての理解は深まったが、時間的にpythonでの実装にあまり触れられてなかったのが残念。
異常検知自体の入門としては非常に参考になった。

Django を Zappaで構築してServerless Python のベストプラクティスを探る

いろいろ調査した結果、python で serverless なら zappa だとなったのでそれの発表。
serverless framework ・ zappa ・ chalice について調査し、結果としてzappaが今のところベストプラクティスとのこと。
zappaはDjango zappaが元になっていて、pythonistaが使うなら一番しっくり来るらしい。
Djangoだけじゃなく、flask等でも使うことができるとのこと

感想

  • 今年は初めてスプリントから参加し、合計3日間参加したが、3日間ともとても楽しかった
  • 1日目のkeynoteが最高だった
  • 去年と比べてハッシュタグをつけてつぶやいている人が減った印象
  • 2日目はyoutube liveから参加していたが、すごく見やすくてスタッフさんすごい

python コマンドラインインタフェース比較

目次

  • 概要
  • 環境
  • 調査対象ライブラリ
  • 比較
  • まとめ
  • おまけ

概要

今までpythonを使用してコマンドラインツールを作成する時は、docoptを使っていたのですが、ドキュメントを書いて、パースするタイミングでエラーが出た時の原因の特定に時間がかかって辛いので、他にいい方法がないか調べてみました。

環境

コードを動かした環境は以下。

調査対象ライブラリ

  • optparse
  • argparse
  • docopt
  • click

optparse

https://docs.python.jp/3.6/library/optparse.html

  • 標準ライブラリ
  • 現在は廃止予定となっており、argparseの利用が推奨(https://www.python.org/dev/peps/pep-0389/)
    • 別に使えないわけじゃないので、とりあえず今回の選定対象には入れてあります
  • ライブラリの機能として以下をサポートしていない(実装しだいで実現できなくはない)
    • 位置引数
    • サブコマンド

argparse

https://docs.python.jp/3.6/library/argparse.html#module-argparse

  • 標準ライブラリ
  • optparseの後継なので、optparseから乗り換えがし易い

docopt

http://docopt.org/

  • ドキュメントベースのコマンドラインインターフェース
  • 同じインターフェースが様々な言語で実装されている

click

http://click.pocoo.org/5/

比較

シンプルなインターフェイスの例としては ls コマンドのような command [option]インターフェイスを、サブコマンドをサポートするインターフェースの例としてgit コマンドのようにgit command [option]インターフェイスを実装して比較します。 実装したコードは下記になります。 https://github.com/replicity/compare_python_command_line_parse_libraries

実装したまとめと使ってみた所感

  • それぞれパースした結果をoptparseはマップとリスト、argparseはNamespace、docoptはマップ、clickはメソッドの引数として扱う
  • オプションの作成方法自体は異なるが、シンプルなインターフェイスの場合には機能的な大きな違いはオプション以外の引数がある時にoptparse以外はエラーになり、optparseはリストとして返すぐらい
  • シンプルなインターフェイスを作るだけなら、optparse以外ならどれを使っても問題はなさそう
  • サブコマンドをサポートするインターフェイスを作成しようとするとoptparseはライブラリの機能としてはサポートしていないので、サブコマンドを判断して処理する箇所を自前で実装することになる
  • どのサブコマンドでも使用するオプション(共有オプション)をサポートしているのはargparseだけ
  • clickは位置引数にhelpメッセージを指定できない : http://click.pocoo.org/6/documentation/

まとめ

結局どれを使うのがいいかというとこですが、機能についてまとめた表が下記になります。

ライブラリ 位置引数 オプション 型変換 ヘルプ自動生成 サブコマンド 共有オプション
optparse × × - (サブコマンドをサポートしていない)
argparse
docopt × - (ヘルプを元にインターフェイスを作成) ×
click △(helpメッセージは設定不可) ×

機能だけで見るならargparseが一番高機能です。
ただ、正直argparseでサブコマンドがあるインターフェイスを作るとそれだけでコード量が多くなって辛いです。
clickはサブコマンドの作成が非常に楽でよく考えられている印象でした。
docoptはドキュメントを最初に書くタイミングである程度実装のイメージが固まったり、設計を見直すきっかけになったりするので、ドキュメントを書くのが苦痛じゃないなら選択肢としては良さげだと思います。
ただ、docoptでエラーが出た時にエラーの原因を特定するのが結構困難なので、そこは注意が必要かと。

まとめると、基本的にはclickを使用して他の言語とインターフェイスを揃えたい時はdocopt、どうしてもargparseじゃないと実装できないものがある場合だけargparseを使うのが良さげ。

おまけ

invoke

コマンドラインインターフェイスを構築するものとして少し毛色が違うがinvokeというライブラリもあるので、使ってみました。 http://docs.pyinvoke.org/en/latest/index.html#

invokeはデコレーターベースのコマンドラインインターフェースで、複数のタスクを定義し、そのタスクを組み合わせて何かをする時に場合にはこちらが良さげでした。
具体的にはtask1の後にtask2を連続で実行するといったような場合。

inv task1 task2

また、オプションの解釈に関しては触ったライブラリの中ではinvokeが一番優秀な印象でした。
詳しくはこちらに書いてあります : http://docs.pyinvoke.org/en/latest/concepts/invoking-tasks.html#task-command-line-arguments

clickとの比較として下記。

ライブラリ 位置引数 オプション 型変換 ヘルプ自動生成 サブコマンド 共有オプション 複数のタスクの連続実行
click △(helpメッセージは設定不可) × ×
invoke △(複数の値を指連続で指定する方法にくせがある) △(boolとそれ以外) × - (サブコマンドをサポートしていない)

python-prompt-toolkit

インタラクティブコマンドラインツールを作成できるライブラリとしてpython-prompt_toolkitがあり、これを利用することで実行後にサブコマンドを選択したり、オプションをインタラクティブにセッティングしていくようなツールを簡単に作ることが可能になる(e.g. click-repl)。
https://github.com/jonathanslenders/python-prompt-toolkit/tree/master

なお、2018/5/5現在pipでいれると1.0.15がインストールされるが、githubには2.0のブランチがあり、リポジトリのREADMEに書かれているように1.0と2.0ではいろいろと変わっているようなので、これから使い方を覚えるなら2.0を使うほうが良さげでした。

Please notice that this is the 2.0 branch of prompt_toolkit. It is incompatible with the 1.0 branch, but much better in many regards. On Pypi you'll still find the 1.0 branch, but a release should follow soon. In the meantime, don't hesitate to test this branch and report any bugs.

ref