RubyのアレはPythonだとどう書くの?(Enumerableモジュール編)
どーも、xyakenです。普段はRubyでコード書いてます。 今回はRubyのEnumerableモジュールのメソッドをいくつかピックアップして、PythonでRubyのあれはどう書くのか調べたり、考えたりしてみました*1。
バージョンはruby 2.4.1p111とPython 3.6.1で動作確認しております。※リストであることを前提にPythonのコードを書いているので、ジェネレータ式ではエラーになるものもあると思います。
RubyのブロックをどうPythonで表現するのかと思ったのですが、内包表記というものを使えばそれっぽく実現できるんですね。
all?
all?/all - シンプルなTruthy, Falsy
[false, true].all? # => false [true, true].all? # => true
all([False, True]) # => False all([True, True]) # => True
all?/all - 条件指定
[1, 2, 3, 4].all? { |x| x > 5 } # => false [1, 2, 3, 4].all? { |x| x < 5 } # => true
all(v > 5 for v in [1, 2, 3, 4]) # => False all(v < 5 for v in [1, 2, 3, 4]) # => True
ただ、次のコードのように、複数行の判定をしたいケースだと、Pythonではシンプルにできないっぽい? です*2。
[1, 2, 3, 4].all? do |x| return false if x < 0 return false if 100 < x true end
以下のように判定用関数を定義してしまうか、
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
で頑張るかでしょうか・・・。
x = True for v in [1, 2, 3, 4]: if x < 0: x = False break if 100 < x: x = False break
個人的には、for
を使う方法はC言語っぽくて嫌だし必要のない思考が必要になるので、自分なら判定用の関数を関数内関数として定義してall
を使うと思います。
追記:条件演算子をネストすることでワンライナーで書けるようです。まあ、間違いなくレビューで書きなおせって言われますね、これは。
all(False if v < 0 else False if 100 < v else True for v in [1,2,3,4])
any?
any?/any - シンプルなTruthy, Falsy
[false, true].any? # => true [nil, false].any? # => false
any([False, True]) # => True any([False, None]) # => False
any?/any - 条件指定
[1, 2, 3, 4].any? { |x| x > 5 } # => false [1, 2, 3, 4].any? { |x| x < 2 } # => true
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
[nil, false].none? # => true [true, false].none? # => false
# notはrubyでいうところの条件否定の"!" not any([None, False]) # => True not any([True, False]) # => False
none?/not any - 条件指定
[1, 2, 3, 4].none? { |x| x > 5 } # => true [1, 2, 3, 4].none? { |x| x < 2 } # => false
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
[nil, true, false].one? # => true [nil, 1000, false].one? # => true [nil, 1000, 99999].one? # => false [nil, false, false].one? # => false
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と比較の組み合わせ - 条件指定
[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
[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 - 指定要素を含むか判定
[1, 2, 3, 4].include?(3) # => true
any(3 == v for v in [1, 2, 3, 4]) # => True
first
first/スライス - 最初のn個の要素を取得する
[1 ,2, 3].first # => 1 [1 ,2, 3].first(2) # => [1, 2]
[1, 2, 3][0] # => 1 [1, 2, 3][:2] # => [1, 2]
map(collect)
map/内包表記 - 単純な変換
[1, 2, 3, 4].map { |x| x * 2 } # => [2, 4, 6, 8]
Pythonは内包表記を使うことでmapと同じことを実現できます。
[v * 2 for v in [1, 2, 3, 4]] # => [2, 4, 6, 8]
count
count/len - 単純に要素数を得る
[1, 2, 3, 4].count # => 4
※配列ならsizeメソッド(lengthメソッド)が定義されているので、要素数を得るだけなら普通はそっちを使います。countメソッドは要素を数え上げるメソッドのため、sizeメソッドに比べて多少速度が遅いです。
len([1, 2, 3, 4]) # => 4
count/count - 指定要素の数を得る
[1, 1, 2, 2, 2, 3].count(2) # => 3
[1, 1, 2, 2, 2, 3].count(2) # => 3
count/lenと内包表記 - 条件を満たす要素数を得る
[1, 2, 3, 4, 5].count {|x| x % 2 == 0 } # => 2
len([v for v in [1, 2, 3, 4, 5] if v % 2 == 0]) #=> 2
内包表記にはif
を追加することで含める要素を絞り込むことができます。
select(find_all)
select(find_all)/内包表記 - 条件に一致する要素をすべて探す
[1, 2, 3, 4, 5].select {|x| 3 < x } # => [4, 5]
[v for v in [1, 2, 3, 4, 5] if 3 < v] # => [4, 5]
reject
reject/内包表記 - 条件に一致する要素をすべて除外する
[1, 2, 3, 4, 5].reject {|x| 3 < x } # => [1, 2, 3]
[v for v in [1, 2, 3, 4, 5] if not 3 < v] # => [1, 2, 3]
detect(find)
detect(find)/内包表記 - 条件に一致する最初の要素を探す
[1, 2, 3, 4, 5].detect { |x| 3 < x } # => 4
# このやり方だと要素がないと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
で処理するしかないのかな?
x = None for v in [1, 2, 3, 4, 5]: if 3 < v: x = v break
Rubyのfind
は見つからない場合はnilになるけど、Pythonの内包表記を使ったやり方だとIndexErrorになるか[]
になります。
find_index
find_index/index - 指定要素が最初に現れる位置を返す
[1, 2, 3, 4].find_index(3) # => 2
[1, 2, 3, 4].index(3) #=> 2
find_index/indexと内包表記 - 指定条件にマッチする最初の要素の位置を返す
[1, 2, 3, 4].find_index { |x| x % 2 == 0 } # => 1
[v % 2 == 0 for v in [1, 2, 3, 4]].index(True) # => 1
detect(find)と同じでfor
でやったほうがよいかも。
x = None for i, v in enumerate([1, 2, 3, 4]): if v % 2 == 0: x = i break
なお、Rubyだと指定条件に一致する要素がなくてもnil
が返るけど、PythonだとValueErrorになります。
[1, 2, 3, 4].find_index(100) # => nil
[1, 2, 3, 4].index(100) #=> ValueError: 100 is not in list
maxとmax_by、minとmin_by
minとmin_byは単純に置き換えればいい話なので省略します。
max/max - 単純な最大値の取得
[1, 3, 4, 2].max # => 4
max([1, 3, 4, 2]) # => 4
max_by/max - 最大値の判定方法を指定した最大値の取得
max_byで大体ことが済んでmaxでブロック渡す必要のあるケースってあんまりない気がするので、複雑なケースは省略します。Pythonで同じことをしようとするとワンライナーでは難しそう。
[1, 3, 4, 2, -100].max { |a, b| a*a <=> b*b } # => -100 [1, 3, 4, 2, -100].max_by { |item| item*item } # => -100
max([1, 3, 4, 2, -100], key = lambda x: x*x) # => -100
max(max_by)/sortedとスライス - 上位n件の要素を取得する
[1, 3, 4, 2, -100].max(2) # => [4, 3] [1, 3, 4, 2, -100].max_by(2) { |item| item*item } # => [-100, 4]
# 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 - 並び替える
# 昇順 [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]
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 - ソート条件を指定したソート
[1, 3, 4, 2, -100].sort_by { |item| item*item } # => [1, 2, 3, 4, -100]
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
と辞書内包表記を使えば同じことができます。
[1, 2, 3, 4, 5, 6].group_by { |v| v % 2 } # => {1=>[1, 3, 5], 0=>[2, 4, 6]}
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に分けるようなものなので、同じやり方でできます。
[1, 2, 3, 4, 5, 6].partition { |v| v % 3 == 0 } # => [[3, 6], [1, 2, 4, 5]]
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 - 配列の配列を作る
[1, 2, 3].zip([50, 60, 70]) # => [[1, 50], [2, 60], [3, 70]]
list(zip([1, 2, 3], [50, 60, 70])) # => [(1, 50), (2, 60), (3, 70)]
用途としてはいくつかの配列を同時にループさせたい時とかに使います。
keys.zip(values).each_with_object({}) do |(key, value), o| o[key] = value end
{ k: v for k, v in zip(keys, values) }
inject(reduce)
inject(reduce)/reduce - 畳込み
[1, 2, 3, 4].inject(:+) # => 10
Pythonではfunctools.reduce
をimportしないと使えません。
from functools import reduce reduce(lambda a,b: a + b, [1, 2, 3, 4]) # => 10
operator
モジュールを使うとRubyと似たような感じでかけます。
from functools import reduce from operator import add reduce(add, [1, 2, 3, 4]) # => 10
takeとdrop
take/スライス - リストの前からn個の要素を取得する
[0, 1, 2, 3].take(2) # => [0, 1]
[0, 1, 2, 3][:2] # => [0, 1]
drop/スライス - リストの前からn番目よりあとの要素を取得する
[0, 1, 2, 3].drop(2) # => [2, 3]
[0, 1, 2, 3][2:] # => [2, 3]
each
each/for - ループ
厳密にはeachはEnumerableモジュールのメソッドではないのですが、内部的に使っているので。
list.each do |value| #... end
for item in list: #...
lazy
lazy/ジェネレータ式 - 遅延評価
(1...1000).lazy.map do |x| x * x end
lazyメソッド実行した場合、Enumerator::Lazyオブジェクトが返るようになります。 https://docs.ruby-lang.org/ja/latest/class/Enumerator=3a=3aLazy.html
(x*x for x in range(1, 1000))
要素を取得するにはnext
組み込み関数を使えばいいのですが、リセットしたり(Rubyでいうところのrewind
)、要素位置を維持したまま値を取得したり(Rubyでいうところのpeek
)といったことはできないっぽい? です(調査不足かも)。
list = (x*x for x in range(1, 1000)) next(list) # => 1 next(list) # => 4 next(list) # => 9
chunk
chunk/itertools.groupby - 要素をチャンクに分ける
group_byみたいなメソッドですが、グループを一つにまとめません。
# 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]]]
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 - 引数のリストを無限に繰り返す
[1, 2, 3].cycle {|x| puts x } # 1、2、3を無限に出力し続ける [1, 2, 3].cycle # 無限に繰り返すEnumeratorオブジェクトを返す
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 - 指定した条件が偽になるまでの要素を切り捨てる
[-11, -33, -55, 1, 3, -100].drop_while {|x| x < 0 } # => [1, 3, -100]
from itertools import dropwhile list(dropwhile(lambda x: x < 0, [-11, -33, -55, 1, 3, -100])) # => [1, 3, -100]
take_while/itertools.takewhile - 指定した条件が偽になるまでの要素を取得する
[-11, -33, -55, 1, 3, -100].take_while {|x| x < 0 } # => [-11, -33, -55]
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 - 要素と要素番号を取得するイテレータを取得する
['a', 'b', 'c'].each_with_index {|char, i| puts "#{i}:#{char}" } # 出力結果 # 0:a # 1:b # 2:c
for i, char in enumerate(['a', 'b', 'c']): print('{0}:{1}'.format(i, char)) # 出力結果 # 0:a # 1:b # 2:c
each_cons
[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
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]
となりほしい結果が得られていることがわかります(スライスも終端値の要素は含めません)。
import itertools def each_cons(xs, n): return itertools.izip(*(xs[i:] for i in xrange(n)))
https://stackoverflow.com/a/12879942
おそらく、Python2のコードなのでPython3でも動くように修正してみます。
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の勉強になりました。小さい課題を多くこなす感じになるので、新しい言語を勉強しようとしているときには良い勉強法かもしれません。