Ruby は「痒いところに手が届く」か

qiita.comいや、Python すばらしいですね。どこまで Ruby でできるか、試してみました。なお、使った Ruby のバージョンは 2.7.0 です。
 

3値以上の比較

1 == 2 == 3  # -> False
1 < 2 < 3 < 4  # -> True

いやー、これは Ruby ではできないですね。なお、Ruby でもこれができるバージョンがあったそうですが、まつもとさんがふつうの(?)比較に戻したそうです。
 

時間(datetime/date)の比較

Python

from datetime import date

feb1 = date(2020, 2, 1)
feb2 = date(2020, 2, 2)
feb3 = date(2020, 2, 3)

feb1 < feb2 <= feb2 < feb3  # -> True

Rubyrequire "date" します。

irb(main):001:0> require "date"
=> true
irb(main):002:0> feb1 = Date.new(2020, 2, 1)
irb(main):003:0> feb2 = Date.new(2020, 2, 2)
irb(main):004:0> feb3 = Date.new(2020, 2, 3)
irb(main):005:0> feb1 < feb2 && feb2 < feb3
=> true

だいたい同じですかね。
 

時間の最大/最小

Pyhton。

from datetime import date

# 意図的に逆順にしてます
dates = [
    date(2020, 2, 3),
    date(2020, 2, 2),
    date(2020, 2, 1),
]

min(dates)  # -> datetime.date(2020, 2, 1)
max(dates)  # -> datetime.date(2020, 2, 3)

Ruby

irb(main):001:0> require "date"
=> true
irb(main):002:0> dates = 3.downto(1).map {|i| Date.new(2020, 2, i)}
irb(main):003:0> dates.min
=> #<Date: 2020-02-01 ((2458881j,0s,0n),+0s,2299161j)>
irb(main):004:0> dates.max
=> #<Date: 2020-02-03 ((2458883j,0s,0n),+0s,2299161j)>

これもだいたい同じですかね。
 

時間の計算

Python

# Input
from datetime import datetime

start = datetime(2020, 2, 1, 10)
goal = datetime(2020, 2, 3, 12)

t = goal - start
print(f'あなたの記録は{t.days}日と{t.seconds}秒です')

# Output
'あなたの記録は2日と7200秒です'

Ruby

irb(main):001:0> require "date"
=> true
irb(main):002:0> start = DateTime.new(2020, 2, 1, 10)
irb(main):003:0> goal = DateTime.new(2020, 2, 3, 12)
irb(main):004:0> t = goal - start
irb(main):005:0> "あなたの記録は#{t.to_i}日と#{(t % 1 * 86400).to_i}秒です"
=> "あなたの記録は2日と7200秒です"

これはちょっと面倒ですねえ。もっとうまいやり方はないかな。
 

時間を連想配列のキーにする

Python

# Input
from datetime import date

counts = {
    date(2020, 2, 1): 0,
    date(2020, 3, 1): 0,
}
counts[date(2020, 2, 1)] += 1
counts[date(2020, 2, 1)] += 1
counts[date(2020, 3, 1)] += 1

print(counts)

# Output
{datetime.date(2020, 2, 1): 2, datetime.date(2020, 3, 1): 1}

Ruby

