社内se × プログラマ × ビッグデータ

プログラミングなどITに興味があります。

Python 言語について 新しく知った 9つのこと

Python の初心者です。
Pythonによるデータ分析入門(第二版) の第二章に Python 言語について書かれていた内容から、自分にとって新しい発見が多かったので、それを列挙します。
既に Python を学んだことがある方にとっては、常識的な内容かと思います。

1. インデントは 4つの空白で
自分は 2つの空白でよく書いてますが、4つがスタンダードらしいです。

2. セミコロンを使えば、複数の変数を1行で定義できる

a = 5; b = 6; c = 7

Java のように各行の最後に ; を置く必要はないですが、あえて置くことで、このような定義が可能になります。
しかし読みづらいので、特に推奨はされないですね。

3. すべてのものがオブジェクトである
Python の開発者が好きな言葉らしいですが、これが意味することについては、まだ理解できていないです。
恐らく、以下の記事を読んだほうが理解が深まりそうです。
http://blog.lerner.co.il/pythons-objects-and-classes-a-visual-guide/

4. リストの場合 a = b は値のコピーではなく、参照先のコピーである

a = [1, 2, 3]
b = a
a.append(4)
b
# -> [1, 2, 3, 4]

a の値を変更すると、b の値も変更されている。
すなわち a と b が参照している値は同じである。
リストの場合だけではないかもしれませんが、とりえあず。

5. 関数に渡す引数がリストの場合も値のコピーではなく、参照先のコピーである

def append_element(list, element):
    list.append(element)

data = [1, 2, 3]
append_element(data, 4)
data
[1, 2, 3, 4]

data を append_element に渡し、関数内で演算後、呼び出し元で data の値を見てみると、関数内で演算した内容が反映されている。
すなわち 関数に渡した data と元の data が参照している値は同じである。
こちらもリストの場合だけではないかもしれませんが、とりえあず。

6. 暗黙の型変換は明らかな場合のみ

a = 4.5
b = 2
a / b
# -> 2.25

float と int の計算時には暗黙的に変換してくれます。

a = 2
b = "m"
print(a + b)
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-41-827396fae8f4> in <module>()
      1 a = 2
      2 b = "m"
----> 3 print(a + b)

TypeError: unsupported operand type(s) for +: 'int' and 'str'

int と str をつなげようとしても、エラーになります。
Java などでは、自動的に int が str に変換されて処理してくれますが、python ではより厳密に型について考えられているようです。

7. isinstance(value, type) で型のチェックができる

a = 5
isinstance(a, int)
# -> True

a = 5
isinstance(a, str)
# -> False

何の型かを調べるなら、type(a) で "int" が得られます。

8. Duck typing
“If it walks like a duck and quacks like a duck, then it’s a duck.”
"もしそれがアヒルのように歩き鳴くのであれば、それはアヒルだ。"
以下の例で言うと、、

def isiterable(obj):
    try:
        iter(obj)
        return True
    except TypeError: # not iterable
        return False

もし、そのオブジェクトからイテレータを作成できるのであれば、それは iterable である。
当然と言えばそうかもしれませんが、あるオブジェクトの型そのものは気にしなくても、その特性について意識する場合、こういったチェックが必要になってくるのだろうと思います。
"Duck typing" について、何となく分かったような気もしますが、まだぼんやりとしています。

9. 値の比較は ==, オブジェクトの比較は is

a = [1, 2, 3]
b = a
b == a # True
b is a # True

b と a は値も参照先も等しい。

a = [1, 2, 3]
c = list(a)
c == a # True
c is a # False

c と a は値は等しいが、参照先は異なる。

所感
細かい点を含めると、一節を読んだだけでも知らないことがたくさんでてきますね。
また、読んだときには分かったような気になってても、忘れてしまうことも多いので、繰り返し練習しながら理解を深めていきたい。

Java シングルトンデザインパターン singleton design pattern

