位置情報によるタイムゾーン取得Gem timezone_finderで学ぶPythonからRubyへの移植法
timezone_finderというGemを登録しました。
緯度・経度を与えると、その緯度・経度でのタイムゾーン文字列を取得することができるライブラリです。海上であっても、最寄りのタイムゾーン文字列を取得することができます。
GeoIPと組み合わせることによって、IPアドレスからタイムゾーンを推定できます。こんな感じ。
require 'maxminddb' require 'timezone_finder' db = MaxMindDB.new('./GeoLite2-City.mmdb') ret = db.lookup(request.remote_ip) tf = TimezoneFinder.create puts tf.timezone_at(ret.location.longitude, ret.location.latitude)
さて、このGemはもともとPythonのライブラリであったtimezonefinderをRubyに移植したものです。
経験上、Pythonのライブラリは、比較的Rubyに移植しやすいです。今回は、移植にときに気をつけた内容をメモしておきます。
みなさんも、PythonにあってRubyにないライブラリがあれば、じゃんじゃん移植しちゃいましょう。
PythonからRubyに移植するときやったこと
Python 3以降の割り算
Python 3以降では、整数同士の割り算が浮動小数点数になる。Python 2で__future__.divisionをimportしてもそうなる。Rubyでは、定数があればその定数を小数化しておくか、Numeric#fdivを使う。
from __future__ import division, print_function print(a / 2) print(a / b)
puts (a / 2.0) puts a.fdiv(b)
dictでキーが見つからないときの挙動
例外出るパターンとデフォルト値設定パターンのマッピングに気をつける。
{'a': 'entry a'}['b'] # raise {'a': 'entry a'}.get('b') # None {'a': 'entry a'}.get('b', 'default') # 'default'
{'a' => 'entry a'}.fetch('b') # raise {'a' => 'entry a'}['b'] # nil {'a' => 'entry a'}.fetch('b', 'default') # 'default'
numpy
Pythonのライブラリ、気軽にnumpy使っていたりする。numpy.arrayは(多次元)配列で。numpy.fromfileは自前で実装した。
from numpy import empty empty_array = empty([2, nr_points], dtype='f8')
empty_array = [[0.0] * nr_points, [0.0] * nr_points] # これだと、中のArrayが同じobjectを指すのでダメ # empty_array = [[0.0] * nr_points] * 2
変数のスコープ
Pythonのdefはクロージャがあり、Rubyのdefはそうでない。グローバル変数化したり、クラス・インスタンス変数化したり、引数に入れたり、好きに処理しよう。
a = [] def funca(): b = [] a.append(1) def funcb(n): b.append(n) funcb(2) return a + b print(funca())
class Klass def initialize @a = [] end def funca() b = [] @a << 1 def funcb(n, b) b << n end funcb(2, b) return @a + b end end puts Klass.new.funca().to_s
pack/unpack
Pythonの場合はstruct配下にあるが、RubyはArray#packとString#unpack。フォーマットの文字列が違うので注意。
from struct import pack # big endian 16bit pack(b'!H', val).unpack(b'!H')[0]
# big endian 16bit [val].pack('S>', val).unpack('S>')[0]
予約語
Pythonでの予約語とRubyでの予約語の違いに気をつける。変数名endとか。
for ID in ids: start = starts[ID] end = ends[ID] print(start, end)
ids.each do |id| start = starts[id] last = latss[id] puts [start, last].to_s end
トリプルクォーテーション
ヒア文字列に変える。ドキュメンテーション文字列の場合は、余裕があればRDoc用のコメントにする。
for文/range
Rubyでもfor式は使えるが、基本Enumerable#eachを使うように書き換える。for残してもいい。
for i in range(n):
(0...n).each do |i| end
for i in range(1, n, 2):
(1...n).step(2) do |i| end
Enumerable#step使ったり。
for i in range(1, n, 2):
(1...n).step(2) do |i| end
uptoや..も好みに応じて。
for i in range(n + 1):
(0..n).each do |i| end # or 0.upto(n).each do |i| end
リスト内包表現
だいたいArray#mapで。
[func(x) for x in list1]
list1.map { |x| func(x) }
list幅の配列初期化に使われている場合はArray#*で。
[False for x in list1]
[false] * list1.length
listへのappend
Array#pushへの置き換えでよいが、個人的にはArray#<<を使うのが好き。
l = [] l.append(1)
l = [] l.push(1) # or l << 1
__future__.print_functionをimportして関数形式のprintを使っているかどうかにもよるが、putsへ寄せる。複数の値を指定している場合は気をつける。
print(a) print(a, b)
puts a puts a, b # puts aとputs bを連続して実行したのと同じ puts [a, b].to_s # こうするとPythonの出力に少し似る puts "(#{a}, #{b})" # a, bが数値ならこれでPythonの出力と合う
デストラクタ
Objectspace.define_finalizerで。
def __init__(self): pass def __del__(self): print('ie-i')
def initialize ObjectSpace.define_finalizer(self, self.class.__del__) end def self.__del__ proc do puts 'ie-i' end end
あとこまかいの
ここらへんは変換を続けていくと、脊髄反射でコンバートできる。
None
=>nil
True
=>true
False
=>false
elif
=>elsif
max(a, b)
=>[a, b].max
min(a, b)
=>[a, b].min
len(a)
=>a.length
floor()
/ceil()
/round()
=>x.floor
/x.ceil
/x.round
int()
/float()
=>to_i
,to_f
tuple
=>Array
- 単なるカッコなのでミスしやすい、注意。Hashのキーにもできる。
set
=>Set
ValueError
=>ArgumentError
- キーワード引数は、場合によってそうしたりそうしなかったり
- 最後の式のreturnを抜いたりとかは趣味で
感想
- PythonからRubyへのライブラリ移植は、ほぼ単純作業でいける
- numpyとか気軽に使ってくるのでガマンする
- 小数、タプルまわりはバグりやすいので気をつける
では、timezone_finderぜひぜひごひいきに。