default eye-catch image.

Snowpipe構築の際の最小権限

Snowpipeは外部ステージ上に置かれたファイルを自動的にSnowflakeテーブルにロードする仕組み。 クラウドプロバイダの機能が透過的に利用されるため、その仕組みを意識する必要がない。 AWS,Azure,GCPがサポートされている。 Snowpipeを設定する際に、登場するオブジェクトにどう権限設定したら良いのかいつも悩む。 オレオレ設定をしてガバくなってないか、調べ直してみて書いてみる。 セキュリティを構成する SYSADMINでリソースを作って、SYSADMINにAssumeRoleして操作をする、というのは良くない。 別にロールを作ってそこに必要な権限を集めると良さそう。 既に外部ステージが存在し、そこに続々とファイルが配置されている。 また、既にSnowflakeにテーブルが存在し、外部ステージに到着するファイルを読み込みたい。 このシチュエーションで、外部ステージとSnowflakeテーブルを結ぶパイプを作成する。 パイプと関連する全リソースにアクセス可能なロールは以下のように作る。 パイプから見れば、外部ステージ、テーブルは既存のオブジェクトであり、 使用する、つまり USAGE が必要。またデータ取得のため、外部ステージから READ が必要。 同様に、パイプから見れば、データベース、スキーマは既存のオブジェクトであり、 USAGEが必要。またデータ投入のため、テーブルに対して INSERT が必要。 パイプがテーブル内のデータを参照して処理をするため テーブルに大して SELECTが必要。 例えば SYSADMIN が PIPE を CREATE した場合、その所有者は SYSADMIN になるが、 これを繰り返すと、SYSADMIN が全ての所有者になってしまう。 今回、Snowpipeに関わるオブジェクトを操作・閲覧可能なロールを作る、という話なので、 新ロールを PIPE の所有者にする。

default eye-catch image.

クエリ同時実行特性の詳細な制御

なんだか分かった気がして分からないSnowflakeのウェアハウスの同時実行特性。 ウェアハウスが1度に何個のクエリを同時処理するか だいぶ抽象化されているがウェアハウスは並列化された計算可能なコンピュータ。 処理すべきクエリが大量にあった場合、1つのウェアハウスがそれらを同時に処理しようとする。 1個のウェアハウスが同時に最大何個のクエリを同時処理するか、 MAX_CONCURRENCY_LEVEL により制御できる。 デフォルト値は 8 に設定されている。 MAX_CONCURRENCY_LEVEL を超えると、クエリはキューに入れられる。 または、マルチクラスタウェアハウスの場合は、次のウェアハウスた立ち上がる。 もちろん、クエリ1個が軽ければ同時処理は早く終わるが、重ければ時間がかかる。 逆に言うと、MAX_CONCURRENCY_LEVEL を小さくすると、1つのウェアハウスが同時処理する 最大クエリ数の上限が下がり、クエリ1個に割りあたるコンピューティングリソースが増える。 MAX_CONCURRENCY_LEVEL を大きくすると、クエリ1個に割り当たるリソースが減る。 同時実行数を減らすとキューイングされる数が増える MAX_CONCURRENCY_LEVEL を小さくするとクエリの処理性能が上がるが、 キューに入るクエリの数が増える。 ちなみにデフォルトだと、キューに入ったクエリは永遠にキューに残り続ける。 キューに入ったクエリをシステム側でキャンセルする時間を STATEMENT_QUEUED_TIMEOUT_IN_SECONDS により設定できる。 ウェアハウスサイズとの関係 ウェアハウスサイズアップはスケールアップ、マルチクラスタはスケールアウト、 なんかと、テキトーに丸めて表現されたりするが、 ウェアハウスサイズアップにより、コンピューティングリソースが増加するため、 同じ MAX_CONCURRENCY_LEVEL であっても クエリ1個に割り当たるリソースが増えることで、 結果的に所要時間が小さくなる。見かけ上、並列性が上がった風になる。 ウェアハウスサイズ、ウェアハウス数の2つのファクタによりリソースの総量が決まるため、 これらを固定した状態で MAX_CONCURRENCY_LEVEL を増やしたとしても、 結果として処理できる量が増えたりはしない様子。 同時実行性能が関係するパフォーマンスのベスプラ お金をかけずに性能が上がるということはない。 MAX_CONCURRENCY_LEVELはあくまで微調整用途。 大量の小さなクエリをガンガン処理するといったケースでは、ベスプラでは 順当にウェアハウス数を増やして、その上でウェアハウスサイズを増やすべきとされている。 この辺りが、「気分でウェアハウスサイズを増やしても性能上がらないなー」 という現象の一因(、かもしれない)。

default eye-catch image.

複数のクラスタリングキーによるMaterialized Viewを作って透過的にベーステーブルのクエリをブーストする

