読者です 読者をやめる 読者になる 読者になる

歩いたら休め

If the implementation is easy to explain, it may be a good idea.

【Ruby】PythonプログラマーがRubyを触って感じたこと

Pythonプログラマーというか、元々Python(ときどきR、C言語)で数値シミュレーションをしていた学生が、就職してRubyでWeb開発を行うにあたって勉強したことを書き連ねていくだけの記事です。

もし自分と同じような立場の人(これから後輩としてもどんどん増えていくかも!)がいたら、「ここを押さえておけばRubyは問題なく書けるよ」と教えられるように書いておきます。というのも、レビューを行っていた先輩とのプログラミングのスキルとの開きがあり、先輩も私も「どこが分かってないのか説明できない」状態になってしまってお互いに困ってしまった経験があるからです。

RubyPythonはよく似ているのですが、思想や見た目で違う部分が多く、片方を勉強するともう片方の理解も深まります。 たまに2ちゃんねるのオカルト板である「見たことある世界によく似た異世界に迷い込んだ」みたいな感覚で、なかなか面白い経験でした。

Rubyのよかった点

まず、Rubyを使ってみて、「これってイケてるな」「勉強になったな」と思ったことを書いていきます。

1. オブジェクト指向が(Pythonよりも)徹底している

Rubyのプログラミングは、だいたい「オブジェクトからメソッド(+ブロック)を呼び出して、その戻り値を利用する」ということで表現できます。(if文などの制御構文など、若干の例外もありますが)

nums = [1, 2, 3]

# 全ての配列の要素に1を足す
nums.map {|n| n + 1}

# 合計を取る(畳み込み)
nums.inject {|x, y| x + y}
nums.inject(:+) # 省略した記法
nums.sum # Ruby2.4から利用できるようになるそうです

対してPythonでは、高階関数やリスト内包表記、合計にはsum関数など、リスト(Rubyの配列に対応するもの)を操作するにしても、色々なことをしているように見えます。

from functools import reduce
nums = [1, 2, 3]

# 全ての配列の要素に1を足す
list(map(lambda n: n + 1, nums)) # map関数を利用した例
[n + 1 for n in nums] # リスト内包表記を使った例

# 合計を取る
sum(nums)
reduce(lambda x, y: x + y, nums) # Rubyと同じく畳み込みを使った例

実はPythonも内部的には__iter__メソッドや__next__メソッドを呼び出しているそうなのですが、 表面上は関数として定義されているように見せているだけです。

これはほぼ書き方の違いだけなのですが、Rubyのほうが「主語がはっきりしていて、何を操作の対象にしているか分かりやすい」という利点があるように思います。 ただし、ブロックをeachから使い始める人が多い分、ラムダ式高階関数を理解しないままmapinjectを使っている人も多いように感じています。

一方、Pythonで数値の合計値にsumという関数を使えばいいというのは、数式の表現に近く、きちんとプログラミングを学んでいない情報系以外の分野の学生には使いやすいように思います。

(完全に余談ですが、Rubyでも2.4からsumメソッドが使えるようになるらしいです。数学のバックグラウンドがある人が、「合計値出すのにinject(:+)ってなんだよ…」って愚痴っていたのを聞いたことあるので個人的には大歓迎です。)

クラスとイテレータ - Dive Into Python 3 日本語版

RubyのMix-inも便利でした。Pythonで同様の機能を実現するには、多重継承を気をつけて使うか、abcというモジュールを使うかするといいらしいです…がPythonであまり大きなコードを書く機会が無かったので、これから身につけたいです。

www.atmarkit.co.jp

2. 破壊的変更が理解しやすい

前項のように、Pythonを利用している間、「オブジェクトの内側からメソッドを呼び出す」という感覚はあまりなく、 「オブジェクトの外側から関数を適用する」という感覚でプログラミングを行っていました。 そのため、「破壊的変更」「ミュータブル/イミュータブル」という概念がいまいちピンと来ていませんでした。

例えば、リスト(配列)をソートする場合、Pythonでは破壊/非破壊で関数とメソッドを使い分ける必要があります。

ソート HOW TO — Python 3.5.2 ドキュメント

nums = [1, 3, 2, 5]

# 非破壊的変更はsorted関数
ret = sorted(nums)
print(ret)
# => [1, 2, 3, 5] # 戻り値はソートされる
print(nums)
# => [1, 3, 2, 5] # もとのオブジェクトは変更されない

# 破壊的変更はlist型のsortメソッド
ret = nums.sort()
print(ret)
# => None # 戻り値はNone
print(nums)
# => [1, 2, 3, 5] # 破壊的変更が加わっている

