Edited at

Python中級者への道しるべ


はじめに

皆さんPythonでプログラミングしてますか?

Pythonの基本的な文法はある程度理解したけど、もう一歩詳しくなりたい、という方のために、中級的なテクニックや書き方について解説します。

この記事をマスターすれば、あなたも明日からPython中級者になれるかも?

では、始めましょう。


変数の代入について

Pythonでは、複数の変数の定義を1行で行うことが可能です。

普通に代入した場合:

x = 10

y = 12
z = 35

print(x, y, z) # 10 12 35

1行で代入する場合:

x, y, z = 10, 12, 35

print(x, y, z) # 10 12 35

上記のように、変数定義を1行で済ますことが可能です。

全ての変数の定義を1行で済ますと可読性が下がりますが、ある程度の関係性がある変数の定義は、1行で済ますと可読性が上がります。

また、同じ値を複数の変数に代入する場合は、下記のように書くことも可能です。

この代入方法ですが、ミュータブル(可変)オブジェクトの場合は、注意が必要です。

詳細は、この記事のコメント欄にshiracamusさんが丁寧に解説して頂いていますので、そちらを参照ください。

x = y = z = 10

print(x, y, z) # 10 10 10


変数の入れ替え

変数の内容を入れ替える場合は、temp変数などを利用するのが一般的かと思いますが、Pythonの場合はさらに簡単に書くことができます。

普通に変数の入れ替えを行う場合:

x, y = 10, 23

tmp = x
x = y
y = tmp
print(x, y) # 23 10

変数の入れ替えを1行で行う場合:

x, y = 10, 23

x, y = y, x
print(x, y) # 23 10

この書き方で変数の入れ替えができるなんてビックリですよね。。


変数の展開

Pythonでは、配列や辞書などを*(アスタリスク)記号を使うことで展開することが可能です。

test = [1, 2, 3]

print([test, 4, 5]) # [[1, 2, 3], 4, 5]
print([*test, 4, 5]) # [1, 2, 3, 4, 5]

上記のコードでは、アスタリスクを使用して、配列を展開して代入しています。アスタリスクを使用しない場合は、配列がネストされた状態で代入されてしまいます。

辞書の場合は、アスタリスクを2つ使います。

test = {'apple': 'red', 'remon': 'yellow'}

print({**test,'cherrie': 'pink'}) # {'apple': 'red', 'remon': 'yellow', 'cherrie': 'pink'}


真と偽の比較

変数が0であるかどうか、NoneであるかどうかをIF文で判定することも多くあるかと思います。ですが、真偽値を実際に判定するロジックを書くのは、パイソニックなコードであるとは言えません。


パイソニック(Pythonic)とは、 「Pythonらしい、シンプルで読みやすいコードの書き方」ということ


普通に0を判定する場合:

test = 0

if test == 0:
print('NG')
else:
print('OK')

Pythonらしい条件の書き方:

test = 0

if not test:
print('NG')
else:
print('OK')

パイソニックなコードでは、偽と判定されるものが事前に決まっているので、決まっているものに関しては明示的に判断ロジックを書きません。

偽と判断されるものには下記があります。

組み込み型 真理値判定


  • 偽であると定義されている定数: None と False

  • 数値型におけるゼロ: 0, 0.0, 0j, Decimal(0), Fraction(0, 1)

  • 空のシーケンスまたはコレクション: '', (), [], {}, set(), range(0)


Noneとの比較、is演算子

Pythonの比較演算子には==isというものがあり、両方とも比較する変数が同じかどうかを確認するために利用できます。

ですが、両方の意味を正確に理解しておかないと、思わぬバグを生んでしまう可能性がありますので、注意が必要です。

Q. Noneとの比較はisを利用すべきか?

A. はい。ですが、よほどのことがない限りは、==を使用した場合と同じ結果が出ます

まず、is演算子の動作について説明します。is演算子は、比較する変数のIDが同じかどうかを確認します。変数のIDとは何かというと、変数の参照先(ポインタ)のことです。

例えば、参照先の値が同じであっても、参照先(ポインタ)が異なっている場合は、is演算子では偽と判定されます。

変数の参照先が同じ場合:

test = 'my name is taro' + '.'

test2 = test
print(id(test)) # 140643210401160
print(id(test2)) # 140643210401160

上記の場合は、test2にtestという変数を代入しているので、testとtest2は同じ参照先を指します。なので、is演算子で比較した場合は、testとtest2は同じものとして判断されます。

