RubyのアレはPythonだとどう書くの?(Enumerableモジュール編)

f:id:monozukuri-bu:20171003203424j:plain

どーも、xyakenです。普段はRubyでコード書いてます。 今回はRubyのEnumerableモジュールのメソッドをいくつかピックアップして、PythonRubyのあれはどう書くのか調べたり、考えたりしてみました*1

バージョンはruby 2.4.1p111とPython 3.6.1で動作確認しております。※リストであることを前提にPythonのコードを書いているので、ジェネレータ式ではエラーになるものもあると思います。

RubyのブロックをどうPythonで表現するのかと思ったのですが、内包表記というものを使えばそれっぽく実現できるんですね。

all?

all?/all - シンプルなTruthy, Falsy

Ruby

[false, true].all? # => false
[true, true].all? # => true

Python

all([False, True]) # => False
all([True, True]) # => True
all?/all - 条件指定

Ruby

[1, 2, 3, 4].all? { |x| x > 5 }  # => false
[1, 2, 3, 4].all? { |x| x < 5 }  # => true

Python

all(v > 5 for v in [1, 2, 3, 4]) # => False
all(v < 5 for v in [1, 2, 3, 4]) # => True

ただ、次のコードのように、複数行の判定をしたいケースだと、Pythonではシンプルにできないっぽい? です*2

Ruby

[1, 2, 3, 4].all? do |x|
  return false if x < 0
  return false if 100 < x
  true
end

以下のように判定用関数を定義してしまうか、

Python

def cond(x):
  if x < 0:
    return False
  if 100 < x:
    return False
  return True

all(cond(v) for v in [1, 2, 3, 4])

あるいは、forで頑張るかでしょうか・・・。

Python

x = True
for v in [1, 2, 3, 4]:
  if x < 0:
    x = False
    break
  if 100 < x:
    x = False
    break

個人的には、forを使う方法はC言語っぽくて嫌だし必要のない思考が必要になるので、自分なら判定用の関数を関数内関数として定義してallを使うと思います。

追記:条件演算子をネストすることでワンライナーで書けるようです。まあ、間違いなくレビューで書きなおせって言われますね、これは。

Python

all(False if v < 0 else False if 100 < v else True for v in [1,2,3,4])

any?

any?/any - シンプルなTruthy, Falsy

Ruby

[false, true].any? # => true
[nil, false].any? # => false

Python

any([False, True]) # => True
any([False, None]) # => False
any?/any - 条件指定

Ruby

[1, 2, 3, 4].any? { |x| x > 5 }  # => false
[1, 2, 3, 4].any? { |x| x < 2 }  # => true

Python

any(v > 5 for v in [1, 2, 3, 4]) # => False
any(v < 2 for v in [1, 2, 3, 4]) # => True

複数行の判定はallと同じように関数内関数を使えばできます。

none?

none?/not any - シンプルなTruthy, Falsy

Ruby

[nil, false].none? # => true
[true, false].none? # => false

Python

# notはrubyでいうところの条件否定の"!"
not any([None, False]) # => True
not any([True, False]) # => False
none?/not any - 条件指定

Ruby

[1, 2, 3, 4].none? { |x| x > 5 }  # => true
[1, 2, 3, 4].none? { |x| x < 2 }  # => false

Python

not any(v > 5 for v in [1, 2, 3, 4]) # => True
not any(v < 2 for v in [1, 2, 3, 4]) # => False

複数行の判定はallと同じように関数内関数を使えばできます。

one?

one?/countと比較の組み合わせ - シンプルなTruthy, Falsy

Ruby

[nil, true, false].one? # => true
[nil, 1000, false].one? # => true
[nil, 1000, 99999].one? # => false
[nil, false, false].one? # => false

Python

len([v for v in [None, True, False] if v]) == 1 # => True
len([v for v in [None, 1000, False] if v]) == 1 # => True
len([v for v in [None, 1000, 99999] if v]) == 1 # => False
len([v for v in [None, False, []] if v]) == 1 # => False

