default eye-catch image.

SnowflakeのTime Travel

SnowPro Coreの頻出テーマだと感じたTime Travel。 資格取得時に固め打ちした記憶があるが、補強ついでにもう少し詳し目に公式を読んでみる。 古くなったり間違っていたりするかもしれないので、事の真偽については公式を参照のこと。 Time Travelの理解と使用 https://docs.snowflake.com/ja/user-guide/data-time-travel.html [arst_toc tag=\"h4\"] Time Travelとは 通常、データ削除後に削除したデータにアクセスするには削除前にデータのバックアップが必要。 バックアップしてリストアして、というのはある意味DB製品の基本的な動作仕様であって、 SnowflakeにもSnowflakeのフルマネージドなポリシーに基づいて仕組みが用意されている。 Snowflakeではデータが自動的・透過的にバックアップされ、 明示的にバックアップ・リストアせずに削除後に削除前のデータにアクセスできる。 何も気にしないでも裏で勝手にバックアップ・削除されるため大分手間が省略される。 当然ストレージコストを余分に消費するが保持期間を設定することでバランスを制御できる。 公式には以下の用途で使われる、と書いてある。 誤って削除したデータの復元 特定時点の復元 任意期間の使用量・操作の分析 データのライフサイクル 重要な観点として、データにはステートがあり、ライフサイクルが決まっている。 ステート 削除種別 用途 通常 - 現在のデータに対するクエリ、DDL、DML、など Time Travel 論理削除 更新・削除された過去のデータへのクエリ過去の特定の時点についてテーブル・スキーマ・DB全体のクローン削除されたテーブル・スキーマ・DBの復元 Fail-safe 物理削除 一定期間(Retantion Period)が過ぎるとデータはFail-safeに移動。操作不可。Snowflakeへ問い合わせて何とかなる可能性がある データの保持期間(Retentiono period) ユーザはデータの保持期間を変更できる。 保持期間は日単位で設定する。デフォルト値は1(24時間)。 ゼロを設定するとTime Travelを使用しない設定。 設定範囲はテーブル種別、Snowflakeのエディションによって異なる。 通常のテーブルについて、エディションごとの設定範囲は以下の通り。 (Temporaryテーブル,Transientテーブルは通常1日を超えて使わないはずなので以下では除外) エディション 0(Time Travelを使用しない) 1日 〜90日 Standard 可 可(デフォルト) 不可 Enterprise+ 可 可(デフォルト) 可 さらに、ACCOUNTADMINロールを持つユーザはユーザの設定範囲を限定できる。 デフォルト値は DATA_RETENTION_TIME_IN_DAYS、 最小値は MIN_DATA_RETENTION_TIME_IN_DAYS。 最小値設定はデフォルト値設定を上書きしない。 デフォルト値が最小値よりも小さい場合、いずれかの大きい方が適用される。 コスト Time Travelは論理削除のステートでありストレージコストがかかる。 データが変更された時点から1日ごとに課金。(ちょっと詳細不明...) テーブルを丸ごとDROPした場合には丸ごと保存されるがなるべく差分が保存される。 ETLなどに使う1日未満のデータはTransientテーブルに格納することになっている。 また、より短いセッション内で使うデータはTemporaryテーブルに格納することになっている。 そのような用途であれば長いTime Travelは不要だし、そもそもFail-safeも不要。 これらのテーブル種別については、Time Travel期間は最大1日となっていて、 さらに後続のステートであるFail-safeに遷移しない。 逆に言うと、Transient,Temporaryテーブルを使うことでTime TravelとFail-safeの 余分なコストを最大1日に抑えることができる。 ちなみにTemporaryテーブルについてはセッションを落としたときにテーブルが破棄されると、 Time Travelの保持期間も終了する。 行ったり来たりだが、Transient,TemporaryについてはFail-safeが無いので Time Travel終了後は完全にアクセス不能となる。 保持期間の変更 テーブルの保持期間を変更すると、現在のデータとTime Travelにある全てのデータに影響する。 変更 影響 保持期間の延長 現在Time Travelにあるデータの保持期間が長くなる。例えば保持期間=10を保持期間=20に変更した場合現在Time Travel3日目のデータの残り期間は7日から17日に伸びる。 保持期間の短縮 現在アクティブなデータには新しい保持期間が適用される。例えば保持期間=10を保持期間=5に変更した場合現在Time Travel 7日目のデータはFail-safeへ遷移。現在Time Travel3日目のデータの残り期間は7日から2日に変わる。 データライフサイクルの遷移はバックグラウンドで非同期に行われるため、 ALTERコマンドで保持期間を変更したとしてすぐに上記の更新が走るわけではない。 オブジェクト階層に対する再帰的な影響 オブジェクトはCompositeパターンに基づき所有関係を持っているが、 階層上、上位のオブジェクトに対する保持期間の変更は再帰的に下位のオブジェクトに反映される。 例えばDBに対する変更はスキーマに対して反映されるなど。 ワイルドカードを使った破壊的な変更は意図しない変更をもたらすため、慎重にやったほうが良い。 最上位のアカウントに対する保持期間の変更は推奨しないという記述がある。 上位オブジェクトのドロップと下位オブジェクトの保持期間 上位オブジェクトをドロップすることで自動的に下位オブジェクトがドロップされる。 その際、下位オブジェクトの保持期間は強制的にドロップした上位オブジェクトの保持期間が設定される。 例えば保持期間10日のデータベースをドロップしたとして、 保持期間15日のスキーマ、テーブルの保持期間は強制的に10日となる。 下位オブジェクトを先にドロップすれば、下位オブジェクトの保持期間が上書きされることはない。 Time Travel中のデータに対するクエリ Time Travel中のデータにアクセスするために特別なストレージにアクセスする、という感じではなく、 SQLの拡張構文が用意され、自然にアクティブなデータとTime Travel中のデータの触り分けができる。 at句とbefore句が用意されている。 例えば公式に書かれている以下のような感じ。 ---at句によりtimestampで指定された時点の履歴データを取得 select * from my_table at (timestamp => \'Fri, 01 May 2015 16:20:00 -0700\'::timestamp_tz) ; ---5分前の時点で履歴データを取得 select * from my_table at (offset => -60*5) ; ---指定されたステートメントによる変更を含まないで、それ以前の履歴データを取得 select * from my_table before (statement => \'8e5d0ca9-005e-44e6-b858-a8f5b37c5726\') ; at句、before句が保持期間外を指す場合、クエリは失敗する。 Time Travel中のオブジェクトのクローン SQLの拡張構文によってTime Travel中のDBやスキーマなどのオブジェクトをクローンできる。 CREATEと共にCLONEを使う。例えば公式に書かれている以下のような感じ。 ---指定されたタイムスタンプで表される日付と時刻のテーブルのクローンを作成 ---my_tableというテーブルをrestored_tableというテーブルにクローン create table restored_table clone my_table at (timestamp => \'Sat, 09 May 2015 01:01:00 +0300\'::timestamp_tz) ; ---現在時刻の1時間前に存在していたスキーマと配下の全てのオブジェクトをクローン create schema restored_schema clone my_schema at (offset => -3600) ; ---指定されたステートメントの完了前に存在していたデータベースと配下の全てのオブジェクトを復元 create database        restored_db clone        my_db before       (statement => \'8e5d0ca9-005e-44e6-b858-a8f5b37c5726\'); CLONEも、指定したオブジェクトの保持期間を超えてTime Travel時間を指定するとエラーとなる。 オブジェクトのドロップと復元 オブジェクトの履歴はオブジェクトに紐づく、という書き方が正しいかは不明だが、 オブジェクト自体をドロップした場合の履歴は、オブジェクト配下の変更・削除の履歴とは少し異なる。 Time Travelは通常差分を履歴として残すが、オブジェクトのドロップによって完全な履歴が残る、 と公式に記述がある。 DROPによってオブジェクトをドロップした後、UNDROPによってドロップしたオブジェクトを復元する。 DROPした後、CREATEしたとしてもUNDROP扱いにはならないし、DROPした古いオブジェクトは残る。 永遠に完全な履歴のhistoryが積み重なっていく、historyのある時点のオブジェクトを対象に UNDROPする、という扱いとなる。 UNDROPにより復元するテーブルと同名称のテーブルが存在する場合エラーとなる。 --- mytableという名前のテーブルをdrop drop table mytable ; --- mytableという名前のテーブルをundrop undrop table mytable ; オブジェクトのhistoryについてもSQLの拡張構文で確認できる。 showとhistoryを合わせて使用する。公式は以下の通り。 オブジェクトの保持期間がすぎてTime Travelから消えるとshow historyで表示されなくなる。 --- mytestdb.myschemaスキーマ配下にあるloadから始まるテーブル名の履歴を表示 show tables history like \'load%\' in mytestdb.myschema ; --- mytestdbデータベース配下のスキーマの履歴を表示 show schemas history in mytestdb ; まとめ 自力でバックアップ・リストア操作なしで、Snowflakeが勝手にオブジェクトをバックアップしてくれる。 SQLの拡張構文を通してアクティブなデータと似た形でオブジェクトをリストアできる。 Time Travelに保持される期間はカスタマイズできる。 みたいなことについて、公式ドキュメントを読んで確認してみた。