一方、Rubyでは配列をソートしたければ、非破壊/破壊的メソッドのsort, sort!メソッドを使い分ければいいだけです。 また、「オブジェクトからメソッドを呼び出す」ことが徹底している分、 破壊的操作も「要するにインスタンスの内部の変数を書き換えているだけでしょ」と理解できました。

ref.xaio.jp

完全に余談ですが、PHPを書いていた(書かされていた)頃は、sort系の関数がいくつも用意されていたり、そのだいたいが破壊的操作だったり、全く理解できませんでした。

3. nilとfalse以外は全てtrue

勉強になったというか、プログラミング言語として分かりやすいのが「nilfalse以外は全てtrue扱いされる」というルールです。

www.rubylife.jp

例えば、正規表現にマッチするかどうかを調べるには、String型のmatchメソッドの戻り値をifに渡すだけです。 matchメソッドはtrue, falseを返すわけではなく、正規表現にマッチしたときはMatchDataオブジェクトを返し、マッチしなかったときはnilを返します

url = 'www.google.com'
if url.match(/google.com/)
  puts 'Google!'
end
# => Google!

Booleanを返すような特別なメソッドを用意することなく、nilを返すメソッドを使うことで、簡潔で分かりやすいコードを書くことができます。

Pythonでは、FalseNoneの他に、空文字や0なども偽として扱われます。

www.pythonweb.jp

Pythonのほうがルールが複雑でイケてないじゃん」と思われるかもしれませんが、 プログラマが持つべき心構え (The Zen of Python)の記事に書かれていた 最大公約数を求めるプログラムを見るとハッとするかもしれません。 (これも、y <= 0みたいに明示的に書いても良い気もしますが)

def gcd(x, y):
    while y:
        x, y = y, x % y
    return x

4. 便利なメソッドや書き方が多い

多様性は善」を一つのスローガン(?)に掲げているだけあり、 Rubyには様々な便利な機能やメソッドが用意されています。 特に、Pythonに無い機能で便利だと思ったのが以下の4つです。

  1. nilガード(||=)
  2. each_with_object
  3. 後置ifによる早期リターン
  4. メソッド名に!や?が使えること

nilガード(||=)

初心者プログラマーRubyに触るとまずググるのに困るアレ(||=)です。 変数が存在しない場合やハッシュのキーが無い場合(nilの場合)に変数を初期化することができます。

Sinatraで、あるGoogleのサービスのAPIを叩く社内用API(Google APIを叩く & DBにログを残す)を作る際、 「id(hogehoge_id)が引数で渡ってこない場合があるため、名前でidを検索する必要がある」ことがありました。

このような場合にnilガードが便利です。

# dataはAPIに渡す情報が入ったハッシュ
data['hogehoge_id'] ||= _search_hogehoge_id(data['hogehoge_name'])
_exec_api(data)

ちなみに、||=の「nilガード」という呼び名はメタプログラミングRubyの記述に倣ったものです。

メタプログラミングRuby 第2版

メタプログラミングRuby 第2版

呼び名については、Rubyistの中でも意見が分かれているようです。レビューするとき不便ですね。

www.softantenna.com

each_with_object

私が最初にRubyを使っていて最初に感じたのが、「内包表記やジェネレーターの代わりになるもの無いの?」ということです。

もちろんmapselect等のメソッドもあるのですが、戻り値が配列に限られていますし、 簡単なフィルターもできないため、Pythonの〇〇内包表記に比べて使い心地が悪く感じます。

そんなPythonプログラマーの需要を満たすのがeach_with_objectメソッドです。

例えばURLの配列(リスト)を、{URL => ページタイトル}というハッシュテーブル(辞書)に変換したいとします。 同時に、ページのURLにgoogleが含まれているものを除外したいとします。

Pythonであれば、辞書内包表記を使って以下のように書くでしょう。

# get_titleはページタイトルを取得する関数
def create_url_table(urls)
  return {url: get_title(url) for url in urls if 'google' not in url}

Rubyでは同様のメソッドを次のように書けます。

# Rubyでは明示的にreturnを書かなくて良い
# ループした結果をハッシュ(デフォルト値では{})のキーに代入していく
def create_url_table(urls)
  urls.each_with_object({}) do |url, hash|
    hash[url] = get_title(url) unless url.match(/google/)
  end
end

宗教上の理由で破壊的操作を受け入れられない人以外は、便利に使えるメソッドだと思います。

qiita.com

ここからはeach_with_objectとは関係ない話です。

RubyPythonのような遅延評価が行いたいなら、Enumerableモジュールのlazyメソッドを呼び出せば、mapselectに遅延評価を適用することができます。

Rubyist Magazine - 無限リストを map 可能にする Enumerable#lazy

