Courses
近年、Pythonのコーディングスキルの需要は高まっています。Pythonのプログラミング力を伸ばすために、コードを改善するのに役立つクールなPythonの小ワザを30個まとめました。今後30日間、毎日1つずつ学び、コードを一流に保つためにPythonのベストプラクティスもぜひご確認ください。
Pythonにまだ自信がない場合は、Pythonスキルトラックで腕を磨くこともできます。
シーケンスとデータ構造の小ワザ
#1 スライス
a = "Hello World!"
print(a[::-1])
"""
!dlroW olleH
"""
スライスは、インデックスに基づいてシーケンスの一部にアクセスできるPythonの機能です。インデックスとはシーケンス内の要素の位置のことです。シーケンス型がミュータブルであれば、スライスを使ってデータの抽出と変更ができます。
注意: イミュータブルなシーケンスにもスライスは使えますが、スライスを変更しようとするとTypeErrorが発生します。
スライスの書式はsequence[start:stop:step]です。start、stop、stepを指定しない場合は既定値が使われます。既定値は次のとおりです。
- 「start」は0
- 「stop」はシーケンスの長さ
- 「step」は1
sequence[start:stop]の形式では、返される要素は開始インデックスからstop - 1(stopインデックスは含まない)までです。
負のインデックスを渡すこともでき、シーケンスを逆順にする際などに使えます。例えば、4要素のリストでは0番目のインデックスは-4でもあり、最後のインデックスは-1でもあります。上の例では、この性質をstepに適用しています。結果として、シーケンスの末尾から0番目まで逆順に文字列が出力されました。
#2 インプレース交換/同時代入
a = 10
b = 5
print(f"First: {a, b}")
"""
First: (10, 5)
"""
a, b = b, a + 2
print(f"Second: {a, b}")
"""
Second: (5, 12)
"""
bの値が12ではなく7になると思ったなら、インプレース交換の落とし穴にはまっています。
Pythonでは、自動アンパッキングにより、1回の代入でイテラブルを複数の変数に展開できます。例えば:
a, b, c = [1, 2, 3]
print(a)
print(b)
print(c)
"""
1
2
3
"""
*を使って複数の値を1つの変数に集めることもできます。これはパッキングと呼ばれるPythonの小ワザです。例を示します。
a, *b = 1, 2, 3
print(a, b)
"""
1 [2, 3]
"""
自動パッキングとアンパッキングを組み合わせることで、同時代入という手法が使えます。これは、複数の値を複数の変数に一度に割り当てるのに役立ちます。
#3 リスト vs. タプル
import sys
a = [1, 2, 3, 4, 5]
b = (1, 2, 3, 4, 5)
print(f"List size: {sys.getsizeof(a)} bytes")
print(f"Tuple size: {sys.getsizeof(b)} bytes")
"""
List size: 52 bytes
Tuple size: 40 bytes
"""
多くのPythonプログラマはリストには馴染みがありますが、タプルはそうでもありません。どちらもイテラブルで、インデックスアクセスができ、異なる型を混在して格納できます。しかし、タプルの方が望ましい場面もあります。
まず、リストはミュータブルで、自由に変更できます。
a = [1,2,3,4,5]
a[2] = 8
print(a)
"""
[1,2,8,4,5]
"""
一方、タプルはイミュータブルで、変更しようとするとTypeErrorが発生します。
このため、タプルはメモリ効率に優れています。Pythonはデータに必要なメモリ量を正確に割り当てられるからです。対してリストでは、後で拡張される可能性に備えて余分なメモリを確保します。これは動的メモリアロケーションと呼ばれます。
要点:データを変更したくない状況では、メモリ面からもタプルをリストより優先すべきです。タプルはリストより高速でもあります。
このチュートリアルでPythonのデータ構造についてさらに学んでください。
#4 ジェネレータ
a = [x * 2 for x in range(10)]
b = (x * 2 for x in range(10))
print(a)
print(b)
"""
[0, 2, 4, 6, 8, 10, 12, 14, 16, 18]
<generator object <genexpr> at 0x7f61f8808b50>
"""
リスト内包表記は、別のイテラブルからリストを作るPython的な方法で、forループよりもずっと高速です。では、角括弧[]を丸括弧()に変えてしまうとどうなるでしょうか?ジェネレータオブジェクトになります。
Pythonでは、内包表記のロジックを丸括弧で囲むと、ジェネレータオブジェクトが作られます。ジェネレータは特別なイテラブルで、リストと違い要素を保持しません。各要素を順に生成する手順と、反復の現在の状態だけを持ちます。
各要素は遅延評価という手法で、要求されたときに初めて生成されます。このPythonの小ワザの主な利点は、列全体を一度に構築しないため、メモリ使用量が少ないことです。
#5 エイリアシング
a = [1, 2, 3, 4 ,5]
b = a
# Change the 4th index in b
b[4] = 7
print(id(a))
print(id(b))
print(a) # Remember we did not explicitly make changes to a.
"""
15136008
15136008
[1, 2, 3, 4, 7]
"""
Pythonはオブジェクト指向言語で、すべてはオブジェクトです。したがって、オブジェクトを識別子に代入することは、そのオブジェクトへの参照を作ることになります。
一方の識別子をもう一方に代入すると、同じオブジェクトを参照する識別子が2つできます。これをエイリアシングと呼びます。一方のエイリアスでの変更はもう一方にも影響します。望ましい場合もありますが、多くは不意を突かれます。
回避策として、ミュータブルなオブジェクトではエイリアシングを避けるか、参照ではなくクローン(複製)を作る方法があります。
もっとも簡単なクローンの作り方は、スライスを利用する方法です。
b = a[:]
これで、bという識別子に新しいリストオブジェクトへの参照が作られます。
ほかにも、別の識別子に代入する際にlist(a)を呼ぶ、copy()メソッドを使う、などの手があります。
#6 not演算子
a = []
print(not a)
"""
True
"""
データ構造が空かどうかを調べる最も手軽な方法は、not演算子を使うことです。Pythonの組み込みnotは論理演算子で、式が真でなければTrue、そうでなければFalseを返します。つまり、真偽値やオブジェクトの真理値を反転します。
if文の中で次のように使うこともあります。
if not a:
# do something...
aがTrueのとき、notはFalseを返し、その逆も同様です。
最初は少し混乱するかもしれませんが、試してみてください。
文字列と出力の小ワザ
#7 f文字列
first_name = "John"
age = 19
print(f"Hi, I'm {first_name} and I'm {age} years old!")
"""
Hi, I'm John and I'm 19 years old!
"""
ときどき、文字列オブジェクトをフォーマットする必要があります。Python 3.6では、この処理を簡単にするf文字列という便利な機能が導入されました。新機能の良さを理解するには、以前のフォーマット方法を知っておくと役立ちます。
以前のフォーマット方法は次のとおりです。
first_name = "John"
age = 19
print("Hi, I'm {} and I'm {} years old!".format(first_name, age))
"""
Hi, I'm John and I'm 19 years old!
"""
本質的に、新しい書き方はより高速で、読みやすく、簡潔で、間違いにくい方法です。
f文字列のもう1つの用途は、変数名と値を一緒に出力することです。これはPython 3.8で導入されました。
x = 10
y = 20
print(f"{x = }, {y = }")
"""
x = 10, y = 20
"""
Pythonのf文字列フォーマットのチュートリアルもご覧ください。
#8 print()関数のendパラメータ
languages = ["english", "french", "spanish", "german", "twi"]
print(' '.join(languages))
"""
english french spanish german twi
"""
print文を、オプション引数を何も指定せずに使うのはよくあることです。そのため、出力をある程度制御できることを知らないPythonistaも少なくありません。
変更できるオプション引数の1つがendです。endは、print呼び出しの末尾に何を表示するかを指定します。
endの既定値は"\n"で、新しい行を開始するという意味です。上のコードではスペースに変更しています。そのため、リストの要素が同じ行に並んで出力されます。
#9 タプルに追記する
a = (1, 2, [1, 2, 3])
a[2].append(4)
print(a)
"""
(1, 2, [1, 2, 3, 4])
"""
すでに見たように、タプルはイミュータブルです(小ワザ#3 リスト vs. タプル)。タプルの状態を変更しようとするとTypeErrorになります。しかし、タプルを「変更できないオブジェクトへの束縛を持つ名前の並び」と考えると、見方が変わります。
このタプルの最初の2要素は整数で、イミュータブルです。最後の要素はリストで、Pythonではミュータブルです。
このリストを「変更できないオブジェクトへの束縛を持つ、並びの中の単なる別の名前」とみなすと、タプルの内側からでもリスト自体は変更できることがわかります。
実務でこれを推奨するか?おそらくしませんが、知っておくと便利な知識です。
#10 辞書のマージ
a = {"a": 1, "b": 2}
b = {"c": 3, "d": 4}
a_and_b = a | b
print(a_and_b)
"""
{"a": 1, "b": 2, "c": 3, "d": 4}
"""
Python 3.9以降では、|(ビット単位のOR)で辞書をマージできます。この小ワザ自体に多く語ることはありませんが、ずっと読みやすい書き方です!
コードスタイルと構文の小ワザ
#11 三項演算子/条件式
condition = True
name = "John" if condition else "Doe"
print(name)
"""
John
"""
上のコードで使っているのは三項演算子(条件式)です。条件がTrueかFalseかに基づいて評価を行います。
同じことは次のようにも書けます。
condition = True
if condition:
name = "John"
else:
name = "Doe"
print(name)
"""
John
"""
どちらも同じ結果ですが、三項演算子を使うと短く明快に書けます。より「Pythonic」な書き方です。
#12 リストの重複を削除
a = [1, 1, 2, 3, 4, 5, 5, 5, 6, 7, 2, 2]
print(list(set(a)))
"""
[1, 2, 3, 4, 5, 6, 7]
"""
リストの重複要素を取り除く最も簡単な方法は、セットに変換してから(必要なら)リストに戻すことです。
ミュータビリティの観点では、セットとリストは似ています。どちらも要素の追加・削除ができますが、性質は大きく異なります。
リストは順序付きで0始まりのインデックスを持つミュータブルな構造です。セットは順序がなくインデックスもありません。セットの要素はイミュータブルな型でなければならず(セット自体はミュータブル)、インデックスで取得したり要素を直接変更しようとするとエラーになります。
もう1つの大きな違いは、セットは重複を許さないことです。これにより、リストから重複要素を取り除けました。
#13 単独のアンダースコア
>>> print(_)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
>>> 1 + 2
3
>>> print(_)
3
アンダースコア(_)はPythonで有効な識別子なので、オブジェクトの参照に使えます。しかし、アンダースコアにはもう1つの役割があり、直前の評価結果を保持します。
ドキュメントには「対話型インタプリタは、直前の評価結果を変数_で利用可能にする(printのような組み込み関数と同様にbuiltinsモジュールに保存される)」とあります。
最初の行でアンダースコアに何も代入していなかったためエラーになりました。しかし、1 + 2の結果を計算したとき、対話型インタプリタはその結果を_に保存しました。
#14 無視したい値にアンダースコア
for _ in range(100):
print("The index doesn't matter")
"""
The index doesn't matter
The index doesn't matter
...
"""
小ワザ#13で見たように、アンダースコア(_)は直前の評価結果にも使われますが、それだけではありません。
プログラム内で不要なオブジェクトや後で使わないオブジェクトを表すためにも使えます。識別子名を使って何もしないと、リンティング時にF841エラー(代入されたが未使用のローカル変数名)が出ますが、アンダースコアなら慣例として問題ありません。
#15 末尾のアンダースコア
list_ = [0, 1, 2, 3, 4]
global_ = "Hi there"
ここまでのアンダースコアの話の続きとして、Pythonのキーワードとの衝突を避ける目的でも使われます。
PEP 8では、末尾のアンダースコア(_)は「Pythonのキーワードとの衝突を避けるため、慣例として用いる」とされています。また「省略形やスペルの崩しより、末尾にアンダースコアを付ける方がよい。したがってlist_はlstより望ましい」とも述べられています。
#16 先頭のアンダースコア
class Example:
def __init__(self):
self._internal = 2
self.external = 20
経験豊富なPythonプログラマは、識別子やメソッド名の先頭にアンダースコアを付けることがよくあります。理由があります。
識別子やメソッドの先頭にアンダースコアを付けるのは、「この変数やメソッドは内部用である」という意味合いです。PEP 8で定義された、他のプログラマへの注意書きのようなもので、Pythonが強制するものではありません。したがって、その指標としての効力は弱いです。
Javaとは異なり、Pythonには厳密なprivate/publicの区別はありません。つまり、コミュニティの合意に基づく意味合いであり、プログラムの挙動には影響しません。
#17 桁区切りのアンダースコア
アンダースコアの最後の小ワザです。ここまで3つの使い方を見てきましたが、Pythonにおけるアンダースコアの役割のチュートリアルでさらに学べます。
number = 1_500_000
print(number)
"""
15000000
"""
もう1つの使い方は、整数・浮動小数点・複素数リテラルの桁区切りの視覚的なセパレータとして使う方法です(Python 3.6で導入)。
長い数値や、意味的に区切った方が読みやすい値の可読性向上が目的です。詳しくはPEP 515をご覧ください。
コードスタイルと構文の小ワザ
#18 __name__ == "__main__"
if __name__ == "__main__":
print("Read on to understand what is going on when you do this.")
"""
print("Read on to understand what is going on when you do this.")
"""
多くのPythonプログラムでこの構文を目にしたことがあるはずです。Pythonは特別な名前"__main__"を使い、実行中のPythonファイルがメインプログラムである場合、これを__name__という識別子に設定します。
このモジュール(Pythonファイル)を別のモジュールからインポートしてそのファイルを実行すると、コード中の条件式は偽になります。別のモジュールからインポートすると、__name__はモジュール名(ファイル名)に設定されるからです。
#19 setdefaultメソッド
import pprint
text = "It's the first of April. It's still cold in the UK. But I'm going to the museum so it should be a wonderful day"
counts = {}
for word in text.split():
counts.setdefault(word, 0)
counts[word] += 1
pprint.pprint(counts)
"""
{'April.': 1,
'But': 1,
"I'm": 1,
"It's": 2,
'UK.': 1,
'a': 1,
'be': 1,
'cold': 1,
'day': 1,
'first': 1,
'going': 1,
'in': 1,
'it': 1,
'museum': 1,
'of': 1,
'should': 1,
'so': 1,
'still': 1,
'the': 3,
'to': 1,
'wonderful': 1}
"""
辞書のさまざまなキーに対して値を設定したいことがあります。例えばコーパス内の単語の出現回数を数えるときです。一般的なやり方は次のとおりです。
- 辞書にキーが存在するか確認する
- 存在すれば値を1増やす
- 存在しなければ追加して1を設定する
コードにすると次のようになります。
counts = {}
for word in text.split():
if word in counts:
counts[word] += 1
else:
counts[word] = 1
これをより簡潔にするには、辞書オブジェクトのsetdefault()メソッドを使います。
メソッドの第1引数は確認したいキー、第2引数はキーが存在しない場合に設定する値です。キーが存在する場合はその値を返すだけで、変更はされません。
プログラム構造の小ワザ
#20 正規表現でのマッチング
import re
number = re.compile(r"(0)?(\+44)?\d{10}")
num_1 = number.search("My number is +447999999999")
num_2 = number.search("My number is 07999999999")
print(num_1.group())
print(num_2.group())
"""
'+447999999999'
'07999999999'
"""
正規表現は、検索するテキストのパターンを指定できます。多くの人はCTRL + F(Windows)で検索できることを知っていますが、探す文字列が正確にわからない場合はどう探せばよいでしょうか?答えはパターンで探すことです。
例えば、英国の電話番号は似たパターンに従います。先頭が0で続けて10桁、あるいは0の代わりに+44で続けて10桁(後者は国際表記)です。
正規表現は大きな時間節約になります。画像の例をコードでルール化して拾おうとすると、正規表現なしでは10行以上かかるかもしれません。
コードを書かなくても、正規表現の仕組みを理解することは重要です。多くのテキストエディタやワープロには、正規表現を使った検索・置換機能があります。
#21 正規表現のパイプ
import re
heros = re.compile(r"Super(man|woman|human)")
h1 = heros.search("This will find Superman")
h2 = heros.search("This will find Superwoman")
h3 = heros.search("This will find Superhuman")
print(h1.group())
print(h2.group())
print(h3.group())
"""
Superman
Superwoman
Superhuman
"""
正規表現にはパイプ(|)という特殊文字があり、複数の表現のうちいずれかにマッチできます。どこでも使え、似たパターンが複数あるときに非常に便利です。
例えば「Superman」「Superwoman」「Superhuman」は同じ接頭辞を持ちます。そこでパイプを使って共通部分を保ち、異なる部分だけを列挙できます。これもまた、時間の節約につながります。
注意点: 同じテキスト内に複数の候補がある場合、最初にマッチしたものが返されます。例:「An example text containing Superwoman, Superman, Superhuman」ではSuperwomanが返ります。
#22 print()関数のsepパラメータ
day = "04"
month = "10"
year = "2022"
print(day, month, year)
print(day, month, year, sep = "")
print(day, month, year, sep = ".")
"""
04 10 2022
04/10/2022
04.10.2022
"""
print()関数の本来の力を知らないPythonプログラマは意外と多いものです。最初のプログラムが「Hello World」だったなら、print()は最初に学んだ組み込み関数のひとつでしょう。画面に整形されたメッセージを表示するのに使いますが、print()にはもっと多くの機能があります。
上のコードでは、整形されたメッセージの表示方法をいくつか示しています。sepは、print()関数のオプション引数で、複数のオブジェクトを渡したときの区切りを指定できます。
既定ではスペース区切りですが、ここではsepを""にしたものと、"."にしたものを示しています。
#23 ラムダ関数
def square(num:int) -> int:
return num ** 2
print(f"Function call: {square(4)}")
"""
Function call: 16
"""
square_lambda = lambda x: x**2
print(f"Lambda function: {square_lambda(4)}")
"""
Lambda functional: 16
"""
ラムダ関数は、Pythonで中級〜上級の表現力を与えてくれます。Intermediate Pythonのコースで学べます。一見難しく見えますが、実はとてもシンプルです。
例では引数を1つだけにしましたが、複数にすることも可能です。
square = lambda a, b: a ** b
print(f"Lambda function: {square(4, 2)}")
"""
16
"""
要するに、lambdaキーワードで小さく制限された無名関数を1行で定義できます。defで宣言した通常の関数と同様に動作しますが、名前を持たない点が異なります。
#24 swapcaseメソッド
string = "SoMe RaNDoM sTriNg"
print(string.swapcase())
"""
sOmE rAndOm StRInG
"""
swapcase()メソッドは、1行で大文字を小文字に、小文字を大文字に入れ替えます。多くの用途があるわけではありませんが、知っておくと便利です。
#25 isalnumメソッド
password = "ABCabc123"
print(password.isalnum())
"""
True
"""
ユーザーにパスワードを入力してもらうプログラムを作るとして、英数字の組み合わせであることを求めたいとします。これは文字列インスタンスでisalnum()を呼べば1行で判定できます。
このメソッドは、すべての文字がアルファベット(A-Za-z)または数字(0-9)かをチェックします。空白や記号(!#%$&? など)が含まれるとFalseになります。
#26 例外処理
def get_ration(x:int, y:int) -> int:
try:
ratio = x/y
except ZeroDivisionError:
y = y + 1
ratio = x/y
return ratio
print(get_ration(x=400, y=0))
"""
400.0
"""
Pythonのプログラムはエラーに遭遇すると終了します。
しかし、エンドユーザーがコードを操作しているときなど、必ずしもそれが望ましくない場合があります。そんなときにプログラムが途中で止まったら困ります。
例外への対処法にはいくつかの考え方があります。多くのPythonプログラマは「許可よりも赦しを乞う方が易しい(EAFP)」という考え方を受け入れています。つまり、あらゆる例外ケースを未然に防ぐのではなく、例外が発生したときにそれを処理できる周辺コンテキストを用意して捕捉する、ということです。
ただし、これは問題発生後に対処する仕組みがある場合にのみ当てはまります。
#27 リストの差分を求める
list_1 = [1, 3, 5, 7, 8]
list_2 = [1, 2, 3, 4, 5, 6, 7, 8, 9]
solution_1 = list(set(list_2) - set(list_1))
solution_2 = list(set(list_1) ^ set(list_2))
solution_3 = list(set(list_1).symmetric_difference(set(list_2)))
print(f"Solution 1: {solution_1}")
print(f"Solution 2: {solution_2}")
print(f"Solution 3: {solution_3}")
"""
Solution 1: [9, 2, 4, 6]
Solution 2: [2, 4, 6, 9]
Solution 3: [2, 4, 6, 9]
"""
Pythonで2つのリストの差分を比較する方法を3つ紹介します。
注意: list_1がlist_2の部分集合だと確実にわかっていない限り、解法1は他の2つと同じにはなりません。
#28 argsとkwargs
def some_function(*args, **kwargs):
print(f"Args: {args}")
print(f"Kwargs: {kwargs}")
some_function(1, 2, 3, a=4, b=5, c=6)
"""
Args: (1, 2, 3)
Kwargs: {'a': 4, 'b': 5, 'c': 6}
"""
関数が受け取る引数の数が事前にわからないとき、*argsと**kwargsを使います。
*argsはキーワードなしの可変長引数をタプルとして受け取り、**kwargsは任意個のキーワード引数を辞書として受け取ります。
実のところ、特別なのはargsやkwargsという単語ではなく、アスタリスク(*)です。つまり、後ろの名前は何でもかまいませんが、argsとkwargsを使うのが一般的で、Python開発者の間での慣習です。
#29 エリプシス
print(...)
"""
Ellipsis
"""
def some_function():
...
# Alternative solution
def another_function():
pass
エリプシスは、3つのドット(...)またはオブジェクト名(Ellipsis)で呼び出せるPythonオブジェクトです。
最も有名な用途は、NumPyでの多次元配列のアクセスやスライスです。例えば次のように使います。
import numpy as np
arr = np.array([[2,3], [1,2], [9,8]])
print(arr[...,0])
"""
[2 1 9]
"""
print(arr[...])
"""
[[2 3]
[1 2]
[9 8]]
"""
もう1つの用途は、未実装の関数内のプレースホルダとして使うことです。
つまり、Ellipsis、...、あるいはpassのいずれでも有効です。
#30 リスト内包表記
even_numbers = [x for x in range(10) if x % 2 == 0 and x != 0]
print(even_numbers)
"""
[2, 4, 6, 8]
"""
最後の小ワザはリスト内包表記です。別のシーケンスからリストを生成するエレガントな方法で、上のコードのように複雑なロジックやフィルタリングも可能です。
同じ目的は別のやり方でも達成できます。例えば、次のようにラムダ関数を使う方法です。
even_numbers = list(filter(lambda x: x % 2 ==0 and x != 0, range(10)))
print(even_numbers)
"""
[0, 2, 4, 6, 8]
"""
しかし、多くのPythonistaは、こちらはリスト内包表記に比べて可読性が低いと主張するでしょう。
FAQs
初心者にとって最も役立つPythonの小ワザは何ですか?
初心者にとって最も即効性があるのはf文字列でしょう。従来の.format()と比べて、文字列フォーマットが速く、読みやすく、ミスもしにくくなります。
Pythonで2つの変数を最速で入れ替える方法は?
同時代入を使います:a, b = b, a。一時変数は不要です。Pythonは右辺を先に完全評価してから代入します。
1行で2つの辞書をマージするには?
Python 3.9以降では|演算子を使います:merged = dict_a | dict_b。旧バージョンでは{**dict_a, **dict_b}を使ってください。
*argsと**kwargsの違いは?
*argsは余分な位置引数をタプルに集約します。**kwargsは余分なキーワード引数を辞書に集約します。肝は単語ではなく*と**の演算子にあります。名前は任意ですが、慣例としてこの名称が使われます。
Pythonでリストが空かどうかを確認するには?
not演算子を使います:if not my_list:。len(my_list) == 0をチェックするよりPythonicです。
リスト内包表記とジェネレータの違いは?
リスト内包表記(角括弧)は、リスト全体を一度にメモリ上に構築します。ジェネレータ(丸括弧)は必要に応じて1つずつ値を生成するため、大きなシーケンスでははるかに少ないメモリで済みます。
三項演算子はいつ使うべき?
両方の結果が短く明快に書ける単純な1行の条件に使ってください:name = "John" if condition else "Doe"。より複雑なら、通常のif/elseブロックの方が読みやすいです。
リストから重複を最も簡単に取り除くには?
set()で包んでから戻します:list(set(my_list))。ただしセットは順序を保持しないため、元の順序は失われます。