singleton design pattern 実装する機会は今までに何度もあったものの、改めて順序だてて考えてみます。
”そのクラスのインスタンスは常に1つしか存在しないことを保証する”こと。

実装にあたって、気をつけたい点

1. どのようにして、インスタンス化を行うのか
2. どのようにして、そのインスタンスが一つだけであることを保証するのか
3. どのようにして、そのシングルのインスタンスを利用できるのか

実装

1. コンフィグファイルを読むためのクラスをシングルトンにしてみます

public class Config {
	
	private static Config config = new Config();
	
	private Config(){}
	
	public static Config getInstance() {
		return config;
	}
	
	public String getString(String key) {
		return "example";
	}
}

ポイント
1. コンストラクタが private なので、外部からこのインスタンスを生成することはできない
2. config のインスタンスが static で生成されているため、メモリ上に1度しか割り当てされない
> この Config クラスがロードしたときだけ、インスタンスが生成される
> スレッドセーフである

欠点
コンストラクタにパラメータを与えることができないので、柔軟性に欠ける

2. 上記の欠点を補うため、getInstance() 内でコンストラクタを生成するようにしてみます

public class Config {
	
	private static Config config;
	private String pamameter;
	
	private Config(String parameter){
		this.pamameter = parameter;
	}
	
	public static Config getInstance(String parameter) {
		if (config == null) {
			config = new Config(parameter);
		}
		return config;
	}
	
	public String getString(String key) {
		return "example" + pamameter;
	}
}

ポイント
1. getInstance がパラメータを受け取り、それをコンストラクタに渡せるようになった
2. config が null の時だけ、インスタンスを生成しようとしている

欠点
スレッドセーフではない

3. 上記の欠点を補うため、getInstance() を synchronize にしてみます

public class Config {
	
	private static Config config;
	private String pamameter;
	
	private Config(String parameter){
		this.pamameter = parameter;
	}
	
	public synchronized static Config getInstance(String parameter) {
		if (config == null) {
			config = new Config(parameter);
		}
		return config;
	}
	
	public String getString(String key) {
		return "example" + pamameter;
	}
}

ポイント
getInstance() を synchronized にしたので、マルチスレッド下でも重複してインスタンスが生成されない

欠点
synchronized が必要なのはインスタンスが生成されるまでだが、getInstance() が呼ばれる度に判定されてしまう
> 不要なオーバーヘッド

かなり頻繁に getInstance() が呼ばれるケースでは、パフォーマンス面で問題になるかもしれませんね。
上記の欠点については、別の機会に考えます。

いまさら IPython を初めて使ってみる

いまさらですが、IPython は pythonインタラクティブシェルを拡張したもので、タブ補完機能などが使えるみたいです。
Python for Data Analysis の本を読む機会があって、そこで初めて試してみました。

起動
ipython コマンドで起動可能です。
実はインストールした覚えがないのですが、恐らく Anaconda を使って python インストールした時に、一緒に入ったのではないかと思います。
Python 3.6.3 に対して、IPython はもう 6.1.0 ですか。

$ ipython
Python 3.6.3 |Anaconda, Inc.| (default, Oct 13 2017, 12:02:49) 
Type 'copyright', 'credits' or 'license' for more information
IPython 6.1.0 -- An enhanced Interactive Python. Type '?' for help.

試行
とりあえず、試してみます。
最近、python2 に慣れているので、単純に文法ミス。。

In [1]: a = 5

In [2]: print a
  File "<ipython-input-2-9d7b17ad5387>", line 1
    print a
          ^
SyntaxError: Missing parentheses in call to 'print'. Did you mean print(a)?

通常のインタラクティブシェルと同様に変数だけ指定しても出力してくれます。
その場合、In に対して Out が分かり易く表示されています。

In [3]: print(a)
5

In [4]: a
Out[4]: 5

文字列の場合。

In [24]: greeting = "hello ipython!!"
In [25]: greeting
Out[25]: 'hello ipython!!'