Pythonのジェネレーターみたいなことがしたいなら、Enumerator::Lazy.newからブロックを呼び出せばできなくはないようです。 …なんだかRubyっぽくないですが。

data = [[1], [2, '2'], ['a', 3, 'b'], [4], ['c']]
generator = Enumerator::Lazy.new(data) do |yielder, item|
  item.each do |x|
    yielder << x if x.is_a?(Integer)
  end
end

puts generator
# => #<Enumerator::Lazy:0x0055634bb03200>
puts generator.first
# => 1
puts generator.force
# => [1, 2, 3, 4]

一度取り出された値(1)がもう一度出てきているので、Pythonのジェネレーターとは少し挙動が違うみたいです。

後置ifによる早期リターン

後置ifも、早期リターンしやすいので便利です。

例えば、Twitterで自動リツイートするbotを作るため、ツイートの配列を渡すとリツイートするメソッドを作りたいとします。 そのとき、引数の配列にnilが含まれる可能性がある場合、その場合はreturnして逃してあげると、 eachメソッドを呼び出す際に変数がnilの場合のエラーを気にせずに済みます。

def post_retweets(tweets)
  return if tweets.nil?
  tweets.each do |t|
    # リツイートする処理
    _post_retweet(t[:tweet_id])
  end
end

mugenup-tech.hatenadiary.com

後置ifが素晴らしいのは、else句が無いことを明示的に示していることです(と個人的には思っています)。

例えばPythonで同様の処理を書くと以下のようになりますが、elseの場合の処理は必要ないの?それとも書き忘れているの?」と不安に感じると思います。 かといって明示的にelse句を書いてネストさせるのもちょっと…と感じてしまいます。

def post_retweets(tweets):
  if tweets is None:
    return
  for t in tweets:
    # リツイートする処理
    _post_retweet(t['tweet_id'])

メソッド名に!や?が使えること

?は真偽値を返すメソッドに、!は破壊的な(または注意すべき)メソッドに付けることが多いです。

Rubyで使われる記号の意味(正規表現の複雑な記号は除く) (Ruby 2.3.0)

ただし、!は破壊的メソッドだけに限らず、例えばハッシュのmergeメソッドupdateメソッドも破壊的であることに注意しましょう。

Rubyの微妙な点

Pythonに比べてプログラミングの自由度が大きい分、微妙だと感じるものも多いです。

ただし、これはプログラミング言語どうこうというより、言語ユーザーの文化的な違いの面が大きいのかもしれません。

1. Rubyオブジェクト指向にこだわりすぎている

Rubyでは、「そんなのいちいちクラス分けなくていいじゃん」と思うようなことでも、Rubyではクラスを分けたりします。例えば、標準ライブラリのnet/http等がそうです。Web APIを叩くときによく使います。

Rubyist Magazine - 標準添付ライブラリ紹介 【第 7 回】 net/http

Class: Net::HTTP (Ruby 2.3.1)

例えば、チャットワークのAPIを利用して、POSTでメッセージを投稿するようなものを想像しましょう。

curl -X POST -H "X-ChatWorkToken: xxxxx" -d "body=チャットワークに投稿" "https://api.chatwork.com/v1/rooms/*****/messages"

上の例を、Rubynet/httpを使ったコードに直すとこんな感じになります。(ただし、curl-to-rubyというサイトで作ったため、もっと簡単な書き方があるかもしれません)

require 'net/http'
require 'uri'

uri = URI.parse("https://api.chatwork.com/v1/rooms/*****/messages")
request = Net::HTTP::Post.new(uri)
request["X-Chatworktoken"] = "xxxxx"
request.set_form_data(
  "body" => "チャットワークに投稿",
)

response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == "https") do |http|
  http.request(request)
end

URI専用の型を作って、それをもとにNet::HTTP::Postインスタンスを作って、パラメータをつけ加えて実行するような形です。

同じ操作をPythonrequestsライブラリで行うと、必要なパラメータを辞書(ハッシュテーブル)で引数に与える形で実現しています。標準ではないものの、net/httpと似た用途でよく使われます。(標準のurllibopen-uriに対応するようなシンプルな用途で使われます。)

import requests

headers = {'X-ChatWorkToken': 'xxxxx'}
params = {'body': 'チャットワークに投稿'}
requets.post('https://api.chatwork.com/v1/rooms/*****/messages', headers=headers, params=params)

私はPythonの書き方のほうがシンプルで分かりやすいと思っています。

同じように、色々なgem(ライブラリ)を使う際に、引数や戻り値がいちいち専用の型のインスタンスで操作することが多く、「これって文字列型でいいじゃん…」と感じることが度々あります。

こちらの記事の後に、

qiita.com

