友だちが「Rubyはいろいろなメソッドがあって柔軟だから、
そっちに慣れちゃうとPython書くときちょっと困るんだよね」と言っていました。
たしかに、Rubyは「配列やハッシュに対してこんな機能があればいいな」と感じたとき、
そのデータ型のリファレンスを調べれば大抵見つかります。
しかし、Pythonでもいろいろな便利な関数を使って同等の機能を実現することができます。
ただし、Rubyに比べてオブジェクト指向っぽくないところがあり、
オブジェクトからメソッドを呼び出すのではなく関数やラムダ式を利用する必要あるので、
最初は少し戸惑うかもしれません。
そこで、「Rubyの便利なメソッドを、Pythonのコードで自然に書くならこうだろう」という記事を
まとめてみることにしました。
といってもキリがないので、配列とハッシュのメソッドを中心にします。
Rubyは2.2.2、Pythonは3.4.1を利用します。
また便宜上、この記事の中ではPythonの「リスト」と「辞書」をそれぞれRubyの対応する型である
「配列」「ハッシュ」と呼ぶことにします。
$ ruby -v
ruby 2.2.2p95 (2015-04-13 revision 50295) [x86_64-darwin14]
$ python -V
Python 3.4.3
余談ですが、Pythonは「ハッシュテーブル」を「辞書」と呼ぶなど、
プログラマ以外にも直感的に分かりやすい名前をつけたがるようです。
リストと言われてLispにあるような連結リストを想像する方もいるかもしれませんが、
実際は配列なのでご注意ください。
既に素晴らしい記事を書いている方がいらっしゃるのでそちらもご覧ください。
車輪の再発明感は拭えませんが、この記事ではもう少し細かいところまでまとめられればと思います。
yoghurt1131.hatenablog.com
each
プリパラアイドルをターミナルに列挙するプログラムを書きたいとします。
Rubyでもfor
の制御構文は用意されているものの、
実際のプログラミングではeach
メソッドを呼び出すほうが好まれているようです。
オブジェクト指向!って感じですね。
idols = %w(laala mirei sophy)
idols.each do |idol|
puts idol
end
ちなみにRubyの元ネタの純粋オブジェクト指向言語のSmalltalkではさらに徹底しており、
条件分岐(if)も真偽値のメソッドとして定義されているという
話を聞いたことがあります。
ハッシュの各要素をループさせることもできます。
idol_group_dict = {
'sorami smile' => %w(laala mirei sophy),
'dressing pafe' => %w(shion dorothy leona)
}
idol_group_dict.each do |group_name, members|
members.each do |idol|
puts "#{idol} is a member of #{group_name}"
end
end
Pythonではfor ~ in ~
の制御構文を使ってこう書きます。
idols = ['laala', 'mirei', 'sophy']
for idol in idols:
print(idol)
ハッシュの各要素をループさせるには、辞書型の.items()
メソッドを利用します。
ふつうにfor
文に渡すとハッシュのキーしか取り出せないので面食らうかもしれません。
idol_group_dict = {
'sorami smile': ['laala', 'mirei', 'sophy'],
'dressing pafe': ['shion', 'dorothy', 'leona']
}
for group_name, members in idol_group_dict.items():
for idol in members:
print('{} is a member of {}'.format(idol, group_name))
ちなみにPython3.6からはf'{idol} is member of {group_name}')
という形でも
文字列リテラルができるようになるそうです。
Rubyに比べて冗長だったので嬉しいです。
each_with_index
インデックスも含めてループさせたい場合、Rubyではeach_with_index
を使うと便利です。
school_rules = %w(生徒は自分に自信を持たなければならない)
school_rules.each_with_index do |rule, i|
puts "パプリカ学園校則第#{i+1}条! #{rule}!"
end
インデックスを0以外から始めたい場合は、each.with_index
を使います。
(each
メソッドでEnumerator型に変換した後、with_index
メソッドを呼び出しています。)
ちなみにeach
だけでなくmap
等の他のメソッドとも組み合わせられます。
school_rules = %w(生徒は自分に自信を持たなければならない)
school_rules.each.with_index(1) do |rule, i|
puts "パプリカ学園校則第#{i}条! #{rule}!"
end
Pythonではenumerate
という関数を利用します。
school_rules = ['生徒は自分に自信を持たなければならない']
for i, rule in enumerate(school_rules):
print("パプリカ学園校則第{}条! {}!".format(i+1, rule))
インデックスを1から始めたい場合は、enumerate
の引数に指定してあげるだけです。
school_rules = ['生徒は自分に自信を持たなければならない']
for i, rule in enumerate(school_rules, 1):
print("パプリカ学園校則第{}条! {}!".format(i, rule))
each_cons
子どものために「おかあさんといっしょ」のエンディングテーマを出力したいお父さんプログラマーも多いことでしょう。
(今でもこの歌なんでしょうか?)
そんなとき、each_cons
を使うと自然に書けます。
song_with_mam = %w(クジラ ラッコ コアラ ライオン 女の子 男の子)
song_with_mam.each_cons(2) do |x, y|
puts "#{x} になりたい #{y}♪"
end
このような場合にはPythonではどう書けば自然なのかパッと思いつきません…。zip
関数を使って
song_with_mam = ['クジラ', 'ラッコ', 'コアラ', 'ライオン', '女の子', '男の子']
for x, y in zip(song_with_mam, song_with_mam[1:]):
print('{}になりたい{}♪'.format(x, y))
とか2つループを回して
song_with_mam = ['クジラ', 'ラッコ', 'コアラ', 'ライオン', '女の子', '男の子']
for i, x in enumerate(song_with_mam):
for j, y in enumerate(song_with_mam):
if j - i == 1:
print('{}になりたい{}♪'.format(x, y))
とか、一つ前の値を残して
song_with_mam = ['クジラ', 'ラッコ', 'コアラ', 'ライオン', '女の子', '男の子']
prev = None
for x in song_with_mam:
if x is not None:
print('{}になりたい{}♪'.format(prev, x))
prev = x
とかになるんでしょうか?
と思ったのですが、Pythonのイテレータを扱う標準ライブラリであるitertools
を使うのが自然かもしれません。
Itertools レシピのサンプルコードを使うとこのようになります。
from itertools import tee
def pairwise(iterable):
a, b = tee(iterable)
next(b, None)
return zip(a, b)
song_with_mam = ['クジラ', 'ラッコ', 'コアラ', 'ライオン', '女の子', '男の子']
for x, y in pairwise(song_with_mam):
print('{}になりたい{}♪'.format(x, y))
要素が3つ以上の場合のすぐに拡張もできそうです。ただ、複数個ずつ要素を取り出すだけなのに、大げさなコードになってしまった感はあります。
ところで、メソッド名のeach_cons
のcons
ってLispのcons
)が由来なんしょうか?
each_slice
既に素晴らしい記事を書いている方がいました。
http://blog.livedoor.jp/dankogai/archives/51838970.html
このエントリを見て、PythonでRubyのeach_sliceを書くとしたらどうなるだろうと思ってやってみた。
こちらもitertools
を使うのが自然のようですね。
Itertools レシピにあるコードがこちらです。
(上の記事ではPython2系の時代なのでizip_longest
という関数名になっています。)
from itertools import zip_longest
def grouper(iterable, n, fillvalue=None):
"Collect data into fixed-length chunks or blocks"
args = [iter(iterable)] * n
return zip_longest(*args, fillvalue=fillvalue)
length (size)
配列やハッシュ、そして文字列等の長さを調べるメソッドです。
triangle = %(kanon pinon junon)
puts "Triangle is a group of #{triangle.length} idols."
Pythonでは、メソッドではなくlen
という関数を使います。Pythonではある程度一般的な操作
(RubyではEnumerable
のメソッドにあるようなもの)は
関数や構文として用意されていることが多いです。
triangle = ['kanon', 'pinon', 'junon']
print('Triangle is a group of {} idols.'.format(len(triangle)))
sort, sort_by, sort!, sort_by!
配列の中身をソートする場合、
Rubyではsort
を呼び出すか、sort_by
のブロックにソート基準を渡すことが多いと思います。
(個人的に、sort
メソッドでブロックを呼び出すとき、宇宙船演算子(<=>
)を定義するか、
戻り値に-1 or 0 or 1を返すブロックを作る必要があって、頭の中がごちゃごちゃしてしまうことが多いです…。)
p [3, 0, 1].sort
names = %w(laala fuwari dore shio)
p names.sort_by { |n| n.size }
p names.sort_by(&:size)
余談ですが、sort_by
やsort
メソッドは安定ソートでない(比較結果が同じ要素を元の順序通りに並ばないことがある)
そうなので注意が必要です。
また、既にご存じだと思いますが、sort!
やsort_by!
オブジェクト自身を変化させる破壊的メソッドです。
コピーを作らないため、破壊的メソッドのほうがパフォーマンスが良い場合が多いようです。
names = %w(laala fuwari dore shio)
names.sort_by! { |n| n.size }
p names
Pythonでは非破壊操作はsorted
関数を、 破壊的操作は配列(リスト型)の.sort
メソッドを使います。
ちなみに、これらのソートは安定ソートだそうです。
print(sorted([3, 0, 1]))
names = ['laala', 'fuwari', 'dore', 'shio']
print(sorted(names, key=len))
破壊的操作の場合のコードです。
names = ['laala', 'fuwari', 'dore', 'shio']
names.sort(key=len)
print(names)
ところで、Rubyのブロックという強力な機能が使えない代わりに、
Pythonでは高階関数で(比較基準となる関数を引数に与えて)機能を実現していることが多いです。
たまにPythonは「配列.size
じゃなくてlen(配列)
なのはオブジェクト指向言語として一貫性がないよね?」
とツッコまれていますが、メソッドとして定義してしまうとnames.sort(key=lambda x: x.size)
のような冗長な書き方になってしまうから、
という理由もあるように思います。
また、Rubyのsort
メソッドにブロックを渡したときのように、2つずつの要素を取り出して比較したい場合もあるかもしれません。
Python2.7では、sorted
関数/.sort
メソッドにcmp
という引数があり、
組み込み関数に宇宙船演算子(<=>
)と同じ挙動のcmp
関数が用意されていたのですが、
Python3.5では削除されています。
Python3.5で2.7のcmp
のような挙動(Rubyのsort
メソッド+ブロックのような挙動)が欲しい場合、
公式ドキュメントによると、
標準ライブラリのfunctools
の中にcmp_to_key
があり、これを使って変換した関数をkey
引数に渡せばいいようです。
旧式の cmp 関数を key 関数に変換するには functools.cmp_to_key() を使用してください。
min, min_by, max, max_by
これもsort
の場合と似ています。
tension = [1, 2, 3, 4, 5]
p tension.max
idols = [
{name: 'mirei', gobi: 'puri'},
{name: 'ajimi', gobi: 'davinci'},
{name: 'cosmo', gobi: 'cosmic'}
]
p idols.max_by { |x| x[:gobi].size }
Pythonの場合、max
の引数key
に関数を渡すこととRubyのmix_by
のような挙動になります。
tension = [1, 2, 3, 4, 5]
print(max(tension))
idols = [
{'name': 'mirei', 'gobi': 'puri'},
{'name': 'ajimi', 'gobi': 'davinci'},
{'name': 'cosmo', 'gobi': 'cosmic'}
]
print(max(idols, key=lambda x: len(x['gobi'])))
reverse, reverse!
これもソートの場合とよく似ており、reversed
関数や.reverse
メソッドを利用します。
ただし、reversed
関数の戻り値は配列(list
)ではなくlistreverseiterator
というイテレーターオブジェクトが返ってくるので注意しましょう。
print(reversed([1, 2, 3]))
print(list(reversed([1, 2, 3])))
公式ドキュメント(http://docs.python.jp/3.5/library/functions.html#reversed)には以下のような記述があります。
要素を逆順に取り出すイテレータ (reverse iterator) を返します。 seq は __reverse__() メソッドを持つか、シーケンス型プロトコル (__len__() メソッド、および、 0 以上の整数を引数とする __getitem__() メソッド) をサポートするオブジェクトでなければなりません。
Pythonは、Rubyに比べてカジュアルに遅延評価を行う言語だからかもしれません。
for x in reversed(long_list):
function(x)
このような場合、reversed
関数が配列を返す実装だった場合、reversed(long_list)
で一度配列全部をループして操作した後に
もう一度for
文のループが回ります。
遅延評価するオブジェクトを返すことで、ループが一度で済むようになっています。
正直、reverse
ではそんなに遅延評価のメリットを感じられませんが、map
やfilter
では役立ちます。
inject (reduce), each_with_object, join
Pythonでもreduce
という畳み込みの関数が用意されているのですが、
Effective Rubyの『項目19 reduceを使ってコレクションを畳み込む方法を身に付けよう』を読んで
その便利さに驚かされたことがあります。
例えば、(以前の記事のそのままの例なのですが)URLの配列(リスト)を、{URL => ページタイトル}というハッシュテーブル(辞書)に変換したいとします。 同時に、ページのURLにgoogleが含まれているものを除外したいとします。
このようなとき、inject
の初期値をハッシュにすることで、ハッシュの中に畳み込んでいくことができます。
def create_url_table(urls)
urls.inject({}) do |hash, url|
hash[url] = get_title(url) unless url =~ %r{google}
hash
end
end
しかし、私は今ではeach_with_object
を利用してます。
def create_url_table(urls)
urls.each_with_object({}) do |url, hash|
hash[url] = get_title(url) unless url =~ %r{google}
end
end
Pythonでは同様の処理を辞書内包表記を使って書くことができます。
def create_url_table(urls):
return {url: get_title(url) for url in urls if 'google' not in url}
もう少し条件が複雑になってくると、ジェネレーターに切り出すようにしています。
『コレクションを畳み込む方法を身に付けよう』で紹介されているような例だと、
内包表記やジェネレーターを使うことが多いです。
def create_url_table(urls):
return dict(_url_generator(urls))
def _url_generator(urls):
for url in urls:
if 'google' not in url:
yield (url, get_title(url))
Rubyでは配列の中身の数値の合計値を計算する際にもinject
を使うため、inject
が登場する機会は多いです。
[1, 2, 3].inject(:+)
[1, 2, 3].sum
一方で、Pythonは組み込み関数であるsum
を利用します。
そのせいか、Python3.*になった際にreduce
は組み込み関数でなくなってしまい、
functools
から呼び出す必要があります。
sum([1, 2, 3])
同じように、一つの文字列に結合する際には、Rubyでは配列から文字列結合のメソッド.join
を呼び出しますが、
puts %(laala mirei sophy).join(', ')
Pythonでは、文字列型の.join
メソッドに配列を渡すようになっています。
これもlen
関数と同じく、オブジェクト指向言語のプログラマーが嫌がることの一つです。
print(', '.join(['laala', 'mirei', 'sophy']))
余談ですが、Rubyにinject
とreduce
の別名のエイリアスが用意されていることについては、
Rubyist Magazine - map と collect、reduce と inject ―― 名前の違いに見る発想の違いの内容が面白かったのでぜひ読んでください。
map (collect), select, reject
配列全体に関数やメソッドを適用するmap
と、
条件に当てはまる要素だけを残すselect
(と真偽値が逆のバージョンのreject
)です。
『Rubyの配列操作をPythonで書くと?』の記事にある通りです。
ただし、Python3.*ではmap
やfilter
が遅延評価されるように変更されています。
詳しくは以下の記事を読んでください。
postd.cc
map
やfilter
などの公開関数と、ジェネレーター内包表記で機能が被っています。
どちらを使ってもいいのですが、私は内包表記のほうをよく使います。
配列の要素の変換とフィルタリングが同時に行えるし、
括弧によって戻り値をリスト、ジェネレーター、集合(set
)を選べるからです。
Rubyには破壊的操作版のmap!
やselect!
もあります。
これは関数の中でメソッドチェーンする際に使っています。
それ以外で使ったことは私はほとんど無いです。
def count_group_mamber(idols, group_name)
idols.filter { |x| x[:group_name] == group_name }.
map { |x| x[:name] }.
size
end
def count_group_mamber(idols, group_name)
arr = idols.filter { |x| x[:group_name] == group_name }
arr.map! { |x| x[:name] }
arr.size
end
RubyでもPythonと同様に遅延評価を行いたいなら、.lazy.map
のように書きます。
詳しくは『怠惰で短気で傲慢な君に贈るRubyの遅延評価』を読みましょう。
empty?
Rubyには、配列が空かどうかを判定する.empty?
というメソッドがあります。
これにより明快なコードが書けるようになります。
例えば、名前の配列(names
)を名簿を管理しているDBに問い合わせるような場合、
もしnames
が空ならDBに問い合わせるまでもなく空の配列を返せばいいとします。
def search_ids(names)
return [] if names.empty?
ids
end
Pythonでは.empty?
のようなメソッドは無いため、空の配列が偽であることを利用して以下のように書くと思います。
def search_ids(names):
if not names:
return []
return ids
Pythonは「あんまり特殊な関数や構文を追加するとわけわかんなくなるから最小限にとどめよう」という
考え方が強いように思います。
一方で、Rubyでは同じメソッドの別名のエイリアスを用意したり、if
とunless
が用意されていたり、
文脈によってきちんと使い分けられれば、コードの意図が明快なプログラミングを行いやすいです。
include?
solami_smile = %w(laala mirei sophy)
puts solami_smile.include? 'sophy'
puts solami_smile.include? 'dorothy'
Pythonではin
という演算子を利用します。
solami_smile = ['laala', 'mirei', 'sophy']
print('sophy' in solami_smile)
print('dorothy' in solami_smile)
また、in
演算子は、ある文字が文字列の中に含まれるかや、ハッシュのキーが存在するか(Rubyの.hash_key?
メソッドの用途)にも使われます。
(Python2.*では辞書型にhas_key
メソッドも存在したのですが、
3系ではin
に統一されました。)
print('la' in 'laala')
print('do' in 'laala')
idol_group_dict = {
'sorami smile': ['laala', 'mirei', 'sophy'],
'dressing pafe': ['shion', 'dorothy', 'leona']
}
print('sorami_smile' in idol_group_dict)
any?, all?
配列の中に〇〇なものがいるかどうか、全部〇〇なのかを調べるとき、any?
やall?
のメソッドは便利です。
例えばプリパラの各チームのメンバーの中に〇〇がいるかどうか確認したいとき、以下のようなコードを書くと思います。
(集合演算を使う方法もあると思いますが)
solami_smile = %w(laala mirei sophy)
solami_dressing = %w(laala mirei sophy shion dorothy)
gaarmageddon = %(aroma mikan gaaruru)
boys = %(meganii leona)
def cute?(pripara_idol)
true
end
p solami_smile.any? { |x| gaarmageddon.include?(x) }
p sorami_dressing.any? { |x| boys.include?(x) }
p solami_smile.all? { |x| solami_dressing.include?(x) }
p solami_smile.all? { |x| cute?(x) }
Rubyのany?
メソッドではtrue
の値が見つかった時点で(all?
はfalse
が見つかった時点で)、
それ以上のループを打ち切ります。
Pythonではany
やall
は関数として用意されています。
真偽値の一覧を返す内包表記と組み合わせて使うことが多いです。
(ここでジェネレーターを使うことで、Rubyと同様にany
関数で評価している中でTrue
が見つかった時点でループを打ち切ることができます)
def is_cute(pripara_idol):
return True
print(all((x in gaarmageddon) for x in solami_smile))
print(any(is_cute(x) for x in solami_smile))
Rubyには他にもone?
やnone?
などのメソッドもあります。
uniq, uniq!
割とPythonを扱う際に鬼門なのが値を一意にするメソッドです。
順番を気にしなくていい & 配列の中の変数がイミュータブルなら、一度集合型(set
)に変換してしまいましょう。
ただし、変数の順番が保持される必要があります。
※ Python3.6からRubyと同じくハッシュの順番が保持されるようになるらしいので、
集合型でも順番が保持されるようになるかもしれません(単なる期待ですが)。
nums = [1, 2, 3, 4, 2, 3, 4]
nums_set = set(nums)
print(nums_set)
print(list(nums_set))
順番を保持したい場合は、『Python Tips:リストから重複した要素を削除したい』で同様の議論がされています。
また、Itertools レシピの中にも、もう少し難しい場合のサンプルがあります。
def f7(seq):
seen = set()
seen_add = seen.add
return [ x for x in seq if x not in seen and not seen_add(x)]
print(f7([1, 2, 3, 4, 2, 3, 4]))
また、sorted
関数でインデックスを基にソートするという手もあります。
nums = [1, 2, 3, 4, 2, 3, 4]
nums = sorted(set(nums), key=nums.index)
ただし、ミュータブルなオブジェクト(例えば配列やハッシュ)は、set
の要素に含めることができません。
そのため、ユニークにする際にエラーが出てしまいます。
idol_groups = [['laala', 'mirei', 'sophy'], ['shion', 'dorothy', 'leona'], ['laala', 'mirei', 'sophy']]
set(idol_groups)
このような場合にどうすればいいのか調べたところ、
『Pythonのリスト内包表記でRubyのuniqメソッドと同じ事をする』
という記事の中にある例がまあ読める例だと感じました。
idol_groups = [['laala', 'mirei', 'sophy'], ['shion', 'dorothy', 'leona'], ['laala', 'mirei', 'sophy']]
print([x for i, x in enumerate(idol_groups) if i == idol_groups.index(x)])
コードの中に頻繁に使うなら、
組み込みのlist
型を継承してUniqueList
みたいな名前で専用のクラスを作ってしまったほうがいいのかもしれません。
freeze
配列やハッシュに限りませんが、オブジェクトに破壊的変更が加えられないように制限するメソッドです。
Pythonではイミュータブルな配列としてタプルが、
イミュータブルな集合型としてfrozensetがあります。
先程uniq
のところで述べた通り、ハッシュのキーや集合型にミュータブルなオブジェクトである配列が指定できません。
そのため、そのような場合に配列を指定する必要があるときにタプルを利用します。
「あれ?じゃあfrozendictは無いの?」と思ったら一度提案されてリジェクトされているようです。
ハッシュのキーにハッシュを指定したい場合(ややこしい)は、.items()
メソッドで変換したものをタプルにすると良いと思います。
profile = {'name': 'mirei', 'gobi': 'puri'}
tuple(profile.items())
slice
配列の一部を取得するメソッドです。Pythonにも、スライスのための専用の表記があります。
www.pythonweb.jp
transpose
行列(2次元配列)の転置を行う場合、Rubyには専用のメソッドが用意されています。
Pythonでは
matrix = [[1, 2, 3], [4, 5, 6]]
print(list(zip(*martix)))
ただし、転置行列が必要なレベルの数値計算を行う場合、numpy
を利用することが多いと思います。
こちらにはtranspose
のメソッドが用意されています。
flatten
こちらも、Pythonには簡単に操作できるメソッドは用意されていません。
www.lifewithpython.com
group_by
私が好きなメソッドの一つです。以前書いた記事のまんまですが、
メソッドチェーンでデータベースのテーブル操作みたいなことが簡単にできます。
name_list = [
{'name' => 'ichigo', 'age' => 16, 'group' => 'soleil'},
{'name' => 'aoi', 'age' => 17, 'group' => 'soleil'},
{'name' => 'ran', 'age' => 17, 'group' => 'soleil'},
{'name' => 'akari', 'age' => 13, 'group' => 'luminas'},
{'name' => 'sumire', 'age' => 14, 'group' => 'luminas'},
{'name' => 'hinaki', 'age' => 14, 'group' => 'luminas'},
]
def col_mean(list, colname)
list.reduce(0){|x, y| x + y[colname]} / list.size.to_f
end
p name_list.
group_by{|raw| raw['group']}.
map{|k, v| {k => col_mean(v, 'age')}}
Pythonでは、itertools
のgroupby
関数で同様の機能が使えます。
ただし、公式ドキュメントにあるとおり、
- 戻り値がハッシュ(辞書)ではなくジェネレーターであること(一度ループさせると中身が消える)
- key 関数の値が変わるたびに新しいグループが作られること
に注意する必要があります。上のようなSQLっぽい集約のために使うなら、あらかじめsorted
関数でソートしておきましょう。
simanman.hatenablog.com
まとめ
他にもRubyのメソッドはたくさんありますが、Rubyに慣れたプログラマーがつまづきやすいところを中心に紹介したつもりです。
(もっと良い方法があれば教えてください!)
「Pythonもイケてるよ!」という記事にしたつもりなのですが、each_cons
やuniq
などは思ったより再現が難しく感じました。
普段使っている便利なメソッドを、自力で再現すると勉強になりますね…。
また、Pythonのitertools
にはまだ便利な機能が眠っている気がしています。
一度調べてみたいです。
このモジュールは イテレータ を構築する部品を実装しています。プログラム言語 APL, Haskell, SML からアイデアを得ていますが、 Python に適した形に修正されています。
やっぱりHaskellを一度ガッツリ触るべきなのかな…。型のしっかりした言語もやりたいし。