もっといい方法ありそう。

one?/countと比較の組み合わせ - 条件指定

Ruby

[1, 2, 3, 4].one? { |x| x > 5 }  # => false
[1, 2, 3, 4].one? { |x| x > 2 }  # => false
[1, 2, 3, 4].one? { |x| x < 2 }  # => true

Python

[v > 5 for v in [1, 2, 3, 4]].count(True) == 1 # => False
[v > 2 for v in [1, 2, 3, 4]].count(True) == 1 # => False
[v < 2 for v in [1, 2, 3, 4]].count(True) == 1 # => True

include?(member?)

include?(member?)/any - 指定要素を含むか判定

Ruby

[1, 2, 3, 4].include?(3) # => true

Python

any(3 == v for v in [1, 2, 3, 4]) # => True

first

first/スライス - 最初のn個の要素を取得する

Ruby

[1 ,2, 3].first # => 1
[1 ,2, 3].first(2) # => [1, 2]

Python

[1, 2, 3][0] # => 1
[1, 2, 3][:2] # => [1, 2]

map(collect)

map/内包表記 - 単純な変換

Ruby

[1, 2, 3, 4].map { |x| x * 2 }  # => [2, 4, 6, 8]

Pythonは内包表記を使うことでmapと同じことを実現できます。

Python

[v * 2 for v in [1, 2, 3, 4]] # => [2, 4, 6, 8]

count

count/len - 単純に要素数を得る

Ruby

[1, 2, 3, 4].count # => 4

※配列ならsizeメソッド(lengthメソッド)が定義されているので、要素数を得るだけなら普通はそっちを使います。countメソッドは要素を数え上げるメソッドのため、sizeメソッドに比べて多少速度が遅いです。

Python

len([1, 2, 3, 4]) # => 4
count/count - 指定要素の数を得る

Ruby

[1, 1, 2, 2, 2, 3].count(2) # => 3

Python

[1, 1, 2, 2, 2, 3].count(2) # => 3
count/lenと内包表記 - 条件を満たす要素数を得る

Ruby

[1, 2, 3, 4, 5].count {|x| x % 2 == 0 } # => 2

Python

len([v for v in [1, 2, 3, 4, 5] if v % 2 == 0]) #=> 2

内包表記にはifを追加することで含める要素を絞り込むことができます。

select(find_all)

select(find_all)/内包表記 - 条件に一致する要素をすべて探す

Ruby

[1, 2, 3, 4, 5].select {|x| 3 < x } # => [4, 5]

Python

[v for v in [1, 2, 3, 4, 5] if 3 < v] # => [4, 5]

reject

reject/内包表記 - 条件に一致する要素をすべて除外する

Ruby

[1, 2, 3, 4, 5].reject {|x| 3 < x } # => [1, 2, 3]

Python

[v for v in [1, 2, 3, 4, 5] if not 3 < v] # => [1, 2, 3]

detect(find)

detect(find)/内包表記 - 条件に一致する最初の要素を探す

Ruby

[1, 2, 3, 4, 5].detect { |x| 3 < x } # => 4

Python

# このやり方だと要素がないとIndexErrorになります
[v for v in [1, 2, 3, 4, 5] if 3 < v][0] # => 4
# スライスを使ったやり方だと要素がなくてもIndexErrorにならない
[v for v in [1, 2, 3, 4, 5] if 3 < v][0:1] # => [4]
[v for v in [1, 2, 3, 4, 5] if 100 < v][0:1] # => []
# これでも同じ
[v for v in [1, 2, 3, 4, 5] if 3 < v][:1] # => [4]
[v for v in [1, 2, 3, 4, 5] if 100 < v][:1] # => []

こうする他思いつかなかったんだけど、他に方法あるのかな。全件比較してしまうので、見つかった時点で切り上げたかったら、forで処理するしかないのかな?

Python

x = None
for v in [1, 2, 3, 4, 5]:
  if 3 < v:
    x = v
    break