default eye-catch image.

SnowflakeのMaterialized View

以前SnowPro core Certificationsに合格したもののなかなか使う機会がなくて、 資格試験対策レベルの薄い知識の維持すら怪しくなってきた。 Materialized Viewについて良くわからず使っていたので、 「やりなおし」のついでに知識をアップデートしていこうと思う記事第2弾。 個人の学び以上でも以下でもなく、内容に誤りがあるかもしれないので、 ことの真偽は公式ドキュメントを参照のこと。 [arst_toc tag=\"h4\"] Materialized Viewとは 何らかの集計を行おうとすると、多くの場合、中間の集計を合わせて最終的な集計結果を得る。 中間の集計を行う際にJOINにより結合を行う場合、それが高コストだと最終的にコスト高になる。 途中の集計結果をどこかに保存できれば、毎回高コストな集計を無駄に実行しなくて良くなる。 そんな時に使うのが Materialized View。「マテビュー」とか省略される。 e-Wordsによると以下の通り。 マテリアライズドビューとは、リレーショナルデータベースで作成されたビューにある程度の永続性を持たせ、参照する度に再検索しなくていいようにしたもの。特定のビューを頻繁に参照する場合に性能が向上する。 SnowflakeにおけるMaterialized Viewについては以下。 マテリアライズドビューの使用 Materialized Viewは透過的にリフレッシュされる 重要な点として、SnowflakeにおけるMaterialized Viewは自動的・透過的にリフレッシュされる。 オリジンとなるデータが変わった場合、アプリケーション側はノータッチでSnowflakeが自動更新する。 アプリケーション側でオリジンの鮮度を意識しないで良いというのはかなり楽。 透過的な自動リフレッシュの機構について、より詳細な内部情報として以下の通り。 ギリギリまでDMLを反映しないでくれる機構がついているっぽい。 クエリの前に実行されたDMLがクエリに影響する場合、クエリ時にDML反映 クエリの前に実行されたDMLがクエリに影響しない場合、スルーで応答 アプリケーション側がノータッチで自動的・透過的にリフレッシュがかかるが、 そのリフレッシュでクレジットが消費される。 普通のViewを使うべきか、Materialized Viewを使うべきか どのようなクエリであればその結果をMaterialized Viewに乗せるべきか。 クエリに時間がかかるなら使うべき。ただしオリジンとなるデータが変われば、 Materialized Viewのリフレッシュが必要となるから、オリジンが頻繁に変わるケースは対象外。 時間がかかる処理として公式には以下の例があがっている。 半構造化データに対するクエリ S3上のファイルなど遅い外部テーブルに対するクエリ 普通のViewにすべきか、Materialized Viewにすべきかの判断基準は以下。 普通のView Materialized View ビューからのクエリ結果(※) 頻繁に更新される ほとんど更新されない クエリ結果の使用 頻繁に使用される あまり使用されない リフレッシュにかかるコスト 処理時間大、ストレージ大 処理時間小、ストレージ小 ※ベースとなるテーブルが「完全に更新されない」まで限定しなくても、「クエリ結果の範囲に限定して更新されない」でOK。 Materialized Viewのパフォーマンス Snowflakeのパフォーマンスを上げる機構として「クエリ結果キャッシュ」がある。 要は同じ条件のクエリに対して、キャッシュがあればクエリを実行せずにキャッシュを返す、というもの。 実際に運用してみると「クエリ結果キャッシュ」を使わせるためには複数の条件があり、 なかなか仕様通りキャッシュを使い続けるのは難しいが、キャッシュが効けば速くなる。 比較することに意味があるのかちょっと怪しいが、 純粋にパフォーマンスを比較すると以下となるらしい。 普通のTable = 普通のView < Materialized View < クエリ結果キャッシュ クエリオプティマイザとMaterialized View アプリケーションがexplicitにMaterialized Viewを指定してやらなくても、 ベーステーブルに対するクエリ結果の行と列がMaterialized Viewに全て含まれている場合、 クエリオプティマイザが自動的にクエリを置換する。 さらに、もしベーステーブルがフィールドによってクラスタ化されていて、 プルーニングが良い結果をもたらすと判断されれば、Materialized Viewではなく ベーステーブルに対するプルーニングが使用される。 BIのようにアプリケーション全域で検索キーが分散し「どう呼ばれるかわからない」ケースでは、 なかなかカスタムでクラスタキーを設計してより良いプルーニング結果を得ることが難しいが、 分析タスクのように「ある程度呼ばれ方が決まっている」ケースならクラスタキーを偏らせる メリットはありそうで、そんなときにMaterialized Viewとベーステーブルのプルーニング、 どちらが良いか、なんてことを考えることもあるんだろう(本当か?) ただ公式には、ベストプラクティスとしてMaterialized Viewを作る場合、 ベーステーブルのプルーニングを解除しMaterialized Viewを優先すべきと記述がある。 マテリアライズドビューとそのベーステーブルをクラスタリングするためのベストプラクティス サブクエリについて暗黙的にMaterialized Viewが使われる、というケースもある。 この場合、クエリプロファイルにシレッとMaterialized Viewが鎮座する、ということになる。 Materialized Viewのメンテナンスコスト コンピューティング、ストレージともにクレジットを使用する。 Materialized Viewに対するクエリ結果が保存され、そのストレージに対してコストがかかる。 頻繁に使われるクエリなのであれば、相対的にストレージのコストが低くなると考えられるが、 もしロクに使われないクエリなのであれば、そのストレージコストが本当に低いのか考えるところ。 透過的なリフレッシュのためにコンピューティングのコストがかかる。 Materialized Viewの上手い使い方 ベーステーブルの多くデータを取得してしまうと、無駄にリソースを使ってしまう。 なるべく行・列が少なくなるようなデータセットをMaterialized Viewに格納すべき。 公式には、ベーステーブルにログがあるとして異常値のみをMaterialized Viewに置く、 という例が示されている。 要はアプリケーションの設計段階で、何をMaterialized Viewとすべきかを検討すべき。 ベーステーブルを頻繁に更新してしまうと、都度自動リフレッシュが実行されてしまう。 そのため、ベーステーブルの更新頻度を下げる必要がある。 SnowflakeはDMLをバッチ処理するように推奨している。 バッチ処理できるようなデータの並びとなるようにしておく必要がある。 DDL,DML 実際にMaterialized Viewを作ってみる。 --- ベーステーブルの作成 create table mv_base (id integer, value integer) ; --- Materialized Viewの作成 create or replace materialized view mv1 as select id, value from mv_base ; ---ベーステーブルにInsert insert into mv_base (id, value) values (1, 100) ; --- Materialized Viewからデータ取得 select id, value from mv1 ; id value --------------- 1 100 ---ベーステーブルにInsert insert into mv_base (id, value) values ( 2,200) ; --- Materialized Viewからデータ取得 select id, value from mv1 ; id value --------------- 1 100 2. 200 Materialized Viewの自動更新を停止することができる。 停止中にデータを取得しようとするとエラーが発生し取得できない。 停止と再開の組みは以下。 alter materialized view mv1 suspend ; select id, value from mv1 ; SQLコンパイルエラー: ビュー「MV1」の展開中の失敗: SQLコンパイルエラー:マテリアライズドビュー MV1 は無効です。 alter materialized view mv1 resume ; select id, value from mv1 ; id value --------------- 1 100 2. 200 まとめ 更新頻度が低く利用頻度が高い中間クエリについて Materialized View に格納すると効果的。 Materialized Viewは自動的・透過的にリフレッシュがかかる。 自動リフレッシュに際してコンピューティングコストがかかるため更新頻度は低い方が良い。 ベーステーブルに対するDMLをバッチ処理とすると自動リフレッシュの頻度を下げられる。 クエリ結果キャッシュよりは遅いが普通のテーブルよりは速い。

default eye-catch image.

Snowflakeのアクセス制御

以前SnowPro core Certificationsに合格したもののなかなか使う機会がなくて、 資格試験対策レベルの薄い知識の維持すら怪しくなってきた。 資格を取得してからかなり経過したこともあり、控えめにいって知識が陳腐化してしまった。 せっかくなので「やりなおし」のついでに知識をアップデートしていこうと思う。 セキュリティ周りについて正直よくわからず操作している感があるため、 今一度ドキュメントを見直してみる。 個人の学び以上でも以下でもなく、内容に誤りがあるかもしれないので、 ことの真偽は公式ドキュメントを参照のこと。 Snowflakeのアクセス制御 https://docs.snowflake.com/ja/user-guide/security-access-control.html [arst_toc tag=\"h4\"] アカウントの管理 例えば以下のように、オブジェクトへのアクセスを制御する。 誰がどのオブジェクトにアクセスできるのか そのオブジェクトに対してどの操作を実行できるのか 誰がアクセス制御ポリシーを作成または変更できるか アクセス制御フレームワーク 一言で「アクセス権」と言ったところで、確かに世の中には様々な意味をもって使われている。 以下、DAC,MAC,RBACの一般的なまとめ。 SnwoflakeはDACとRBACの両方に基づいてアクセス制御をおこなう。 名称 誰が制御するか 説明 任意アクセス制御DAC:Discretionary Access Control 所有者 オブジェクトには所有者がいて所有者が他者に対してオブジェクトへのアクセスを許可する例えばLinuxのファイルパーミッション。POSIXのACL。実質的に作成したリソースに対するアクセス制御の権限を与えられている。ユーザの自由度が高く管理者に手間をかけない。ルールの統一が難しく、セキュリティ面で効果を期待できない。 強制アクセス制御MAC:Mandatory Access Control 管理者 管理者がアクセスする側(サブジェクト)とされる側(オブジェクト)の両方に対してセキュリティレベルを設定する。例えばレベル1のサブジェクトはレベル 3のオブジェクトにアクセスできない等。所有者であろうとも管理者が定めた規則によりアクセスできないなどの特徴。 ロールベースアクセス制御RBAC:Role-based access control 管理者 セキュリティ概念としてはDACとMACの中間。DACと同様にサブジェクトとオブジェクトに対するアクセス制御を行うが、サブジェクトに対して「ロール」を設定し「ロール」の範囲で自由にオブジェクトにアクセスできる。つまり1つ1つのサブジェクトに個別にアクセス制御をかけるだけではなく複数のサブジェクトにアクセス制御をかける。「組織」、「部署」に親和性が高い。それぞれの部署向けにロールを作成し、部署に属したサブジェクトがロールの範囲でオブジェクトにアクセスできる。 DACとRBACの両方、とはいったいどういうことか。 まずRBACの側面から説明できるアクセス制御は以下の通り。 オブジェクトにアクセスする能力を「権限」と呼ぶ。「権限」を「ロール」に付与する。 「ロール」を他の「ロール」に割り当てたり、「ユーザ」に割り当てる。 こうして「ユーザ」は「ロール」の範囲でオブジェクトにアクセスできる。 ここまでで、「ユーザ」が「ロール」を介してオブジェクトにアクセスできる構造となる。 これだけだとRBACの説明の通り、管理者によってのみアクセス制御がおこなわれ「ユーザ」はおこなわない。 さらに、オブジェクトには「所有者」がいる。 オブジェクトを作成すると、「所有」という名前の「権限」ができる。 「所有者」には「所有権限」が割り当たった「ロール」が紐づく。 「所有権限」がある「ロール」を持っていると、オブジェクトに対する権限をロールにGRANT,REVOKEできる。 「所有権限」がある「ロール」を他に移すこともできる。 通常「所有者」にはオブジェクトに対する全ての権限が与えられる。 つまり、「所有者」であれば、DACのように「ユーザ」がアクセス制御を書き換えることができる。 DACをRBACで実装している、といった感じ。 基本的にはRBACだが、所有者に限りロールを変更できる自由さがある。 オブジェクトの階層構造と所有 SnowflakeにおいてオブジェクトはOOPでいうCompositeパターンに従う。 上位オブジェクトは下位オブジェクトのコンテナとなり、全体として階層構造を形成する。 例えば「組織」は「アカウント」を所有できるし、「アカウント」は「ユーザ」を所有できる。 オブジェクトには、オブジェクトに対してSQLを実行する権限があったりする。 例えばvwhにはSQLを実行する権限があり、もしその権限を付与されたロールをもっていれば、vwhでSQLを実行できる。 またテーブルにデータを追加する権限が付与されたロールをもっていれば、テーブルにデータを追加できる。 システム定義ロール RBACとDACを両立する上でその境界にある概念の解釈が微妙なものがあったりする。 DACにより所有者は所有するオブジェクトに関して生殺与奪の権を持つことになっているが、 いくつかのロールについてはお上が決めたルールに逆らえない。 このようなロールを「システム定義ロール」と呼び、所有者が放棄できないし、ロールから権限を無くせない。 ロール名 説明 ORGADMIN 組織レベルで運用を管理するためのロール。組織内にアカウントを作成する、組織内の全アカウント表示、組織全体の使用状況などの表示。 ACCOUNTADMIN SYSADMINとSECURITYADMINの2つをラップするロール。システムにおける最上位のロール。アカウント内の限られた数のユーザにのみ付与。 USERADMIN ユーザ、ロールの管理ができる。CREATE USER、CREATE ROLEの権限が付与されている。アカウントにユーザ、ロールを作成できる。 SECURITYADMIN USERADMINロールがSECURITYADMINロールに付与されている。USERADMINに加え、オブジェクトへのアクセス権を付与する権利が与えられている。 SYSADMIN アカウントでウェアハウス、データベースを作成する権限が与えられている。システム管理者に付与する。間違ってACCOUNTADMINロールをシステム管理者に付与しないこと。 PUBLIC 全てのユーザー、ロールにデフォルトで割り当てられるロール。PUBLICロールはオブジェクトを所有できる。全てのユーザに割り当てられているため、全てのユーザがPUBLICロールが所有するオブジェクトにアクセスできる。明示的なアクセス制御が不要で誰でも触れてよいオブジェクトをPUBLICに所有させる。 カスタムロール USERADMINロールを付与されているユーザによって、 オブジェクトを所有するロールを新たに作成できる。 ただし、RBACによってDACを実現している都合上、Snowflakeの掟に従ってロールを作るべき。 RBACベースのDACにおいて、「システム管理者」だとか「ユーザのレベル」はあくまでも「上位のロール」を付与されているか、でしか決まらない。 システム内のオブジェクトの所有者として機能するロールを作成する場合、 RBACベースのDACに配慮しないと、 「システム管理者」ですら触れない謎のオブジェクトを作り出してしまう。 「システム管理者」は、アカウント内のお全てのオブジェクトを表示、管理できるようにしたい。 もしSYSADMINロールにカスタムロールが割り当てられていないなら、 システム管理者はそのカスタムロールが所有するオブジェクトを表示、管理できない。 SECURITYADMINロールのみが表示し管理できる、という謎の状況になってしまう。 だから、新たに作成するロールは必ずSYSADMINロールに付与する必要がある。 ロールの所有関係は階層構造を持てるから、階層を上に辿ると必ずSYSADMINがある必要がある。 推奨されるロールの階層構造 Snowflakeが推奨するロールの階層構造は以下のような感じ。 矢印は「付与関係」。矢印の先のロールに、矢印の元のロールが付与されている。 ACCOUNTADMINには全てのロールが間接的に付与された状態とすること。 カスタムロールはSYSADMINに間接的に付与された状態とすること。 カスタムロールをSYSADMINを超えて直接ACCOUNTADMINに付与しないこと。 カスタムロールをSECURITYADMIN、USERADMINに付与しないこと。 最初にハマるダメケースとベストプラクティス 最上位のロールであるACCOUNTADMINを割り当てるユーザは組織内で限定するべき。 逆に言うと、もし1人しかいないACCOUNTADMINロールを持つユーザがDBを作ってしまったならば、 他のACCOUNTADMINロールを持たないユーザがそのDBを表示・管理することはできない。 ベストプラクティスは、オブジェクトの所有者となるロールをSYSADMIN配下にぶら下げること。 SYSADMINロールが付与されたユーザであれば、オブジェクトの表示・管理ができる状態とすること。 テクニックとしてオブジェクトの所有権とオブジェクトに対する権限を分ける手法があり、 もし所有者となるロールに全ての権限を付与すれば、そのロールさえユーザに割り当てれば、 ユーザはオブジェクトに対する表示・管理ができるようになる。 一方、所有者となるロールに異なるロールを割り当て、所有者となるロールには権限A、 下位のロールには権限Bを与える、という構成とすることもできる。 その際、所有者となるロールには権限A,権限Bが付与された状態となる。 SYSADMINの下に複数の管理ロールをぶら下げ、 それら全てのロールに共通して権限を付与したい、という場合には、 それぞれの管理ロールに対して、その共通の権限を付与したロールを割り当てれば良い。 この状態にするためにロールを使い分ける。 USERADMINロールを付与されたユーザがユーザとそのユーザ用のロールを作成する SYSADMINロールを付与されたユーザがオブジェクトを作成する SECURITYADMINロールを付与されたユーザが新たなユーザのロールに所有権を移動する まとめ SnowflakeのセキュリティはRBACベースのDAC。 基本的にはRBACなので管理者がロールを作って割り当てる。 ただDACを実現するためにロールがオブジェクトを所有するという概念が導入されていて、 RBACとDACの境界に解釈が難しい部分がある。Snowflakeが定めるベストプラクティスに従うと良い。 ACCOUNTADMINでDBを作りまくって他人から見えない問題をシュッと解決できる。

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.

flattenでcollectionを平坦化する

Laravelで多次元配列を1次元化するflatten()が便利だった. 連想配列の値のみを収集して1次元配列にしてくれる。 $ ./vendor/bin/sail artisan tinker Psy Shell v0.11.1 (PHP 8.1.2 — cli) by Justin Hileman >>> $collection = collect([ \'hoge\' => [1, 2, 3], ... \'fuga\' => \'aiueo\', ... \'foo\' => 1, ... \'bar\' => null ... ]); => IlluminateSupportCollection {#3529 all: [ \"hoge\" => [ 1, 2, 3, ], \"fuga\" => \"aiueo\", \"foo\" => 1, \"bar\" => null, ], } >>> $collection->flatten(); => IlluminateSupportCollection {#3525 all: [ 1, 2, 3, \"aiueo\", 1, null, ], }

default eye-catch image.

Grafanaプラグインを読んでいく – simpod-json-datasource plugin

最も単純そうなDataSourceプラグインを読んでいく。 プラグインは simpod-json-datasource plugin。 配布はここ。 JSONを返す任意のURLにリクエストを投げて結果を利用できるようにする。 TypeScriptで書かれている。 The JSON Datasource executes JSON requests against arbitrary backends. JSON Datasource is built on top of the Simple JSON Datasource. It has refactored code, additional features and active development. install インストール方法は以下の通り。 初回だけgrafana-serverのrestartが必要。 # install plugin $ grafana-cli plugins install simpod-json-datasource # restart grafana-server $ sudo service grafana-server restart build ビルド方法は以下の通り。 yarn一発。 # build $ cd /var/lib/grafana/plugins/simpod-json-datasource $ yarn install $ yarn run build 設定 データソースの追加で、今回インストールした DataSource/JSON (simpod-json-datasource)を 選択すると、プラグインのコンフィグを変更できる。 要求するURL一覧 URLはBaseURL。 このプラグインはURLの下にいくつかのURLが存在することを想定している。 つまり、それぞれのURLに必要な機能を実装することでプラグインが機能する。 用語については順に解説する。 必須 Method Path Memo GET / ConfigページでStatusCheckを行うためのURL。200が返ればConfigページで「正常」となる。 POST /search 呼び出し時に「利用可能なメトリクス」を返す。 POST /query 「メトリクスに基づくデータポイント」を返す。 POST /annotations 「アノテーション」を返す。 オプショナル Method Path Memo POST /tag-keys 「ad-hocフィルタ」用のタグのキーを返す。 POST /tag-values 「ad-hocフィルタ」用のタグの値を返す。 メトリクス Grafanaにおいて、整理されていないデータの中から\"ある観点\"に従ったデータを取得したいという ユースケースをモデル化している. 例えば、サーバ群の負荷状況を監視したいというケースでは「サーバ名」毎にデータを取得したいし、 センサ群から得られるデータを監視したいというケースでは「センサID」毎にデータを取得したい. これらの観点は、Grafanaにおいて「メトリクス」または「タグ」として扱われる。 センサAからセンサZのうちセンサPとセンサQのデータのみ表示したい、という感じで使う。 ここで現れるセンサAからセンサZが「メトリクス」である。 クエリ変数のサポート (metricFindQuery) 「クエリ変数」は、「メトリクス」を格納する変数である。 データソースプラグインがクエリ変数をサポートするためには、 DataSourceApi クラスの metricFindQuery を オーバーライド する. string型のqueryパラメタを受け取り、MetricFindValue型変数の配列を返す. 要は以下の問答を定義するものである。 - 質問 : 「どんなメトリクスがありますか?」 - 回答 : 「存在するメトリクスはxxx,yyy,zzzです」 ちなみに、metricFindQueryが受け取るqueryの型は、 metricFindQueryを呼び出すUIがstring型で呼び出すからstring型なのであって、 UIを変更することで別の型に変更することができる。 以下、simpod-json-datasource の metricFindQuery の実装。 // MetricFindValue interfaceはtext,valueプロパティを持つ // { \"label\":\"upper_25\", \"value\": 1}, { \"label\":\"upper_50\", \"value\": 2} のような配列を返す. metricFindQuery(query: string, options?: any, type?: string): Promise { // Grafanaが用意する interpolation(補間)関数 // query として \"query field value\" が渡されたとする // interpolated は { \"target\": \"query field value\" }のようになる. const interpolated = { type, target: getTemplateSrv().replace(query, undefined, \'regex\'), }; return this.doRequest({ url: `${this.url}/search`, data: interpolated, method: \'POST\', }).then(this.mapToTextValue); } // /searchから返ったJSONは2パターンある // 配列 ([\"upper_25\",\"upper_50\",\"upper_75\",\"upper_90\",\"upper_95\"])か、 // map ([ { \"text\": \"upper_25\", \"value\": 1}, { \"text\": \"upper_75\", \"value\": 2} ]) // これらを MetricFindValue型に変換する mapToTextValue(result: any) { return result.data.map((d: any, i: any) => { // mapの場合 if (d && d.text && d.value) { return { text: d.text, value: d.value }; } // 配列の場合 if (isObject(d)) { return { text: d, value: i }; } return { text: d, value: d }; }); } 以下、simpod-json-datasource が メトリクスを取得する部分のUI。 FormatAs, Metric, Additional JSON Dataの各項目が選択可能で、 各々の変数がViewModelとバインドされている. コードは以下。React。 FormatAsのセレクトボックスのOnChangeでsetFormatAs()が呼ばれる。 MetricのセレクトボックスのOnChagneでsetMetric()が呼ばれる。 Additional JSON DataのonBlurでsetData()が呼ばれる。 import { QueryEditorProps, SelectableValue } from \'@grafana/data\'; import { AsyncSelect, CodeEditor, Label, Select } from \'@grafana/ui\'; import { find } from \'lodash\'; import React, { ComponentType } from \'react\'; import { DataSource } from \'./DataSource\'; import { Format } from \'./format\'; import { GenericOptions, GrafanaQuery } from \'./types\'; type Props = QueryEditorProps; const formatAsOptions = [ { label: \'Time series\', value: Format.Timeseries }, { label: \'Table\', value: Format.Table }, ]; export const QueryEditor: ComponentType = ({ datasource, onChange, onRunQuery, query }) => { const [formatAs, setFormatAs] = React.useState<selectableValue>( find(formatAsOptions, option => option.value === query.type) ?? formatAsOptions[0] ); const [metric, setMetric] = React.useState<selectableValue>(); const [data, setData] = React.useState(query.data ?? \'\'); // 第2引数は依存する値の配列 (data, formatAs, metric). // 第2引数の値が変わるたびに第1引数の関数が実行される. React.useEffect(() => { // formatAs.value が 空なら何もしない if (formatAs.value === undefined) { return; } // metric.value が 空なら何もしない if (metric?.value === undefined) { return; } // onChange(..)を実行する onChange({ ...query, data: data, target: metric.value, type: formatAs.value }); onRunQuery(); }, [data, formatAs, metric]); // Metricを表示するセレクトボックスの値に関数がバインドされている. // 関数はstring型のパラメタを1個とる. const loadMetrics = (searchQuery: string) => { // datasourceオブジェクトの metricFindQuery()を呼び出す. // 引数はsearchQuery. 戻り値はMetricFindValue型の配列(key/valueが入っている). // 例えば { \"text\": \"upper_25\", \"value\": 1}, { \"text\": \"upper_75\", \"value\": 2} return datasource.metricFindQuery(searchQuery).then( result => { // { \"text\": \"upper_25\", \"value\": 1}, { \"text\": \"upper_75\", \"value\": 2} から // { \"label\": \"upper_25\", \"value\": 1}, { \"label\": \"upper_75\", \"value\": 2} へ const metrics = result.map(value => ({ label: value.text, value: value.value })); // セレクトボックスで選択中のMetricを取得し setMetric に渡す setMetric(find(metrics, metric => metric.value === query.target)); return metrics; }, response => { throw new Error(response.statusText); } ); }; return ( <> <div className=\"gf-form-inline\"> <div className=\"gf-form\"> <select prefix=\"Format As: \" options={formatAsOptions} defaultValue={formatAs} onChange={v => { setFormatAs(v); }} /> </div> <div className=\"gf-form\"> <asyncSelect prefix=\"Metric: \" loadOptions={loadMetrics} defaultOptions placeholder=\"Select metric\" allowCustomValue value={metric} onChange={v => { setMetric(v); }} /> </div> </div> <div className=\"gf-form gf-form--alt\"> <div className=\"gf-form-label\"> <label>Additional JSON Data </div> <div className=\"gf-form\"> <codeEditor width=\"500px\" height=\"100px\" language=\"json\" showLineNumbers={true} showMiniMap={data.length > 100} value={data} onBlur={value => setData(value)} /> </div> </div> </> ); // } }; クエリ変数から値を取得 (queryの実装) 続いて、選択したメトリクスのデータ列を得るための仕組み. query()インターフェースを実装する. QueryRequest型の引数を受け取る. 引数には、どのメトリクスを使う、だとか、取得範囲だとか、様々な情報が入っている。 QueryRequest型の引数を加工した値を、/query URLにPOSTで渡す. /query URLから戻ってきた値は DataQueryResponse型と互換性があり、query()の戻り値として返る. // UIから送られてきたQueryRequest型の変数optionsを処理して /query に投げるJSONを作る。 // QueryRequest型は当プラグインがGrafanaQuery型interfaceを派生させて作った型。 query(options: QueryRequest): Promise { const request = this.processTargets(options); // 処理した結果、targets配列が空なら空を返す。 if (request.targets.length === 0) { return Promise.resolve({ data: [] }); } // JSONにadhocFiltersを追加する // @ts-ignore request.adhocFilters = getTemplateSrv().getAdhocFilters(this.name); // JSONにscopedVarsを追加する options.scopedVars = { ...this.getVariables(), ...options.scopedVars }; // /queryにJSONをPOSTで投げて応答をDataQueryResponse型で返す。 return this.doRequest({ url: `${this.url}/query`, data: request, method: \'POST\', }); } // UIから送られてきたQueryRequest型変数optionsのtargetsプロパティを加工して返す // processTargets(options: QueryRequest) { options.targets = options.targets .filter(target => { // remove placeholder targets return target.target !== undefined; }) .map(target => { if (target.data.trim() !== \'\') { // JSON様の文字列target.data をJSONオブジェクト(key-value)に変換する // reviverとして関数が指定されている // valueが文字列であった場合にvalue内に含まれる変数名を置換する // 置換ルールは cleanMatch メソッド target.data = JSON.parse(target.data, (key, value) => { if (typeof value === \'string\') { return value.replace((getTemplateSrv() as any).regex, match => this.cleanMatch(match, options)); } return value; }); } // target.targetには、変数のプレースホルダ($..)が存在する. // grafanaユーザが入力した変数がoptions.scopedVarsに届くので、 // target.target内のプレースホルダをoptions.scopedVarsで置換する。 // 置換後の書式は正規表現(regex) if (typeof target.target === \'string\') { target.target = getTemplateSrv().replace(target.target.toString(), options.scopedVars, \'regex\'); } return target; }); return options; } // cleanMatch cleanMatch(match: string, options: any) { // const replacedMatch = getTemplateSrv().replace(match, options.scopedVars, \'json\'); if ( typeof replacedMatch === \'string\' && replacedMatch[0] === \'\"\' && replacedMatch[replacedMatch.length - 1] === \'\"\' ) { return JSON.parse(replacedMatch); } return replacedMatch; } TypeScriptに明るくない場合、以下を参照。 - Typescript-array-filter - 【JavaScript】map関数を用いたおしゃれな配列処理 - TypeScript - String replace() JavaScriptのreplaceとGrafanaのreplaceが混在していて、 IDEが無いとかなり厳しい感じ。 - Interpolate variables in data source plugins - Advanced variable format options query()のパラメタについて、 Grafanaのデフォだとstring型で来るのだが、プラグインが必要に応じて好きに変更できる。 つまりquery()に値を投げる部分をプラグインが変更できるため、変更に応じて受け側も変更する。 当プラグインはDataQueryRequestインターフェースを派生させた型を用意している。 - DataQueryRequest interface getTemplateSrv()はGrafanaのユーティリティ関数。 - getTemplateSrv variable - Github getTemplateSrv - Variable syntax 現在アクティブなダッシュボード内の変数が全て得られる。 * Via the TemplateSrv consumers get access to all the available template variables * that can be used within the current active dashboard. import { getTemplateSrv } from ‘@grafana/runtime’; const templateSrv = getTemplateSrv(); const variablesProtected = templateSrv.getVariables(); const variablesStringfied = JSON.stringify( variablesProtected ); const variables = JSON.parse( variablesStringfied ); URLに投げるJSONは例えば以下の通り。 { \"app\": \"dashboard\", \"requestId\": \"Q171\", \"timezone\": \"browser\", \"panelId\": 23763571993, \"dashboardId\": 1, \"range\": { \"from\": \"2015-12-22T03:16:00.000Z\", \"to\": \"2015-12-22T03:17:00.000Z\", \"raw\": { \"from\": \"2015-12-22T03:16:00.000Z\", \"to\": \"2015-12-22T03:17:00.000Z\" } }, \"timeInfo\": \"\", \"interval\": \"50ms\", \"intervalMs\": 50, \"targets\": [ { \"refId\": \"A\", \"data\": \"\", \"target\": \"upper_50\", \"type\": \"timeseries\", \"datasource\": \"JSON-ikuty\" } ], \"maxDataPoints\": 1058, \"scopedVars\": { \"variable1\": { \"text\": [ \"upper_50\" ], \"value\": [ \"upper_50\" ] }, \"__interval\": { \"text\": \"50ms\", \"value\": \"50ms\" }, \"__interval_ms\": { \"text\": \"50\", \"value\": 50 } }, \"startTime\": 1605108668062, \"rangeRaw\": { \"from\": \"2015-12-22T03:16:00.000Z\", \"to\": \"2015-12-22T03:17:00.000Z\" }, \"adhocFilters\": [] } URLから返る値は例えば以下の通り. [ { \"target\": \"pps in\", \"datapoints\": [ [ 622, 1450754160000 ], [ 365, 1450754220000 ] ] }, { \"target\": \"pps out\", \"datapoints\": [ [ 861, 1450754160000 ], [ 767, 1450754220000 ] ] }, { \"target\": \"errors out\", \"datapoints\": [ [ 861, 1450754160000 ], [ 767, 1450754220000 ] ] }, { \"target\": \"errors in\", \"datapoints\": [ [ 861, 1450754160000 ], [ 767, 1450754220000 ] ] } ] アノテーションのサポート (annotationQueryの実装) Grafanaには、ユーザがグラフの中に注意を喚起するラベル(またはラベルの範囲)を 設定する機能がある。 ユーザが自力で書くだけでなく、プラグインがアノテーションを提供することもできる。 simpod-json-datasourceプラグインは、アノテーションを提供する機能を有する。 公式の説明はこちら。 例えば下図で薄く赤くなっている部分が プラグインによるアノテーション。 実装方法は以下。 - アノテーションサポートを有効にする - annotationQueryインターフェースを実装する - アノテーションイベントを作成する アノテーションサポートを有効にするためには、 plugin.json に以下を追加する。 { \"annotations\": true } simpod-json-datasourceプラグインのannotationQueryの実装は以下。 annotationQuery( options: AnnotationQueryRequest ): Promise { const query = getTemplateSrv().replace(options.annotation.query, {}, \'glob\'); const annotationQuery = { annotation: { query, name: options.annotation.name, datasource: options.annotation.datasource, enable: options.annotation.enable, iconColor: options.annotation.iconColor, }, range: options.range, rangeRaw: options.rangeRaw, variables: this.getVariables(), }; return this.doRequest({ url: `${this.url}/annotations`, method: \'POST\', data: annotationQuery, }).then((result: any) => { return result.data; }); } プラグインからURLにPOSTのBODYで渡ってくるデータは以下。 { \"annotation\": { \"query\": \"hogehoge\", \"name\": \"hoge\", \"datasource\": \"JSON-ikuty\", \"enable\": true, \"iconColor\": \"rgba(255, 96, 96, 1)\" }, \"range\": { \"from\": \"2015-12-22T03:16:16.275Z\", \"to\": \"2015-12-22T03:16:18.102Z\", \"raw\": { \"from\": \"2015-12-22T03:16:16.275Z\", \"to\": \"2015-12-22T03:16:18.102Z\" } }, \"rangeRaw\": { \"from\": \"2015-12-22T03:16:16.275Z\", \"to\": \"2015-12-22T03:16:18.102Z\" }, \"variables\": { \"variable1\": { \"text\": [ \"All\" ], \"value\": [ \"upper_25\", \"upper_50\", \"upper_75\", \"upper_90\", \"upper_105\" ] } } } これに対して以下の応答を返すとイメージのようになる。 以下はAnnotationEvent型と型が一致している。 isRegionがtrueの場合、timeEndを付与することでアノテーションが領域になる。 isRegionがfalseの場合、timeEndは不要。 tagsにタグ一覧を付与できる。 [ { \"text\": \"text shown in body\", \"title\": \"Annotation Title\", \"isRegion\": true, \"time\": \"1450754170000\", \"timeEnd\": \"1450754180000\", \"tags\": [ \"tag1\" ] } ]

default eye-catch image.

PostgreSQL スキーマをコピーする

スキーマをコピーする方法はない。 代わりに以下の方法で同じ効果を得る。 スキーマ名Aをスキーマ名Bに変更する スキーマ名Bの状態でpg_dumpする スキーマ名Bをスキーマ名Aに変更する スキーマ名Bを作成する pg_dumpしたファイルをリストアする Statementは以下の通り。 $ psql -U user -d dbname -c \'ALTER SCHEMA old_schema RENAME TO new_schema\' $ pg_dump -U user -n new_schema -f new_schema.sql dbname $ psql -U user -d dbname -c \'ALTER SCHEMA new_schema RENAME TO old_schema\' $ psql -U user -d dbname -c \'CREATE SCHEMA new_schema\' $ psql -U user -q -d dbname -f new_schema.sql $ rm new_schema.sql [arst_adsense slotnumber=\"1\"]

default eye-catch image.

Amazon Redshift概要 アーキテクチャ

やはりAWSの公式のドキュメンテーションは読みやすいので、 公式を上から順に舐めていくスタイルで理解していく。 今回は一番最初のアーキテクチャ概要。 [arst_toc tag=\"h4\"] アーキテクチャ 大きなデータを扱おうとする何かは分散アーキテクチャで解決しようとする。 と言っても、大抵は\"代表するノード\"と\"ワーカーノード\"のセットなのでデジャブ感がある。 ちなみにTableauServerが内部設計を細かく書いていて面白かった。 以下、Amazon Redshiftのアーキテクチャを表す図. (公式) Amazon Redshiftは複数のクラスタから構成される。 クラスタはリーダーノードと複数のコンピューティングノードから構成される。 クライアントアプリケーションからは唯一リーダーノードと呼ぶノードを参照できる。 コンピューティングノードはクライアントアプリケーションから見えない場所に配置されリーダーノードが代表してコンピューティングノードを操作する。 リーダーノード クライアントアプリケーションは PostgreSQL用の JDBC/ODBCドライバを使用してリーダーノード通信できる。 実行計画に基づいてコードをコンパイルしコンパイル済みのコードをコンピューティングノードに配布してからデータの一部を各コンピューティングノードに割り当てる。 コンピューティングノード コンパイル済みのコードを実行し中間結果をリーダーノードに返送する。 中間結果はリーダーノードで最終的に集計される。 コンピューティングノードのCPU、メモリ、ストレージはノードのタイプによって異なる。ノードの数、種類を増強することでスケールアップできる。 ノードスライス コンピューティングノードはスライスに分割されている。 各スライスにはノードのメモリとディスク容量の一部を割り当てられている。 リーダーノードがスライスへのデータ分散を管理し、クエリ、データベース操作のワークロードをスライスに分配する。 スライスは並列処理を行って操作を完了する。 内部ネットワーク リーダーノードとコンピューティングノードの間はプライベートで非常に高速なネットワーク。 コンピューティングノードは独立したプライベートネットワークに配置される。 RDBとの互換性 Amazon Redshift は PostgreSQLを大規模データ用に拡張したミドルウェアである。 標準的なRDBMSと同様にデータの挿入、削除、トランザクション処理を実行できる。行指向から列指向に拡張されており、行指向を前提としたクエリは苦手。

default eye-catch image.

(今さら)Docker composeでWordPress環境を用意する

Hello World. docker-composeを使ってコンテナ間の繋がりを定義してみるデモに超速で入門する。 ゼロから書くと不要な時間を要するので、こちらを参考にさせていただいた。 写経する中でポイントを咀嚼していく。 ~/dockercompose_test というディレクトリを作成し、 その中で作業する。 docker-compose.yml 構成を記述する設定ファイル。ymlで書く。 ansibleでymlには慣れているので嬉しい。 version: \"3\" services: db: image: mysql:5.7 #container_name: \"mysql57\" volumes: - ./db/mysql:/var/lib/mysql restart: always environment: MYSQL_ROOT_PASSWORD: root_pass_fB3uWvTS MYSQL_DATABASE: wordpress_db MYSQL_USER: user MYSQL_PASSWORD: user_pass_Ck6uTvrQ wordpress: image: wordpress:latest #container_name: \"wordpress\" volumes: - ./wordpress/html:/var/www/html - ./php/php.ini:/usr/local/etc/php/conf.d/php.ini restart: always depends_on: - db ports: - 8080:80 environment: WORDPRESS_DB_HOST: db:3306 WORDPRESS_DB_NAME: wordpress_db WORDPRESS_DB_USER: user WORDPRESS_DB_PASSWORD: user_pass_Ck6uTvrQ ルートはservices. 名称の理解がフワフワだが、コンテナをサービスと読んでいることが多いくらいの認識. 配下に db と wordpress が存在する。おなじみの構成を定義している。 db dbの定義についてパラメタを追っていく. パラメタ 値 説明 image mysql57 pull してくるイメージを書く. mysql57という名前のイメージをpullする. volumes - ./db/mysql:/var/lib/mysql コンテナ内の/var/lib/mysql を ホストの./db/mysql にマウントする(で良いか?) restart always 再起動ポリシー(コンテナ終了時に再起動するための仕組み)を設定する.再起動ポリシーを設定しておくことで、Dockerデーモンの起動時やホストOSの起動時に自動的にコンテナを開始できる。 alwaysの他にいくつかあるか今はスキップ. environment MYSQL_ROOT_PASSWORD: root_pass_fB3uWvTSMYSQL_DATABASE: wordpress_dbMYSQL_USER: userMYSQL_PASSWORD: user_pass_Ck6uTvrQ コンテナの環境変数を定義する. 環境変数名はコンテナ依存. wordpress wordpressの定義についてパラメタを追っていく. パラメタ 値 定義 image wordpress:latest pull してくるイメージを書く. wordpressという名前のイメージをpullする. バージョンは最新. volumes ./wordpress/html:/var/www/html./php/php.ini:/usr/local/etc/php/conf.d/php.ini コンテナ内のディレクトリをホストのディレクトリにマウントする.マウント先を定義する. restart always 再起動ポリシーをalwaysに設定.内容はdbと同じ. depends_on - db サービスの依存関係を定義する.要は起動する順番を定義する. wordpressのdepends_onにdbを設定することで,wordpressよりも先にdbを起動できる.dbの起動が完了するまで待ってくれるという意味ではない! Control startup and shutdown order in Compose. ports 8080:80 コンテナの80番をホストの8080にマップする. http://localhost:8080 とかするとコンテナの80番が開く. environment WORDPRESS_DB_HOST: db:3306WORDPRESS_DB_NAME: wordpress_dbWORDPRESS_DB_USER: userWORDPRESS_DB_PASSWORD: user_pass_Ck6uTvrQ wordpressコンテナの環境変数を設定する.環境変数はコンテナ依存. docker compose up 以下で定義したdocker-compose.ymlに基づいて構築が始まる. -dはDetachedMode. これによりバックグラウンドで実行される. $ docker-compose up -d ホストで http://localhost:8080 を開くと以下の通り. 永続化の確認 言語を設定しwordpressのインストールを完了させる. db(MySQL)に加えられた変更はホストにマウントされたファイルに反映される. 以下により環境を停止した後, 再度upしたとしても, ホストにマウントされたファイルへの変更が反映され, インストール済みの状態で立ち上がる. $ docker-compose down $ docker-compose up -d まとめ docker-compose を使った WordPress環境構築のデモに超速で入門した. 一緒にコンテナを永続化するデモにも入門した.

default eye-catch image.

(今さら)DockerでWordPress環境を用意する

最小の手数でHello world. とりあえず最小の手数でwordpressを起動してみる。 イメージのダウンロード docker pullでMySQLとWordPressのイメージをダウンロードする。 イメージはサービス単位。 \"MySQL\"を実現するためのOSとミドルウェア。 \"WordPress\"を実現するためのOSとミドルウェア。例えばWebサーバも含んでいる。 まずはMySQL。 $ docker pull mysql:5.7.21 5.7.21: Pulling from library/mysql 2a72cbf407d6: Pull complete 38680a9b47a8: Pull complete 4c732aa0eb1b: Pull complete c5317a34eddd: Pull complete f92be680366c: Pull complete e8ecd8bec5ab: Pull complete 2a650284a6a8: Pull complete 5b5108d08c6d: Pull complete beaff1261757: Pull complete c1a55c6375b5: Pull complete 8181cde51c65: Pull complete Digest: sha256:691c55aabb3c4e3b89b953dd2f022f7ea845e5443954767d321d5f5fa394e28c Status: Downloaded newer image for mysql:5.7.21 docker.io/library/mysql:5.7.21 次にWordPress。何も指定しないと最新が落ちる様子。 $ docker pull wordpress Using default tag: latest latest: Pulling from library/wordpress bb79b6b2107f: Pull complete 80f7a64e4b25: Pull complete da391f3e81f0: Pull complete 8199ae3052e1: Pull complete 284fd0f314b2: Pull complete f38db365cd8a: Pull complete 1416a501db13: Pull complete be0026dad8d5: Pull complete 7bf43186e63e: Pull complete c0d672d8319a: Pull complete 645db540ba24: Pull complete 6f355b8da727: Pull complete aa00daebd81c: Pull complete 98996914108d: Pull complete 69e3e95397b4: Pull complete 5698325d4d72: Pull complete b604b3777675: Pull complete 57c814ef71bc: Pull complete ed1877bc3d14: Pull complete 673ead1d3971: Pull complete Digest: sha256:46fc3c784d5c4fdaa46977debb83261d29e932289a68739f1e34be6b27e04f87 Status: Downloaded newer image for wordpress:latest docker.io/library/wordpress:latest MySQLコンテナを起動 コンテナ(イメージ)を起動する。 $ docker run --name test-mysql -e MYSQL_ROOT_PASSWORD=test-pw -d mysql 013a2eb6b5b1c0b0f61e85cace6540ec036be80c9f85e8c9b5ed3e114a4cc8e8 パラメタは以下の通り。 Option Value 説明 --name test-mysql コンテナに名前を付ける。この例であれば test-mysql -e MYSQL_ROOT_PASSWORD=test-pw コンテナの環境変数を設定する。MYSQL_ROOT_PASSWORDという環境変数としてtest-pwを設定 -d - DetachedMode(Background)を指定する。指定がない場合Foregroud. WordPressコンテナを起動 WordPressコンテナを起動する。 $ docker run --name test-wordpress --link test-mysql:mysql -d -p 8080:80 wordpress a1301075d3667de7eddd9edc0c46edaeb4346a5c46ef444538c9cf9987f31471 パラメタは以下の通り。 Option Value 説明 --link test-mysql:mysql コンテナを連携させる。書式は --link [コンテナ名]:[エイリアス]。test-mysqlがコンテナ名(前述)で、mysqlがそのエイリアス。 -p 8080:80 HostとGuestのポートマッピング。Hostの8080をGuestの80にマッピングする。 Hostの8080にWordPressコンテナ内の80がマップされた。 http://localhost:8080/ でWordPressの言語選択画面が表示される。 非同期で起動したコンテナにアタッチ docker execで非同期に起動したWordPressコンテナ内のディレクトリにアクセスできる。 この例だと/var/www/html。 ゴニョゴニョいじると変更が反映される。 $ docker exec -it test-wordpress /bin/bash root@a1301075d366:/var/www/html# もちろん、コンテナを落とすと変更は失われる。 まとめ DockerでWordPressを動かすデモに超速で入門した。