ついに Python を学び始めた。おすすめの学習リソースをまとめつつ、学習中のメモもここに置いておく。
学習リソースおすすめ
-
卡瓦邦噶! - 如何学Python?
とても詳しい Python 学習リソース集。入門から中級、深掘り、面接対策まで、だいたい何でも揃っている。作者には別ページもある:卡瓦邦噶! - 珍藏资料。ここにも良いものが多い。
-
Piglei - Python 工匠
Python は自由度が高いが、その代償として上下限の差が大きくなりがち。
この本は動くコードを良いコードにする方法や、Python の言語特性を活かしてPythonicなコードを書く話をしている。
Python だけでなく、変数命名や、大規模プロジェクトでのコードスタイル統一など、他言語にも通じる内容もある。
内容は読みやすい。GitHub 版は無料で読めて、気に入ったら書籍版(Web 版より詳しい)も買える。 -
Python Documents
何手も経た資料を読んでも理解できないとき、公式ドキュメントをめくると理解できることがある。できるならドキュメントを多めに読む。
卡瓦邦噶!とPython 工匠の両方で、標準ライブラリの車輪を知るために Python Documents - itertools を見ておくのがおすすめだと言っていた。車輪の再発明を避けられる。 -
CS61A
有名なCS61A。名前だけはずっと聞いているけど、まだ見ていない。 -
捕蛇者说
ポッドキャスト。名前の蛇はもちろんPythonのこと。
たまに聞くと新しいリソースに出会える。上で挙げた
卡瓦邦噶!の作者laixintaoはホストの一人でもある。
メモ
以下は Python 学習中に取ったメモ。読んだ内容の抜粋や言い換えに、自分が価値があると思ったポイントを足している。記憶の定着にもなるし、あとで見返すのにも便利。
== None or is None
Reading Python 工匠: 与 None 值的比较
On 2024.07.14
None 判定は is None を使う
Python では値を比較する方法が2つある:== と is。
作者の説明:
==:二者が指す 値 が一致するかis:二者がメモリ上の同一オブジェクトを指すか、つまりid(x)がid(y)と等しいか
Noneは Python のシングルトンオブジェクト。変数が None かどうかを判定したいときは==ではなくisを使う。厳密に「その変数が None である」を表すのはisだけ。
== は実際には __eq__ というマジックメソッドを呼び出す。__eq__ を自作すると真偽値に影響を与えられる。
None 判定では == ではなく is を使うべき。
__eq__ をオーバーライドする必要がある例
図書版 Python 工匠 の 12.1.2 比较运算符重载 で、__eq__ をオーバーライドすべき例が紹介されている。
例えば、正方形を表すクラス Square が辺長を受け取って面積を計算する:
class Square: """正方形
:param length: 边长 """
def __init__(self, length): self.length = length
def area(self): return self.length ** 2明らかに辺長が等しい2つの正方形は同じものとして扱えそう。
でもデフォルトでは Python は別オブジェクトとみなして False になる。
>>> x = Square(4)>>> y = Square(4)>>> x == yFalseそこで __eq__ と、他の5つの比較演算子 __ne__、__lt__、__le__、__gt__、__ge__ を実装する必要がある。
functools の @total_ordering デコレータを使えば手間が減る。
__eq__ に加えて、__lt__、__le__、__gt__、__ge__ のうち1つだけ実装すれば良い。
from functools import total_ordering
@total_orderingclass Square: """正方形
:param length: 边长 """
def __init__(self, length): self.length = length
def area(self): return self.length ** 2
def __eq__(self, other): if isinstance(other, self.__class__): return self.length == other.length return False
def __lt__(self, other): if isinstance(other, self.__class__): return self.length < other.length return NotImplementedこれで Square クラスは普通に大小比較できるようになる。
可迭代オブジェクト(iterable)とイテレータ(iterator)
Reading Python 工匠: 区分迭代器与可迭代对象
On 2024.07.14
iterable とは
可迭代オブジェクト(iterable)の公式定義 はこう:
An object capable of returning its members one at a time. Examples of iterables include all sequence types (such as list, str, and tuple) and some non-sequence types like dict, file objects, and objects of any classes you define with an
__iter__()method or with a__getitem__()method that implements sequence semantics.
つまり iterable は 一度に1つずつ要素を返せる 型のこと。
反復するたびに要素を1つずつ取り出し、最後まで走査する。
list、str、tuple は当然この条件を満たす。
ドキュメントにある dict や file objects も iterable と見なせるのは自然(細部は分からない)。
また __iter__() を自作すれば任意の class を iterable にできる。
set(集合)は無序だから反復できないと思っていた。
でもこのページを読んで:Python Documents: Howto - Functional、set も反復できると知った。
調べると、set は反復順序が一定とは限らない。要素順は hash 値に依存し、set が変化すると順序が変わりうる。
iterator との違い
iterable が分かったので、next() で反復してみる。
>>> # 定义一个简单 list>>> l = [4, 3, 2, 1]
>>> # 看看这个列表>>> l[4, 3, 2, 1]>>> # 嗯,没问题
>>> # 迭代一下>>> next(l)Traceback (most recent call last): File "<stdin>", line 1, in <module>TypeError: 'list' object is not an iterator>>> # 报错: list 不是迭代器なぜ動かない?
ドキュメントに戻ると、可迭代オブジェクト(iterable)の公式定義 には続きがある:
Iterables can be used in a for loop and in many other places where a sequence is needed (zip(), map(), …). When an iterable object is passed as an argument to the built-in function iter(), it returns an iterator for the object. This iterator is good for one pass over the set of values. When using iterables, it is usually not necessary to call iter() or deal with iterator objects yourself. The for statement does that automatically for you, creating a temporary unnamed variable to hold the iterator for the duration of the loop.
- iterable は for ループなど、シーケンスが必要な場所で使える
- iterable を
iter()に渡すと、その iterable のiteratorが返る- iterator は一回きりの走査に向く
- iterable を使うとき、通常
iter()を呼んだり自分で iterator を扱う必要はない- for 文が自動で iterator を管理してくれる
つまり for i in range(4): は range(4) を回しているのではなく、iter(range(4)) が返す iterator を回している。
Python の for がその過程を自動化しているだけ。
REPL で確認:
>>> # 接着刚才的 list>>> l[4, 3, 2, 1]
>>> # 对 list 对象调用 iter()>>> iter(l)<list_iterator object at 0x7faffaac46d0>>>> # 创建了一个 list_iterator 对象
>>> # 对 iterator 使用 next()>>> next(iter(l))4>>> # 成功拿到 list 第一个值iterator に対して next() し続ければ、順番に list の値が取れる。
>>> l[4, 3, 2, 1]>>> iter(l)<list_iterator object at 0x7faaf1ac46d0>>>> next(iter(l))4>>> next(iter(l))4>>> next(iter(l))4>>> next(iter(l))4>>>本来は 4, 3, 2, 1 のはずだが、next(iter(l)) は毎回 list の先頭を返してしまう。
原因は next(iter(l)) の中の iter(l) にある。
iter(l) を呼ぶたびに新しい iterator が作られる。
だから next(iter(l)) は毎回新品 iterator の先頭を取っているので、常に先頭しか出ない。
REPL で見ると:
>>> iter(l)<list_iterator object at 0x7faaf1ae4430>>>> iter(l)<list_iterator object at 0x7faaf1ac46d0>>>> iter(l)<list_iterator object at 0x7faaf1c2d240>3回 iter(l) を呼ぶと、3つの list_iterator(at の後のアドレスが違う)が返る。
では iterator の全要素を next() で取りたいなら、中継役を置く。
>>> # 可迭代对象>>> l[4, 3, 2, 1]
>>> # 调用 next 会报错>>> next(l)Traceback (most recent call last): File "<stdin>", line 1, in <module>TypeError: 'list' object is not an iterator
>>> # 把迭代器对象赋值给 l_iterator>>> l_iterator = iter(l)
>>> # next 它>>> next(l_iterator)4>>> next(l_iterator)3>>> next(l_iterator)2>>> next(l_iterator)1>>> next(l_iterator)Traceback (most recent call last): File "<stdin>", line 1, in <module>StopIterationこれで iterator の全要素が取れた。
generator と generator expression
iterator は便利だが一回きりの消耗品。毎回準備が必要だと利便性が落ちる。
そこで Python には iterator を簡単に使う方法が2つある。
-
generator
function 内でreturnの代わりにyieldを使う。そうすると関数がシーケンスを用意して、next()が値を取り出せる。 -
generator expression
よくあるfor i in range(4):はその例。Python Documents: generator expression の例:
>>> sum(i*i for i in range(10)) # sum of squares 0, 1, 4, ... 81285
iterator の要素数を数えるには
普通の list は len(list) で要素数が取れる。
でも iterator は怠惰(lazy)で、走査しないと要素数が分からない。
しかも一度最後まで走査すると、その iterator は失効する。
数える意味はあまりない気がする(
iteratorは一回きりの消耗品。使い切って捨てる。
何度も回したいなら list みたいな普通のオブジェクトが必要。
Ref:
Python Documents: iterable
Python Documents: iterator
Python Documents: generator
Python Documents: generator expression
関数型プログラミング
Reading Python Documents: Functional Programming HOWTO
On 2024.07.15
省流:最後の段落は lambda を使うな、という話
Fredrik Lundh once suggested the following set of rules for refactoring uses of lambda:
- Write a lambda function.
- Write a comment explaining what the heck that lambda does.
- Study the comment for a while, and think of a name that captures the essence of the comment.
- Convert the lambda to a def statement, using that name.
- Remove the comment.
I really like these rules, but you’re free to disagree about whether this lambda-free style is better.
lambda を使うと2行減るかもしれないが、理解コストは一気に跳ね上がる。
可読性と理解コストの観点から、関数型プログラミングはおすすめしない。素直に def と for で書くのが良い。
この記事の前半の導入が良い。プログラミング言語の問題解決の流儀は4つに分かれる、と書いている:
Procedual:手続き型(例:C、Pascal、Unix Shell)
Declarative:SQL
Object-oriented:オブジェクト指向
Functional:関数型
Python は柔軟なので、手続き型でもオブジェクト指向でも関数型でも、状況に応じて選べる。
この記事には iterable / iterator / generator の例がたくさんあって、前節の復習にもなった。
ついでに「使えそうな関数」をいくつか見かけた:
-
filter(predicate, iter)
(x for x in range(10) if is_even(x))と同じ効果 -
enumerate(iter, start=0)
index と value を同時に取るのによく使う -
any(iter) and all(iter)
iterator 内のブール値を判定 -
zip(iterA, iterB, …, strict=False)
Excel の行列変換みたいなイメージ。デフォルトは最短の iterator の長さに揃える。
全て同じ長さであることを厳密に要求するならstrict=True。 -
itertools.count(start, step)
無限長 iterator を返す -
itertools.repeat(elem, [n])
elem を n 回繰り返す -
itertools.islice(iter, [start], stop, [step])
スライス。引数1つは stop、2つは start と stop、3つは start/stop/step -
itertools.tee(iter, [n])
iterator を n 份に複製する。調べるとデータ分析やログ分析などで使われるっぽい。
同じデータを複数回分析したいとき、tee で iterator を分岐させて別々に分析できる。 -
itertools.filterfalse(predicate, iter)
filter の逆 -
itertools.dropwhile(predicate, iter)
名前の通り -
itertools.combinations(iterable, r)
組み合わせitertools.combinations([1, 2, 3, 4, 5], 2) =>
(1, 2), (1, 3), (1, 4), (1, 5),
(2, 3), (2, 4), (2, 5),
(3, 4), (3, 5),
(4, 5)
読み終わった。結構面白かった。
追記:itertools.product(*iterables, repeat=1) はループのネストに相当する。
大規模プロジェクトでよく使うツール
Reading 图书版 Python 工匠
13.1 开发大型项目常用工具介绍章
On 2024.07.16
Linter:
- flake8:コードスタイルチェック(カスタマイズ可)
- black:より厳格なスタイルチェック(ほぼカスタマイズ不可)
- isort:import を並べ替える
- pre-commit:commit 前にチェックを強制
- mypy:型注釈を使った静的型チェック
よくある組み込みコンテナ型 4 種
Reading Python 工匠:4. 容器的门道
On 2024.07.11
Python でよく使う組み込みコンテナ型は4種類:list、tuple、dict、set。
- List:順序あり、可変、重複可。順序付きで保存/アクセスしたいときに向く。
- Tuple:順序あり、不変、重複可。不変なデータを保持したいときに向く。
- Dictionary:順序あり(Python 3.7+)、可変、キーは一意。キーで高速に値を探したいときに向く。
- Set:順序なし、可変、重複不可。一意な要素と集合演算(積集合/和集合)に向く。
Python 工匠:写更快的代码
-
list を頻繁に拡張する/新しい list を作るのを避ける
- iterator を多用する:
yield、generator expression - モジュールが提供する lazy オブジェクトをなるべく使う:
re.findallではなくre.finditer- iterable なファイルオブジェクトを直接使う:
for line in fp(for line in fp.readlines()ではなく)
- iterator を多用する:
-
list の先頭操作が多いなら deque を使う
list は配列(Array)実装なので、先頭に挿入すると(
list.insert(0, item))、後ろの要素をすべて動かす必要があり、計算量はO(n)。つまり先頭挿入は、末尾 append(list.append(item)はO(1))よりずっと遅い。こういう操作が多いなら、list の代わりに collections.deque を検討する。deque は両端キューで、先頭/末尾どちらも
O(1)。 -
メンバー存在判定は set/dict を使う
ある要素がコンテナに含まれるか判定するなら、list より set のほうが向いている。
item in [...]はO(n)、item in {...}はO(1)。dict と set はハッシュテーブル実装だから。Hint: TimeComplexity - Python Wiki を読むのを強く勧める。コンテナ型の計算量の話がまとまっている。
dict の実装詳細に興味があるなら、Raymond Hettinger の講演 Modern Dictionaries(YouTube) も強くおすすめ。
境界条件と海象演算子
Reading Python 工匠:15. 在边界处思考
Reading Python 工匠:16. 语句、表达式和海象操作符
On 2024.07.16
図書版 Python 工匠 にはこの2章は収録されていない。
境界で考える
作者は、言語機能を活かして境界ケースをもっとエレガントに処理するのが良いと考えている。Python だと:
- 境界ケースは
if elseよりtryで例外を捕まえる
Python コミュニティはLBYL (Look Before You Leap)よりEAFP (sEasier to Ask for Forgiveness than Permission)を好む。Python の例外は軽い。
おすすめ記事:Write Cleaner Python: Use Exceptions - コンテナに要素がないときの処理
collections.defaultdictは dict の「キーがない」ケースを扱える
setdefaultで値を取って dict を更新する
dict.popで存在しないキーを削除する
list の slice は IndexError を投げないので、境界処理が不要 orの短絡評価でデフォルト値を設定するとき、None以外にも空コンテナはFalseになるので、境界処理はifのほうが正確- ユーザー入力の検証を手書きしない。pydantic やフレームワークの検証モジュールを使う
%、abs()、math.floor()を使いこなす
文と式、そして海象演算子
海象演算子 (walrus operator) は代入と式を合体させたもの。
条件分岐、ループ、内包表記などで使えて、重複コードを減らせる。
ただし理解コストも上がるので、精錬さを盲目的に追わないほうがいい。
海象演算子は関数型っぽい。短くなるけど理解コストが上がる。