位置情報によるタイムゾーン取得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

print

__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ぜひぜひごひいきに。