同様にNoneのIDについても確認してみましょう(IDは環境によって異なる可能性があります)

test = None

test2 = None
print(id(test)) # 9107296
print(id(test2)) # 9107296

Noneはどこで定義されようとも、同じオブジェクトを指します。なので、Noneかどうかを比較する場合は、is演算子を利用すると、本当に比較対象がNoneかどうかを確認することができます。==演算子の場合は、演算子オーバーロードという機能を使い、演算子自体の挙動を変更することができるため、正確にNoneかどうかの判断ができない可能性があります。(演算子オーバーロードをしない場合は、is演算子でも==演算子でもNoneの比較は同じ結果となります)

Q. None以外の比較にisを使うべきではないか?

A. はい。None以外の比較にisを使うと、思わぬバグを発生させる可能性があります。

特に、文字列の変数の比較で、想定外の挙動をすることがありますので、is演算子を多用することは危険です。

例えば、以下のようなコードを考えてみます。

test = 'my name is taro' + '.'

test2 = 'my name is taro' + '.'
print(id(test)) # 140196053288256
print(id(test2)) # 140196053288328

print(test) # my name is taro.
print(test2) # my name is taro.

if test is test2:
print('same')
else:
# こちらが出力される
print('not same')

if test == test2:
# こちらが出力される
print('same')
else:
print('not same')

testとtest2には同じ内容の文字列が入っているにもかかわらず、参照先が異なります。なので、is演算子で比較すると、二つの変数は別物として判断され、==演算子だと同じものと判断されます。==演算子は、変数内の内容を確認して判断するため、is演算子と比べると処理速度が遅いというデメリットはありますが、None以外の比較でis演算子の利用は混乱のもとですので、おススメしません。

ですが例外的に、is演算子を利用すると、コードがきれいに書ける場合もあります。(コメント欄のrighteousさんのコメントを参照ください)


短絡評価

他の言語には短絡評価という評価法がありますが、Pythonにもあります。

短絡評価をうまく利用すると、IF文のロジックをスマートに書くことが可能です。

例えば、下記のようなコードを考えてみます。

test = {'age': 20}

if 'age' in test:
if test['age'] >= 20:
print('大人です')

まず、test変数にageという属性が存在しているかどうかを最初のIF文でチェックしています。これは、存在しない属性にアクセスした場合に、エラーになることを防ぐためです。二つ目のIF文でage属性の値が20以上かどうかを確認しています。

では、短絡評価を使って書き直してみましょう。

test = {'age': 20}

if 'age' in test and test['age'] >= 20:
print('大人です')

単純にIF文をand演算子でつなげただけ、と思われるかもしれません。ですが、and演算子の左右を入れ替えると、意味する挙動が変わります。

短絡評価(and演算子とor演算子)の正確な挙動としては、最初の条件が真(or演算子)または、偽(and演算子)の場合は、次の処理を実施しない、というものになります。

上記の場合は、'age' in testの評価結果が偽の場合は、test['age'] >= 20が評価されません。なので、test変数にage属性が存在する場合のみ、test['age'] >= 20が評価される、という条件を達成することが可能となります。


比較演算子の組み合わせ

数値の比較を下記のように行うことは多くあるかと思います。

x = 10

if x >= 0 and x <= 20:
print('OK')
else:
print('NG')

ですが、Pythonの場合は、下記のように書くことが可能です。

x = 10

if 0 <= x <= 20:
print('OK')
else:
print('NG')


三項演算子

変数に値を代入する場合に、条件によって代入する値を変えたい、という状況は多くあるかと思います。

例えば、変数がNoneの場合や、特定のキーの場合などです。

その際に、三項演算子を利用して変数の代入を行うと、スマートに記述することができます。

Pythonにおける三項演算子の記述方法は下記となります。

(変数) = (条件が真のときの値) if (条件式) else (条件が偽のときの値) 

三項演算子を使用しない場合のコード

test = 15

if test >= 0:
message = '正の数または0です'
else:
message = '負の数です'
print(message)

三項演算子を使用した場合のコード

test = 15

message = '正の数または0です' if test >= 0 else '負の数です'
print(message)

上記のように、IF文の部分を1行で記述することが可能となるため、代入している部分が1行となり、コードの可読性が上がります。注意点としては、三項演算子において、複数条件で分岐する(elifを使いたい場合)は、三項演算子をネストする必要があります。ですが、三項演算子のネストは、コードの可読性を著しく低下させますので、おススメしません。