Rubyfindは見つからない場合はnilになるけど、Pythonの内包表記を使ったやり方だとIndexErrorになるか[]になります。

find_index

find_index/index - 指定要素が最初に現れる位置を返す

Ruby

[1, 2, 3, 4].find_index(3) # => 2

Python

[1, 2, 3, 4].index(3) #=> 2
find_index/indexと内包表記 - 指定条件にマッチする最初の要素の位置を返す

Ruby

[1, 2, 3, 4].find_index { |x| x % 2 == 0 } # => 1

Python

[v % 2 == 0 for v in [1, 2, 3, 4]].index(True) # => 1

detect(find)と同じでforでやったほうがよいかも。

Python

x = None
for i, v in enumerate([1, 2, 3, 4]):
  if v % 2 == 0:
    x = i
    break

なお、Rubyだと指定条件に一致する要素がなくてもnilが返るけど、PythonだとValueErrorになります。

Ruby

[1, 2, 3, 4].find_index(100) # => nil

Python

[1, 2, 3, 4].index(100) #=> ValueError: 100 is not in list

maxとmax_by、minとmin_by

minとmin_byは単純に置き換えればいい話なので省略します。

max/max - 単純な最大値の取得

Ruby

[1, 3, 4, 2].max # => 4

Python

max([1, 3, 4, 2]) # => 4
max_by/max - 最大値の判定方法を指定した最大値の取得

max_byで大体ことが済んでmaxでブロック渡す必要のあるケースってあんまりない気がするので、複雑なケースは省略します。Pythonで同じことをしようとするとワンライナーでは難しそう。

Ruby

[1, 3, 4, 2, -100].max { |a, b| a*a <=> b*b } # => -100
[1, 3, 4, 2, -100].max_by { |item| item*item } # => -100

Python

max([1, 3, 4, 2, -100], key = lambda x: x*x) # => -100
max(max_by)/sortedとスライス - 上位n件の要素を取得する

Ruby

[1, 3, 4, 2, -100].max(2) # => [4, 3]
[1, 3, 4, 2, -100].max_by(2) { |item| item*item } # => [-100, 4]

Python

# minの場合はreverseの指定を外せばよい
sorted([1, 3, 4, 2, -100], reverse = True)[:2] # => [4, 3]
sorted([1, 3, 4, 2, -100], key = lambda x: x*x, reverse = True)[:2] # => [-100, 4]

全要素をソートしてから先頭のn件を取得しているので効率は悪い。

sortとsort_by

sort/sorted - 並び替える

Ruby

# 昇順
[1, 3, 4, 2, -100].sort # => [-100, 1, 2, 3, 4]
# 降順
[1, 3, 4, 2, -100].sort { |a, b| b <=> a}  # => [4, 3, 2, 1, -100]

Python

sorted([1, 3, 4, 2, -100]) # => [-100, 1, 2, 3, 4]
sorted([1, 3, 4, 2, -100], reverse = True) # => [4, 3, 2, 1, -100]
sort_by/sorted - ソート条件を指定したソート

Ruby

[1, 3, 4, 2, -100].sort_by { |item| item*item } # => [1, 2, 3, 4, -100]

Python

sorted([1, 3, 4, 2, -100], key = lambda x: x*x) # => [1, 2, 3, 4, -100]

group_by

group_by/itertools.groupby - 対象のグルーピングをする

Pythonでもitertoolsモジュールにあるgroupbyと辞書内包表記を使えば同じことができます。

Ruby

[1, 2, 3, 4, 5, 6].group_by { |v| v % 2 } # => {1=>[1, 3, 5], 0=>[2, 4, 6]}

Python

from itertools import groupby
sorted_list = sorted([1, 2, 3, 4, 5, 6], key = lambda x: x % 2)
{ k: list(v) for k, v in groupby(sorted_list, key = lambda x: x % 2) }
# => {0: [2, 4, 6], 1: [1, 3, 5]}

注意点がgroupbyの第一引数がグルーピングと同じ方法でソートされている必要があるということです。

partition