%run ファイル名 を指定することで、ローカルにあるスクリプトファイルを実行することも可能です。

In [23]: %run test.py
this is test.

集合の中のデータ長が長くなると、改行して表示してくれる。

In [17]: data = {i : i + 1000000000 for i in range(7)}

In [18]: data
Out[18]: 
{0: 1000000000,
 1: 1000000001,
 2: 1000000002,
 3: 1000000003,
 4: 1000000004,
 5: 1000000005,
 6: 1000000006}

うれしい補完機能。
f:id:blueskyarea:20180718231354p:plain

感想
補完機能もついていますし、環境があるのであれば通常のインタラクティブシェルの代わりに使ってみたくなるものでした。

ユーザー類似度の計算(協調フィルタリングの前段階) python

今回は Mario さん、Peach さん、Koopa さんに5つのゲームを遊んでもらい、それぞれに5段階点数で評価をつけてもらいました。
※登場人物はフィクションです
その結果が以下です。
data.py

review={
 'Mario': {
 'OCTOPATH TRAVELER': 3.5, 'MARIO TENNIS ACE': 3.0, 'Splatoon2': 2.5, 'Legend of Zelda: Breath of the Wild': 3.5, 'Xenoblade 2': 2.5
  },
 'Peach': {
 'OCTOPATH TRAVELER': 1.5, 'MARIO TENNIS ACE': 3.5, 'Splatoon2': 5.0, 'Legend of Zelda: Breath of the Wild': 5.0, 'Xenoblade 2': 3.0
  },
 'Koopa': {
 'OCTOPATH TRAVELER': 3.0, 'MARIO TENNIS ACE': 3.5, 'Splatoon2': 3.5, 'Legend of Zelda: Breath of the Wild': 4.0, 'Xenoblade 2': 3.5
  }
}

この結果から、これらの3人が”どの位似ている”のかを求めていきます。
いわゆる 協調フィルタリングと呼ばれるものに使われたりするものです。
何らかの評価データから、ユーザー同士の類似度を計算し、似ているユーザーが好むものは、そのユーザーにも好まれやすいという仮説の元、レコメンデーションに使われたりします。

ここでは、ユーザー同士の類似度を以下で計算しました。
user-similarity.py

import sys
from math import sqrt
from data import review

def calc_similarity(member1, member2):
  games = set(review[member1].keys())
  distance_list = []

  for game in games:
    distance = pow(review[member1][game] - review[member2][game], 2)   <- (1)
    distance_list.append(distance)

  print "similarity of " + member1 + " and " + member2
  print 1/(1 + sqrt(sum(distance_list)))  <- (2)

if __name__ == '__main__':
  args = sys.argv
  calc_similarity(args[1], args[2])

1. それぞれの評価(点数)の差を2乗したものを”距離”とし、それぞれの評価が以下に離れているかを数字で表します。
2乗にしているのは、あくまでその距離を絶対値として評価するためですね。

2. それぞれのゲームに対する”距離”の和を求めてから、その平行根を求めています。
※sqrt(sum(distance_list))の部分
この値が大きいほど、”距離が離れている”= "類似していない" ということになります。
そのため、”類似している” 度合いを取るために、その逆数を取る(1から割る)ようにしています。

ちなみに 1 + sqrt(sum(distance_list)) のようにに +1 していますが、もし距離がゼロの場合、sqrt(sum(distance_list)) が 0 になります。
よって、1 / 1 + sqrt(sum(distance_list)) = 1/1 という計算式になり、類似度が"1" という結果になります。
これが完全に一致している状態です。

では、それぞれの類似度をみていきます。

$ python user-similarity.py Mario Peach
similarity of Mario and Peach
0.217129272955

$ python user-similarity.py Mario Koopa
similarity of Mario and Koopa
0.37617851153

$ python user-similarity.py Peach Koopa
similarity of Peach and Koopa
0.294298055086

