default eye-catch image.

ひたすらPythonチュートリアル第4版を読んでみる

Pythonの入門書「Pythonチュートリアル」。 もともとPython作者のGuido van Rossum自身が書いたドキュメントが出展で、 理解のしやすさを目指して日本語訳が作られている。 Pythonの更新に対応するため幾度か改版され、第4版は3.9対応を果たしている。 タイトルの通りひたすら「Pythonチュートリアル第4版」を読んでみる。 全てを1つの記事に書くスタイル。読み進めた部分を足していく。 [arst_toc tag=\"h3\"] Pythonインタープリタの使い方 対話モード コマンドをttyから読み込むモード。 >>> で複数行のコマンドを受け付ける。 2行目から...で受け付ける。 > python 月 4/11 23:35:41 2022 Python 3.9.11 (main, Apr 11 2022, 01:59:37) [Clang 10.0.1 (clang-1001.0.46.4)] on darwin Type \"help\", \"copyright\", \"credits\" or \"license\" for more information. >>> hoge = True >>> if hoge: ... print(\"hoge is true\") ... hoge is true ソースコードエンコーディング shebangとは別にファイルの先頭に特殊コメントを書くことでファイルのencodingを指定できる。 UTF8の場合は記述しない。非UTF8の場合にのみ書く。shebangがある場合2行目。 ちなみにコメントは「coding[=:]s*([-w.]+)」の正規表現にマッチすればよい。 #!/bin/sh # 🍣🍣🍣 coding=cp1252 🍣🍣🍣 とはいえ、教科書的には「# -*- coding: cp1252 -*-」。 気楽な入門編 対話モードの最終評価値はアンダースコア(_)に格納される。へぇ。 型と変数と評価 #加算 >>> 1+1 2 #減算 >>> 5-4 1 #乗算 >>> 3*2 6 #除算 >>> 5/3 1.6666666666666667 >>> 1*(3+9) 12 #変数への代入と評価 >>> hoge=100 >>> hoge 100 #最終評価値の記憶(アンダースコア) >>> price = 100 >>> tax = 0.25 >>> price * tax 25.0 >>> price + _ 125.0 文字列 シングルクォートまたはダブルクォート。バックスラッシュでエスケープ。 文字列リテラルにrを前置することでエスケープ文字をエスケープしない.へぇ。 >>> str = \'hogehoge\'; >>> str2 = str + \'100t200\'; >>> str2 \'hogehoge100t200\' >>> print(str2) hogehoge100 200 >>> str3 = str + r\'100t200\'; >>> str3 \'hogehoge100\\t200\' いわゆるヒアドキュメント。複数行の文字列リテラルはトリプルクォート。 >>> print(\"\"\" ... This is a pen. ... This was a pen. ... This will be a pen. ... \"\"\"); This is a pen. This was a pen. This will be a pen. 文字列リテラルを列挙すると結合される。 phpのドット演算子とは異なり文字列リテラルのみに作用する。 文字列リテラルと変数は無理。 phpに慣れてるとやりかねない。 >>> text = (\'文字列1\' ... \'文字列2\' \'文字列3\' ... \'文字列4\') >>> text \'文字列1文字列2文字列3文字列4\' >>> text2 = \'hogehoge\' >>> text text2 File \"\", line 1 text text2 ^ SyntaxError: invalid syntax インデックス演算子で文字列内の文字(1文字の文字列)にアクセス可。 負の値を指定すると後ろから何個目...というアクセスの仕方ができる。0と-0は同じ。 範囲外アクセス(Out of bounds)でエラー。 >>> str3 = \'123456789\' >>> str3[3] \'4\' >>> str3[-2] \'8\' >>> str3[0] \'1\' >>> str3[-0] \'1\' >>> str3[100] Traceback (most recent call last): File \"\", line 1, in IndexError: string index out of range 文字列とスライス スライス演算子で部分文字列にアクセス可。始点は含み終点は含まない。 >>> str3[2:5] \'345\' >>> str3[3:] \'456789\' >>> str3[-2:] \'89\' >>> str3[:5] \'12345\' 参考書にスライスについて面白い書き方がされている。 インデックスとは文字と文字の間の位置を表す。最初の文字の左端がゼロ。 インデックスiからインデックスjのスライス[i:j]は境界iと境界jに挟まれた全ての文字。 例えば[2,5]は t h o 。 +---+---+---+---+---+---+ | P | y | t | h | o | n | +---+---+---+---+---+---+ 0 1 2 3 4 5 6 -6 -5 -4 -3 -2 -1 スライスには範囲外アクセス(Out of range)はない。超えた分を含む最大を取ってくれる。 >>> str3[2:100] \'3456789\' Pythonの文字列はImmutable。インデックス演算子によりアクセスした部分文字を書き換えられない。 >>> str3[3] = \'A\' Traceback (most recent call last): File \"\", line 1, in TypeError: \'str\' object does not support item assignment コピーして新しい文字列を作って加工する。 >>> str4 = str3[2:5] >>> str4 = str4 + \"hoge\" >>> str4 \'345hoge\' リスト シンプルなコレクション。異なる型の値を格納できる。 リストはミュータブルでスライスアクセスによりシャローコピーを返す。 スライスで戻る新たなリストは元のリストのポインタで値を変更できる。 >>> list = [1,2,3,4,5] >>> list[2:4] = [100,200] >>> list [1, 2, 100, 200, 5] >>> list[:] = [] >>> list [] >>> list.append(100) >>> list [100] #入れ子 >>> list = [1,2,3,4,5,[6,7]] >>> list [1, 2, 3, 4, 5, [6, 7]] フィボナッチ数列 簡単なフィボナッチ数列を例にPythonのいくつかのフィーチャーが説明されている。 まず、多重代入が言語仕様としてサポートされている。 真偽のモデルは「0でない値が真、0だけが偽」のパターン。 ブロックをインデントで表現する。同一ブロックはインデントが揃っている必要がある。 >>> a,b = 0, 1 >>> while a < 10: ... print(a) ... a, b = b, a + b 0 1 1 2 3 5 8 制御構造 if ブロックはインデントで表現。else ifの短縮系として elif を使用できる。 if .. elif .. elif .. else 。 elifを続けて書ける。 >>> x = int(input(\"整数を入力:\")) 整数を入力:100 >>> if x < 0: ... x = 0 ... print('負数はゼロ') ... elif x == 0: ... print('ゼロ') ... elif x == 1: ... print('1つ') ... else: ... print('もっと') for C形式、つまり初期値、反復間隔、停止条件の指定では書けないのがポイント。 シーケンス(リスト、文字列)のアイテムに対してそのシーケンス内の順序で反復を書くことになる。 >>> words = [ \'hoge\', \'fuga\', \'foo\'] >>> for w in words: ... print(w, len(w)) ... hoge 4 fuga 4 foo 3 シーケンス内のアイテムがシーケンスの場合、アイテムを直接受け取れる。 >>> users = [ [\'kuma\',1], [\'peco\', 2], [\'hoge\', 3]] >>> for user, status in users: ... print(user, status) ... kuma 1 peco 2 hoge 3 Cスタイルの反復条件をループ内で変更する際に終了判定が複雑になるように、 Pythonのスタイルであっても反復対象のシーケンスを直接変更すると面倒なことになる。 本書では、シーケンスをコピーし新しいシーケンスを作って操作する例が示されている。 まぁそうだろうが、本書のここまで辞書(dict)の説明は出てきていない。まぁいいか。 >>> users = { \'hoge\':100, \'fuga\':200, \'peco\':300 } >>> for user, status in users.copy().items(): ... if status == 200: ... del users[user] ... >>> users {\'hoge\': 100, \'peco\': 300} >>> active_users = {} >>> for user, status in users.items(): ... if status == 300: ... active_users[user] = status ... >>> active_users {\'peco\': 300} range 任意の反復を実行するために反復条件を表すシーケンスを定義してやる必要がある。 ビルトイン関数のrange()を使うことで等差数列を持つiterableを生成できる。 range()は省メモリのため評価時にメモリを確保しない。 つまり、range()が返すのはiterableでありシーケンスではない。 第3引数はステップで省略すると1が使われる。 先頭から順に評価時に消費され遂には空になる、というイメージ。 >>> for i in range (1,100,10): ... print(i) ... 1 11 21 31 41 51 61 71 81 91 とはいえ他の処理でシーケンスを作成済みで再利用するケースは多い。 iterableではなく既にコレクションが存在する場合、以下のようになる。 >>> a = [\'hoge\', \'fuga\', \'kuma\',\'aaa\',\'bbb\'] >>> for i in range(len(a)): ... print(i, a[i]) ... 0 hoge 1 fuga 2 kuma 3 aaa 4 bbb iterableを引数に取る関数はある。例えばsum()はiterableを引数に取り合計を返す。 >>> sum(range(10)) 45 ループのelse forループでiterableを使い果たすか1件も消費できないケースでforループにつけたelseが評価される。 ただしforループをbreakで抜けた場合はforループのelseは評価されない。 例えば2から9までの数値について素数か素数でなければ約数を求める処理を構文で表現できる。 ループのelseはtryによる例外評価に似ているという記述がある。え..? 要は「forの処理が期待したパスを通らない場合に評価される」ということだろうか... イマジネーションの世界.. >>> for n in range(2, 10): ... for x in range(2, n): ... if n % x == 0: ... print(n, \'equals\', x, \'*\', n/x) ... break ... else: ... print(n, \'is a prime number\') ... 2 is a prime number 3 is a prime number 4 equals 2 * 2.0 5 is a prime number 6 equals 2 * 3.0 7 is a prime number 8 equals 2 * 4.0 9 equals 3 * 3.0 pass 構文的に文が必要なのにプログラム的には何もする必要がないときにpassを使う。 もうこれ以上説明は不要。やはり原著は良い。 >>> r = range(1,10) >>> for i in r: ... if i % 2 == 0: ... print(i) ... else: ... pass ... 2 4 6 8 関数の定義 本書においてスコープの実装が書かれている。言語仕様をわかりやすく説明してくれている。 プログラミング言語自体の実装において変数などのシンボルはスコープの範囲で格納され参照される。 本書においてPythonのスコープは内側から順に以下の通りとなると記述がある。 より外側のスコープのシンボル表は内側のスコープのシンボル表に含まれる。 内側のスコープから外側のシンボル表を更新することはできない。 関数内スコープ 関数を定義したスコープ グローバルスコープ ビルトインスコープ >>> hgoe = 100 >>> def bar(): ... hoge = 200 ... print(hoge) ... >>> bar() 200 >>> hoge 100 引数はcall by object reference Pythonの関数の引数は値渡しなのか参照渡しなのか。原著には簡潔に答えが書かれている。 関数のコールの時点でその関数にローカルなシンボル表が作られる。 ローカルなシンボル表に外側のシンボル表の値の参照がコピーされる。まさに事実はこれだけ。 call by valueに対して、call by object referenceという表現がされている。 引数が巨大であっても関数のコールの度に値がコピーされることはないし、 関数スコープで引数を弄っても外側のスコープに影響することはない。 関数の戻り値 Pythonにはprocedureとfunctionの区別がない。全てfunction。 procedureであっても(つまり明示的にreturnで返さなくても)暗黙的にNoneを返す。 >>> def bar(): ... hoge = 100 ... >>> print(bar()) None >>> def foo(): ... hoge = 100 ... return hoge ... >>> print(foo()) 100 本書で書かれているフィボナッチ級数をリストで返す関数を定義してみる。 >>> def fib(n): ... result = [] ... a, b = 0, 1 ... while a >> fib(100) [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89] 引数のデフォルト引数 デフォルト値の評価は関数を定義した時点で定義を書いたスコープで行われる。 まさに原著に書かれているこの書かれ方の通り。 >>> N=300 >>> def foo(hoge, fuga=100, bar=N): ... print(hoge, fuga, bar) ... >>> foo(100) 100 100 300 >>> foo(100,200) 100 200 300 >>> foo(100,200,500) 100 200 500 そして、デフォルト値の評価は一度しか起きない。デフォルト値がリストなどの可変オブジェクトの場合、 定義時に1度だけデフォルト値が評価されるだけで、コール時にはデフォルト値は評価されない。 本書の例がわかりやすかった。 >>> def foo(hoge,L=[]): ... L.append(hoge) ... return L ... >>> foo(100) [100] >>> foo(200) [100, 200] >>> foo(300) [100, 200, 300] キーワード引数 キーワード引数によりコール時の引数の順序を変更できる。 デフォルト引数の定義がキーワード引数の定義を兼ねている。 デフォルト定義がない引数は位置が制約された位置引数。 位置引数は必須でありキーワード引数よりも前に出現する必要がある。 >>> def foo(hoge, fuga=100, bar=N): ... print(hoge, fuga, bar) >>> foo(100,fuga=500) 100 500 300 「*名前」を引数に設定すると、仮引数にない位置指定型引数を全て含むタプルが渡る。 「**名前」を引数に設定すると、仮引数に対応するキーワードを除いた全てのキーワード引数がdictで渡る。 dict内の順序は関数のコール時の指定順序が保持される。 >>> def aaa(kind, *arguments, **keywords): ... for arg in arguments: ... print(arg) ... for kw in keywords: ... print(kw,\':\',keywords[kw]) ... >>> aaa(\"111\", \"222\", \"333\", hoge=\"444\", fuga=\"500\", poo=\"600\") 222 333 hoge : 444 fuga : 500 poo : 600 位置のみ,位置またはキーワード,キーワードのみ指定 引数は位置引数,キーワード引数のいずれにでもなることができるが出現位置は決められている。 引数リストの前半は位置引数, 後半はキーワード引数であり, 位置引数はMust、キーワード引数はOptional。 Optionalな部分は位置引数なのかキーワード引数なのか文脈で決まることになる。 言語仕様によって,どの引数が「位置引数限定」,「キーワード引数限定」,「どちらでも良い」かを指定できる。 特殊引数 / と * を使用する。 /の前に定義した引数は位置引数としてのみ使用できる。 また / と * の間に定義した引数は位置引数,キーワード引数のいずれでも使用できる。 * の後に定義した引数はキーワード引数としてのみ使用できる。 /が無ければ位置引数指定がないことを表す。*が無ければキーワード指定がないことを表す。 つまり / も * もない場合は、全ての引数が位置引数にもキーワード引数にもなれるデフォルトの挙動となる。 >>> def f(pos1, pos2, /, pos_or_kwd, *, kwd1, kwd2): ... print(pos1, pos2) ... print(pos_or_kwd) ... print(kwd1, kwd2) ... >>> f(10,20,30,kwd1=40,kwd2=50) 10 20 30 40 50 # 前から最大3個しか位置引数になれないため5個渡すとエラーとなる >>> f(10,20,30,40,50) Traceback (most recent call last): File \"\", line 1, in TypeError: f() takes 3 positional arguments but 5 were given # h, zを位置引数に限定。キーワード指定して呼ぶとエラーとなる >>> def j(h,z,/): ... print(h,z) ... >>> j(200, z=100) Traceback (most recent call last): File \"\", line 1, in TypeError: j() got some positional-only arguments passed as keyword arguments: \'z\' # h, zをキーワード引数に限定。位置指定して呼ぶとエラーとなる >>> def n(*,h,z): ... print(h, z) ... >>> n(100, z=200) Traceback (most recent call last): File \"\", line 1, in TypeError: n() takes 0 positional arguments but 1 positional argument (and 1 keyword-only argument) were given 本書に微妙な部分を説明する記述があった。 位置引数nameと, キーワード引数リストargsを取る関数fooを定義し, nameというキーを持つdictを第2引数として渡した場合, nameは必ず位置引数に設定され, argsには含まれない。そのような呼び方をすると呼んだ時点でエラーとなる。 >>> def foo(name, **args): ... return \'name\' in args ... >>> foo(1, **{\'name\': 2}) Traceback (most recent call last): File \"\", line 1, in TypeError: foo() got multiple values for argument \'name\' 引数リストにおいてnameを位置引数に限定した場合, **{\'name\':2}はnameに設定されず, *argsで受けられるようになる。 >>> def bar(name,/,**args): ... return \'name\' in args ... >>> bar(1, **{\'name\': 3}) True どの引数を位置引数限定,キーワード引数限定にすべきか手引きが書いてある。 ただ、ちょっとアバウトすぎるというか決めてに書ける。 位置引数にすべき場合は以下。 引数名に本当に意味がない場合 呼び出し時に引数の順序を強制したい場合 いくつかの位置引数と任意個数のキーワード引数を取りたい場合 キーワード引数に限定すべき場合は以下。 引数名に意味がある場合 明示することで関数宣言が理解しやすくなる場合 引数の位置に頼らせたくない場合 特に、キーワード引数とした場合将来引数名が変更されたときに破壊的変更になるから API定義時には位置引数とすべき、なんて書いてある。え... 位置引数の扱いが変わり、渡した引数が意図しない使われ方をすることを許容するのだろうか。 任意引数 仮引数リストの末尾に*から始まる仮引数を置くと任意の引数を吸収するタプルで受けられる。 # hogeは仮引数, hoge以降に指定した任意の数の値をタプルargsで受ける。 >>> def k(hoge, *args): ... print(hoge) ... print(\'/\'.join(args)) ... >>> k(100,\'a\',\'b\',\'c\',\'d\') 100 a/b/c/d 任意引数以降は全てキーワード引数となる。任意引数以降に位置引数を定義することはできない。 キーワード引数はOKなので,任意引数の後ろに新たな引数を置くことはできる。 その引数はキーワード引数となる。 >>> def concat(*args, sep=\'/\'): ... return sep.join(args) ... >>> concat(\'hoge\',\'fuga\',\'foo\') \'hoge/fuga/foo\' 引数のアンパック 変数のコレクションがあり、コレクションから変数にバラす操作をアンパックという。 引数として渡すべき変数の位置でコレクションからアンパックする、という操作をおこなえる。 *演算子によりシーケンスをアンパックできる。 例えば、シーケンス [1,5] があり、このシーケンスからrange(1,5) を作る場合は以下。 >>> cols = [1, 5] >>> v = range(*cols) >>> v range(1, 5) また**演算子によりdeictionaryをアンパックできる。 >>> def z(hoge=300, fuga=500): ... print(hoge, fuga) ... >>> z() 300 500 >>> dict = { \'hoge\': 100, \'fuga\' : 200 } >>> z(**dict) 100 200 lambda式 無名関数。関数オブジェクトを返す。通常の関数とは異なり単一の式しか持てない制限がある。 2個の引数を取り,それぞれの和を求める関数オブジェクトを返すlambdaを定義し使ってみる。 >>> bar = lambda a,b : a+b >>> bar(100,200) 300 lambdaが定義された位置の外側のスコープの変数を参照できる。 これはlambdaが関数のシュガーシンタックスで、関数の入れ子を書いているのと同じだから。 例えば以下のように1個の引数xをとるlambdaにおいて外側にある変数nを参照できる。 >>> def make_incrementor(n): ... return lambda x: x + n ... >>> f = make_incrementor(42) >>> f(0) 42 >>> f(10) 52 ドキュメンテーション文字列(docstring) 関数定義の中にコメントを書くPython固有のコメント仕様について決まりがまとまっている。 1行目は目的を簡潔に要約する。英文の場合大文字で始まりピリオドで終わること。 よくあるダメコメントパターンの1つである変数名自体の説明は避けるなどが書かれている。 2行目は空行。3行目以降の記述と1行目の要約を視覚的に分離する。 関数オブジェクトの__doc__属性を参照することでdocstringを取得できる。 >>> def my_func(): ... \"\"\"Do nothing, but document it. ... ... No, really, it doesn\'t do anything. ... \"\"\" ... pass >>> print(my_func.__doc__) Do nothing, but document it. No, really, it doesn\'t do anything. 関数アノテーション ユーザ定義関数で使われる型についてのメタデータ情報を任意に付けられる。 アノテーションは関数の__annotations__属性を参照することで取得できる。 仮引数のアノテーションは仮引数名の後にコロンで繋いで指定。 関数の型のアノテーションは def の最後のコロンの手前に->で繋いで指定。 >>> def f(ham: str, eggs: str = \'eggs\') -> str: ... print(\"Annotations:\", f.__annotations__) ... print(\"Arguments:\", ham, eggs) ... return ham + \' and \' + eggs ... >>> f(\'hoge\') Annotations: {\'ham\': , \'eggs\': , \'return\': } Arguments: hoge eggs \'hoge and eggs\' コーディング規約(PEP8) ざっくりPEP8の要点が書かれている。 インデントはスペース4つ。タブは使わない。 1行は79文字以下 関数内で大きめのブロックを分離するために空行を使う コメント行は独立 docstringを使う 演算子の周囲やカンマの後ろにはスペースを入れるがカッコのすぐ内側にはいれない クラス、関数名は一貫した命名規則を使う。クラス名はUpperCamelCase、関数名はlower_case_with_underscores メソッドの第1引数は常にself エンコーディングはUTF8 データ構造 リストの操作 コレクションに対する操作方法が解説されている。破壊的メソッドはデータ構造を変更した後Noneを返す。 # 末尾に追加 >>> hoge = [1,2,3,4,5] >>> hoge.append(6) >>> hoge [1, 2, 3, 4, 5, 6] # iterableを追加 >>> hoge.extend(range(7,9)) >>> hoge [1, 2, 3, 4, 5, 6, 7, 8] # これは以下と等価 >>> hoge = [1,2,3,4,5] >>> hoge[len(hoge):] = range(6,9) >>> hoge [1, 2, 3, 4, 5, 6, 7, 8] # insert >>> hoge.insert(3,100) >>> hoge [1, 2, 3, 100, 4, 5, 6, 7, 8] # remove >>> hoge.remove(3) >>> hoge [1, 2, 100, 4, 5, 6, 7, 8] # pop >>> hoge.pop() 8 >>> hoge [1, 2, 100, 4, 5, 6, 7] # pop(i) >>> hoge.pop(4) 5 >>> hoge [1, 2, 100, 4, 6, 7] # clear >>> hoge.clear() >>> hoge [] # [] >>> hoge = [1,2,3,4,5] >>> hoge[2:4] [3, 4] # count(i) リスト内のiの数を返す。リストの個数ではない >>> hoge.count(3) 1 # reverse >>> hoge.reverse() >>> hoge [5, 4, 3, 2, 1] >>> fuga = hoge.copy() >>> fuga [5, 4, 3, 2, 1] リストは比較不可能な要素を持つことができるが、sort()等のように順序を使うメソッドは比較を行わない。 >>> bar = [3,1,2,4,5] >>> bar.sort() >>> bar [1, 2, 3, 4, 5] >>> foo = [3,1,2,4,None,5] >>> foo [3, 1, 2, 4, None, 5] >>> foo.sort() Traceback (most recent call last): File \"\", line 1, in TypeError: \'<' not supported between instances of 'NoneType' and 'int' リストをスタック、キューとして使う 引数無しのpop()により末尾の要素を削除し返すことができる。append()とpop()でLIFOを作れる。 insert()とpop(0)によりFIFOを作ることもできるが,押し出されるデータの再配置により遅いため, deque()を使うとよい。deque()は再配置がなく高速。 # LIFO >>> stack = [1,2,3,4,5] >>> stack.append(6) >>> stack.pop() 6 >>> stack [1, 2, 3, 4, 5] # FIFO (Slow) >>> stack.insert(0,100) >>> stack.pop(0) 100 >>> stack [1, 2, 3, 4, 5] # FIFO (Fast) >>> from collections import deque >>> queue = deque([1,2,3,4,5]) >>> queue deque([1, 2, 3, 4, 5]) >>> queue.popleft() 1 >>> queue deque([2, 3, 4, 5]) リスト内包(list comprehension) list comprehensionの日本語訳がリスト内包。本書には等価な変形が書かれていて、説明にはこれで十分なのではないかと思う。 # forを使って2乗数からなるシーケンスを取得 >>> for x in range(10): ... squares.append(x**2) ... >>> squares [0, 1, 4, 9, 16, 25, 36, 49, 64, 81] # Lambdaを使った等価表現 >>> squares2 = list(map(lambda x: x**2, range(10))) >>> squares2 [0, 1, 4, 9, 16, 25, 36, 49, 64, 81] # list comprehension >>> squares3 = [x**2 for x in range(10)] >>> squares3 [0, 1, 4, 9, 16, 25, 36, 49, 64, 81] 構文としては以下。 式 for節 0個以上のfor節やif節 2重のforを1つのリスト内包表記できる。外側のfor,内側のfor,ifの出現順序が保持されていることに注意、という記述がある。 # forによる表現 >>> for x in [1,2,3]: ... for y in [3,1,4]: ... if x != y: ... combs.append((x,y)) ... >>> combs [(1, 3), (1, 4), (2, 3), (2, 1), (2, 4), (3, 1), (3, 4)] # list comprehension >>> [(x,y) for x in [1,2,3] for y in [3,1,4] if x != y] [(1, 3), (1, 4), (2, 3), (2, 1), (2, 4), (3, 1), (3, 4)] タプルのリストなんかも作れる。 >>> [(x, x**2) for x in [1,2,3]] [(1, 1), (2, 4), (3, 9)] 式を修飾できる。 >>> from math import pi >>> [str(round(pi,i)) for i in range(1,6)] [\'3.1\', \'3.14\', \'3.142\', \'3.1416\', \'3.14159\'] 入れ子のリスト内包 本書には入れ子のリスト内包の等価表現が書かれている。 行列の転値を得る例で説明されているので追ってみる。 # 元の行列 >>> matrix = [ ... [1, 2, 3, 4], ... [5, 6, 7, 8], ... [9, 10, 11, 12], ... ] >>> matrix [[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12]] # 2重ループを全てforで書き下した >>> transposed = [] >>> for row in matrix: ... transposed_row = [] ... for i in range(4): ... transposed_row.append(row[i]) ... transposed.append(transposed_row) ... >>> transposed [[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12]] # 1つのループをfor、もう1つをリスト内包 >>> transposed = [] >>> for i in range(4): ... transposed.append([row[i] for row in matrix]) ... >>> transposed [[1, 5, 9], [2, 6, 10], [3, 7, 11], [4, 8, 12]] # 全部リスト内包 >>> [[row[i] for row in matrix] for i in range(4)] [[1, 5, 9], [2, 6, 10], [3, 7, 11], [4, 8, 12]] zip関数 例えばforループにおいて複数のiterableオブジェクトの要素を同時に取得したいときzip()を使う。 何とも書きづらいが, zip(hoge,fuga,foo)とすることでhoge,fuga,fooを1つにまとめることができ, それをforループ内の変数に展開することができる。 # zip()について >>> hoge = [1,2,3] >>> fuga = [4,5,6] >>> foo = [7,8,9] >>> zip(hoge,fuga,foo) # hoge, fuga, fooを固めたものから 変数x,y,zで取り出す >>> for x,y,z in zip(hoge,fuga,foo): ... print(x,y,z) ... 1 4 7 2 5 8 3 6 9 matrix=[[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12]]をアンパックすることで、 [1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12]が得られる。 これをzip()に与えると3つの要素を持つ4つのタプルにアクセス可能なオブジェクトが得られる。 forループの変数で受けると(1,5,9),(2,6,10),(3,7,11),(4,8,12)が得られる。 >>> for x in zip(*matrix): ... print(x) ... (1, 5, 9) (2, 6, 10) (3, 7, 11) (4, 8, 12) >>> list(zip(*matrix)) [(1, 5, 9), (2, 6, 10), (3, 7, 11), (4, 8, 12)] del リストの要素をインデックス指定またはスライス指定で削除できる。 変数自体を削除できる。 >>> hoge = [1,2,3,4,5,6] >>> del(hoge[3]) >>> hoge [1, 2, 3, 5, 6] >>> del(hoge[2:5]) >>> hoge [1, 2] >>> del hoge >>> hoge Traceback (most recent call last): File \"\", line 1, in NameError: name \'hoge\' is not defined タプル リスト、タプルともにシーケンスだがリストはmutable(可変体)、タプルはimutable(不変体)。 シーケンスであるから、文字列、リストと同様にインデックスアクセスできる。 本書では空のタプル、要素数が1のタプルの作り方が紹介されている。 >>> t = 1,2,3,4,5 >>> t (1, 2, 3, 4, 5) >>> u = t, (1,2,3,4,5) >>> u ((1, 2, 3, 4, 5), (1, 2, 3, 4, 5)) >>> u[1][2] 3 # 要素数がゼロのタプルを作る >>> empty = () >>> empty () # 要素数が1のタプルを作る >>> singleton = \'hoge\' , >>> singleton (\'hoge\',) # 1個の要素を()で囲ってもタプルにならない! >>> singleton2 = (\'hoge\') >>> singleton2 \'hoge\' タプルパッキングとシーケンスアンパッキングについて紹介されている。 要はカンマで区切った一連の要素はタプルに入る。 また、右辺のシーケンス(タプルでなくても良い)の要素を左辺の変数に代入できる。 多重代入はシーケンスアンパッキングであるという記述がある。 # タプルパッキング >>> foo = 1,2,3 >>> foo (1, 2, 3) # シーケンスアンパッキング >>> a,b,c = foo >>> a 1 >>> b 2 >>> c 3 集合 重複しない要素を順序を持たないで保持するコレクション。いわゆる集合演算を備えている。 主に存在判定に用いるという記述がある。重複と順序がなければ任意の値へ高速にアクセス可能なデータ構造で実装できる。 空集合の作り方は少し異なる。間違って空の辞書を作ってしまわないように注意。 >>> hoge = {1,2,3,4,5} >>> hoge {1, 2, 3, 4, 5} # 空の集合 >>> phi = set() >>> phi set() # 空のディクショナリ >>> phi2 = {} >>> phi2 {} 集合内包も可。 >>> z = set() >>> for x in \'abracadabra\': ... if x not in \'abc\': ... z.add(x) ... >>> z {\'d\', \'r\'} >>> z2 = { x for x in \'abracadabra\' if x not in \'abc\'} >>> z2 {\'d\', \'r\'} 辞書 連想配列。キーをインデックス、スライスで書き換えられないデータ構造。 辞書は、値を何らかのキーと共に格納しキー指定で値を取り出すことを目的とするデータ構造。 存在するキーを再代入することで上書き。存在しないキーによるアクセスはエラー。 キーに対してimmutableである前提を置くことでインデックス、スライスで書き換えられないことを保証する。 数値、文字列、immutableな要素だけからなるタプルはキーになる。 可変な要素を持つタプルやリストについては、キー自体を変更できてしまうことになるからNG。 言い換えると辞書は「キー:バリュー」を要素とする集合。 # 初期化 >>> c = { \'hoge\': 100, \'fuga\': 200, \'foo\': 300 } >>> c {\'hoge\': 100, \'fuga\': 200, \'foo\': 300} # キーバリュー追加 >>> c[\'bar\'] = 400 >>> c {\'hoge\': 100, \'fuga\': 200, \'foo\': 300, \'bar\': 400} # キーによるアクセス >>> c[\'fuga\'] 200 # キーバリューの削除 >>> del(c[\'foo\']) >>> c {\'hoge\': 100, \'fuga\': 200, \'bar\': 400} # キーの存在チェック >>> \'hoge\' in c True >>> \'hogehoge\' in c False 注釈に「連想記憶(associative memories)という名前のデータ型をもったプログラム言語はない」という記述がある。 この辺りの使われ方がカオスな言語としてphpがあると思うが、phpは「配列」の添え字として数値も文字列も使える、 という仕様であって「連想配列」という型があるわけでない。 # 順序なしでキーをリスト化 (キーの登録順??) >>> list(c) [\'hoge\', \'fuga\', \'bar\'] # キーでソートしてキーをリスト化 >>> sorted(c) [\'bar\', \'fuga\', \'hoge\'] 辞書内包もできる。 >>> { x: x**2 for x in (2,4,6)} {2: 4, 4: 16, 6: 36} 辞書の初期化は色々バリエーションがある。 # dictのコンストラクタにタプルのリストを指定する >>> d = dict([(\'hoge\',100),(\'fuga\',200),(\'foo\',300)]) >>> d {\'hoge\': 100, \'fuga\': 200, \'foo\': 300} # dictのコンストラクタに個数可変のキーワード引数を指定する >>> e = dict(hoge=100,fuga=200,foo=300) >>> e {\'hoge\': 100, \'fuga\': 200, \'foo\': 300} ループの仕方 辞書からキーバリューを取る。 >>> hoge = { \"hoge\" : 100, \"fuga\" : 200, \"foo\" : 300 } >>> for k, v in hoge.items(): ... print(k,v) ... hoge 100 fuga 200 foo 300 シーケンスからインデックスと値をとる。 >>> fuga = [ 1, 3, 5, 7 ] >>> for i,j in enumerate(fuga): ... print(i, j) ... 0 1 1 3 2 5 3 7 2つ以上のシーケンスから同時に値をとる。 >>> ary1 = [ \"a\", \"b\", \"c\" ] >>> ary2 = [ 100, 200, 300 ] >>> for i, j in zip(ary1, ary2): ... print(i, j) ... a 100 b 200 c 300 条件 条件についての諸々が書いてある。 論理演算子の優先順位は not > and &gt or。なので A and not B or C = (A and (not B)) or C。 論理演算子andとorは短絡評価。if A and B and C において BがFalseであればCは評価されない。 最後に評価された A and B が全体の評価結果となる。 比較は連鎖可能。if a < b == c と書くと、a < b と b == c の2つが評価される。 a > 1 and b > 3 を 1 < a < 3 と書ける。 式の中での代入は:=演算子を使わないとできない。 # 式の中での代入は:= >>> if a := 100 == 100 : ... print(\"hoge\") ... hoge # C風の書き方はNG >>> if a = 100 == 100 : File \"\", line 1 if a = 100 == 100 : ^ SyntaxError: invalid syntax シーケンスの比較 同じシーケンス型同士を比較が出来てしまう。 前から順に再帰的に要素を比較する。ある時点で要素が異なっていればその比較結果が最終結果。 最後まで要素が同じであれば、シーケンスは同じ判定になる。 片方が短い場合、短い方が小となる。 文字の比較はUnicodeコードポイント番号の比較が行われる。 異なる型の比較の場合、オブジェクトがその比較をサポートしている限り行われる。 比較をサポートしていない場合エラー。 >>> (1,2,3) >> (1,2,3) >> (1,2) >> \'a\' < 'b' >> \'c\' < 'b' >> 10 >> 1 == \"1\" False # 整数と文字列の > はサポートされていないためエラー >>> 1 > \"1\" Traceback (most recent call last): File \"\", line 1, in TypeError: \'>\' not supported between instances of \'int\' and \'str\' モジュール 呼び出し元のシンボル表を汚さないimport hoge.pyというファイルに関数fugaを用意しモジュールhogeをインポートする。 関数fuga()の完全な名称はhoge.fuga。hogeはモジュール名,fugaはモジュール内の関数名。 モジュールはimport元とは異なるローカルなシンボル表を持つ。 importによってモジュール内のシンボルが呼び出し元のシンボル表を汚すことはない。 ~/i/pytest cat hoge.py 26.7s  土 4/30 14:40:15 2022 def fuga(v): print(v) ~/i/pytest python Python 3.9.11 (main, Apr 11 2022, 01:59:37) [Clang 10.0.1 (clang-1001.0.46.4)] on darwin Type \"help\", \"copyright\", \"credits\" or \"license\" for more information. >>> >>> import hoge >>> hoge.fuga(123) 123 >>> foo = hoge.fuga >>> foo(321) 321 モジュール内のシンボルを呼び出し元のシンボル表に直接取り込む とはいえ、モジュール名を修飾しなければならないのはあまりに遠すぎる。 モジュールではなくモジュール内のシンボルを直接呼び出し元に取り込むことができる。 以下の通りhogeモジュール内の関数fugaを呼び出し元のシンボル表に直接ロードし呼び出している。 なお、この場合モジュール自体は呼び出し元のシンボル表に取り込めない。 呼び出し元に同名のシンボルがある場合、上書きされる。 >>> def fuga(v): ... print(v**2) ... >>> fuga(3) 9 >>> from hoge import fuga >>> fuga(3) 3 より楽をしたいのであればimport * を使うとモジュール内のアンダースコア(_)で始まるシンボル以外の全てを読み込むことができる。 ただ、シンボル名を指定しないで呼び出し元のシンボル表を上書きするのはあまりに乱暴なので、通常推奨されない。 >>> from hoge import * >>> fuga(300) 300 モジュール内のシンボルをインポートする際に、呼び出し元のシンボルを上書きしないために、 別名をつけてインポートすることができる。 >>> from hoge import fuga as foo >>> foo(3) 3 モジュールはimportされた最初の1回だけ評価される。 関数であれトップレベルに書いたコードであれ最初の評価時に1回実行される。 ロード済みのモジュールを変更する場合インタープリタの再ロードが必要となる場合がある。 または明示的にimportlib.reload()を使ってモジュールをリロードする。 >>> import importlib >>> importlib.reload(hoge) モジュールから他のモジュールをimportすることはできる。 慣例ではimport文はモジュールの先頭で記述すべきだが先頭でなくても許容される。 モジュールをスクリプトとして実行可能にする pythonコマンドの引数としてモジュールを渡すと、モジュール内において__name__が__main__となる。 これを利用して、pythonコマンドの引数として実行された場合にのみ動くコードを付与できる。 まぁ、モジュール単体でスクリプトからデバッグする時なんかに使うんだろう。 # hoge.py def fuga(v): print(v) if __name__ == \"__main__\": import sys fuga(int(sys.argv[1])) # モジュールのインポート時はifブロック内は評価されない >>> from hoge import fuga >>> # pythonコマンドの引数として実行した場合にifブロック内が評価 ~/i/pytest python hoge.py 3 1100ms  水 5/ 4 21:30:09 2022 3 モジュール検索パス 指定したモジュールを探す順序。同名のモジュールが複数ある場合には優先してインポートされる。 例えば hoge という名前をモジュール名として指定した場合、hoge.py を探し出す。 ビルトインモジュール内。無ければ以下 sys.path変数に格納されるディレクトリリスト。初期値は以下。 入力スクリプトがあるディレクトリ、カレントディレクトリ/li> 環境変数 PYTHONPATH インストールごとのデフォルト? やたら曖昧で文書を読むのが嫌になるような書かれ方をしている。合っているのか?解釈してみる。 sys.pathはappend()等により変更できる。sys.pathの初期値は直感と合うように構成されている。 基本的にはプロジェクトディレクトリにモジュールを配置する訳で、標準ライブラリよりも先に ユーザ定義モジュールが読まれるように探してもらいたい。 ユーザ定義モジュールが無い場合に標準ライブラリを探して欲しい訳だから、 標準ライブラリはsys.pathの後の方に配置する。 標準ライブラリと同じ順位の位置にユーザ定義モジュールを置くと「置き換え」の扱いとなる。 この「置き換え」について事故が起こらないような仕組みがあり後述する。 コンパイル済みPythonファイル モジュールの読み込みを高速化する目的で、 Pythonはモジュールファイルをプラットフォーム非依存の形式でキャッシュする。 あくまでも読み込みが高速化されるだけで、読み込まれたコードの実行が速くなる訳ではない。 キャッシュ場所は__pycache__ディレクトリ。 キャッシュヒット判定はモジュールファイルの最終更新日時で行われる。 つまり新しいモジュールファイルがあればヒットせずソースが読まれる。 モジュールのソースを削除しキャッシュだけを配置すると、 常にキャッシュが読まれる。この仕組みにより「ソース無し配布」が可能になる。 スクリプトから読み込む場合、常にキャッシュは使われない。 パッケージ 直感的には名前空間の定義。異なる名前空間のモジュール同士、シンボル名の衝突を避けられる。 公式リファレンスは以下。インポートシステム 多くの処理系で、名前空間を解決するために結構泥臭い実装になっている部分。 以下のディレクトリ階層と__init__.pyにより、dir1、dir1_1、dir1_2パッケージを定義する。 tree . 水 5/ 4 22:32:48 2022 . └── dir1 ├── __init__.py ├── dir1_1 │   ├── __init__.py │   ├── p1.py │   ├── p2.py │   └── p3.py └── dir1_2 ├── __init__.py ├── q1.py ├── q2.py └── q3.py dir1パッケージの下にdir1_1、dir1_2パッケージがある。dir1_1パッケージの下にp1,p2,p3モジュールがある。 p1,p2,p3はモジュールであり、実際には各モジュール内に関数やクラスなどのimportすべきシンボルがある。 例えばp1の中にhoge_p1()という関数があるとして、以下でhoge_p1をimportできる。 なお、dir1直下の__init__.pyには\"__init__.py dir1\"、 dir1_1直下の__init__.pyには\"__init__.py dir1_1\"という文字列をprint()している。 # dir1.dir1_1パッケージのp1モジュールをインポートしhoge_p1()を実行 >>> import dir1.dir1_1.p1 __init__.py dir1 __init__.py dir1_1 >>> p1.hoge_p1() This is p1. 読み込みシーケンスとしては、まず dir1直下の__init__.py内のコードが実行され dir1名前空間の初期化が終わる。 次にdir1_1直下の__init__.py内のコードが実行され、dir1_1名前空間の初期化が終わる。 __init__.pyを置くことで初めてdir1,dir1_1が名前空間であることが定義される。 ワイルドカードimport dir1.dir1_1の下にある p1,p2,p3...を呼び出すために dir1.dir1_1.p1 のようにモジュール名(p1)までを 指定しないといけないのであれば、p1,p2,p3それぞれを個別にimportしないといけなくなる。 またもしp4が追加された場合、 呼び出し元にp4のimportを追加しないといけなくなるかもしれない。 dir1.dir1_1をimportするだけでp1,p2,p3を呼び出せることを期待してしまう。 それを実現するために__init__.pyを使うことができる。 ワイルドカード(*)を使ったimportを行う際、__init__.pyに対象のモジュールを__all__に 定義しておかないと、ワイルドカード(*)importでは何もimportされない。 例えば、dir1_1直下の__init__.pyで__all__としてp1とp2を指定しp3を指定しない場合、 p1,p2はimportされるがp3はimportされない。このように明示しないと*によるimportは出来ない。 # dir1/dir1_1/__init__.pyの記述 __all__ = [\"p1\",\"p2\"] # *を使ったimportと実行 >>> from dir1.dir1_1 import * __init__.py dir1 __init__.py dir1_1 >>> p1.hoge_p1() This is p1. >>> p1.hoge_p3() Traceback (most recent call last): File \"\", line 1, in AttributeError: module \'dir1.dir1_1.p1\' has no attribute \'hoge_p3\' また、別のやり方として、__init__.pyにモジュールのimportを書いておくやり方をしている人がいた。 ディレクトリと対応するパッケージをimpoortすることで同時に配下のモジュールからシンボルをimportする。 この例だと__all__を設定した方が良さそうだが、__init__.pyの動作を理解の助けになる。 # dir1/dir1_1/__init__.pyを以下の通りとする from .p1 import hoge_p1 from .p2 import hoge_p2 print(\"__init__.py dir1_1\") # ワイルドカードimport >>> from dir1.dir1_1 import * __init__.py dir1 __init__.py dir1_1 >>> p1.hoge_p1() This is p1. >>> p2.hoge_p2() This is p2. 何やら歴史的な経緯があるようで、かなり分かりづらい仕様となっている。 「名前空間パッケージ」と「普通のパッケージ」のようなカオスな世界が広がっている。 python3.3以降、ディレクトリ内に__init__.pyを置かなくても、ディレクトリ階層を名前空間として 認識してくれるような振る舞いになっている。ただ、この振る舞いは名前空間パッケージの一部でしかなく、 無条件に「python3.3以降は__init__.pyは不要である」ということではない。 PEP 420: Implicit Namespace Packages Native support for package directories that don’t require __init__.py marker files and can automatically span multiple path segments (inspired by various third party approaches to namespace packages, as described in PEP 420) 入出力 文字列のフォーマット 他言語にある変数内展開と近いのはf-string。接頭辞fをつけた文字列の内部にブラケットで括った 式を記述すると、そのブラケット内の変数が文字列に展開される。 式の後ろにフォーマット指定子を指定することで細かい表現ができる。 >>> year = 2020 >>> event = \'hoge\' >>> f\'Results of the {year} {event}\' \'Results of the 2022 hoge\' >>> import math >>> f\'πの値はおよそ{math.pi:.3f}である。\' \'πの値はおよそ3.142である。\' >>> table = {\'hoge\':100,\'fuga\':200,\'foo\':300} >>> for key,value in table.items(): ... print(f\'{key:10} ==> {value:10d}\') ... hoge ==> 100 fuga ==> 200 foo ==> 300 stringモジュール内にあるTmeplateクラスにも近い機能がある。 SQLのプレースホルダリプレイスメントのような使い方で文字列をフォーマットできる。 >>> from string import Template >>> hoge = 100 >>> fuga = 200 >>> s = Template(\'hoge is ${hoge}, fuga is ${fuga}\') >>> print(s.substitute(hoge=hoge,fuga=fuga)) hoge is 100, fuga is 200 str.format()により、文字列の中にプレースホルダを配置し、渡した変数でリプレースする。 プレースホルダ内に位置情報を含めない場合、format()に渡した値が左から順番にリプレースされる。 位置引数やキーワード引数とすることもできる。その場合format()に渡す値の順序に囚われない。 他言語で良くやるコレクションを渡して文字列に展開する方法が書かれている。 # プレースホルダ空文字. フォーマット指定子. >>> yes_votes = 42_572_654 >>> no_votes = 43_132_495 >>> percentage = yes_votes / (yes_votes + no_votes) >>> \'{:-9} YES votes {:2.2%}\'.format(yes_votes, percentage) \' 42572654 YES votes 49.67%\' # 位置引数 >>> f\'This is {0}, That is {2}, This was {1}, That was {4}\'.format(1,2,3,4) \'This is 0, That is 2, This was 1, That was 4\' # キーワード引数 >>> aaa = 300 >>> bbb = 400 >>> \'This is {aaa}, that is {bbb}.\'.format(aaa=aaa,bbb=bbb) \'This is 300, that is 400.\' # dictを渡す >>> table = {\'hoge\': 1, \'fuga\':2, \'foo\': 3} >>> \'hoge is {0[hoge]:d}, fuga is {0[fuga]:d}, foo is {0[foo]:d}\'.format(table) \'hoge is 1, fuga is 2, foo is 3\' # **表記でdictを渡す(可変長引数) >>> \'hoge is {hoge:d}, fuga is {fuga:d}, foo is {foo:d}\'.format(**table) \'hoge is 1, fuga is 2, foo is 3\' 単純に加算演算子+を使って文字列を結合して自力でフォーマットできる。 その際、オブジェクトを文字列に型変換する必要がありstr()を使う。 >>> s2 = \'String 1 is \' + str(hoge) + \',String 2 is \' + str(fuga) >>> s2 \'String 1 is 100,String 2 is 200\' 右寄せはrjust()、左寄せはljust()、中央寄せはcenter()。指定した幅の中で文字列を寄せる。 指定した幅よりも値が長い場合切り詰めない。切り詰める場合、スライスで部分文字列を取得。 print()に複数の値を与えると、各値の間に空白が1つ挿入される。 print()はデフォルトで末尾が改行となるが、キーワード引数でendとして空文字を 渡すことで末尾を空文字に書き換えられる。 # 右寄せ >>> for x in range(1,11): ... print(repr(x).rjust(2), repr(x*x).rjust(3), end=\' \') ... print(repr(x*x*x).rjust(4)) ... 1 1 1 2 4 8 3 9 27 4 16 64 5 25 125 6 36 216 7 49 343 8 64 512 9 81 729 10 100 1000 ゼロ埋めはzfill()。右寄せして左側にゼロを埋める。 >>> for x in range(1,11): ... print(repr(x).zfill(5)) ... 00001 00002 00003 00004 00005 00006 00007 00008 00009 00010 C言語のprintf()風の文字列補完 正直最初からこれを使っておけば良い気がするが、printf()のような文字列補完ができる。 >>> \'This is %d, That is %d, This was %d, That was %d\' % (1,2,3,4) \'This is 1, That is 2, This was 3, That was 4\' ファイルの読み書き C言語のfopen()を単純化したようなインターフェースが備わっている。 モードは\'r\'が読み取り専用、\'w\'が書き込み専用、追記なら\'a\',読み書き両用なら\'r+\'。 省略時には\'r\'。それぞれモード文字の末尾に\'b\'を付与することでバイナリ対応可。 開いたファイルはclose()により必ず閉じる必要があり、try-finallyのパターンで対応する。 withを利用することでclose()を省略しつつclose()のコールを保証できる。 withはGCによりリソースを破棄する。実際の破棄はGCのタイミング次第。 # try-finally >>> def open_hoge(): ... try: ... fh = open(\'hoge.txt\', \'r\') ... read_data = f.read() ... finally: ... fh.close() ... >>> open_hoge() # with >>> def open_hoge2(): ... with open(\'hoge.txt\',\'r\') as f: ... read_data = f.read() ... >>> open_hoge2() >>> read(SIZE)によりファイルからデータを読み取る。テキストモードの場合、単位は[文字]。 テキストモードの場合UNICODEでもASCIIでも指定した文字だけ取得してくれる。 バイナリモードの場合、単位は[バイト]。 SIZEのデフォルトは-1が指定されていて、ファイル内の全てを読み取る。 省略するとSIZE=-1が使われる。 >>> with open(\'hoge.txt\',\'r\') as f: ... v = f.read(1) ... print(v) ... h テキストファイルから各行にアクセスする、というのが良くある使い方。 readline()はファイルから改行コード単位に1行読み込む。 ファイルオブジェクトが開かれている限り,コールにより次の行を読み進める。 最終行を読み取った後、readlineは空文字を返すようになる。 >>> fh = open(\'hoge.txt\',\'r\') >>> fh.readline() \'hogehogen\' >>> fh.readline() \'fugafugan\' >>> fh.readline() \'foofoon\' >>> fh.readline() \'\' ファイルオブジェクトにループをかけると省メモリで全行を読み取れる。 >>> with open(\'hoge.txt\') as f: ... for line in f: ... print(line,end=\'\') ... hogehoge fugafuga foofoo そして readlines(),list()により各行をシーケンスで取得できる。 >>> with open(\'hoge.txt\') as f: ... ls = f.readlines() ... print(ls) ... [\'hogehogen\', \'fugafugan\', \'foofoon\'] >>> with open(\'hoge.txt\') as f: ... l = list(f) ... print(l) ... [\'hogehogen\', \'fugafugan\', \'foofoon\'] write()によりファイルに書き込める。 非文字列を書き込む場合はstr()などにより先に文字列化する必要がある。 >>> with open(\'fuga.txt\',\'w\') as f: ... f.write(\'This is testn\') ... 13 #書き込んだキャラクタの数。 >>> with open(\'fuga.txt\') as f: ... print(f.readline()) ... This is test # シーケンスを文字列化して書き込む >>> with open(\'fuga.txt\',\'w\') as f: ... ary = [1,2,3,4,5] ... f.write(str(ary)) ... 15 >>> with open(\'fuga.txt\') as f: ... l = f.readline() ... print(l) ... [1, 2, 3, 4, 5] 構造があるデータをjsonで保存 dumps()により構造化データをJSONにシリアライズできる。 dumps()とwrite()を組み合わせるかdump()を使うことでJSONをファイルに書き込める。 # dictをJSONにシリアライズ >>> ary = { \'hoge\':100, \'fuga\':200, \'foo\':300 } >>> json.dumps(ary) \'{\"hoge\": 100, \"fuga\": 200, \"foo\": 300}\' # 一度にdictをシリアライズしてファイルに書き込む >>> dict = {\'hoge\':100, \'fuga\':200, \'foo\':300} >>> with open(\'fuga.txt\',\'w\') as f: ... json.dump(dict,f) ... >>> with open(\'fuga.txt\') as f: ... print(f.readlines()) ... [\'{\"hoge\": 100, \"fuga\": 200, \"foo\": 300}\'] # JSONをでシリアライズ >>> js = json.dumps(dict) >>> js \'{\"hoge\": 100, \"fuga\": 200, \"foo\": 300}\' >>> jjs = json.loads(js) >>> jjs {\'hoge\': 100, \'fuga\': 200, \'foo\': 300} # ファイル内のJSONをdictにデシリアライズ >>> with open(\'fuga.txt\') as f: ... v = json.load(f) ... print(v) ... {\'hoge\': 100, \'fuga\': 200, \'foo\': 300} 続く...

default eye-catch image.

Pythonデータ構造2

引き続きPythonデータ構造。 入れ子のリスト内包 入れ子のリスト内包を説明するために転置行列が使われていてわかりやすい。 このあたりがデータ処理用言語である所以な気がする。 matrix = [ [1,2,3,4], [5,6,7,8], [9,10,11,12] ] # 全行を出力 for i in range(4): for x in matrix: print(x) [1, 2, 3, 4] [5, 6, 7, 8] [9, 10, 11, 12] # 全行のi列目をリスト化 for i in range(4): v = [row[i] for row in matrix] print(v) [1, 5, 9] [2, 6, 10] [3, 7, 11] [4, 8, 12] # 同じことをワンライナーで v = [[row[i] for row in matrix] for i in range(4)] print(v) [1, 5, 9] [2, 6, 10] [3, 7, 11] [4, 8, 12] リストの要素削除 del文で要素を削除する。 a = [1,2,3,4,5] del a[3] print(a) [1, 2, 3, 5] del a[:] print(a) [] タプル 似て非なるデータ構造リストとタプル。リストは変更可能。タプルは変更不能。 タプルの要素として変更可能な変数を含めることはできる。 作り方が特殊。要素をカンマ区切りで並べるとタプルができる。 要素が1個の場合は、他の変数と区別が付かないので末尾にカンマを配置する。 要素が0個の場合は、()とする。 empty = () print(empty) () tupple = 100,200,300 print(tupple) (100, 200, 300) onetupple = 100, print(onetupple) (100,) 集合 データ分析用言語なのでこういうのは豊富。順序無し、重複無しのデータを格納する。 重複データを加えるときに重複が除去される。 hoge = {\'hoge1\',\'hoge2\',\'hoge3\',\'hoge4\'} print(hoge) set([\'hoge4\', \'hoge1\', \'hoge2\', \'hoge3\']) fuga = {\'fuga1\',\'fuga1\',\'fuga1\'} print(fuga) set([\'fuga1\']) リスト内包と同じ書き方で集合内包を記述できる。 v = {x for x in \'abcdefg\' if x not in \'abc\'} set([\'e\', \'d\', \'g\', \'f\']) ディクショナリ 連想配列。key-valueを格納する。 初期化の方法が複数あるので流してみる。 dic = dict([(\'hoge\',100),(\'fuga\',500),(\'hogehog\',1000),(\'fugafuga\',2000)]) print(dic) {\'fugafuga\': 2000, \'fuga\': 500, \'hogehog\': 1000, \'hoge\': 100} dic2 = dict(hoge=100, fuga=500, hogehgoe=1000, fugafuga=2000) print(dic2) {\'fugafuga\': 2000, \'fuga\': 500, \'hogehgoe\': 1000, \'hoge\': 100} ループ内でディクショナリのKeyValueを一度に取る方法を試してみる。 for k,v in dic.items(): print(k,v) (\'fuga\', 500) (\'hogehog\', 1000) (\'hoge\', 100) ちなみに、普通のリストは先頭から0,1,2,.... のように順序数が振られているはずで、 それを取得するにはenumerate()関数を使う。ループ外で変数を確保してループ内でインクリメントしたりしない。 l = [\'hoge\',\'fuga\',\'hogehoge\',\'fugafuga\'] (0, \'hoge\') (1, \'fuga\') (2, \'hogehoge\') (3, \'fugafuga\') 2つ以上のシーケンスにループをかけるとき普通はn重ループを書くと思うけれども、 Pythonはzip()関数とアンパッキングを使って1行で書くことができる。 これは良くみるし、Pythonのキモである気がする。 qq = [\'aaa1\', \'bbb1\',\'ccc1\',\'ddd1\'] rr = [100, 200, 300, 400] for q, r in zip(qq,rr): print(\'The value of qq is {0}. The value of rr is {1}\'.format(q,r)) The value of qq is aaa1. The value of rr is 100 The value of qq is bbb1. The value of rr is 200 The value of qq is ccc1. The value of rr is 300 The value of qq is ddd1. The value of rr is 400 逆順,ソート後ループ 正順のシーケンスにreversed()関数をかますと非破壊で逆順ソートできる。 それをループに使うと逆順ループができる。 sorted()関数をかますと非破壊でシーケンスをソートできる。 それをループに使うとソート後ループができる。 n = [1,2,3,4,5,6] for i in reversed(n): print(i) 6 5 4 3 2 1 r = [\'alpha\',\'beta\',\'gamma\',\'omega\',\'epsilon\'] for i in sorted(r): print(i) alpha beta epsilon gamma omega 演算子の優先順位 比較演算子は全て同じ優先順位。以下は左から順に比較が行われる。 まずa<bの比較が行われ、次にb==cの比較が行われる。a<bかつb==cであれば以下は真。 a < b == c 不思議な感じだけれども、これにより以下の比較がワンライナーでできる。 a > 1 and a <3 1 < a < 3 ブール演算子は比較演算子よりも優先順位が低い。 and,orの優先順位は同じ。notはand,orより高い。以下は同じ。 A and not B or C (A and (not B)) or C ブール演算は左から順番に評価され、途中で結論が確定したときに評価は中断される。 Cと異なり、式の途中で代入することはできない。つまり以下みたいなことはできない。 if a=10<20: シーケンスの比較 同じシーケンス型の変数を比較できる。辞書順で要素を比較していき全てが同じであれば真を返す。 両者が異なっていれば、異なっていた箇所の要素の辞書順の大小を返す。 いずれかが片方のサブセットである場合、短い方が小、長い方が大となる。

default eye-catch image.

Pythonデータ構造

リスト 覚えるものでもないのだけれども一度は通しておきたいリストのメソッド。 大方の言語と違って、基本的に破壊的メソッドだらけ。 append()で末尾に追加。 v = [] v.append(10) v[len(v):] = [30] print(v) [10 20] リストを伸長。 r = [20] v.extend(r) print(v) [10 20 30] リストに挿入。 v.insert(0,100) print(v) [100, 10, 30, 20] 要素の削除。 v.remove(10) print(v) [100, 30, 20] 要素のインデックス。indexOf(...)みたいな。 要素が無いと-1を返しそうだけれどもエラー。 idx = v.index(20) print(idx) idx2 = v.index(1000) print(idx2) ValueError: 1000 is not in list 含まれる要素xの個数を返す不思議なメソッド。 cnt = v.count(20) print(cnt) 要素を逆順にする。コピーしたものを逆順にするのではなくそのものを逆順にする。 v.reverse() [20, 30, 100] コピーする。 vv = v.copy() vv[0] = \'hoge\' print(v) print(vv) [20, 30, 100] [\'hoge\', 30, 100] リスト内包 例えば0から9までの値の2乗のリストを作成するコードは以下。 squares = [] for x in range(10): squares.append(x**2) [0, 1, 4, 9, 16, 25, 36, 49, 64, 81] Pythonっぽくワンライナーで書くとこうなる。 map関数は高階関数。第1引数として関数、第2引数としてシーケンスを取る。 lambdaは関数のSyntaxSugarなので第1引数はlambdaも渡せる。 渡したシーケンスの全要素についてlambda式を実行した結果を返す。 range(10)は0から9まで。lambda x:x**2は引数を2乗する。 なのでmap(lambda x: x**2, range(10))は、0 1,4,9,...を返す。 それをlist()でリスト化する。 squares = list(map(lambda x: x**2, range(10))) print(squares) [0, 1, 4, 9, 16, 25, 36, 49, 64, 81] Pythonでは、これが言語仕様で出来るようになっている。リスト内包。 []の中の最初は式。式に続けてfor式,if式を書ける。 squares2 = [x**2 for x in range(10)] print(squares2) [0, 1, 4, 9, 16, 25, 36, 49, 64, 81] より複雑な式,for式をリスト内包にしてみる。以下は等価。 cs = [] for x in [1,2,3]: for y in [3,1,4]: if x != y: cs.append((x,y)) print(cs) [(1, 3), (1, 4), (2, 3), (2, 1), (2, 4), (3, 1), (3, 4)] cs2 = [(x,y) for x in [1,2,3] for y in [3,1,4] if x != y] print(cs2) [(1, 3), (1, 4), (2, 3), (2, 1), (2, 4), (3, 1), (3, 4)] 色々な書き方が出来る。戻り値をタプルにしたい場合は()で囲う必要がある。 vec = [-4, -2, 0 ,2, 4] cs3 = [x*2 for x in vec] print(cs3) [-8, -4, 0, 4, 8] cs4 = [x for x in vec if x >= 0] print(cs4) [0, 2, 4] cs5 = [(x, x**2) for x in range(10)] print(cs5) [(0, 0), (1, 1), (2, 4), (3, 9), (4, 16), (5, 25), (6, 36), (7, 49), (8, 64), (9, 81)] 以下面白い。リスト内包には入れ子の関数も含められる。 from math import pi cs6 = [str(round(pi, i)) for i in range(1,6)] print(cs6)

default eye-catch image.

Python制御構造2

任意引数 大方の言語と同じように任意引数を定義できる。任意引数の位置に渡した引数はタプルに変換される。 複数の変数を1つのタプルに変換する操作を、詰める(pack)と言ったりする。 もちろん任意引数の後はキーワード引数しか置けない。 def concat(*args,sep=\"/\"): return sep.join(args) val=concat(\'hoge\',\'fuga\',\'hogehoge\',\'fugafuga\') print(val) hoge/fuga/hogehoge/fugafuga unpack 逆に1つのタプルなりリストを複数の変数にバラすことをunpackと言ったりする。 hoge = [1,2,3] x,y,z = hoge print(x) print(y) print(z) 1 2 3 lambda 大方の言語と同じように無名関数を書ける。lambdaは1行しか書けない。 中では関数になっている。関数が関数外の変数を参照できるのと同様に lambdaからlambda外の変数を参照できる。 def make_incrementor(n): return lambda x:x+n f = make_incrementor(42) v =f(0) print(v) 42 r =f(10) print(r) 52 関数アノテーション 関数の引数、戻り値に付加情報を付けることができる。 PHPのTypeHintingみたいに、引数の型,戻り値の型を書ける。 型として実在しないクラスを指定するとエラーになる。 def f(hoge: str, fuga: str = \'fuga\') -> str: print(\"Annotations:\", f.__annotations__) print(\"Arguments:\", hoge, fuga) return hoge + \' and \' + fuga f(\'1\',\'2\') Annotations: {\'hoge\': , \'fuga\': , \'return\': } Arguments: 1 2 コーディングスタイル Pythonのコーディングスタイル。なんだかその理由がどうでも良い感じ。 やるのであればRubyのように外れたらエラーにするべきだし。 インデントはスペース4個。タブは使わない。 79文字以下で折り返す。 関数、クラス、関数内の大きめのブロックを分離するために空白行を使用 コメントは文末ではなく行として独立すべき docstringを使う 演算子の周囲やカンマの後ろはスペースをいれる。 括弧のすぐ内側にはスペースをいれない。 関数名、メソッドはlower_case_with_underscores UTF-8を使う。

default eye-catch image.

python制御構造1

個人的には、言語に入門する際にはオンラインより書籍が適切と考えている。 そして最初の一冊はなるべく薄いものを選ぶべきと考えている。 読んだ後、あえて文章として書き出すと良さそう。行間なりツボを自分の言葉でおまとめ。 あそこにこう書いてあった..というように思い出せると良いかなと。 まずは制御構造。 if ... elif ... else ブロックの開始はコロン。ブロックはインデントで表現。 else ifは繋げて書く。elif,elseはオプション。 x = 20 if x < 0: print("負の値") elif x == 0: print("ゼロ") elif x == 1: print("いち") else: print("その他") for 初期値、終了値、増分を書くCスタイルと違う。シーケンスに対してforeachをかけるスタイルのみ。 ループにかけたからといってシーケンスがコピーされることはなく変更することができる。 当然不安定なのでシーケンスをコピーしてループに使うべき、。 words = [1,2,3,4,5] for x in words: print(x) # for x in words[:] words.insert(0,x) シーケンスを最後まで読み終わった後に実行するブロックを定義できる。 forと同じインデント位置にelseを書く。 words = [1,2,3,4,5] for x in words: print(x) else: print(\"finish\") 1 2 3 4 5 finish range ループはシーケンスを処理するのみなので、シーケンスが無い場合は作る必要がある。 nからn-1ならrange(n)。初期値、終了値、ステップはrange(初期値,終了値,ステップ)。 range(n)はシーケンスを作るのではなく反復可能体を返す。つまり、range()の応答時には シーケンスはメモリ確保されておらず評価時に初めてメモリ確保される。ref.C++ iterator。 for x in range(10): print(x**2) 0 1 4 9 16 25 36 49 64 81 for x in (range(0,10,3)) print(x) 0 3 6 9 シーケンスを最初から最後までなめるのは以下。 ary = [\"hoge1\",\"hoge2\",\"hoge3\"] for x in range(len(ary)): print(ary[x]) hoge1 hoge2 hoge3 break,continue forループ,whileループを抜ける構文。 breakでforループを抜けた場合、for,whileに対応するelse:ブロックは評価されない。 words = [1,2,3,4,5] for x in words: print(x) if x==3: break else: print(\"finish\") 1 2 3 pass, ... pass,または...と書くと何もしない行を書くことができる。 コードに対称性が無くなったときにあえて書いておきたくなりそう。 これは悪くないかも.. words = [1,2,3,4,5] for x in words: print(x) if x==3: ... # don\'t forget elif x==4: pass # don\'t forget else: print(\"finish\") 1 2 3 4 5 finish 関数 関数の書き方。大方の言語のようにドキュメンテーションのやり方がある。 何故か関数の中に書く。docstringという。不思議。 後で自動集計してくれる。他のフォーマットを知っておきたい。 形式言語処理っぽい書き方だと、defの実行により新しいシンボル表が作られる。 関数内のあらゆる代入は新しいシンボル表に書かれる。(ローカルスコープ)。 関数内ではこのシンボル表しか参照することができず、implicitにグローバル変数を 参照できない。ほぅほぅ。 ローカルスコープからは、global識別子によりシンボルを修飾することで初めてグローバルの シンボル表にアクセスできる。 実引数がコピーされてローカルシンボル表に加えられるのではない。 関数の実引数の参照がローカルシンボル表に加えられる。 def fib(n): \"\"\"nまでのフィボナッチ級数\"\"\" a,b = 0,1 while a < n: print(a, end=' ') a,b = b, a+b print() fib(100) 0 1 1 2 3 5 8 13 21 34 55 89 関数名は1つのシンボルとして使える。以下のように f = fib f(100) 0 1 1 2 3 5 8 13 21 34 55 89 関数は\"return 値\"により値を返す。\"return\"またはreturnを書かない場合,Noneを返す。 関数のデフォルト値 大方の言語と同じ書き方でデフォルト値を書ける。 def fib(n=100): \"\"\"nまでのフィボナッチ級数\"\"\" a,b = 0,1 while a < n: print(a, end=' ') a,b = b, a+b print() fib() 0 1 1 2 3 5 8 13 21 34 55 89 関数のデフォルト値は1回しか評価されない。最初に1度だけ評価された後使い回される。 以下は\"[1],[2],[3]\"とならない。 def hoge(x,L=[]): L.append(x) return L [1] [1, 2] [1, 2, 3] こう書いておくと期待通りになる。 def fuga(x,L=None): if L is None: L = [] L.append(x) return L キーワード引数 大方の言語と同じ書き方でキーワード引数を書ける。 引数の位置から解放される。キーワード引数でない引数は前に持ってくる。 def fib(n=100): \"\"\"nまでのフィボナッチ級数\"\"\" a,b = 0,1 while a < n: print(a, end=' ') a,b = b, a+b print() fib(n=200) 0 1 1 2 3 5 8 13 21 34 55 89 144

default eye-catch image.

sklearnに頼らずRidge回帰を自力で書いてみて正則化項の影響を考えてみるテスト

[mathjax] タイトルの通り。Losso回帰と違って損失関数を偏微分するだけで出来そうなのでやってみる。 Ridge回帰は線形回帰の1種だけれども、損失関数として最小二乗法をそのまま使わず、 (L_2)ノルムの制約を付けたものを使う((L_2)正則化)。 データとモデル 教師データ(boldsymbol{y})、訓練データ(boldsymbol{x})があるとする。 (または目的変数(boldsymbol{y})、説明変数(boldsymbol{x})があるとする。) 例えば(p)次の属性データが(n)個あり、それらと結果の対応が分かっている状況。 begin{eqnarray} boldsymbol{y} &=& begin{pmatrix} y_1 \\ y_2 \\ vdots \\ y_p end{pmatrix} , boldsymbol{x} &=& begin{pmatrix} x_{11} & x_{21} & cdots & x_{n1} \\ x_{12} & x_{22} & cdots & x_{n2} \\ vdots & vdots & ddots & vdots \\ x_{1p} & x_{2p} & cdots & x_{np} end{pmatrix} end{eqnarray} モデルは以下。特徴ベクトル(boldsymbol{w})は訓練データの重み。 特徴空間において損失を最小化する特徴ベクトルを求める問題。 begin{eqnarray} boldsymbol{y} &=& boldsymbol{w} boldsymbol{x} + k \\ boldsymbol{w} &=& begin{pmatrix} w_1 & w_2& cdots &w_p end{pmatrix} end{eqnarray} 損失関数 普通の2乗損失に正則化項((L_2)ノルムを定数倍した値)を付けたものを損失関数として利用する。 正則化項の係数はハイパーパラメータとして調整する値。逆数なのはsklearnに従う。 begin{eqnarray} L(boldsymbol{w}) = |boldsymbol{y} - boldsymbol{w} boldsymbol{x}|^2 +C |boldsymbol{w}|^2 end{eqnarray} 特徴ベクトルは以下。(mathjaxでargminが出せない...) begin{eqnarray} newcommand{argmin}[1]{underset{#1}{operatorname{arg},operatorname{min}};} boldsymbol{w} = argmin w L(boldsymbol{w}) = argmin w |boldsymbol{y} - boldsymbol{w} boldsymbol{x}|^2 + C |boldsymbol{w}|^2 end{eqnarray} 特徴ベクトルを求める 勾配=0と置けば上の式の解を得られる。 損失関数が微分可能だからできる技。 begin{eqnarray} frac{partial L(boldsymbol{w})}{partial boldsymbol{w}} &=& 2 boldsymbol{w}^T (boldsymbol{y} - boldsymbol{w} boldsymbol{x}) + C boldsymbol{w} \\ &=& 0 end{eqnarray} 変形する。 begin{eqnarray} 2 boldsymbol{x}^T (boldsymbol{x}boldsymbol{w}-boldsymbol{y}) + C boldsymbol{w} &=& 0 \\ boldsymbol{x}^T (boldsymbol{x}boldsymbol{w}-boldsymbol{y}) + C boldsymbol{w} &=& 0 \\ boldsymbol{x}^T boldsymbol{x} boldsymbol{w} -boldsymbol{x}^T boldsymbol{y} + Cboldsymbol{w} &=& 0 \\ (boldsymbol{x}^T boldsymbol{x} +C E) boldsymbol{w} &=& boldsymbol{x}^T boldsymbol{y} \\ boldsymbol{w} &=& (boldsymbol{x}^T boldsymbol{x} + C E)^{-1} boldsymbol{x}^T boldsymbol{y} end{eqnarray} テストデータを作る 練習用にsklearnのbostonデータを使ってみる。 ボストンの住宅価格が目的変数、属性データが説明変数として入ってる。 import pandas as pd import numpy as np from pandas import Series,DataFrame import matplotlib.pyplot as plt from sklearn.datasets import load_boston boston = load_boston() boston_df = DataFrame(boston.data) boston_df.columns = boston.feature_names print(boston_df.head()) boston_df[\"PRICE\"] = DataFrame(boston.target) # CRIM ZN INDUS CHAS NOX RM AGE DIS RAD TAX PTRATIO B LSTAT PRICE # 0 0.00632 18.0 2.31 0.0 0.538 6.575 65.2 4.0900 1.0 296.0 15.3 396.90 4.98 24.0 # 1 0.02731 0.0 7.07 0.0 0.469 6.421 78.9 4.9671 2.0 242.0 17.8 396.90 9.14 21.6 # 2 0.02729 0.0 7.07 0.0 0.469 7.185 61.1 4.9671 2.0 242.0 17.8 392.83 4.03 34.7 # 3 0.03237 0.0 2.18 0.0 0.458 6.998 45.8 6.0622 3.0 222.0 18.7 394.63 2.94 33.4 # 4 0.06905 0.0 2.18 0.0 0.458 7.147 54.2 6.0622 3.0 222.0 18.7 396.90 5.33 36.2 散布図行列を表示してみる。 PRICEと関係がありそうなZN,RM,AGE,DIS,LSTATの5個を使ってみる。 pg = sns.pairplot(boston_df) plt.show() pg.savefig(\'boston_fig.png\') 特徴ベクトルを自力で計算する これを自力で計算してみる。(C=0.01)、(C=0)、(C=100)としてみた。 begin{eqnarray} boldsymbol{w} &=& (boldsymbol{x}^T boldsymbol{x} + C E)^{-1} boldsymbol{x}^T boldsymbol{y} end{eqnarray} X_df = boston_df.drop(columns=[\'CRIM\',\'INDUS\',\'CHAS\',\'NOX\',\'RAD\',\'TAX\',\'PTRATIO\',\'B\',\'PRICE\']) X = X_df.values y = boston.target.T C1 = 0.01 C2 = 0 C3 = 100 e = np.identity(5) w1 = np.dot( np.linalg.inv(np.dot(X.T , X) + C1 * e), np.dot(X.T,y)) w2 = np.dot( np.linalg.inv(np.dot(X.T , X) + C2 * e), np.dot(X.T,y)) w3 = np.dot( np.linalg.inv(np.dot(X.T , X) + C3 * e), np.dot(X.T,y)) print(w1) # [ 0.05338557 5.40396159 -0.01209002 -0.83723303 -0.63725397] print(w2) # [ 0.05338539 5.40403743 -0.01209427 -0.83728837 -0.63725093] print(w3) # [ 0.05612977 4.76664789 0.02374402 -0.38576708 -0.66137596] (C=0)のとき、つまり最小二乗法のとき。 sklearnを使う sklearnのridge回帰モデルを使うと以下みたいになる。 from sklearn.linear_model import Ridge from sklearn.model_selection import train_test_split Xf_train,Xf_test,yf_train,yf_test = train_test_split(X,y,random_state=0) ridge = Ridge().fit(Xf_train,yf_train) print(f\"accuracy for training data:{ridge.score(Xf_train,yf_train):.2}\") print(f\"accuracy for test data:{ridge.score(Xf_test,yf_test):.2f}\") # accuracy for training data:0.68 # accuracy for test data:0.58 print(ridge.coef_) # [ 0.06350701 4.3073956 -0.02283312 -1.06820241 -0.73188192] 出てきた特徴ベクトルを並べてみる 自力で計算したものとsklearnに計算してもらったものを並べてみる。 似てるのか似ていないのかよくわからない .. けど、RMの寄与度が高いというのは似ている。 # 自力で計算 (C=100) # [ 0.05612977 4.76664789 0.02374402 -0.38576708 -0.66137596] # sklearnで計算 # [ 0.06350701 4.3073956 -0.02283312 -1.06820241 -0.73188192] 自力で計算したモデルの正答率を求めてみないとなんとも... そして、正規化項の係数の大小がどう影響するのか、あまり良くわからなかった..。 (L_2)ノルムの制約を付けると、パラメタの大小が滑らかになると言いたかったのだけども。 あと、訓練データに対して68%、テストデータに対して58%という感じで、 大して成績が良くない...。 

default eye-catch image.

NumPy uniqe, File I/O

集合関数 集合関数。ndarrayから重複を取り除きsortした結果を返す。 2dであってもその中から要素を抜き出して1dにする。 hoges = np.array([\"hoge\",\"fuga\",\"hoge\",\"fuga\"]) print(np.unique(hoges)) # [\'fuga\' \'hoge\'] fugas = np.array([[\"hoge\",\"fuga\",\"hoge\",\"fuga\"],[\"hoge2\",\"fuga2\",\"hoge2\",\"fuga2\"]]) print(np.unique(fugas)) # [\'fuga\' \'fuga2\' \'hoge\' \'hoge2\'] ファイルI/O pandasを使わずとも、NumPyだけでファイルI/Oができる。 以下でhoges.npyという無圧縮バイナリファイルが作られる。 それを読み込んで出力する。 hoges = np.array([\"hoge\",\"fuga\",\"hoge\",\"fuga\"]) np.save(\'hoges.npy\',hoges) fugas = np.load(\'hoges.npy\') print(fugas) # [\'hoge\' \'fuga\' \'hoge\' \'fuga\'] 複数の配列を同時に書き込むこともできる。 キーワードを指定して書き込む。キーワードを指定して1つずつ読み込む。 読み込む時はキーワードを指定して参照したときに遅延ロードされる。 hoges = np.array([\"hoge1\",\"fuga1\",\"hoge1\",\"fuga1\"]) fugas = np.array([\"hoge2\",\"fuga2\",\"hoge2\",\"fuga2\"]) np.savez(\'hogefuga.npz\', hoges=hoges, fugas=fugas) hogefugas = np.load(\'hogefuga.npz\') hoges_l = hogefugas[\'hoges\'] fugas_l = hogefugas[\'fugas\'] print(hgoes_l) # [\'hoge1\' \'fuga1\' \'hoge1\' \'fuga1\'] print(fugas_l) # [\'hoge2\' \'fuga2\' \'hoge2\' \'fuga2\']

default eye-catch image.

線形サポートベクトル分類器で画像認識するテスト

線形サポートベクトル分類器で画像認識する流れを理解したので、 定着させるために記事にしてみます。 当然、モデルの数学的な理解がないとモデルを解釈することは不可能だし、 正しいハイパーパラメータを設定することも不可能なので、数学的な理解は不可欠。 NumPy、pandas、matplotlibに慣れないと、そこまで行くのに時間がかかります。 こちらはPythonプログラミングの領域なので、数こなして慣れる他ないです。 機械学習用のサンプル画像で有名なMNISTを使ってNumPy、pandasの練習。 手書き文字認識用の画像データを読み込んでみる。サイズは28x28。各々1byte。 MNISTの手書き文字認識画像の読み込み まず読み込んでみて、データの形を出力してみる。 X_trainは、要素が3個のTupleが返る。3次。 1番外が60000。28x28の2次のndarrayが60000個入っていると読む。 1枚目の画像データはX_train[0]によりアクセスできる。 import tensorflow as tf minst = tf.keras.datasets.mnist (X_train,y_train),(X_test,y_test) = mnist.load_data() print(X_train.shape) # (60000, 28, 28) y_trainは要素が1個のTupleが返る。1次。 1枚目から60000枚目までの画像が0から9のいずれに分類されたかが入っている。 y_train[0]が4なら、1枚目の画像が4に分類された、という意味。 print(y_train.shape) # (60000,) データセットの選択 X_train,y_train、X_test,y_testから、値が5または8のものだけのViewを取得する。 そのために、まず値が5または8のものだけのインデックスを取得する。 NumPyのwhereはndarrayのうち条件を満たす要素のインデックスを返す。 X_trainに入っている60000件の2d arrayのうち、 値が5または8のインデックス(0-59999)を取得するのは以下。 index_train = np.where((X_train==5)|(X_train==8)) print(index_train) # (array([ 0, 11, 17, ..., 59995, 59997, 59999]),) index_test = np.where((X_test==5)|(X_test==8)) print(index_test) # (array([ 8, 15, 23, ..., 9988, 9991, 9998]),) インデックスを使って絞り込む。 X_train,y_train = X_train[index_train],y_train[index_train] X_test,y_test = X_test[index_test],y_test[index_test] print(X_train.shape) # (11272, 28, 28) print(X_test.shape) # (1866, 28, 28) 前処理 0-255の間の値を0-1の間の値に変換する(正規化)。 28x28の画像(2darray)を1x784(1darray)に整形する(平坦化)。 X_train,X_test = X_train / 255.0, X_test / 255.0 X_train = X_train.reshape(X_train.shape[0], X_train.shape[1] * X_train.shape[2]) X_test = X_test.reshape(X_test.shape[0], X_test.shape[1] * X_test.shape[2]) ベストなハイパーパラメータの選択 線形サポートベクトル分類器を作成する。 from sklearn.svm import LinearSVC linsvc = LinearSVC(loss=\"squared_hinge\",penalty=\"l1\",dual=False) 線形サポートベクトル分類器のハイパーパラメータCの選択 逆正則化パラメータCをGridSearchCVで探す。MBP2013Laterで学習(fit)に5分くらいかかった。 GridSearchCVからはC=0.2がbestと返ってくる。 from sklearn.model_selection import GridSearchCV param_grid = {\"C\":[0.025,0.05,0.1,0.2,0.4]} model = GridSearchCV(estimator=linsvc, param_grid=param_grid,cv=5,scoring=\"accuracy\",return_train_score=True) model.fit(X_train,y_train) print(model.cv_results_[\"mean_train_score\"]) # array([0.96291693, 0.96775192, 0.97059085, 0.97340754, 0.97626859]) print(model.cv.results_[\"mean_test_score\"]) # array([0.95626331, 0.95990064, 0.96158623, 0.9625621 , 0.96105394]) print(model.best_params_) # {\'C\': 0.2} 学習、精度評価 C=0.2を使って新しく学習させる。 linsvc = LinearSVC(loss=\"squared_hinge\",penalty=\"l1\",dual=False,C=0.2) linsvc.fit(X_train,y_train) 訓練データ、テストデータに対して正答率を求める。 訓練データについて97.2%、テストデータについて96.2%。 過学習すると訓練データが高くテストデータが低くなる。 from sklearn.metrics import accuracy_score pred_train = linsvc_best.predict(X_train) acc = accuracy_score(y_true = y_train,y_pred = pred_train) print(acc) # 0.9723207948899929 pred_test = linsvc_best.predict(X_test) acc = accuracy_score(y_true = y_test,y_pred = pred_test) print(acc) # 0.9619506966773848 モデルの解釈可能性 [mathjax] 線形SVMの決定境界(f(x))の係数をヒートマップっぽく表示して、どの係数を重要視しているかを確認する。 基本的に真ん中に画像が集まっているので、28x28の隅は使わないのが正しそう。 正則化パラメータによって係数の大きさを制御しているため、正則化パラメータを変えると係数が変わる。 今回のは(L_1)正則化なので、係数が0のものが増える..らしい(..別途調べる..)。 (f(x) = w_0 + w_1 x_1 + w_2 x_2 + cdots w_{784} x_{784}) import matplotlib.pyplot as plt weights = linsvc_best.coef_ plt.imshow(weights.reshape(28,28)) plt.colorbar() plt.show()

default eye-catch image.

NumPy vector operations, universal functions, matplotlib, 3項演算, 次元削減

universal functions ndarrayの全ての要素に対して基本的な計算を実行する。 以下オペランドが1つの単項universal functions。 abs,sqrt,square,exp,log,sign,ceil,floor,rint,modf,isnan,sin,cos,arcsin,arccosなどがある。 array = np.arange(10) print(array) # [0 1 2 3 4 5 6 7 8 9] sqrt = np.sqrt(array) print(sqrt) # [0. 1. 1.41421356 1.73205081 2. 2.23606798 # 2.44948974 2.64575131 2.82842712 3. ] exp = np.exp(array) print(exp) # [1.00000000e+00 2.71828183e+00 7.38905610e+00 2.00855369e+01 # 5.45981500e+01 1.48413159e+02 4.03428793e+02 1.09663316e+03 # 2.98095799e+03 8.10308393e+03] 以下、オペランドが2つの2項universal functions。 いずれかのうち最大の値を残すmaximum()。 add,subtract,divide,power,maximum,minimum,copysign,greater,lessなどがある。 x = np.random.randn(10) y = np.random.randn(10) print(x) # [ 1.3213258 0.12423666 -1.45665939 -1.49766467 -0.6129116 2.00056744 # -0.00816571 0.63247747 0.29497652 0.80000291] print(y) # [-0.76739214 0.95151629 0.03208859 0.40641677 0.82635027 1.01773826 # 0.75601178 0.25200147 1.59929321 0.6251983 ] z = np.maximum(x,y) print(z) # [1.3213258 0.95151629 0.03208859 0.40641677 0.82635027 2.00056744 # 0.75601178 0.63247747 1.59929321 0.80000291] [mathjax] matplotlibにndarrayを引数として渡せば簡単にプロットできる。 (z=sqrt{x^2+y^2})をプロットしてみる。 import numpy as np import matplotlib.pyplot as plt points = np.arange(-5,5,0.01) xs,ys = np.meshgrid(points,points) z = np.sqrt(xs**2 +ys**2) plt.imshow(z, cmap=plt.cm.gray) plt.colorbar() plt.title(\"Image plot\") plt.show() 3項演算子 where マスクの論理値に従って2つのndarrayのうちいずれかの値を選択してリストに書く。 3項演算子を使ってPythonのlistに入れる方法は以下。 xa,xbはndarrayだが最終的なr1はPythonオブジェクト。 import numpy as np xa = np.array([1,2,3,4,5]) xb = np.array([6,7,8,9,10]) cnd = np.array([True,True,False,False,False]) r1 = [(x if c else y) for x,y,c in zip(xa,xb,cnd)] print(r1) 対して、ndarrayに対して直に3項演算子を実行するwhereがある。 import numpy as np xa = np.array([1,2,3,4,5]) xb = np.array([6,7,8,9,10]) cnd = np.array([True,True,False,False,False]) r2 = np.where(cnd,xa,xb) print(r2) 数学関数,統計関数,次元削減 (n)次のndarrayをある軸について集計して(n-1)次のndarrayにする。 集計方法としていくつかの数学関数、統計関数が用意されている。 以下5x4(2次)のndarrayについて、それぞれの列について平均を取り4列(1次)のndarrayにしている。 さらに列の平均を取りスカラーにしている。 import numpy as np ary = np.random.randn(5,4) print(ary) # [[-1.84573174 1.84169514 1.43012623 -0.5416877 ] # [-1.03660701 0.63504086 -0.12239017 -0.77822113] # [ 0.1711323 -0.16660851 -0.7928288 1.17582814] # [-0.29302267 -0.23316282 1.70611457 0.53870384] # [-0.46513289 -1.12207588 0.01930695 0.49635739]] print(ary.mean(axis=0)) # [-0.6938724 0.19097776 0.44806576 0.17819611] print(ary.mean(axis=1)) # [ 0.22110048 -0.32554436 0.09688078 0.42965823 -0.26788611] print(ary.mean()) # 0.030841804893752683

default eye-catch image.

NumPy ndarray assignment, vector operation, indexing, slicing, bool indexing, transposition

大規模高速計算を前提にC言語との接続を前提にしていて、配列処理に寄せることになる。 ndarrayで確保するメモリはPythonとは別(プロセス?)で確保される。 一通り流してみる。 shape()で配列の形を応答する。2行3列。 import numpy as np data = np.random.randn(2,3) shape = data.shape print(shape) print(data) # (2, 3) # [[ 0.79004157 0.45749364 0.90854549] # [-1.91791968 2.80050094 -0.60338724]] ndarrayを作る ndarrayを作る方法は以下。 data1 = [1,2,3,4,5] data2 = [6,7,8,9,10] data = np.array([data1,data2]) print(data) # [[ 1 2 3 4 5] # [ 6 7 8 9 10]] rng = np.arange(5) print(rng) # [0 1 2 3 4] ones = np.ones((5,5)) print(ones) # [[1. 1. 1. 1. 1.] # [1. 1. 1. 1. 1.] # [1. 1. 1. 1. 1.] # [1. 1. 1. 1. 1.] # [1. 1. 1. 1. 1.]] # 零行列 zeros = np.zeros((3,5)) print(zeros) # [[0. 0. 0. 0. 0.] # [0. 0. 0. 0. 0.] # [0. 0. 0. 0. 0.]] # 未初期化の配列確保 empties = np.empty((5,3)) print(empties) # [[-1.72723371e-077 -1.72723371e-077 2.24419447e-314] # [ 2.24421423e-314 2.24421423e-314 2.24563072e-314] # [ 2.24421559e-314 2.24563072e-314 2.24421570e-314] # [ 2.24563072e-314 2.24421558e-314 2.24563072e-314] # [ 2.24421562e-314 2.24563072e-314 2.24421577e-314]] # 指定値で埋める fulls = np.full((2,3),5) print(full) # [[5 5 5] # [5 5 5]] # 単位行列 identities = np.identity(5) print(identities) # [[1. 0. 0. 0. 0.] # [0. 1. 0. 0. 0.] # [0. 0. 1. 0. 0.] # [0. 0. 0. 1. 0.] # [0. 0. 0. 0. 1.]] ndarrayのデータ型 ndarrayで確保されるメモリのデータ型。 実際に型に従ってメモリが確保されているため、簡単にCに渡せる。 ary = np.array((1,2,3),dtype=np.float64) print(ary) # [1. 2. 3.] # float64をint32でキャスト ary_int = ary.astype(np.int32) print(ary_int) # [1 2 3] # キャストできないとコケる ary_str = np.array([\'hoge\',\'fuga\']) ary_str_int = ary_str.astype(np.int32) # ValueError: invalid literal for int() with base 10: \'hoge\' ベクトル演算 配列に寄せる醍醐味。Pythonに数値計算用のオペランドが用意されていることがあって、 割と自然に書ける。 ary = np.array([[1,2,3],[4,5,6]]) print(ary * ary) # [[ 1 4 9] # [16 25 36]] print(ary - ary) # [[0 0 0] # [0 0 0]] print(ary * 2) # [[ 2 4 6] # [ 8 10 12]] print(ary ** 2) # [[ 1 4 9] # [16 25 36]] スライスとView 巨大なメモリへのアクセス高速化のために、np.arrayに対するスライスによるアクセスは、 同じメモリを指すViewを返す。Viewに対する操作は元のメモリを変更する。 Copyする場合は明示的にCopyをする必要がある。 ary = np.arange(10) print(ary) # [0 1 2 3 4 5 6 7 8 9] ary[5] = 500 print(ary) # [ 0 1 2 3 4 500 6 7 8 9] ary[3:5] = 999 print(ary) # [ 0 1 2 900 900 500 6 7 8 9] copied = ary.copy() print(copied) # [ 0 1 2 900 900 500 6 7 8 9] 2次元のnp.array。要素へのアクセスの仕方は2通り。 ary2d = np.array([[1,2,3],[10,20,30]]) print(ary2d) # [[ 1 2 3] # [10 20 30]] print(ary2d[1]) # [10 20 30] print(ary2d[1][0]) # 10 print(ary2d[1,0]) # 10 n次元arrayへスカラーでインデックス参照するとn-1次元が戻る。 スライス参照はn次元が戻る。 ary2d = np.array([[1,2,3],[10,20,30],[100,200,300]]) print(ary2d[1]) # [10 20 30] print(ary2d[:2]) # [[ 1 2 3] # [10 20 30]] print(ary2d[1,:2]) # [10,20] Viewの選択 ndarrayから欲しいViewを選択するために色々と条件をつけられる。 例えば、bool index参照。 data = np.random.randn(7,4) print(data) # [[-0.69179761 -1.30790477 1.7224557 -0.67436315] # [ 0.45457462 0.24713663 -0.84619583 -0.31182853] # [-1.36397651 0.51770088 -1.8459593 -1.75146057] # [ 2.38626251 -0.4747874 -0.49951212 0.61803437] # [ 1.00048197 1.21838773 -0.4828001 0.9952139 ] # [ 0.17838262 1.687342 0.81501139 -1.12800811] # [ 0.65216988 -2.57185067 0.29802975 0.28870091]] recs = np.array([\'apple\',\'orange\',\'banana\',\'mountain\',\'river\',\'moon\',\'snow\']) print(recs==\'mountain\') # [False False False True False False False] print(data[recs==\'mountain\']) # [[ 2.38626251 -0.4747874 -0.49951212 0.61803437]] reshape reshape()を使って行列の形を変える。例えば1x15のndarrayを3x5のndarrayに変換。 もちろんCopyではなくView。これは頻出っぽい。 ちなみに、転値は専用のメソッド(T)が用意されている。 data1 = np.arange(15) print(data1) # [ 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14] data2 = data1.reshape(3,5) print(data2) # [[ 0 1 2 3 4] # [ 5 6 7 8 9] # [10 11 12 13 14]] data3 = data2.T print(data3) # [[ 0 5 10] # [ 1 6 11] # [ 2 7 12] # [ 3 8 13] # [ 4 9 14]]