partition/itertools.groupby - 対象を条件を満たすものと満たさないものに分ける

実質的にgroup_byでtrueとfalseに分けるようなものなので、同じやり方でできます。

Ruby

[1, 2, 3, 4, 5, 6].partition { |v| v % 3 == 0 } # => [[3, 6], [1, 2, 4, 5]]

Python

from itertools import groupby
sorted_list = sorted([1, 2, 3, 4, 5, 6], key = lambda x: x % 3 == 0, reverse = True)
[ list(v) for k, v in groupby(sorted_list, key = lambda x: x % 3 == 0) ]
# => [[3, 6], [1, 2, 4, 5]]

zip

zip/zip - 配列の配列を作る

Ruby

[1, 2, 3].zip([50, 60, 70]) # => [[1, 50], [2, 60], [3, 70]]

Python

list(zip([1, 2, 3], [50, 60, 70])) # => [(1, 50), (2, 60), (3, 70)]

用途としてはいくつかの配列を同時にループさせたい時とかに使います。

Ruby

keys.zip(values).each_with_object({}) do |(key, value), o|
  o[key] = value
end

Python

{ k: v for k, v in zip(keys, values) }

inject(reduce)

inject(reduce)/reduce - 畳込み

Ruby

[1, 2, 3, 4].inject(:+) # => 10

Pythonではfunctools.reduceをimportしないと使えません。

Python

from functools import reduce

reduce(lambda a,b: a + b, [1, 2, 3, 4]) # => 10

operatorモジュールを使うとRubyと似たような感じでかけます。

Python

from functools import reduce
from operator import add

reduce(add, [1, 2, 3, 4]) # => 10

takeとdrop

take/スライス - リストの前からn個の要素を取得する

Ruby

[0, 1, 2, 3].take(2) # => [0, 1]

Python

[0, 1, 2, 3][:2] # => [0, 1]
drop/スライス - リストの前からn番目よりあとの要素を取得する

Ruby

[0, 1, 2, 3].drop(2) # => [2, 3]

Python

[0, 1, 2, 3][2:] # => [2, 3]

each

each/for - ループ

厳密にはeachはEnumerableモジュールのメソッドではないのですが、内部的に使っているので。

Ruby

list.each do |value|
  #...
end

Python

for item in list:
  #...

lazy

lazy/ジェネレータ式 - 遅延評価

Ruby

(1...1000).lazy.map do |x|
  x * x
end

lazyメソッド実行した場合、Enumerator::Lazyオブジェクトが返るようになります。 https://docs.ruby-lang.org/ja/latest/class/Enumerator=3a=3aLazy.html

Python

(x*x for x in range(1, 1000))

要素を取得するにはnext組み込み関数を使えばいいのですが、リセットしたり(Rubyでいうところのrewind)、要素位置を維持したまま値を取得したり(Rubyでいうところのpeek)といったことはできないっぽい? です(調査不足かも)。

Python

list = (x*x for x in range(1, 1000))
next(list) # => 1
next(list) # => 4
next(list) # => 9

chunk

chunk/itertools.groupby - 要素をチャンクに分ける

group_byみたいなメソッドですが、グループを一つにまとめません。

Ruby

# Enumeratorオブジェクトを返すのでto_aをしている
[3, 4, 4, 5, 6, 8, 2, 9 , 7, 1].chunk {|x| x % 2}.to_a
# => [[1, [3]], [0, [4, 4]], [1, [5]], [0, [6, 8, 2]], [1, [9, 7, 1]]]

Python

from itertools import groupby
a = groupby([3, 4, 4, 5, 6, 8, 2, 9 , 7, 1], key = lambda x: x % 2)
# => <itertools.groupby object at 0x7f4a3da0e8b8>
item = next(a) # => (1, <itertools._grouper object at 0x7f4a3d976550>)
list(item[1])  # => [3]
item = next(a) # => (0, <itertools._grouper object at 0x7f4a3da0dba8>)
list(item[1])  # => [4, 4]
item = next(a) # => (1, <itertools._grouper object at 0x7f4a3d9764e0>)
list(item[1])  # => [5]
item = next(a) # => (0, <itertools._grouper object at 0x7f4a3da0dba8>)
list(item[1])  # => [6, 8, 2]