ということで、Mario さんと Koopa さんが似た趣向を持っていて、Mario さんと Peach さんがあまり似ていないという結果になりました。
確かに、Mario さんと Koopa さんでは、最大で 1.0 しか評価の差はありませんが、Mario さんと Peach さんでは 2.5 の差がある評価もありました。

もし Koopa さんに何かレコメンドしたいのであれば、Mario さんが好んでいるものをレコメンドすれば、刺さりやすいだろうということになります。

何らかのデータを元に仮説を立てるという作業は面白いと思います。
あとは、それを実際に検証が出来ると良いのですが。

5つのプログラミング テクニック(参照元あり)

参照元の記事は以下です。自分なりに読み解いてみたいと思います。
https://nobugsproject.com/2017/04/07/5-great-programming-techniques-every-developer-should-know/

この手の英語の記事はたくさんあって、それぞれ違う観点で書いてあったりするから面白いです。

1. if 条件は必要とするメソッドの中に入れてしまおう

Encapsulate って直訳すると、カプセル化になると思うのですが、Java 用語のカプセル化とは違う意味で混乱しそうだったので、単純に”入れる”という表現で解釈しました。
一応、同じ例を参照元と同じですが書いてみると、
悪い例

if (value > 500) {
  applyDiscountForGreaterThan500();
} else if (value > 1000) {
  applyDiscountForGreaterThan1000();
}

良い例

applyDiscountIfValueIsGreaterThan500();
applyDiscountIfValueIsGreaterThan1000();

これ以上の解説は特に書いてありませんが、とりあえず、各メソッドは呼び出すけれど、実際に適用するかどうかは、各メソッドの中で判断させるべきという意味かと思います。
value が 1000 より大きい時に、両方のメソッドが適用されそうだけど良いのだろうか。とか変なところが気になります。

そもそも、悪い例に書いてあるコードでは、下のメソッドが呼ばれることはないので、条件式を逆にすべきなのですが、その不具合を避けるという意味も込められているのでしょうか。

2. package/class/method/variable の名前をちゃんと考えよう

これは分かり易いと思います。
package や class 名は、そのオブジェクトの役割に沿った名前にした方が良いと思いますし、method や variable についても、それが何なのか出来るだけ具体的な表現をすることで、プログラムの流れを追いやすくなります。
最初に思い浮かんだ名前を使うんじゃなくて、もっと良く考えてねって言ってますね。
これはとても同意したいです。

あいまいな名前の例

Double clientValue; // クライアントの何の値なの ?
 
void hide() // なにが隠れるの ?
 
class Historic {} // このクラスはすべての歴史なの ?
3. IDE のショートカットを使いこなそう

これは耳が少し痛いです。
IDE って便利なショートカットを備えていると思うのですが、覚えるのが面倒で、使っていないショートカットもたくさんあります。
この記事では eclipse を例に挙げていますが、使ったことがないのもあります。

もしショートカットを使わずに手動でやってしまったら、一旦その処理を元に戻して、ショートカットでやり直しなさいとのこと(覚えるために)。
確かに生産性が上がるとは思うので、なるべく意識して使うようにはしたいです。

4. 必要とする機能はメソッド化してしまおう

同じような処理を何度も書くくらいなら、メソッドに入れて使うようにすべきということと思います。

Calendar c = Calendar.getInstance();
c.setTime(new Date());
c.add(Calendar.DATE, +1);
Date userDate = c.getTime();

この例にあるような日付の計算などは、共通で何度も呼び出される可能性が高い処理だと思いますので、ユーティリティクラスにまとめてしまうのが良いだろうと思います。
もっと言うなら、ライブラリにしてしまって、アプリケーション間でも共有できるようにしても良いと思います。

5. コードレビューする文化をつくろう