同じリファクタリングPythonで行った記事を見ると、Ruby/Pythonでの文化の違いが分かると思います。

qiita.com

2. 変な記号がいっぱいある

Rubyに慣れていない間、既存のコードを読み解く際、変な記号がいっぱいあって困りました。例えばclass << selfという記述があり、ようやくググると特異クラスという単語が現れ(特異クラスってなんだ?)…みたいな感じに、ちょっとした記述が分からずにハマることが案外多かったです。

Rubyist Magazine - Ruby 初級者のための class << self の話 (または特異クラスとメタクラス)

これを友人のRubyプログラマーに愚痴ったら「Perlにはもっと意味不明な記号があるよ」と言われました。ひとまず下のページをブックマークしておくといいでしょう。

Rubyで使われる記号の意味(正規表現の複雑な記号は除く) (Ruby 2.3.0)

また、文字列型('string')とシンボル型(:symbol)が別々に存在することも、おそらく、ハッシュのキーや言語自体の機能を操作するために、イミュータブルな(破壊的変更ができない)文字列型のようなものが必要だったんじゃないかと思います。 ただし、別に文字列型だけでも対応できたんじゃないかなと思います。

togetter.com

ちなみにPythonでは通常の文字列型が破壊的変更不可能なので、Rubyのシンボル型のような役割も兼ねています。代わりに、ハッシュテーブルのキーにミュータブルなオブジェクトが使えないようになっており、イミュータブルな配列型(タプル型)が用意されている点がちょっと不自然かもしれません。

www.yoheim.net

3. ブロック(ラムダ式)が強力すぎる

Rubyのブロック(メソッドからラムダ式を呼び出す機能)が強力すぎる分、気をつけないとすぐにごちゃごちゃしたコードになる印象があります。

Pythonではラムダ式の表現力を意図的に落としていて、「これ以上複雑にするなら関数分けてね」と制限しているのですが、 Rubyでは「プログラマーよ、望むままを行え」ということを戒律にしているようです。ベルセルクの使徒みたいです。

Rubyist Magazine - Rubyist のための他言語探訪 【第 1 回】 Python

こちらの記事に書かれていますが、

しかし、この文法では、ブロックを含む構文は値を持つ式として用いることができませんから、Rubyで頻繁に使われるブロック付きメソッド呼び出しのようなことはできません。 Python には名前のない関数を作る lambda という文法がありますが、関数本体部分には式ひとつしかかけないという大きな制限があるため、条件分岐ひとつ使うことができません。 そのような場合、Python では名前を付けた関数を定義し、その関数を引数として使うのが一般的のようです。 Ruby では

ary.map {|x| x**2}

となるものが、Python では

map(lambda x: x**2, ary)

となり、lambda の本体が1つの式では表現しきれなくなると

def mapper(x): .....

map(mapper, ary)

と書き換える必要があります。ブロックによるうれしさとのトレードオフの関係と言っても良いかもしれません。

Pythonでは「リストの要素一つを操作する小さい関数を作る」 → 「その関数をmapやリスト内包表記でリスト全体に適用する」という順序で考えていたので、 Rubymapのブロックの中にごちゃごちゃ書くのに違和感があります。

先輩から「if文の無いeach文はmapに書き換えられるよ!」と説明を受けたことがあるのですが、 私はmapは配列を数学の写像として操作する関数やメソッドで、 eachとは違うニュアンスで捉えていた(実行する順序が関係ないとか)のでちょっとビックリしました。

ただし、強力なメソッドを適切に使うことで、意図の伝わりやすいコードになるとも思うので、注意すれば問題ないかなと思っています。

まとめ & おすすめの書籍

意外と長くなってしまいました。

プログラミング初心者の後輩が入ってきたとして、↑のような説明してもあんまり伝わらない(少なくともPython知ってないと分からない)し、 私自身まだ糞みたいなプログラマーなので、どこかに認識違い等があると思います。

Ruby勉強したい人は次の2冊をおすすめします。めっちゃ無難な取り合わせです。

初めてのRuby

初めてのRuby

Effective Ruby

Effective Ruby

逆に、RubyプログラマーPythonを勉強したいなら、以下の3点を押さえておけば、ちゃんとしたプログラムが書けると思います。

  1. リスト内包表記
  2. ジェネレーター(遅延評価)
  3. ふつうに定義した関数がオブジェクトであること

その他の困る点は、先人が素晴らしい記事を書いてくださっているのでそちらを参考にしましょう。

qiita.com

「2と3のどちらのバージョンを身につければいいの?」という人もいると思いますが、 3系で困る(例えば使いたいライブラリが2系のみ)ことは全くといいほど無くなりました。 3系を勉強しましょう。

入門 Python 3

入門 Python 3