複雑な条件になる場合は、三項演算子ではなく、IF文で条件分岐することもあります。


文字列中に変数の値を埋め込む

Pythonで文字列内に変数の値を埋め込む方法としては、4通りあるかと思います。


  • 文字列を+演算子で連結

  • f文字列を使う

  • format関数を使う

  • %を使う方法

name = 'taro'

# 文字列を+演算子で連結
print('私は' + name + 'です')

# f文字列を使う
print(f'私は{name}です')

# format関数を使う
print('私は{}です'.format(name))

# %を使う方法
print('私は%sです' % name)

文字列を+演算子で連結の方法は、コードの可読性が低く、コードの編集容易性も低いのでお勧めしません。(連結に用いている+演算子はそもそも意味がないし、新しく追加するときも、+演算子を書かなければならない)

f文字列を使うformat関数を使うは正直どちらでもいいのかなと、思っています。ですが、プロジェクトのコード内で両方の記述が使用されているのは望ましくありません。どちらかに統一すべきであると考えています。


配列のFOR文

配列をFOR文で処理することは多くあるかと思います。

その際に使う便利なテクニックを紹介します。


FOR EACHを使う

普通にFOR文を使う場合:

test = [1, 10, 15, 16]

for index in range(len(test)):
print(test[index], end=' ') # 1 10 15 16

FOR EACHを使う場合:

test = [1, 10, 15, 16]

for value in test:
print(value, end=' ') # 1 10 15 16

FOR EACHを使うと、FOR文でループする対象が、インデックスから配列の要素になります。

なので、配列の要素を一つずつ処理する、という要件の場合は、FOR EACHの書き方を使用した方が、コードの可読性が向上します。

また、FOR EACHの中でもインデックスを使用したい場合もよく遭遇します。

その場合は、下記のようにすると配列の要素とインデックスの両方を取得できるため、スマートに書くことができます。

test = [1, 10, 15, 16]

for index, value in enumerate(test):
# index: 0 value: 1 , index: 1 value: 10 , index: 2 value: 15 , index: 3 value: 16 ,
print('index:', index, 'value:', value, ',', end=' ')


逆順で配列の要素を取得する

あまり遭遇することはありませんが、配列の要素を逆順で取得したい場合についても言及します。

普通に逆順で配列の要素を取得する場合:

test = [1, 10, 15, 16]

for i in range(len(test)-1, -1, -1):
print(test[i], end=' ') # 16 15 10 1

reversed関数を使う場合:

test = [1, 10, 15, 16]

for value in reversed(test):
print(value, end=' ') # 16 15 10 1

reversed関数を使う方法以外にも、スライスを使って配列の要素を逆順にする方法もあります。詳細はc-yanさんのコメントをご参照ください。

Pythonにおいてスマートにプログラミングを行うためには、組み込み関数についてある程度理解しておくことが重要です。

本記事内で利用している、range関数やlen関数、reversed関数などについて書かれています。

組み込み関数


辞書のFOR文

辞書をFOR文で処理することも多いかと思います。

辞書のFOR文の書き方もたくさんあるのですが、基本的にはitems関数を使う方法を覚えておけば問題ありません。

普通に辞書をFOR文で処理する場合:

test = {'apple': 'red', 'remon': 'yellow'}

for key in test:
print('key:', key, 'value:', test[key], ',', end=' ') # key: apple value: red , key: remon value: yellow ,

items関数を使う場合:

test = {'apple': 'red', 'remon': 'yellow'}

for key,value in test.items():
print('key:', key, 'value:', value, ',', end=' ')


配列の重複を削除する

遭遇頻度はあまりないのですが、何度か遭遇するかと思います。

配列の中身をチェックして、自前で重複を排除する関数を作ることも可能ですが、もっと便利に重複を削除することができます。

test = [1, 2, 3, 3, 5, 1]

test2 = ['a', 'b', 'a', 'd', 'aa']

print(list(set(test))) # [1, 2, 3, 5]
print(list(set(test2))) # ['a', 'd', 'b', 'aa']

ミソは、Set型に変換することで自動的に重複を排除できる点ですね。Set型に変換後、再度リスト型に戻しています。


配列のソート

配列のソートを自前で実装することはありません。

既存の関数を使ってソートを実現することが可能です。

test = [10, 25, 4, 100, 69]

print(sorted(test)) # [4, 10, 25, 69, 100]
print(sorted(test, reverse=True)) # [100, 69, 25, 10, 4]