自分のソースコードを他人に見られたくないとか、恥ずかしいとか思うかもしれないけど、勇気を出して他人に見てもらうべきということですね。
確かに、他人に見てもらうことで、自分が気づかなかった観点でより良いコードが書けることに気づいたり、バグが見つかったり、メリットはたくさんあるように思います。
そういったことが出来る文化や、仲間に出会えるかって、実はとても重要なことなのかもしれません。

感想

一応、すごいテクニックという見出しの記事でしたが、比較的に基本的な内容で読みやすかったです。
ただ、どれも大切な習慣であることには違いないと思います。

hbase shell non-interactive mode

hbase shell は実行したいコマンドを渡すことで、起動しなくてもコマンドの実行結果を受け取れます。

# echo "list" | hbase shell

list
TABLE
0 row(s) in 0.4830 seconds

[]

コマンドの終了コードも受け取れます。

# echo $?
-> 0

これは便利なのですが、もし実行が何らかの理由で失敗した場合も、コマンドの終了コードは 0 (成功) で返ってきます。

# echo "invalid" | hbase shell

invalid
NameError: undefined local variable or method `invalid' for #<Object:0x79b18230>
# echo $?
0

出力されたメッセージを解析しないとダメかなと思ったら、non-interactive mode と呼ばれるモードがありました。
オプション n を付けるだけです。

# echo "invalid" | hbase shell -n
ERROR NameError: undefined local variable or method `invalid' for #<Object:0x3dce6dd8>
# echo $?
1

不要なメッセージも表示されなくなったし、コマンドの終了コードは 1 (失敗) で返ってきてくれます。

やや古いドキュメントですが、このオプション付きで実行することが普通みたいですね。

ドキュメントをチェックして良かった。

hadoop namenode スタート直後は hdfs コマンドが動かない ?

Hadoop の構築スクリプトを作っています。
namenode を起動した後に、必要なディレクトhdfs 上に作成するため、以下のようにスクリプトを記述。

/usr/hdp/2.6.2.0-205/hadoop/sbin/hadoop-daemon.sh start namenode
hdfs dfs -mkdir -p /user/my_data

ところが、mkdir コマンドの実行中にエラーが発生。

mkdir: Call From hdp-rm1001.docker/172.20.0.2 to hdp-rm1001.docker:9000 failed on connection exception: java.net.ConnectException: Connection refused; For more details see:  http://wiki.apache.org/hadoop/ConnectionRefused

ConnectionRefused ? ということでしたが、同ホスト内での通信なので、ネットワークの問題ではなさそう。
ただ、この 9000 番ポートが LISTEN 状態にはなっていなかった。

少し、待ってから手動で mkdir のコマンドを試してみると、動いた!
試しに namenode 起動した後、60 秒 sleep させてみると。

/usr/hdp/2.6.2.0-205/hadoop/sbin/hadoop-daemon.sh start namenode
sleep 60
hdfs dfs -mkdir -p /user/my_data

エラー発生しない!
ということは、namenode 起動直後は hdfs とコミュニケーションを取る準備がまだ出来ていないということでしょうか。
このタイミングにおいては、9000 番ポートが LISTEN 状態になっていました。

一応、datanode については、既に起動してあったので、恐らく namenode の起動タイミングによる問題と想定。
ちなみに sleep 時間を 10 秒に短縮すると、ダメでした。
そして namenode の起動ログを見てみると、実は1分くらい起動に要している!

2018-07-09 12:35:11,995 INFO org.apache.hadoop.hdfs.server.namenode.NameNode: STARTUP_MSG:
2018-07-09 12:36:02,526 INFO org.apache.hadoop.hdfs.server.namenode.NameNode: NameNode RPC up at: hdp-rm1001.docker/172.20.0.2:9000

namenode start 投げてから、1分以内に hdfs コマンド投げる必要はないので、困ることはないかと。
ただ、自分は ConnectionRefused というメッセージに踊らされ、しばらく混乱してしまいました。。

反省点
start コマンド投げたからと言って、起動完了したわけではない。