基本的には同一のテーブルに対して設定できるクラスタリングキーは2個から3個。 ただし2個以上設定すると、1個の場合と比較して原理的に効果が落ちる。 パフォーマンスを落とさずにクラスタリングキーを追加できないか。 ベーステーブルへのクエリ実行において透過的にMaterialized Viewが使われる Snowflakeの特徴を利用して、任意の個数のクラスタリングパターンを追加する。 そんな内容が書かれた記事を見つけたので読書感想文を書いてみる。 Defining multiple cluster keys in Snowflake with materialized views [arst_toc tag=\"h4\"] オレオレクラスタリングの必要性と難しさ 色々な理由でナチュラルクラスタリングに頼らず自力でクラスタリングキーを設定する場合がある。 その際、クエリの傾向をみて、より適切なプルーニングが行われるカラムを選ばないといけない。 (\"適切な\"といのは、想定する使われ方と異なる使われ方がされた場合にプルーニング対象が減り クエリパフォーマンスが落ちる頻度を減らすということ。) ナチュラルクラスタリングは良く出来ていて、オレオレキーがナチュラルキーを超えるのは難しい。 結構なケースで「あっちが立てばこっちが立たず」という状況になる(、と思う)。 腕力に任せてオレオレキーの数だけテーブルを作る ストレージの料金は安いのだから、クラスタリングキーの数だけテーブルを作ってしまえ。 という、マッチョなやり方を取ることもできる。 同一テーブルに対して一度に設定できるクラスタリングキーは限られているが、 CREATE TABLE文に CLUSTER BY を付けることできるので、 クラスタリングキーごとにテーブルを複数作ってしまえば、この問題はある程度解決する。 注文管理テーブル的なテーブル\"注文\"があったとして、\"注文日付\"、\"顧客キー\"、\"店員キー\"の いずれかを条件とするクエリが均等に来るだろうという前提。 以下のようにCREATE TABLE文に CLUSTER BY をつける。 --注文日付をクラスタリングキーとしてテーブルを再作成 create table \"注文_clustered_by_注文日付\" cluster by (\"注文日付\") as ( select \"注文日付\", \"注文キー\", \"顧客キー\", \"店員キー\" from \"テストDB\".\"テストスキーマ\".\"注文\" ) --顧客キーをクラスタリングキーとしてテーブルを再作成 create table \"注文_clustered_by_顧客キー\" cluster by (\"顧客キー\") as ( select \"注文日付\", \"注文キー\", \"顧客キー\", \"店員キー\" from \"テストDB\".\"テストスキーマ\".\"注文\" ) --店員キーをクラスタリングキーとしてテーブルを再作成 create table \"注文_clustered_by_店員キー\" cluster by (\"店員キー\") as ( select \"注文日付\", \"注文キー\", \"顧客キー\", \"店員キー\" from \"テストDB\".\"テストスキーマ\".\"注文\" ) もちろん通常のTableであれば利用者側が用途に応じて利用するテーブルを選択しないといけない。 利用者側にそれぞれのテーブルを提供し、利用者は用途に応じてテーブルを使用する必要がある。 もう一歩、使い勝手が良くない。利用者が1つのテーブルを触る際に、意識せずに(透過的に) 最適なクラスタリングが行われたデータにアクセスさせることはできないか。 Materialized Viewを使うとこれが可能になる。 Materialized Viewの復習 Snowflakeに限らず、普通のRDBMSにも Materialized View は存在する。 Materialized Viewの各列に設定したサブクエリを事前計算して、クエリ時に再利用する仕組み。 以下のようにすると、cntは事前に処理され、クエリ時に再利用される。 create materialized view \"注文_aggregated_by_日付\" as ( select \"日付\", count(*) as cnt from \"注文\" group by 1 ) Snowflakeの場合、ベーステーブルに対してクエリを行うと、自動的にMaterialized Viewが評価される。 例えば、以下のクエリは\"注文\"ではなく\"注文_aggregated_by_日付\"が評価される。 select \"日付\", count(*) as cnt from \"注文\" group by 1 meta-dataにMaterialized Viewの定義が記録され、特定のルールでベーステーブルを触ると meta-dataからルックアップされたMaterialized Viewが評価される、的な動きだろうと思っている。 ベーステーブルを変更(INSERT,UPDATE)すると、自動的にMaterialized Viewに反映される。 その際、ベーステーブルにおいて自動クラスタリングが行われる。(瞬時ではなく後で非同期に行われる) Materialized Viewの効果的な使い方 ベーステーブルをあらかじめ\"日付\"でソートしておく。 ナチュラルキーとして\"日付\"が選ばれて自動クラスタリングが行われ、\"日付\"のブロックごとに パーティションが並ぶことになる。 create table \"注文\" as ( select \"日付\", \"注文キー\", \"顧客キー\", \"店員キー\" from \"テストDB\".\"テストスキーマ\".\"注文\" order by \"日付\" ) \"注文\"テーブルは\"日付\"の順で並んでいるのだから、\"日付\"を条件としたクエリを叩くと、 超絶有効にプルーニングが効くことになる。 select \"日付\", count(*) as cnt from \"注文\" where \"日付\" between date\'1993-03-01\' and date\'1993-03-31\' group by 1 \"注文\"テーブルに対して\"顧客キー\"を指定してクエリを行なった場合、 もし Materialized View を作っていなかったとしたら、Full Scan的な検索になるはず。 select * from \"注文\" where \"顧客キー\"=52671775 そこで、以下のように\"顧客キー\"をクラスタリングキーとしてMaterialized Viewを作っておく。 create materialized view \"注文_clustered_by_顧客キー\" cluster by(\"顧客キー\") as ( select \"日付\", \"注文キー\", \"顧客キー\", \"店員キー\" from \"注文\" ) ; すると、以下のクエリは\"注文\"ではなく \"注文_clusterd_by_顧客キー\"に対して行われる。 select * from \"注文\" where \"顧客キー\"=52671775 Materialized Viewなので、ベーステーブルである\"注文\"に対して追加・更新を行うと、 ベーステーブルから派生させてMaterialized Viewも自動更新される。 ユーザには\"注文\"を提供するだけで良い。 (Materialized Viewへのアクセス権が必要) コストが許す限り望むだけのMaterialized Viewを作っておくことで、 ベーステーブルに対するクエリが最適なクラスタリングが行われたMaterialized Viewに 通される。

default eye-catch image.

列レベルセキュリティのドキュメントを読んでみる

アーキテクチャの理解がまぁまぁ進んでホワイトペーパーを読んで「へー」と思える感じになったが、 ガバナンス系の機能について完全に知ったかレベルで「言葉だけ聞いたことがあるな」ぐらい。 知識の幅と深さを広げてみようと思う。妄想と憶測の個人のメモなのはご容赦ください。 公式は以下。 列レベルのセキュリティについて 列レベルセキュリティの下にダイナミックデータマスキング、外部トークン化という単元がある。 今回はこれらを読んでいく。 [arst_toc tag=\"h4\"] 中央集権的な権限管理 Enterpriseな分野において\"職務分掌\"という考え方がある。SoD, Segregation of Duties。 職務分掌とは、社内においてそれぞれの役職や部署、個人が取り組む業務を明確にし、責任の所在や業務の権限を整理・配分することです。1人の担当者が2つ以上の職務を重複して持つことで不正を行う機会が発生してしまうことを未然に防ぎ、内部統制やコンプライアンスを実現します。 職務分掌/ポリシー管理 ユーザ・ロールに付与するアクセス権を中央管理できるか否かを表す言葉。 セキュアビューを使うことによってもユーザ・ロールに対して見せる/見せないを制御できるが、 セキュアビューはRBACに基づく権限設定するわけで、所有者に全能の権限変更を許してしまう。 つまり、せっかく閲覧制限しても所有者であれば自力で閲覧可能に出来てしまう。 こういうことがないように、中央集権的な権限管理を行いたいという欲求がある。 \"SoD対応\"という感じで使われる。ダイナミックデータマスキングと外部トークン化はSoD対応。 見えてはいけないデータをどうやって隠すか? ざっくり、DBに生データを格納して読み取り時に必要に応じてマスクする方法と、 DBにマスクしたデータを保存して読み取り時に復元する方法がある。 前者はダイナミックデータマスキング、後者は外部トークン化。 どちらを利用するか選択するか、という話ではなく、 ダイナミックデータマスキングを行う際に追加で外部トークン化するか否か、決めるイメージ。 DB内のデータを外部トークン化して物理的に読めない状態としないでも、 生データの読み取り時に動的にマスクすることで要件を達成することはできる。 ニーズに合わせてどちらがより適切か選択する必要があるが、これが結構細かい。 「どちらがより強力か」みたいな簡単な比較にはならない。 外部トークン化は、同一のデータを同一のマスク値に変換する特徴がある。 中身は見えないがカウントを取るとその数は一致する。 対して、ダイナミックデータマスキングは、見えない人には無いものとして扱われるから、 マスク値を使ってグループ化したり、カウントを取ったりすることはできない。 その他、それぞれの比較が以下に書かれている。 ダイナミックデータマスキングまたは外部トークン化の選択 マスキングポリシー マスキングポリシーはスキーマレベルのオブジェクト。テーブルと独立して定義できる。 要は、保護対象と保護方法が独立していて、保護対象に依存しない形で保護方法を定義する。 クエリの実行時にマスキングポリシーが評価されて、クエリ実行者は適用済みデータを取得する。 ユーザIDとかロールとかによって保護するか否かを制御する内容を書いておく。 ロールAには見えるけど、ロールBには見えない、という結果になる。 公式に実行イメージがあるので貼ってみる。 マスキングポリシーを作ってテーブルのカラムにあてているんだな、とか、 文字列型のカラム値を加工して文字列型の値を返す何かを書くんだな、とか分かる。 -- Dynamic Data Masking create masking policy employee_ssn_mask as (val string) returns string -> case when current_role() in (\'PAYROLL\') then val else \'******\' end; -- Apply Masking Policy to Table column ALTER TABLE {テーブル名} ALTER COLUMN {カラム名} SET MASKING POLICY employee_ssn_mask 通常、所有者はオブジェクトに対して全権を持つが、マスキングポリシーはアクセス制御と 独立していて、所有者に対してデータを保護することもできる。それぐらい独立している。 「機密を守る」専門の部署を作って、そこで統一的にデータ保護を設計する、 みたいな運用をすることもできる。 UDFsを使ってマスキングポリシーの管理をしやすくする 上のような感じでマスキングポリシーを作ってルールをハードコードし続けると、 いわゆるコードクローンが発生したり、共通化が難しくなるなど、結構面倒なことになる。 こういったケースには、ユーザ定義関数 (User Defined Functions, UDFs)が有効。 例えば、以下のように SHA2 でコードするUDFを定義し、マスキングポリシー内でUDFを 評価することができる。こうしておけば UDFを再利用したりリファクタしたりしやすくなる。 CREATE OR REPLACE FUNCTION .sha2_array(SRC ARRAY) RETURNS ARRAY LANGUAGE SQL AS $$ ARRAY_CONSTRUCT(SHA2(ARRAY_TO_STRING(SRC, \',\'))) $$ ; CREATE OR REPLACE MASKING POLICY .array_mask AS (val ARRAY) RETURNS ARRAY -> CASE WHEN .has_mask_full_access(current_role()) THEN val -- unmask WHEN .has_mask_partial_access(current_role()) THEN .sha2_array(val::ARRAY)::ARRAY -- partial mask ELSE TO_ARRAY(\'***MASKED***\') -- mask END; ALTER TABLE IF EXISTS .test MODIFY COLUMN names SET MASKING POLICY .array_mask; マスキングポリシー条件 上の実行イメージのように、手続き型の処理ではなく、関数評価による宣言的な設定を行う。 上の例ではマスキング対象を判定するcase whenを使用している。 この条件として、条件関数、コンテキスト関数、カスタム資格テーブルを利用できると記述がある。 上の例では、current_role コンテキスト関数を使用し、ロールに応じたマスキングを行う。 公式にはコンテキスト関数のうち invoker_role, invoker_share について言及がある。 カスタム資格テーブルはマルチテナントデザインパターンのホワイトペーパーに出てきた。 ユーザID、またはロールIDと、それらが何のリソースにアクセスできるかを1つのテーブルに したもの。例えば current_role に対して該当列をマスクするか否かを自前のテーブルを 使って表現できる。深読みしないでおく.. セキュアビューが増えてしまう代替案としてマスキングポリシーを使う セキュアビューを利用することによってユーザ・ロールに対する行レベルセキュリティを実現できる。 (列レベルセキュリティではないが、選択的にデータを見せるという意味で同列) CREATE TABLE widgets ( id NUMBER(38,0) DEFAULT widget_id_sequence.nextval, name VARCHAR, color VARCHAR, price NUMBER(38,0), created_on TIMESTAMP_LTZ(9)); CREATE TABLE widget_access_rules ( widget_id NUMBER(38,0), role_name VARCHAR); CREATE OR REPLACE SECURE VIEW widgets_view AS SELECT w.* FROM widgets AS w WHERE w.id IN (SELECT widget_id FROM widget_access_rules AS a WHERE upper(role_name) = CURRENT_ROLE() ) ; BI用途が例として上がっているが、ビューからビューを作って、その先のビューを.. といったようにビューは大量に作られることが常なので、セキュアビューだけによって ユーザ・ロールに対する閲覧制限をかけるというのはかなり大変なことになる。 テーブル・ビューなどのオブジェクトと独立して定義できるマスキングポリシーなら、 どれだけBIが複雑になっても確かに管理が容易になりそう。 マスキング対象列からマスキング非対象列へのコピー 結構ハマったのでメモ。ハマって苦しんだ時何度読んでも理解できなかった...。 考慮事項 ダイナミックデータマスキングが設定された列の値を使うとき、 使う人が非マスク値(生データ)を閲覧できる場合は生データをコピーしてしまう。 生データが閲覧できずマスク値が表示されてしまう場合はマスク値をコピーしてしまう。 外部トークン化 外部関数を使って外部APIに実装されたトークン化処理を実行する、ということなのだが、 信じられないぐらい実装例をWebで見つけられなかった。(探し方が悪いのか..) 外部関数とは、要はクラウド上で利用可能なAPIを指す。 AWSで言うとAPI Gatewayの先にLambdaがあり、API統合により透過的に利用可能にしたもの。 Lambdaにトークン化コードを書いておきAPI Gatewayから呼べるようにしてAPI統合する。 既成の外部トークンプロバイダが存在し、Snowflakeから以下が利用できる様子。 ALTR,Baffle,Comforte,Fortanix,MicroFocus CyberRes Voltage, Protegrity,Privacera,SecuPI,Skyflow ちょっと深そうなので省略。

default eye-catch image.

マルチテナント設計方針, Design Patterns for Building Multi-Tenant Applications on Snowflakeを読んでみて

全般的に参考にできるリソースが少ない中で、公式が出すホワイトペーパーは本当に参考になる。 マルチテナントの設計方針が書かれている以下のホワイトペーパーを読んでみた。 もちろん、マルチテナント的な応用をする際に参考になりそうな話ばかりなのだが、 それだけに限らない「オブジェクト配置に関する設計の大方針」が説明されているな..と。 Design Patterns for Building Multi-Tenant Applications on Snowflake この記事では、ホワイトペーパーを読んで気づいた何かを書き連ねてみる。 もちろんホワイトペーパーのメインはマルチテナントなのだからそう読むべきだが、 「テナント」を「グルーピングされたデータ」と仮定して追加で得られた感想を書いてみる。 英語力、文章の読解力の不足、認識違い、などなどにより誤りが多数あるかもしれませんが、 個人のメモ、ということでその点ご容赦ください。。 [arst_toc tag=\"h4\"] オブジェクト単位で持つかテーブル内に混在させるか SnowflakeはDatabase,Schema,Tableなどのオブジェクトが割と疎に結合する構造になっている。 Snowflakeのベストプラクティスを重視すると、オブジェクト同士を疎結合したくなる欲求にかられる。 確かにZero Copy CloningやSharingなど、アーキテクチャの優位さを活用しやすいのだが、 脳死で疎結合にする前に、密結合パターンとのメリデメを一応考えた方が良いと思う。 疎結合にするということは、結合により多くの手間がかかるということだと思う。 実際にオブジェクト単位にデータを保持しようとすると、オブジェクトの数が増えすぎたり、 増えたオブジェクト間のデータを結合するのに手間を要したり、一言で書くと面倒になる。 本当にリソース単位で持つべきなのか、従来のように1つのリソースの中で論理的なグルーピングを するのに留めた方が良いのか、ちょっと考えた方が良いと思う。 1つのテーブルにテナントを混在させる (Multi-tenant table,MTT) 複数のテナントを扱おうとしたとき、「テナントID」などを利用して1つのテーブルを共有するモデル。 何しろ、テナントがいくら増えても1つのテーブルで対処できるという簡単さがある。 そのテーブルに付随する様々なオブジェクトが増えることがない。 レコード数が増えることでクエリのパフォーマンスが落ちるのではないか、という懸念については、 ちょうど「テナントID」をクラスタキーと設定することで、テナントがマイクロパーティションに 割り当たることになり、むちゃくちゃ効率的にプルーニングが効くため、問題ない。(エアプ) テナントが1つのテーブルを利用するためにテナント間でスキーマ定義を合わせる必要があり、 差異がある場合には、最小公倍数的な定義にする必要があり無駄が発生する。 1つのクエリで複数のテナントを横断できることがメリットでありデメリットでもある。 原理的に他のテナントのデータを触れてしまうということは便利だが確実に安全でない。 権限モデルの適用対象となるテーブルよりも小さい単位で論理的なブロックを作ると、 結果として権限モデルが関与しない場所で、列レベルセキュリティ、行アクセスポリシー、 資格テーブル等を駆使して「自前の権限設計」をしないといけなくなる。 CLONEの最小粒度はテーブル、SHAREの最小粒度はデータベースであり、 これらを積極的に使った「Snowflakeっぽい」疎結合なアーキテクチャを導入しづらい。 後述するが、複数のテナントでウェアハウスを強制的に共有してしまう。 つまりテナントごとのコンピュートコストを算出できないし、テナントごとにスケールできない。 共有することでコスト効率は良くなるため、コスト算出を分離しないで良いならメリット。 オブジェクトごとにテナントを配置する(Object per tenant,OPT) テナントをTable単位,Schema単位,Database単位など、オブジェクト単位で分離する。 1回のSQLでオブジェクト横断的にアクセスできなくなるのがメリットでありデメリット。 何しろ、RBACによるアクセス制御が効くようになるのでセキュリティ設計が簡単になる。 オブジェクトを疎結合しましょう、というSnowflake的なデザインパターンだと思う。 箱を増やすと、それに付随するリソースを何個か作らないといけないのが常なので、 扱うテナントが増えるとすぐに管理できないくらいオブジェクト数が増える。 ホワイトペーパーには10から100数十ぐらいではないか、と書かれている。 個人的にはテナントを3個も作ったらもうIaCツールが欲しくなる。 CLONE,SHARE はOPTを前提にしているとも言える。 OPTを選択するとSnowflakeっぽいアーキテクチャにしやすい(と思う)。 アカウントごとのテナントを配置する(Account per tenant,APT) テナントをアカウント単位で分離するという考え方。いわゆる「dedicated」な配置方式。 同一アカウント内で複数のオブジェクトに触ることは出来るが、アカウントを跨って触ることはできない。 物理的に触れない場所に分けておく方式。OPTよりもさらにグループ間の独立性を高めることができる。 リソースの共有を物理的に遮断する配置方式なので、コンピュートの効率性は良くない。 テナントごとに完全にコストを見積もることができる。 アカウントの上に「組織」の概念があるため、管理上の連続性はある。 ただ、大体のクラウドサービスと同様に、アカウントの上の管理は「取ってつけた感」があると思う。 そもそも「組織」内の複数アカウントを紐づける概念なので、 テナントがビジネス上の異なる組織であったりしたら、その時点で怪しくなる。 今回は薄く読んでおくに留める。 MTT,OPT,APTまとめ ホワイトペーパーにPros.Cons.がまとまっていたのでざっくり意訳してみる。(テキトー...)。 特にOPT,APTのスケーラビリティが低く、用途によってMTT一択となる可能性がある。 MTT OPT APT データモデルの特徴 各テナントは同一のShapeを持つ必要がある。「テナントID」等によりグループを識別できるようにする。 各テナントは異なるShapeを持つことができる。 各テナントは異なるShapeを持つことができる。 スケーラビリティ 上限なし 10から数百では? 10から数百では? セキュリティ的な何か 開発者がセキュリティを意識する必要がある。開発者がRBAC,列レベルセキュリティについて熟練していないといけない。 権限管理テーブルの管理さえ厳密にしておけば、あとはRBACにさえ気をつければ良い。 最大限、セキュリティに振ったモデル。いわゆるdedicatedな環境。 備考 必ずコンピュートを共有することになる。結果としてコストが浮くし運用が楽になる。 要件次第でグループ間の結合度を自由に決められる。つまり、コンピュートを共有するか否かを決められる。共有すればコストが浮くがグループ間の連携に際して考えないといけないことが増える。 RDBMSを使ったレガシープラットフォームをリプラットする際に親和性が高いのではないか。 欠点 マルチリージョンのShareが難しくなる。高パフォーマンス化のためにシャーディング(クラスタリング?)が必要かもしれない。レコード数が増えるため更新系(UPDATE,MERGE)が大変になる。1つのテナントについてのコストを見積もるのが大変になる。 オブジェクトを作ることは簡単だが、似て非なるオブジェクトの一貫性を保つのは難しい。オブジェクトの増加に伴い、オブジェクト同士を同期するのが大変になる。(IaCのような)自動化の仕組みが必要。 OPTど同様にアカウントを作ることは簡単だが似て非なるアカウントの一貫性を保つのは難しい。コンピュートを共有できなくなる。結果として高コストとなる。OPTど同様に(IaCのような)自動化の仕組みが必要。 MTTで考慮すべきデザインパターン集 同一テーブル内でテナント同士を論理的に分離する方法 まず、異なるテナントを同一のテーブルで認識するため、テナントの識別子を持たせる。 RBACに基づいたアクセス制限が出来ないため、アクセス制限を行う論理的な仕組みが必要となる。 ホワイトペーパーで紹介されているのは、資格テーブル(Entitlements Table)を用意するというもの。 資格テーブルに、どのユーザ、ロールがどのグループにアクセスできるか書いておき、JOINして使う。 Snowflakeにおいて、Viewは背後にある(Underlying)テーブルをViewの外から見えてしまう。 例えばSnowsightでViewの定義を見るとCREATE VIEW文の定義が見える。 Secure Viewを使用すると見えないため、Secure View上で資格テーブルとの結合を行う。 資格テーブルの保護 資格テーブルはグループを論理的に分離する上で重要で、管理を厳密にしないといけない。 誰もが触れる場所に資格テーブルがあったりすると、開発者が意図しないところで不用意に変更されてしまう場合がある。 Secure Viewと資格テーブルを異なるスキーマに配置し、開発者だけが資格テーブルを触れるように しておけば、少なくとも非開発者が資格テーブルを変更してしまう、という事故を防げる。 例えば「ユーザが増える度にINSERT/UPDATEして資格テーブルを書き換える」ような運用をすると、 どこかでミスってガバい資格テーブルになってしまうかもしれない。 ユーザ・ロールの権限の変更により自動的に資格テーブルが更新されるような仕組みを用意しておくべき。 常に回帰テストを実施して、ガバい論理テーブルにならないよう注意すべき。 テナントとパーティションの割り付けとクエリ効率 (根拠が薄いのだが)、クラスタキーを手動設定してオレオレクラスタリングを強制したとしても、 Snowflakeが自動的に決めた(Naturalな)クラスタリングに勝てない場合が多い気がする。 要は、将来叩かれるであろうあらゆるクエリのWHERE句かJOINのON句がいかに多くのパーティションを プルーニングするか、が大事だが、普通、そんなに期待通りプルーニングできないと思う。 テナントIDをクラスタキーとして、そのテナントのデータだけを取得する、ということは、 原理的には該当テナントのパーティション以外をプルーニングすることを保証する訳で、 これは結構期待できるのではないかと思う。(ちょっとやってみたい) ベスプラでは、クラスタキーは2,3個程度とされていて、他に日付などを設定する。 グループの識別子をソートして並べておく。 通常、背後でSnowflakeが自動的にクラスタリングを処理してくれる(自動クラスタリング)。 もしテナント数が増加してテーブルのレコード数が増えると、自動クラスタリングの負荷が上がる。 INSERT,UPDATE等の追加変更操作をトリガとして自動クラスタリングが走るので、 頻繁に追加変更操作をするシナリオだと超絶課金されてしまうかもしれない。 フロント側から透過的にSnowflakeのSSO機能を利用する 昔、Laravel製のWebアプリにOAuthによるSSO機能を実装したことがある。 SnowflakeはSAMLによるSSOもサポートしていて、ほぼノーコードでIdPとのSSOを実現できる。 SnowsightでSnowflakeを使う分にはポチポチ連携して終了で良いのだが、 通常、SnowflakeのフロントにBIツールなどのアプリケーションが存在し、 ユーザはフロントの認証機構を介して透過的にSnowflakeのデータにアクセスしたい。 フロント側のアプリはSnowflakeの以下の機構を利用してSSOを実現することになる。 カスタムクライアント用のSnowflake OAuth の構成 公開鍵認証方式によるOAuthアクセストークンを介したセッションへの認可が行われる。 アクセストークンに対するアクセスとセキュリティはフロント側アプリの実装に任される。 例えば、AWS Secrets Managerなどを利用することで、アプリ側が堅牢になったりする。 フロント側アプリへのログインにより、有効なアクセストークンと対応するセッションが得られ、 セッションを経由して、セッションに紐づくデータにアクセス可能となる。 Snowflake側で認可を無効にすることができる。 とはいえ、OAuthに対応したBIツールなんかでは、画面ポチポチだけで連携できるので便利。 作業用データベースを分離する RBACによるアクセス制御によって、「ここで何をすべきか」、「誰が何に触れるべきか」を定義する。 データベースを分離して権限設定することにより、これらが明確になり管理を単純化できる。 例えば、ELTの際に作成する中間テーブルなどは本体のリソースと異なり一般ユーザに開放する必要はないため、 公開用データベースとは異なる別のデータベースに作成し、別に権限設定をすべき。 開発者のみが中間テーブルなどにアクセスできる仕組みを構築できる。 ワークロードを分離する Snowflakeの大原則は、ウェアハウスの稼働時間に対して課金されること。 ウェアハウスのサイズを大きくすることでワークロードの処理時間が短くなるのであれば、 大きなウェアハウスでワークロードを短時間に終わらすことが目指すところ。 スケールアップにだけ目が行って稼働時間の短縮を忘れるとコスト的に酷い目に合う。 例えば、BIのような用途だと、小さなウェアハウスを予測可能な時間帯で回すことになる。 一方、AdHocな用途だと、大きなウェアハウスを予測不可能なタイミングで回すことになる。 これらのワークロードを1つのウェアハウスに盛ると、大きなウェアハウスを長時間稼働させてしまう。 「小サイズ高稼働」と「大サイズ低稼働」を分けておけば、大サイズは高稼働しないので、 結果的に安くなる。 マルチテナント的には、テナントにデフォルトで共有ウェアハウスを割り当てておき、 オプションで(有料で)占有ウェアハウスを割り当てる、なんていうパターンもある。 ユーザに適切なウェアハウスを割り当てる 複数のウェアハウスを使用・操作できるようにロールのアクセス制御を行いユーザにアタッチすると、 ユーザはセッション内にウエアハウスを選択できるようになる。 ユーザに対してデフォルトのウェアハウスを設定することで、より適切なルーティングが可能となる。 (ロールに対してデフォルトのウェアハウスを設定することはできない。あくまでユーザに設定する。) OPTで考慮すべきデザインパターン集 OPTの特徴 テナントをオブジェクト毎に分離する方法。オブジェクトを疎結合する大方針にあったやり方。 最も簡単に綺麗に分離できる。 データベース毎か、スキーマ毎か、テーブル毎か、パイプラインが満たす要件、開発のライフサイクル、 テナント毎のデータにどれだけ一貫性があるか、などにより選択する。 また、テナント数はどの程度か、テナントは何個のテーブルを使用するか、 CLONEするか、レプリケーションするか、など多くのことを想定しないといけない。 CLONEはデータベース、スキーマ、テーブルの各層で効くが、レプリケーションはDB単位だから、 その観点でオブジェクトを選択しないといけない。 複数のオブジェクトの作成を自動化する テナントが増えると、そのオブジェクトに関係するアクセス権などを維持するオブジェクトを追加する 必要が発生する。それぞれのテナントに個別の設定もあれば一貫性を保つべきものもある。 テナントが少ないうちは手でポチポチやっていても良いが、多くなってくると大変になる。 そういった背景があって、リソース作成を自動化したいという欲求が発生する。 外部のいろいろなIaCツールを使って自動化すると便利になる。 この辺り、昔Webアプリを書いていた頃お世話になっていた何かと久々に再会。 要はアプリケーション開発の一部としてSnowflakeリソースを一貫性をもって構築する。 マイグレーションツールのFlywayがSnowflake対応している。 Flywayを使ってスキーマ定義の一貫性を保てる。 (対応している範囲であれば)Terraformを使ってリソースの一貫性を保てる。 MTTと比較したOPTにおけるユーザルーティングの重要性 Authenticating and authorizingの節、読解が難しい。 ユーザを正しいデータベース、ウェアハウスに繋ぐ方法はMTTと同様だがMTTよりも重要である、 なぜMTTの時よりも重要になるのか、は、MTTと比較して文脈(context)が異なるから、とのこと。 MTTの場合、オブジェクトを共有するため、アクセス権はオブジェクトへのルーティングではなく、 オブジェクト内の資格テーブルが決定する一方で、OPTの場合は、まさにアクセス権そのものになるから、 とか、オブジェクトごとにテナントを分離するのだから、ユーザを誤ったテナントに誘導してはならない、 とか、そういうことを言いたいのだと思う。 重要な位置がMTTとOPTで異なりOPTの場合は重要、ということだろう。 MTT,OPTによるコスト最適なingest/transformation ELTLのingest/transformation用テーブルの配置方法によってコストが変わる。 しばしば、1つのオブジェクトにワークロードを詰める場合、個別のオブジェクトに分散させる よりも低コストである。例えば、複数テナントを分離した各テナント毎に ELTL ingest/transformation用テーブルを用意し、それぞれ別々にパイプラインを 走らせるよりも、共通のテーブルに対して1つのパイプラインを走らせる方が 低いコストで済む。(理由が明示されていないが、感覚的にはオブジェクトごとにクエリが 分割されてしまうことでクエリの実行回数が増加し、結果として長い時間ウェアハウスが 稼働していまうからだと考える) いろいろ組み合わせが考えられる。OPTとMTTを組み合わせたhybrid構造によりコストと 機能を最適化する。つまり、ingest/transformationを共通オブジェクトで行い、その後、 各テナント毎のオブジェクトに展開するか、そのまま共通のデータベースに展開するかを選ぶと良い。 レプリケーションはDB単位なのでOPTが適切 レプリケーションはデータベース単位なので、テナントがデータベース内のリソースを 共有してしまっていると、個別のテナントを選択してレプリケーションすることが難しくなる。 そういった用途があるのであればOPTとしてデータベース単位にテナントを分けるべき。 APTの特徴 1つのテナント毎に1つのSnowflakeアカウント、ウェアハウス、1つのデータベースを用意する。 ただし、アカウントを跨いで出来ることは限られるため、どうしても例外が発生してしまう。 基本的には、アカウント内に存在するデータベースに対してアカウント内のウェアハウスで処理するが、 共通データベースのingestion/transformationデータをSHAREして持ってきたり、 持ってきたデータに追加のELTLをかけたりするために、追加のウェアハウスが必要となる場合がある。 アカウントを跨ってウェアハウスを共有することはできないので、APTの場合最もコストがかかる。 APTで考慮すべきデザインパターン集 APTの認証 フロント側の認証の方法はMTT,OPTと同じものが大体機能する。 APTはアカウント毎にログインURLが異なる。ユーザとアカウントが1対1対応するため、 フロント側を介さずにユーザが直接Snowflakeアカウントにログインすることも可能となる。 評価 評価基準:データ配置/セキュリティ観点 3つのモデルを評価すべきだが、SnowflakeはMTTから評価をスタートすることを推奨している。 つまり、最初にMTTが機能するか、そうでないなら何故かを評価すべき。 データの配置方法・セキュリティ観点での評価チャートは以下の観点に依存する。 データが配置・暗号化に関する契約上の責任 データが配置・暗号化に関する規制上の責任 データの配置・暗号化に関する情報セキュリティ基準 アプリケーション所有者がRBACの強制をどう見ているか アプリケーション所有者が資格テーブルに対する行レベルセキュリティをどう見ているか 顧客のアプリケーションへのアクセス方法 テナント間でテーブルスキーマ等のシェイプがどれだけ近いか MTTを軸に、以下のチャートを見て判断する。 Snowflakeの暗号化フレームワーク Snowflakeの暗号化体系について詳細を知らなかったので一旦まとめてみる。 暗号化キーの階層モデル Snowflakeの大前提の1つに、すべてのデータはat restで暗号化される、という点がある。 セキュリティ標準に基づいてAES256ビット暗号化を行う。その際、暗号化キーを管理する。 暗号化キーの管理方法が厄介なのだが、独自の管理を行うため基本的にユーザの手間はない。 暗号化キー管理については以下。 Snowflakeの暗号化キー管理を理解する Snowflakeにおいて、暗号化キーは階層モデルに基づいて暗号化される。 暗号化キーは以下の4つのレイヤから構成され、各レイヤと対応するキーが用意される。 上位レイヤのキーが下位レイヤのキーを暗号化する。 もちろん、各レイヤの暗号化キーは各レイヤのオブジェクトの暗号化に使われる。 1. ルートレイヤ (Root Keys) 2. アカウントレイヤ (Account Master Keys) 3. テーブルレイヤ (Table Master Keys) 4. ファイルレイヤ (File Keys) 下のレイヤにおいて、より狭い範囲を適用範囲とすることとなる。 ルートキーだけだと巨大な範囲を1つのキーで暗号化せざるを得ないが、 細分化しておくことで、各暗号化キーが対象とする範囲を狭くすることができる。 アカウントレベルで、アカウント毎に暗号化キーが用意されることに注意する。 アカウント毎に異なる暗号化キーによりAES256暗号化が行われ分離される。 暗号化キーのローテーション どれだけ堅牢に暗号化キーを管理していたとしても、長時間晒されることはリスク。 なので、定期的に暗号化キーをローテーションしよう、という発想になる。 Snowflakeにおいて、各暗号化キーは30日ごとに自動的に更新される。 常に最新の暗号化キーで暗号化が行われ、古くなったキーは復号にのみ使われる。 こうすることで、同じ暗号化キーの有効期限を限定できるようになる。 Tri-secret Security Snowflakeをホストするプラットフォームで顧客が管理する暗号化キーを管理する。 Snowflakeが用意する暗号化キーと顧客管理の暗号化キーをセットで使って 複合マスターキーを作る。 複合マスターキーはアカウントレイヤのマスターキーとして使われ、 階層内の(テーブル、ファイルの)暗号化キーをラップする。 (暗号化キーをさらに暗号化するのであって、生データを暗号化したりはしない。) 複合マスターキー内の顧客管理キーを無効化すると、複合マスターキーが関与する 範囲のデータを復号できなくなり、一段セキュリティレベルが上がる。 顧客管理キーは\"アカウントレベル\"のみ。テーブル、ファイルレベルは無い。 \"Tri\"ということで3つの要素からなるセキュリティフレームワークなのだが、 その構成は以下。この項は 2つ目の\"顧客管理キー組み入れ\"。 1. Snowflakeの暗号化フレームワーク 2. 顧客管理キー組み入れ 3. Snowflakeの組み込みユーザ認証 評価基準:暗号化/データ分離/保護の観点 暗号化/データ分離/保護の観点でマルチテナントを選ぶケースもある。 クラウド上でテナントを論理的に分離している訳ではない、という原理があり、 その上で、Tri-secretセキュリティにより暗号レベルで分離している。 MTT<OPT<<APTの順にその分離の強度が上がっていく。どこに落ち着かせるか。 Tri-secretの顧客管理キー組み入れでアップデートされる暗号化キーはアカウントレベルのみ 暗号化キーの階層管理はアカウント、テーブル、ファイルの各レイヤで効き、アカウント間のアクセスを防ぐ。ただしShareした場合は別 Snowflakeは複数のテナントをクラウド上で管理する。物理的に分離している訳ではないが異なる暗号化キーにより暗号レベルで分離する データベースやスキーマは論理的な区分である。物理的にデータを分離している訳ではない 以下の観点で決めていく。 ネットワークポリシー要件。MTT,OPTのように論理的なユーザレベルの制御で良いか。またはAPTのようにアカウントレベルの個別の制御が必要か (APTのように)テナント毎に仮想ウェアハウスを分離し、ウェアハウスキャッシュに生成されるデータを分離する必要があるか テナント毎のコスト管理をどのように行い顧客に課金するか。もしウェアハウスのコンピュートをテナントで共有する場合、経験的で不正確な方法でコスト管理をする必要がある 顧客はどのような方法でSnowflakeにアクセスするか どれぐらい多くのテナントが同時に1つのウェアハウスを使用するか 評価基準:リソース利用、ネットワークポリシーの観点 最後の項目。結構際どいことが書いてある。仮想ウェアハウスキャッシュはキャッシュが作られたときのRBAC,SecureViewも再利用するらしいw IPアドレスによるネットワークアクセス制御はアカウント単位のユーザレベル。(OPT以下では全テナント共有となる) 仮想ウェアハウスキャッシュは、それ以降に発生するクエリにおいて、クエリの一部または全部で再利用される。RBACまたはSecureViewは再利用される Snowflakeのコストはウェアハウス単位で課金される。複数テナントがウェアハウスを共有した場合、クエリ単位、ユーザ単位、テナント単位のコスト見積もりを正確に出せない ユーザ・テナント単位にウェアハウスの使用制限をつけることはできない まとめ Design Patterns for Building Multi-Tenant Applications on Snowflakeを読んでみて、 思ったところを書いてみた。当然、マルチテナントの設計方針について詳細に書かれているが、それ以上に、 「似て非なるデータをどうやって持つか」という、重要な観点がチラチラ気になる内容だった。 ということで1万字越えの記事は終了。

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.

Dartでバイナリ配布可能なCLIツールを作る

エンジニアのスキルセットは基本が重要!、とか考えていると永遠にHelloWorldしてしまう。 そこそこ長い間同じことを考えることで深い洞察ができるようになる効果はあると思うが、 それとは別に趣味とか知的好奇心とか、興味ドリブンでやってみたいという何かは永遠に満たされない。 Dart-langに慣れるために今欲しいツールをDart-langで書いてみる。 なんでDart-langなのか、とか細かいことは気にしない。 [arst_toc tag=\"h4\"] インストール Flutter 1.21からFlutter-SDKに完全なDart-SDKが含まれる. FlutterをやるならFlutterを入れた方が良い. Dart-langは別でインストールできる. 軽いのでFlutterをやらないならこちらが良い. $ brew tap dart-lang/dart $ brew install dart $ dart --version Dart SDK version: 2.16.0 (stable) (Mon Jan 31 15:28:59 2022 +0100) on \"macos_x64\" 空プロジェクトを作る Dart-langのパッケージマネージャはpub. dart-langをインストールするとパスが通り使えるようになる. 空プロジェクトの足場を作るパッケージを使う. ちなみに足場はScaffold. 舞台裏はStagehand. $ pub global activate stagehand $ mkdir dart-cli-sample $ stagehand console-full 出来上がった雛形の構成は以下. よくある構成なので説明は省略. . ├── CHANGELOG.md ├── README.md ├── analysis_options.yaml ├── bin │   └── dart_cli_sample.dart ├── lib │   └── dart_cli_sample.dart ├── pubspec.lock ├── pubspec.yaml └── test └── dart_cli_sample_test.dart Hello World 雛形のエントリポイントは bin/dart_cli_sample.dart にある main(). 別途、lib/dart_cli_sample.dart にあるコードをimportしている. CLIの雛形だからargumentsを引数にとる. Listで渡される. まぁ普通. import \'package:dart_cli_sample/dart_cli_sample.dart\' as dart_cli_sample; void main(List arguments) { print(\'Hello world: ${dart_cli_sample.calculate()}!\'); } で、lib/dart_cli_sample.dart はどうなっているかというと以下みたいな感じ. int型を返すcalculate()という関数が定義されていて6*7の計算結果を返す. int calculate() { return 6 * 7; } インタラクティブに実行するには、dartコマンドにエントリポイントを渡す. 6*7=42がちゃんと出力された. $ dart bin/dart_cli_sample.dart Hello world: 42! バイナリ生成と実行 これやりたいためにDart-langを選んでみた. Macでバイナリ生成する場合Mac用のバイナリしか作れないといったように、 残念ながらクロスプラットフォーム非対応. CIを構築して各プラットフォーム用に実行しないといけない. $dart compile --help 544ms  日 2/ 6 02:19:51 2022 Compile Dart to various formats. Usage: dart compile [arguments] -h, --help Print this usage information. Available subcommands: aot-snapshot Compile Dart to an AOT snapshot. exe Compile Dart to a self-contained executable. jit-snapshot Compile Dart to a JIT snapshot. js Compile Dart to JavaScript. kernel Compile Dart to a kernel snapshot. Run \"dart help\" to see global options. $ dart compile exe bin/dart_cli_sample.dart -o bin/out1 12.9s  日 2/ 6 02:18:30 2022 Info: Compiling with sound null safety Generated: /Users/ikuty/ikuty/dart-cli-sample/bin/out1 $ ./bin/out1 Hello world: 42! exeオプションでself-contained、つまりDartランタイムが無い環境で実行可能ファイルを作成できる. コンパイル方式としてAOT(Ahead Of Time)、JIT(Just In Time)を選べるという充実ぶり. バイナリのサイズは, self-containedの場合, 5,033,856 bytes(約5MB) だった. $ dart compile aot-snapshot bin/dart_cli_sample.dart -o bin/out2 日 2/ 6 02:47:32 2022 Info: Compiling with sound null safety Generated: /Users/ikuty/ikuty/dart-cli-sample/bin/out2 $ dartaotruntime bin/out2 日 2/ 6 02:47:52 2022 Hello world: 42! aot-snapshotオプションにより, プラットフォーム用の共有ライブラリとアプリケーションコードを分けられる. self-containedと同様にAOTはMacOS,Windows,Linuxそれぞれのプラットフォームが提供される. dartaotruntimeというコマンドにより実行する. バイナリのサイズは 905,072 (約900KB)だった. $ dart compile jit-snapshot bin/dart_cli_sample.dart -o bin/out3 日 2/ 6 02:53:02 2022 Compiling bin/dart_cli_sample.dart to jit-snapshot file bin/out3. Info: Compiling with sound null safety Hello world: 42! $ dart run bin/out3 1005ms  日 2/ 6 02:53:23 2022 Hello world: 42! jit-snapshotオプションにより,JIT実行可能なバイナリを出力できる. プラットフォーム固有のDart中間コードを生成する. dart compile時に1度実行されて処理結果が表示される. ソースコードをparseした結果を事前に準備し,JIT実行時に再利用することで処理速度を上げる. ちょっと詳しくは不明だがAOTよりも高速に処理できる可能性がある. バイナリサイズは 4,824,016bytes. (約4.8MB)だった. $ dart compile kernel bin/dart_cli_sample.dart -o bin/out4 日 2/ 6 03:05:54 2022 Compiling bin/dart_cli_sample.dart to kernel file bin/out4. Info: Compiling with sound null safety $ dart run bin/out4 699ms  日 2/ 6 03:06:02 2022 Hello world: 42! kernelオプションにより,プラットフォーム非依存のKernelASTを生成する. 出力されたバイナリのサイズは1056 bytes (約1KB)だった.ソースコードのパスが含まれており, おそらくソースコードを同時に配布する必要がある. AOTより遅い. まとめ Dart-langのHelloWorldコードを作成し各種コンパイルオプションを試した. Go-langのそれとは異なりクロスプラットフォームのバイナリを生成できないが, 複数のコンパイルオプションが用意されていて,様々なパターンの運用に対応できそう.

default eye-catch image.

AWS SAM CLIを使ってローカルでLambda関数をビルド・実行・デプロイする

Lambdaで何かをするときチマチマAWSコンソールを触らないといけないとなると面倒すぎる。 ローカルでデバッグ・デプロイできるとかなり楽になる。AWS SAMを使ってみる。 AWS SAM(Serverless Application Model)。広くAWSのServerlessサービスがまとめられている。 AWS SAM CLIのGAは2020年8月。それから何回かアップデートされている。 AWS SAMの実体はCloudFormation。CloudFormationを使ってリソースの構築が走る。 普段CloudFormationを使っていないとSAMのコマンドがコケた時に意味不明なエラーで悩むことになる。 で、悩みながらHelloWorldしてみた。 [arst_toc tag=\"h4\"] Permissions CloudFormationで各種リソースを作る仕組みであるため、同等のPermission設定が必要。 https://docs.aws.amazon.com/ja_jp/serverless-application-model/latest/developerguide/sam-permissions.html AWS SAM は、AWS リソースへのアクセスを制御するために、AWS CloudFormation と同じメカニズムを使用します。詳細については、AWS CloudFormation ユーザーガイドの「Controlling access with AWS Identity and Access Management」を参照してください。 サーバーレスアプリケーションを管理するためのユーザー権限の付与には、3 つの主なオプションがあります。各オプションは、ユーザーに異なるレベルのアクセスコントロールを提供します。 - 管理者権限を付与する。 - 必要な AWS 管理ポリシーをアタッチする。 - 特定の AWS Identity and Access Management (IAM) 許可を付与する。 必要な管理ポリシーは以下。 AWSCloudFormationFullAccess IAMFullAccess AWSLambda_FullAccess AmazonAPIGatewayAdministrator AmazonS3FullAccess AmazonEC2ContainerRegistryFullAccess 触るユーザーにロールを割り当て、上記の管理ポリシーをアタッチしておくこと。 aws configureでprofileを設定しておいて、samコマンドのオプションにprofileを渡せる。 インストール homebrewでインストール。 $ brew tap aws/tap $ brew install aws-sam-cli $ sam --version SAM CLI, version 1.37.0 初期化 sam initでプロジェクトディレクトリを作成できる。 対話的に雛形を作るか、またはテンプレートを読み込む。 Lambdaで使える言語は割と多いが、NodejsとPythonがほとんどとのこと。 NodejsがMost popular runtimeとして扱われてるんだな。 Python書きたくないなというか。all right $ mkdir samtest && cd samtest $ sam init Which template source would you like to use? 1 - AWS Quick Start Templates 2 - Custom Template Location Choice: 1 Cloning from https://github.com/aws/aws-sam-cli-app-templates Choose an AWS Quick Start application template 1 - Hello World Example 2 - Multi-step workflow 3 - Serverless API 4 - Scheduled task 5 - Standalone function 6 - Data processing 7 - Infrastructure event management 8 - Machine Learning Template: 1 Use the most popular runtime and package type? (Nodejs and zip) [y/N]: y Project name [sam-app]: ----------------------- Generating application: ----------------------- Name: sam-app Runtime: nodejs14.x Architectures: x86_64 Dependency Manager: npm Application Template: hello-world Output Directory: . Next steps can be found in the README file at ./sam-app/README.md プロジェクト内は以下のような構成となった。 sam-app/ │ .gitignore │ README.md │ template.yaml ├─events │ event.json └─hello-world │ .npmignore │ app.js │ package.json └─tests └─unit test-handler.js app.jsがコード本体. Hello World.が書かれている。 eventを受け取るlambdaHandlerというアロー関数があって200を返してる。 // const axios = require(\'axios\') // const url = \'http://checkip.amazonaws.com/\'; let response; /** * * Event doc: https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html#api-gateway-simple-proxy-for-lambda-input-format * @param {Object} event - API Gateway Lambda Proxy Input Format * * Context doc: https://docs.aws.amazon.com/lambda/latest/dg/nodejs-prog-model-context.html * @param {Object} context * * Return doc: https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html * @returns {Object} object - API Gateway Lambda Proxy Output Format * */ exports.lambdaHandler = async (event, context) => { try { // const ret = await axios(url); response = { \'statusCode\': 200, \'body\': JSON.stringify({ message: \'hello world\', // location: ret.data.trim() }) } } catch (err) { console.log(err); return err; } return response }; ビルド そもそもどういう仕組みなのかというと、Lambdaの実行環境をエミュレートしたコンテナが背後にあり、 その中でコードを実行する、ということになっている。それがゴニョゴニョと隠蔽されている。 Lambda関数のコードをビルドしてデプロイ用の「アーティファクト」を作る。 $ sam build Building codeuri: /Users/ikuty/ikuty/samtest/sam-app/hello-world runtime: nodejs14.x metadata: {} architecture: x86_64 functions: [\'HelloWorldFunction\'] Running NodejsNpmBuilder:NpmPack Running NodejsNpmBuilder:CopyNpmrc Running NodejsNpmBuilder:CopySource Running NodejsNpmBuilder:NpmInstall Running NodejsNpmBuilder:CleanUpNpmrc Build Succeeded Built Artifacts : .aws-sam/build Built Template : .aws-sam/build/template.yaml Commands you can use next ========================= [*] Invoke Function: sam local invoke [*] Test Function in the Cloud: sam sync --stack-name {stack-name} --watch [*] Deploy: sam deploy --guided ローカルで実行 そしてローカルで実行。 Lambdaをエミュレートするコンテナが動いてapp.jsにあるアロー関数が評価される。 1発目は重いが2発目以降は結構速い。 $ sam local invoke Invoking app.lambdaHandler (nodejs14.x) Image was not found. Removing rapid images for repo public.ecr.aws/sam/emulation-nodejs14.x Building image..................................................................................................................................................................................................................................................................................................................................................................................................................... Skip pulling image and use local one: public.ecr.aws/sam/emulation-nodejs14.x:rapid-1.37.0-x86_64. Mounting /Users/ikuty/ikuty/samtest/sam-app/.aws-sam/build/HelloWorldFunction as /var/task:ro,delegated inside runtime container START RequestId: e0bbec88-dafd-4e3c-8b5e-5fcb0f38f1fa Version: $LATEST END RequestId: e0bbec88-dafd-4e3c-8b5e-5fcb0f38f1fa REPORT RequestId: e0bbec88-dafd-4e3c-8b5e-5fcb0f38f1fa Init Duration: 0.47 ms Duration: 195.40 ms Billed Duration: 196 ms Memory Size: 128 MB Max Memory Used: 128 MB {\"statusCode\":200,\"body\":\"{\"message\":\"hello world\"}\"}⏎ デプロイ 以前はもっと面倒だったらしい。新しいSAMではコマンド1発でデプロイできる。 ただし、1回目と2回目以降でフローが異なる。 1回目ではsamconfig.tomlという設定ファイルを作成する。 2回目以降、作成済みのsamconfig.tomlを使ってデプロイが行われる。 $ sam deploy -g Configuring SAM deploy ====================== Looking for config file [samconfig.toml] : Not found Setting default arguments for \'sam deploy\' ========================================= Stack Name [sam-app]: AWS Region [ap-northeast-1]: #Shows you resources changes to be deployed and require a \'Y\' to initiate deploy Confirm changes before deploy [y/N]: y #SAM needs permission to be able to create roles to connect to the resources in your template Allow SAM CLI IAM role creation [Y/n]: y #Preserves the state of previously provisioned resources when an operation fails Disable rollback [y/N]: y HelloWorldFunction may not have authorization defined, Is this okay? [y/N]: y Save arguments to configuration file [Y/n]: y SAM configuration file [samconfig.toml]: SAM configuration environment [default]: Looking for resources needed for deployment: Creating the required resources... Successfully created! Managed S3 bucket: aws-sam-cli-managed-default-samclisourcebucket-h0aw0pxx8pxv A different default S3 bucket can be set in samconfig.toml Saved arguments to config file Running \'sam deploy\' for future deployments will use the parameters saved above. The above parameters can be changed by modifying samconfig.toml Learn more about samconfig.toml syntax at https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-config.html ... (省略) 最後の文節にあるように、samconfig.tomlを変更することで構成を変更できる。 この後、実際にCloudFormationスタックのアップロード/実行が走りリソースが組み上がる。 2回目以降、-gオプション抜きでsam deployを実行すると以下。 $ sam deploy File with same data already exists at sam-app/e32dcdf231268fbcad9915436e787001, skipping upload Deploying with following values =============================== Stack name : sam-app Region : ap-northeast-1 Confirm changeset : True Disable rollback : True Deployment s3 bucket : aws-sam-cli-managed-default-samclisourcebucket-h0aw0pxx8pxv Capabilities : [\"CAPABILITY_IAM\"] Parameter overrides : {} Signing Profiles : {} Initiating deployment ===================== File with same data already exists at sam-app/9a813032a850e5b7fb214dffc5ac5783.template, skipping upload Waiting for changeset to be created.. Error: No changes to deploy. Stack sam-app is up to date Webコンソールで動作確認 Webコンソール上、生成されたLambda関数を確認できる。 HelloWorldが書かれたapp.jsが見える。 Testでサンプルイベントを送るとHelloWorldが200で返ってきた。OK。

default eye-catch image.

ACID特性 (ACID Property)

経験的にトランザクションの性質を知っている気になっているけれど、 ではACID特性のそれぞれを言葉で説明してみて, と言われると難しい. おそらくAtomicityだけをACID特性と言ってきた気がする. Wikipediaから. トランザクション分離レベルもこの際まとめておく. [arst_toc tag=\"h4\"] 不可分性(Atomicity) トランザクションに含まれるタスクが複数ある場合、全てのタスクが完全に完了するか、または全く実行されないか、いずれかであることを保証すること。 口座Aから口座Bに対して1万円送金する. 口座Aから1万円を引くタスクと口座Bに1万円を足すタスクの片方だけが実行されるとおかしなことになる. 両方のタスクが成功して取引引きが完了するか、両方のタスクが失敗して取引が失敗するかいずれか 一貫性 (Consistency) トランザクションの開始から終了までの間、操作対象のデータが正常範囲内に収まることを保証すること. 口座Aから口座Bに送金するケースで、口座Aに1万円しかないのに2万円送金しようとして一時的に口座Aが-1万円になることは一貫性に反する. 一貫性に反するイベントが発生したときにトランザクションを終了する. 独立性 (Isolation) トランザクション内の複数の操作は外部からは隠蔽されることを表す. 外部からはトランザクションの入りと出だけを知ることができる. 口座Aから口座Bに送金するケースで、口座Aから口座Bに1万円を送金する際に、中間状態として口座Aから1万円を減らしただけの状態が発生するものとする. 外部からは中間状態は見ることができず、口座Aから1万円が減り口座Bに1万円が足された状態のみを知り得る. 永続性 (Durability) DBMSの管理上の話. トランザクションが完了した場合,障害を受けたとしても完了後の状態を保持できることを表す. 通常、トランザクション操作はトランザクションログとしてストレージに記録される. トランザクションログはトランザクションの履歴で巻き戻したりできる. システムに異常が発生した場合、トランザクションんログを使って異常発生前の状態まで復旧できる. ACIDの現実 ACID特性を厳密に実装しようとすると、より広範囲のデータにアクセスする必要が発生する. 広範囲のデータにロックを掛けたり更新したりなどでパフォーマンスが落ちる. 実際はある程度妥協して実装される. ACID特性を実現する処理自体が失敗する可能性もある. ファイルシステムやバックアップ方式の工夫により冗長化する. 全ての処理を一度に実行することが求められるが、それは現実的には難しい. ログ先行書き込みとシャドウページング. トランザクション分離レベルを設定することで、トランザクションの並列実行時の厳密性とパフォーマンスのトレードオフを制御できる トランザクション分離レベル Dirty read. トランザクションAとトランザクションBが並列実行. AはBの途中の状態を見ることができる. Non-repeatable read (Fuzzy read). トランザクションAとトランザクションBが並列実行. Aが同じデータを2度読む. 1度目はBが書いていない. 2度目はBが書いている. Aから見て1度目と2度目のデータが異なるか消えているように見える. Phantom read. Non-repeatable readと似ているが、特にAの繰り返し読み込みの間にBがデータを挿入し、Aから見て突然新しいデータが出現したように見えること. 微妙な違いだが、過去から現在に渡って存在しているものの過去の状態が見えることと、過去存在していないが現在見えることは異なり、それぞれ名前がついている. ACID特性の厳密な実装にはパフォーマンス劣化とのトレードオフがあるため、 概念的に、使う側がトレードオフをコントロールできるようになっている. それがトランザクション分離レベル. あくまで概念のためDBMSによってその扱いが異なる. 分離レベル Dirty read Non-repeatable read Phantom read Read Uncomitted発生する発生する発生する Read Comitted発生しない発生する発生する Repeatable Comitted発生しない発生しない発生する Serializable発生しない発生しない発生しない

default eye-catch image.

Auroraの機能など

知識がないのに経験だけ積んだって力にならないんだよね。という話を聞いて腑に落ちた。 資格を取るために学んだことは、日々悩み考える色々な出来事を説明するための武器になる。 今自分は何をやろうとしていているのか、経験して後から回想するのでは余りに効率が悪い。 今回はAurora。やはり高いので個人では手が出ないのだけれど、 それなりの仕事であれば第1選択になり得る。 RDSと比較して圧倒的に高機能で運用時に困りそうなユースケースが通常の機能として既に備わっている. 参考書を1周したので、(著作権侵害にならないように)要約して自分の言葉でまとめていく。 [arst_toc tag=\"h4\"] クォーラムモデル コンピューティングリソースとストレージが分離している. コンピューティングとストレージを独立して管理する. コンピューティングリソース、ストレージ共に3AZに分散してレプリケートする. 1AZにコンピューティングリソース1台、ストレージ2台. 6台のストレージのうち2台が故障しても読み書き可. 3台が故障すると書き込みが不可となるが読み込み可. RDSはスタンバイレプリカとリードレプリカが別扱いだが Auroraはスタンバイ、リード共に共通. プライマリ、レプリカ、ストレージ(ボリューム)をセットでクラスタと呼ぶ. 可用性 読み書き可能なクラスターエンドポイント. 読み取り専用エンドポイント、任意のインスタンスにつなぐエンドポイントなどなど. クラスタ内の1台が読み書き用, 他は読み取り専用なので、読み書き用が落ちたときに読み取り専用が読み書き用に昇格する. これがフェイルオーバーの概念. クラスターエンドポイントに繋いでおくと、エンドポイント先で障害時に勝手にフェイルオーバーが発生する レプリカにはフェイルオーバー優先度をつけられる. 優先度が高い方が優先的にフェイルオーバー先になる. 同じだとインスタンスの大小で決まる. 多くの場合、フェイルオーバーの時間はRDSよりも短い. 通常、プライマリにのみキャッシュが効く. フェイルオーバーでキャッシュヒットしなくなる. クラスターキャッシュ管理をONにするとフェイルオーバー時に引き継がれる. 複数のリージョンに跨ってクォーラムモデルを配置するAuroraグローバルデータベース. DR対策. リージョン間のデータコピーは1秒未満. 複数のリージョンに跨ってクラスタを配置するクロスリージョンレプリケーション. DR対策. レプリカ間のデータコピーに時間がかかる. 通常クラスタ内の1台が読み書き可能で他は読み取り専用だが、全てを読み書き可能にできる. パフォーマンス 書き込み性能を上げるにはインスタンスサイズを上げる. Auroraレプリカはスタンバイレプリカ、リードレプリカを兼ねる. リードレプリカとして使うと読み込み性能が上がる. 読み込みエンドポイントは全ての読み込み用レプリカを代表する. アプリ側からは1個だが中は数台. Aurora AutoScaling. 読み込みクラスタのCPUまたは接続数が閾値以下になったときに自動スケールする. Aurora Serverless. インスタンス数,インスタンスサイズを自動スケールする. 未使用時に勝手に落ち,高負荷時に勝手に上がる. Aurora Serverlessは, 前提として利用頻度が少なくほとんど未使用だが、変化するときは大きく変化する、というアプリに適している. スケールアップは限界がある. つまり重量級のクエリの高速化には限界がある. スケールアウトはより柔軟なので多数のクエリの同時実行はより簡単に対応できる. セキュリティ 基本的にRDSと同様. VPC内に設置する. NACL、SGを使ってアクセス制御する. データ格納時・転送時に暗号化する. IAMロールを使ったクレデンシャルレス化. Auroraの監査機能は Advanced Auditing. 記録するクエリ種別を選択できる. CloudWatchLogsに転送可. コスト Auroraレプリカ1台ごとの稼働時間で課金. Aurora Serverlessはキャパシティユニット単位で課金. (cf.DynamoDB) RDSはインスタンスとストレージが密結合しているためストレージ容量はインスタンスに紐づく. インスタンス作成時に確保した量に課金. Auroraはストレージが分離しているためAuroraレプリカとは関係なく使った容量だけ重量課金. データを削除して未使用領域が出ると自動的に課金対象が減る. 通信はVPC外へのアウトバウンドにのみ課金. メンテナンス メンテナンスウインドウまたは即時で実行. Auroraではクラスター単位でパラメータを管理する.設定はクラスタ内のレプリカ全てに適用される. インスタンス単位のパラメータ管理もできる. ZDP(Zero Day Patch). ベストエフォートでダウンタイムなしのパッチ適用をおこなう. パッチ適用中も接続が維持される. バックアップ システムバックアップはメンテナンスウインドウで日時で行われる. データバックアップは継続的かつ増分的な自動バックアップ. 保持期間中の任意の時点へ復元できる. (PITR) データバックアップの保持期間は1日-35日. 0日(無効)には出来ない. S3に保存される. 手動でスナップショットを取得可能. システムバックアップ、データアップバック共に復元先は新しいAuroraクラスター. 保持期間の任意の時点を指定する. Auroraクローン. ストレージではなくコンピューティング部分のみをコピーする. リードレプリカ複製による読み取り性能向上. 1回でも書き込みしようとするとストレージ部分がコピーされる. データエクスポート、分析等読み込み専用のタスクに使う. Aurora MySQLでのみバックトラックを使用可能. 最大24時間前までSQL操作を遡れる. S3エクスポート. RDSはスナップショット作成操作だが, AuroraはSQLクエリ操作. モニタリング CloudWatchによるメトリクス監視. 拡張モニタリングによる詳細メトリクス監視. コンピューティングに関わる(CPU,メモリ等)をインスタンス単位のメトリクスとしてCloudWatchLogsで監視. ストレージに関わるクラスタ単位のメトリクスとしてCloudWatchLogsで監視. 監視が上手くいっているかを確認するため、障害を自力でシミュレートできる. 障害挿入クエリ. DBインスタンス,Auroraクラスタ,ディスクの障害. CloudWatchLogsよりもリアルタイム性があるデータベースアクティティストリーム. Amazon Kinesisにリアルタイムで入る. Kinesisに入ったデータストリームをElasticSearch等で可視化する. その他 Aurora MySQL. SQLからLambda関数を呼べる. Aurora MySQL. SQLからSageMakerエンドポイントを呼べる.