kick the base

Houdiniと、CG技術と、日々のこと。

Pythonic!: スライシング

Pythonらしい、シンプルで読みやすいコードのことをPythonicと呼びますが、本ブログでは不定期連載記事としてPythonicなコードを考える記事を書いていこうと思います。

特徴として、良いコード(Pythonicなコード)と悪いコード(Pythonicでないコード)を用意し、その考え方を学んでいければと思っています。

今回は「文字列を逆順にする手法」について。

環境

  • OS X 10.11.6
  • Python 3.5.1

ルール

1行目はname = 'めんたいこ'とし、それを逆順にした文字列「こいたんめ」をprintで出力すればOKとします。

悪いコード

※ ここで言う悪いコードは動作はするが美しくないコードを指します

name = 'めんたいこ'
name_list = [s for s in name]
name_list.reverse()
name_reverse = ''.join(name_list)
print(name_reverse)

コードの解説に移ります。(1行目は割愛します)

2行目

name_list = [s for s in name]

これをprintしてみると['め', 'ん', 'た', 'い', 'こ']となります。

ここではリスト内包を使っています。

'めんたいこ'という文字列(イテレート可能オブジェクト)からひとつずつ文字を取り出し要素にしたリストになります。

3行目

name_list.reverse()

ここで使用しているreverseメソッド破壊的なメソッドです。

破壊的と言うのはメソッドを実行したオブジェクト自体を変更するという意味になります。

処理が破壊的か非破壊的かを意識するのは大切なことですので、下記に例を見てみましょう。

my_list = [1, 2, 3, 4, 5]

print(my_list.reverse())  # None
print(my_list)  # [5, 4, 3, 2, 1]

my_list.reverse()は破壊的な処理なので、my_list自体を変更し、戻り値はありません。(ココ重要)

非破壊処理で逆順リストを作りたい場合は下記方法があるでしょう。

my_list = [1, 2, 3, 4, 5]
my_reverse_list = list(reversed(my_list))

print(my_reverse_list)  # [5, 4, 3, 2, 1]
print(my_list)  # [1, 2, 3, 4, 5]

重要なのは2行目です。reversed関数my_listを渡し、それをlistでキャスト*1しています。

reversed関数list_reverseiterator型を戻り値とするため、リストに変換する必要があったわけです。

このlist_reverseiterator型のところは少々難しいかもしれないので初学者の方は後回しにしても良いと思いますが、reverseメソッドが破壊的処理かつ戻り値を持たないことは必ずおさえておきましょう。

4行目

name_reverse = ''.join(name_list)

3行目でname_list['こ', 'い', 'た', 'ん', 'め']になったため、それを文字列型のjoinメソッドでつなぎ、変数name_reverseに代入しています。

そして5行目はもう説明不要ですね。printしているだけです。

いかがでしょうか。手順が複雑でスマートではありません。

以下にPythonicな書き方をご紹介しましょう。

良いコード

name = 'めんたいこ'
print(name[::-1])

まさかの1行ですね。詳しく解説していきましょう。

シーケンス型とは

インデックスを指定して要素にアクセスできるデータ型をシーケンス型と呼びます。*2

リストなどはイメージしやすいでしょう。

my_list = [1, 3, 5, 7, 11]
print(my_list[2])  # 5

インデックスは0から始まり1,2,3...と数えていきます。my_list[2]はリストの先頭から3番目の要素ということですね。

そしてPythonでは文字列型もシーケンス型です。実際に見てみましょう。

name = 'めんたいこ'
print(name[2])  # た

Pythonにおけるシーケンス型はリスト、タプル、文字列、rangeなどがあります。

シーケンス型のインデクシング

ここではシーケンス型の要素にアクセスする様々な方法をご紹介します。

範囲指定と省略

シーケンス型[start:end:step]

シーケンス型へのアクセスは、このようなカタチとなります。

インデックス 意味
start 開始インデックス。含まれる
end 終了インデックス。含まれない(進行方向のひとつ手前まで)
step 何個飛ばしかを指定(マイナスの値でカウントダウン)

下記画像はインデックスの例ですが、インデックスにマイナスの値を用いることで、末尾から数えてアクセスすることも可能であることに注目してください。

リスト[-1]文字列[-1]も末尾の要素を返します。便利ですね。

f:id:kickbase:20161012215555j:plain

week = [s for s in '月火水木金土日']  # ['月', '火', '水', '木', '金', '土', '日']

print(week[2])         # 水
print(week[-1])        # 日
print(week[-2])        # 土

print(week[2:5])       # ['水', '木', '金']  
print(week[2:])        # ['水', '木', '金', '土', '日'] 

print(week[0::2])      # ['月', '水', '金', '日'] 
print(week[::2])       # ['月', '水', '金', '日'] 
print(week[::-2])      # ['日', '金', '水', '月'] 
print(week[3::2])      # ['木', '土']
print(week[-2:-5:-1])  # ['土', '金', '木']
print(week[::-1])      # ['日', '土', '金', '木', '水', '火', '月']

print(week)            # ['月', '火', '水', '木', '金', '土', '日']

このように要素を切り出すことをスライスと呼びます。言葉で解説するより実際にprintで出力を確認しながら確かめたほうが分かりやすいかと思います。

stepがマイナスとプラスの場合分けで開始インデックス・終了インデックスのデフォルト値(省略したときの値)が異なる点に気をつけてください。

上記最後の行でprint(week)を実行していますが、1行目の定義時と変化がないことに注目してください。 スライスは元のオブジェクトを破壊することなく新しいオブジェクトを生成するのが特徴です。うまく活用していきましょう。

文字列を逆順に取り出す

上記を踏まえ、最後にお題の復習です。

name = 'めんたいこ'
print(name[::-1])

name[::-1]の部分ではステップを逆方向(-1)、開始インデックス・終了インデックスを両方共省略したスライシングといえます。

ステップが逆方向の場合に開始インデックスを省略するとインデックス-1からスタートし、終了インデックスも省略されているため頭までスライスされています。

よって、末尾から先頭に向かってひとつずつ切り出したので逆順になったわけです。

まとめ

スライスをうまく使うことでシーケンス型から新しいオブジェクトを作り出すことができます。

イメージ通りスライシングできるまで練習してみると良いでしょう。

*1:型変換のこと

*2:順序があるコレクションと言う見方もできます