irb(main):001:0> require "date"
=> true
irb(main):002:1* counts = {
irb(main):003:1*   Date.new(2020, 2, 1) => 0,
irb(main):004:1*   Date.new(2020, 3, 1) => 0,
irb(main):005:0> }
irb(main):006:0> counts[Date.new(2020, 2, 1)] += 1
irb(main):007:0> counts[Date.new(2020, 2, 1)] += 1
irb(main):008:0> counts[Date.new(2020, 3, 1)] += 1
irb(main):009:0> counts
=> {#<Date: 2020-02-01 ((2458881j,0s,0n),+0s,2299161j)>=>2,
    #<Date: 2020-03-01 ((2458910j,0s,0n),+0s,2299161j)>=>1}

ほぼ同じ。
 

キーが連想配列に含まれるか

Python

d = {
    'foo': 1,
    'bar': 2,
    'baz': 3,
}

print('foo' in d)  # -> True

Ruby

irb(main):001:0> h = {foo: 1, bar: 2, baz: 3}
irb(main):002:0> h.has_key?(:foo)
=> true

わかりやすいですね。
 

連想配列からキーを抽出

Python

d = {
    'foo': 1,
    'bar': 2,
    'baz': 3,
}
print(list(d))  # -> ['foo', 'bar', 'baz']

Ruby

irb(main):001:0> h = {foo: 1, bar: 2, baz: 3}
irb(main):002:0> h.keys
=> [:foo, :bar, :baz]

これもわかりやすいですね。
 

連想配列から値を抽出

Python

d = {
    'foo': 1,
    'bar': 2,
    'baz': 3,
}
print(list(d.values()))  # -> [1, 2, 3]

Ruby

irb(main):001:0> h = {foo: 1, bar: 2, baz: 3}
irb(main):002:0> h.values
=> [1, 2, 3]

これもわかりやすいですね。
 

連想配列からキーと値のペアを抽出

Python

d = {
    'foo': 1,
    'bar': 2,
    'baz': 3,
}

for key, value in d.items():
    print(key, value)

# Output
foo 1
bar 2
baz 3

Ruby

irb(main):001:0> h = {foo: 1, bar: 2, baz: 3}
irb(main):002:0> puts h.map {_1.join(" ")}
foo 1
bar 2
baz 3

map を使うのは Ruby らしいのでは? もちろんふつうに each あるいは each_pair で取り出すこともできます。
 

2重配列を連想配列へ変換

Python

# Input
l = [
    ['Yamada', 'baseball'],
    ['Tanaka', 'soccer'],
    ['Sato', 'tennis'],
]
dict(l)

# Output
{'Yamada': 'baseball', 'Tanaka': 'soccer', 'Sato': 'tennis'}

Ruby

irb(main):001:1* ary = [
irb(main):002:1*   [:Yamada, :baseball],
irb(main):003:1*   [:Tanaka, :soccer],
irb(main):004:1*   [:Sato, :tennis],
irb(main):005:0> ]
irb(main):006:0> ary.to_h
=> {:Yamada=>:baseball, :Tanaka=>:soccer, :Sato=>:tennis}

ほぼ同じですね。
 

多重配列のループ

Python

# Input
rows = [
    ['yamada', 20],
    ['tanala', 18],
    ['sato', 18],
]

for name, age in rows:
    print(f'{name}さんは{age}歳です')
else:
    print('紹介終わり')

# Output
'yamadaさんは20歳です'
'tanalaさんは18歳です'
'satoさんは18歳です'
'紹介終わり'

Ruby

irb(main):001:1* rows = [
irb(main):002:1*   [:yamada, 20],
irb(main):003:1*   [:tanala, 18],
irb(main):004:1*   [:sato, 18],
irb(main):005:0> ]
irb(main):006:0* puts rows.map {|name, age| "#{name}さんは#{age}歳です"} + ["紹介終わり"]
yamadaさんは20歳です
tanalaさんは18歳です
satoさんは18歳です
紹介終わり

いいんじゃないでしょうか。
 

多重配列から必要な要素だけ取り出しつつループ

Python

# Input
l = [
    ['Yamada', 'Taro', 20, 'baseball'],
    ['Tanaka', 'Jiro', 18, 'circle'],
]

# 先頭を取り出す
for last, *others in l:
    print(last, others)
print()

# 末尾を取り出す
for *others, circle in l:
    print(circle, others)
print()

# 最初の2要素を取り出す
# (他の要素が要らない場合はダブルアンダースコアを指定するのがPython流)
for last, first, *__ in l:
    print(last, first)

# Output
Yamada ['Taro', 20, 'baseball']
Tanaka ['Jiro', 18, 'circle']

baseball ['Yamada', 'Taro', 20]
circle ['Tanaka', 'Jiro', 18]

Yamada Taro
Tanaka Jiro

Ruby

irb(main):001:1* ary = [
irb(main):002:1*   [:Yamada, :Taro, 20, :baseball],
irb(main):003:1*   [:Tanaka, :Jiro, 18, :circle],
irb(main):004:0> ]
irb(main):005:0> #先頭を取り出す
irb(main):006:0> puts ary.map {|last, *others| "#{last} #{others.inspect}"}
Yamada [:Taro, 20, :baseball]
Tanaka [:Jiro, 18, :circle]
irb(main):007:0> #末尾を取り出す
irb(main):008:0> puts ary.map {|*others, circle| "#{circle} #{others.inspect}"} 
baseball [:Yamada, :Taro, 20]
circle [:Tanaka, :Jiro, 18]
irb(main):009:0> #最初の2要素を取り出す
irb(main):010:0> puts ary.map {|last, first, *_| "#{last} #{first}"}
Yamada Taro
Tanaka Jiro

ほとんど同じだなあ。
では Python

# Input
l = [
    ['a', 'b', 'c', ['d', 'e', 'f']],
]

for one, *__, (*__, two) in l:
    print(one, two)

# Output
a f

Ruby では? これはせっかくなので、Ruby 2.7.0 のパターンマッチを使ってみましょう。

irb(main):001:0> ary = [:a, :b, :c, [:d, :e, :f]]
irb(main):002:1* case ary
irb(main):003:1*   in [one, _, _, [_, _, two]]
irb(main):004:0> end
irb(main):005:0> "#{one} #{two}"
=> "a f"

おお、いいですね。
 

カウンター付きループ

Python

# Input
rows = [
    ['Yamada', 20],
    ['Tanaka', 18],
    ['Sato', 16],
]

for i, (name, age) in enumerate(rows, start=1):
    print(f'{i}行目 : 氏名={name}, 年齢={age}')

# Output
'1行目 : 氏名=Yamada, 年齢=20'
'2行目 : 氏名=Tanaka, 年齢=18'
'3行目 : 氏名=Sato, 年齢=16'

Ruby

irb(main):001:1* rows = [
irb(main):002:1*   [:Yamada, 20],
irb(main):003:1*   [:Tanaka, 18],
irb(main):004:1*   [:Sato, 16],
irb(main):005:0> ]
irb(main):006:1* puts rows.map.with_index(1) {|(name, age), i|
irb(main):007:1*   "#{i}行目:氏名=#{name}, 年齢=#{age}"
irb(main):008:0> }
1行目:氏名=Yamada, 年齢=20
2行目:氏名=Tanaka, 年齢=18
3行目:氏名=Sato, 年齢=16

Ruby らしいですね。
 

連想配列のキー無しエラーを防ぐ

Python

d = {
    'Yamada': 20,
    'Tanaka': 18
}
d.get('Yamada')  # -> 20
d.get('Sato')  # -> None
d.get('Sato', '年齢なし')  # -> 年齢なし

Ruby。これは Python とは挙動がちがいますね。どちらがよいでしょうか。

irb(main):001:0> h = Hash.new("年齢なし")
irb(main):002:0> h.merge!({Yamada: 20, Tanaka: 18})
=> {:Yamada=>20, :Tanaka=>18}
irb(main):003:0> h[:Yamada]
=> 20
irb(main):004:0> h[:Sato]
=> "年齢なし"

 

配列/連想配列を展開して関数へ渡す

Python

# Input
def func(a, b, c=None, d=None):
    print(a)
    print(b)
    print(c)
    print(d)


l = ['aaa', 'bbb']
d = {'c': 'ccc', 'd': 'ddd'}

func(*l, **d)

# Output
aaa
bbb
ccc
ddd

Ruby。キーワード引数は Ruby 2.7 で整理されました。

irb(main):001:1* def func(a, b, c: nil, d: nil)
irb(main):002:1*   puts a
irb(main):003:1*   puts b
irb(main):004:1*   puts c
irb(main):005:1*   puts d
irb(main):006:0> end
=> :func
irb(main):007:0> ary = [:aaa, :bbb]
irb(main):008:0> h = {c: :ccc, d: :ddd}
irb(main):009:0> func(*ary, **h)
aaa
bbb
ccc
ddd

ほぼ一緒ですね。
 

all関数

Python

l = [
    True,
    1,
    "foo",
]
all(l)  # -> True

l = [
    True,
    1,
    "",
]
all(l)  # -> False

Ruby。Enumerable#all? メソッドがあります。

irb(main):001:0> ary = [true, 1, "foo"]
irb(main):002:0> ary.all?
=> true
irb(main):003:0> ary = [true, 1, nil]
irb(main):004:0> ary.all?
=> false

ほぼ同じなのだけれど、Ruby では空文字列は true なので、少し変えました。
 

any関数

all関数の場合とほぼ同じなので、省略します。Ruby では Enumerable#any? を使います。one? や none? メソッドもあります。
 

集合演算(Set型)

Ruby では require "set" をすれば Python とほぼ同じなので省略。
 

collections / itertools パッケージ

collections.Counter

Python。同一要素の数を数えます。

# Input
import collections

l = ['a', 'b', 'c', 'a', 'a', 'c']
c = collections.Counter(l)
print(c.most_common())

# Output
[('a', 3), ('c', 2), ('b', 1)]

RubyRuby 2.7 から Enumerable#tally が入りました。

irb(main):001:0> ary = [:a, :b, :c, :a, :a, :c]
irb(main):002:0> ary.tally
=> {:a=>3, :b=>1, :c=>2}

 

collections.defaultdict

Python

# Input
import json
import collections

# defaultdict()の引数には関数(callable)ならなんでも渡せる
groups = collections.defaultdict(list)

# 普通の連想配列だと "baseballというキーは存在しない" といったエラーが発生する
groups['baseball'].append('yamada')
groups['tennis'].append('tanaka')
groups['baseball'].append('sato')

print(json.dumps(groups))

# Output
{"baseball": ["yamada", "sato"], "tennis": ["tanaka"]}

Ruby。Hash の default_proc を使います。

irb(main):001:0> groups = Hash.new {|h, k| h[k] = []}
irb(main):002:0> groups[:baseball] << :yamada
irb(main):003:0> groups[:tennis] << :tanaka
irb(main):004:0> groups[:baseball] << :sato
irb(main):005:0> groups
=> {:baseball=>[:yamada, :sato], :tennis=>[:tanaka]}

また、Python

# Input
import json
from collections import defaultdict

nested = defaultdict(lambda: defaultdict(int))

nested['a']['a'] += 1
nested['a']['a'] += 1
nested['a']['b'] += 1
nested['b']['c'] += 1

print(json.dumps(nested))

# Output
{"a": {"a": 2, "b": 1}, "b": {"c": 1}}

に対しては、Ruby なら

irb(main):001:0> nested = Hash.new {|h, k| h[k] = Hash.new(0)}
irb(main):002:0> nested[:a][:a] += 1
irb(main):003:0> nested[:a][:a] += 1
irb(main):004:0> nested[:a][:b] += 1
irb(main):005:0> nested[:b][:c] += 1
irb(main):006:0> nested
=> {:a=>{:a=>2, :b=>1}, :b=>{:c=>1}}

とすればよいです。
 

itertools.product

Python

# Input
import itertools

a = ['a1', 'a2']
b = ['b1', 'b2', 'b3']
c = ['c1']

list(itertools.product(a, b, c))

# Output
[('a1', 'b1', 'c1'),
 ('a1', 'b2', 'c1'),
 ('a1', 'b3', 'c1'),
 ('a2', 'b1', 'c1'),
 ('a2', 'b2', 'c1'),
 ('a2', 'b3', 'c1')]

Ruby には Array#product があります。

irb(main):001:0> a = [:a1, :a2]
irb(main):002:0> b = [:b1, :b2, :b3]
irb(main):003:0> c = [:c1]
irb(main):004:0> a.product(b, c)
=> [[:a1, :b1, :c1], [:a1, :b2, :c1], [:a1, :b3, :c1],
    [:a2, :b1, :c1], [:a2, :b2, :c1], [:a2, :b3, :c1]]

 

itertools.chain.from_iterable

Python

# Input
import itertools

l = [
    ['a1', 'a2'],
    ['b1', 'b2', 'b3'],
]

list(itertools.chain.from_iterable(l))

# Output
['a1', 'a2', 'b1', 'b2', 'b3']

これは Ruby では Array#flatten でおしまいです。

irb(main):001:1* ary = [
irb(main):002:1*   [:a1, :a2],
irb(main):003:1*   [:b1, :b2, :b3],
irb(main):004:0> ]
irb(main):005:0> ary.flatten
=> [:a1, :a2, :b1, :b2, :b3]

 

ioパッケージ

Python

# Input
import io


def writer(f, text):
    f.write(text)


def printer(f):
    print(f.read())


sio = io.StringIO()

writer(sio, 'foo\n')
writer(sio, 'bar\n')
writer(sio, 'baz\n')

sio.seek(0)
printer(sio)

# Output
foo
bar
baz

Ruby では StringIO を使います。

irb(main):001:0> require "stringio"
=> true
irb(main):002:1* def writer(f, text)
irb(main):003:1*   f.write(text)
irb(main):004:0> end
=> :writer
irb(main):005:1* def printer(f)
irb(main):006:1*   print f.read
irb(main):007:0> end
=> :printer
irb(main):008:0> sio = StringIO.new("tmp", "w+")
irb(main):009:0> writer(sio, "foo\n")
irb(main):010:0> writer(sio, "bar\n")
irb(main):011:0> writer(sio, "baz\n")
irb(main):012:0> sio.rewind
irb(main):013:0> printer(sio)
foo
bar
baz

 

タプル(tuple)

Ruby には immutable なタプルはありません。では、Python

# Input
from collections import defaultdict

data = [
    {'circle': 'baseball', 'name': 'yamada', 'age': 10},
    {'circle': 'baseball', 'name': 'sato', 'age': 10},
    {'circle': 'baseball', 'name': 'suzuki', 'age': 11},
    {'circle': 'tennis', 'name': 'tanaka', 'age': 10},
]

per_circle_age = defaultdict(list)

for v in data:
    k = (v['circle'], v['age'])  # (サークル名, 年齢) というタプルを生成
    per_circle_age[k].append(v['name'])  # タプルをキーにして集計

for (circle, age), members in per_circle_age.items():
    print(f'{circle}に所属している{age}歳のメンバー:{members}')

# Output
"baseballに所属している10歳のメンバー:['yamada', 'sato']"
"baseballに所属している11歳のメンバー:['suzuki']"
"tennisに所属している10歳のメンバー:['tanaka']"

これを Ruby でどうするかというと、ふつうに Array と Hash でいけます。

irb(main):001:1* data = [
irb(main):002:1*   {circle: :baseball, name: :yamada, age: 10},
irb(main):003:1*   {circle: :baseball, name: :sato, age: 10},
irb(main):004:1*   {circle: :baseball, name: :suzuki, age: 11},
irb(main):005:1*   {circle: :teniss, name: :tanaka, age: 10},
irb(main):006:0> ]
irb(main):007:0> per_circle_age = Hash.new {|h, k| h[k] = []}
irb(main):008:1* data.each do |h|
irb(main):009:1*   k = [h[:circle], h[:age]]
irb(main):010:1*   per_circle_age[k] << h[:name]
irb(main):011:0> end
irb(main):012:1* puts per_circle_age.map {|(circle, age), members|
irb(main):013:1*   "#{circle}に所属している#{age}歳のメンバー:#{members.inspect}"
irb(main):014:0> }
baseballに所属している10歳のメンバー:[:yamada, :sato]
baseballに所属している11歳のメンバー:[:suzuki]
tenissに所属している10歳のメンバー:[:tanaka]

 

dataclassesパッケージ

Python

import dataclasses


# frozen=True とすると不変(immutable)なオブジェクトとして扱える
@dataclasses.dataclass(frozen=True)
class User:
    last_name: str
    first_name: str

    def full_name(self):
        return f'{self.last_name} {self.first_name}'


yamada = User(last_name='Yamada', first_name='Taro')
tanaka = User(last_name='Tanaka', first_name='Jiro')

yamada.full_name()  # -> Yamada Taro

# 簡単に連想配列へ変換出来ます
dataclasses.asdict(yamada)  # -> {'last_name': 'Yamada', 'first_name': 'Taro'}

# 比較が可能です
yamada2 = User(last_name='Yamada', first_name='Taro')
yamada == yamada2  # -> True
yamada == tanaka  # -> False
yamada in [yamada2]  # -> True

# "frozen=True" とした場合は値の再代入は出来ません
yamada.last_name = 'Sato'  # -> FrozenInstanceError: cannot assign to field 'last_name'

# immutableなので連想配列のキーとして使えます
d = {yamada: 'foo', tanaka: 'bar'}

# 集合演算(Set型)も可能です
{yamada, tanaka} & {yamada2}  # -> {User(last_name='Yamada', first_name='Taro')}

Ruby なら、Struct クラスを使うとよいのかな。

irb(main):001:1* User = Struct.new(:last_name, :first_name) do
irb(main):002:2*   def full_name
irb(main):003:2*     "#{last_name} #{first_name}"
irb(main):004:1*   end
irb(main):005:0> end
irb(main):006:0> yamada = User.new(:Yamada, :Taro)
irb(main):007:0> tanaka = User.new(:Tanaka, :Jiro)
irb(main):008:0> yamada.full_name
=> "Yamada Taro"
irb(main):009:0> yamada.to_h
=> {:last_name=>:Yamada, :first_name=>:Taro}
irb(main):010:0> yamada2 = User.new(:Yamada, :Taro)
irb(main):011:0> yamada == yamada2
=> true
irb(main):012:0> yamada == tanaka
=> false
irb(main):013:0> h = {yamada => :foo, tanaka => :bar}
irb(main):014:0> [yamada, tanaka] & [yamada2]
=> [#<struct User last_name=:Yamada, first_name=:Taro>]

だいたいいけますね。


どうでしょうか。Ruby もなかなかいけるでしょう?