ソートでキーとしたいものを明示的に指定することも可能です。

test = [[10, 25], [4, 100], [69, 71]]

print(sorted(test, key=lambda item: item[1])) # [[10, 25], [69, 71], [4, 100]]

上記コードでは、配列内の2番目の要素をキーとしてソートしています。


sorted関数のソートはあまり速度が速くありません。速度面で問題が発生した場合は、numpyなどのライブラリを利用することをお勧めします。



辞書配列のソート

辞書配列をソートしたい場合もあるかと思います。

その場合は、下記のように明示的にキーを指定することで、対応可能です。

test = [{'name': 'taro', 'age': 18},{'name': 'jiro', 'age': 12}]

# [{'name': 'jiro', 'age': 12}, {'name': 'taro', 'age': 18}]
print(sorted(test, key=lambda item: item['age']))


リスト内包表記

リスト内包表記は非常に便利で、処理速度の速い記述方法になりますので、ぜひ習得しておきたい書き方になります。

リスト内包表記は、下記のように記述します。

[(配列に追加する値) for (イテレータオブジェクトの一つの要素) in (イテレータオブジェクト)]

普通に偶数のリストを作成する場合:

test = []

for index in range(5):
test.append(index * 2)
print(test) # [0, 2, 4, 6, 8]

リスト内包表記でリストを作成する場合:

# [0, 2, 4, 6, 8]

print([index * 2 for index in range(5)])

なんと1行で書くことができました。

なお、IF文(後置IF)を用いて条件を絞り込むことも可能です。

# 10の倍数を含まない偶数配列を作成する

# [2, 4, 6, 8, 12, 14, 16, 18, 22, 24, 26, 28, 32, 34, 36, 38]
print([index * 2 for index in range(20) if (index * 2) % 10])

リスト内包表記では、IF,ELSEで条件を分岐させることも可能です。

また、辞書にも同様に内包表記がありますが、今回は省略します。


all関数とany関数

Pythonには、すべての要素がTrueかどうかを判定するall関数と、いずれかの要素がTrueかどうかを判定するany関数があります。

all関数とany関数をうまく使うことで、スマートなプログラムを書くことができます。

例えば、配列の中身がすべて0以上かどうかを判断するプログラムを書く場合を考えてみます。

普通に実装する場合:

test = [-1, 5, 2]

judge = True
for value in test:
if value < 0:
judge = False
result = '中身はすべて0以上です' if judge else '負の数が含まれています'
print(result)

all関数を利用した場合:

test = [-1, 5, 2]

judge = all(map(lambda item: item >= 0, test))
result = '中身はすべて0以上です' if judge else '負の数が含まれています'
print(result) # 負の数が含まれています

3行を1行で書くことができました。

では、プログラムの解説に移ります。

まず、map関数は、配列のそれぞれの要素に関数を適用することができる関数です。

map(lambda item: item >= 0, test)を適用した段階で、それぞれの要素が0以上であるかを判断したマップオブジェクトが新規に作成されます。つまり、False, True, Trueのマップオブジェクトが作成されるということです。

そして、all関数にて、すべての要素がTrueかどうかを判定している、という流れになります。

一点注意点として、all関数は空の配列をTrueとして返しますので、配列が空であるかどうかは、別途分岐を書く必要があります。(marmalade_boyさんコメントありがとうございます)


iterable の全ての要素が真ならば (もしくは iterable が空ならば) True を返します


組み込み関数 all


コンテキストマネージャーとwithブロック

コンテキストマネージャーの詳細な説明は実施しませんが、withブロックについてはある程度知っておくことをお勧めします。

例えば、ファイルをオープンする場合を考えてみます。

ファイルの処理中に例外が発生してファイルがクローズされない状況に対応するために、try節で囲み、finallyでファイルをクローズします。

fd = open("filename", "r")

try:
file_content = file.read()
raise Exception('test')
finally:
fd.close()

ですが、毎回try節を書くのも、ファイルをクローズするのも面倒なので、withブロックを使います。

with open("filename", "r") as fd:

file_content = file.read()
raise Exception('test')

上記のように書くことで、withブロック内で例外が発生した場合でも、自動的にファイルのクローズが実施されます。


最後に

長文を読んで頂きありがとうございます。

Pythonはまだまだ奥が深い言語ですので、上記で紹介したものはほんの一部になります。

ですが、少しでも皆さんのお役になれば幸いです。

では(^^♪