cycle

cycle/itertools.cycle - 引数のリストを無限に繰り返す

Ruby

[1, 2, 3].cycle {|x| puts x } # 1、2、3を無限に出力し続ける
[1, 2, 3].cycle # 無限に繰り返すEnumeratorオブジェクトを返す

Python

from itertools import cycle

# 1、2、3を無限に出力し続ける
for v in cycle([1, 2, 3]):
  print(v)

# 無限に繰り返すイテレータを返す
cycle([1, 2, 3])

drop_while, take_while

drop_while/itertools.dropwhile - 指定した条件が偽になるまでの要素を切り捨てる

Ruby

[-11, -33, -55, 1, 3, -100].drop_while {|x| x < 0 } # => [1, 3, -100]

Python

from itertools import dropwhile
list(dropwhile(lambda x: x < 0, [-11, -33, -55, 1, 3, -100])) # => [1, 3, -100]
take_while/itertools.takewhile - 指定した条件が偽になるまでの要素を取得する

Ruby

[-11, -33, -55, 1, 3, -100].take_while {|x| x < 0 } # => [-11, -33, -55]

Python

from itertools import takewhile
list(takewhile(lambda x: x < 0, [-11, -33, -55, 1, 3, -100])) # => [-11, -33, -55]

each_with_index

each_with_index/enumerate - 要素と要素番号を取得するイテレータを取得する

Ruby

['a', 'b', 'c'].each_with_index {|char, i| puts "#{i}:#{char}" }
# 出力結果
# 0:a
# 1:b
# 2:c

Python

for i, char in enumerate(['a', 'b', 'c']):
  print('{0}:{1}'.format(i, char))
# 出力結果
# 0:a
# 1:b
# 2:c

each_cons

Ruby

[1, 2, 3, 4, 5].each_cons(3).to_a # => [[1, 2, 3], [2, 3, 4], [3, 4, 5]]

どう実装すればいいか検討がつかずググったら、ドンピシャなstackoverflowの質問があったので、そこの回答を引用します。 https://stackoverflow.com/questions/5878403/python-equivalent-to-rubys-each-cons

Python

def each_cons(x, size):
    return [x[i:i+size] for i in range(len(x)-size+1)]

https://stackoverflow.com/a/5878474

リストの要素番号をこねくり回している感じでわかりづらいのですが、len(x)が100でsizeが3とした場合、range(len(x) - size + 1)の部分はrange(0,98)になります。rangeは終端値を含めないためfor i in range(0, 98)iは0から97の値を取ります。x[i:i+size]部分はx[0:3]x[1:4]…、x[97:100]となりほしい結果が得られていることがわかります(スライスも終端値の要素は含めません)。

Python

import itertools
def each_cons(xs, n):
    return itertools.izip(*(xs[i:] for i in xrange(n)))

https://stackoverflow.com/a/12879942

おそらく、Python2のコードなのでPython3でも動くように修正してみます。

Python

def each_cons(xs, n):
  return zip(*(xs[i:] for i in range(n)))

ジェネレータでは使えないとかツッコミが入っていはするのですが、かなりクールなコードです。

残りものとまとめ

力尽きたので残りはリストアップだけしておきます。

  • chunk_while
  • flat_map(collect_concat)
  • each_slice
  • each_with_object
  • grep
  • grep_v
  • minmax
  • minmax_by
  • reverse_each
  • slice_after
  • slice_before
  • slice_when
  • to_h

今回、Rubyのコードと同等のことをPythonで書くということをしたわけですが、かなりPythonの勉強になりました。小さい課題を多くこなす感じになるので、新しい言語を勉強しようとしているときには良い勉強法かもしれません。

*1:Rubyの勉強にもなったのは秘密だ

*2:条件式に意味はありません。'0 <= x && x <= 100'と同じことです。