sankantsuのブログ

技術メモ・競プロなど

ゴリラ.vim #29 に参加してきました

2023/12/13 にゴリラ.vim という vimmer が集まるオフラインイベントに参加しました。 本記事では発表の様子や参加した感想について書きます。

gorillavim.connpass.com

発表資料については conpass のページに発表者のみなさんがアップロードしてくださっています。 そちらもご覧ください。

開始前

電車を逆に乗ってしまったり、新宿で迷子になったりしてだいぶ余裕をもって出たはずが受付開始からだいぶ遅れた到着になってしまいました。 とはいえ、最初の発表には間に合ったのでなんとか致命傷にはならずにすみました。 会場はユニークビジョン株式会社さんで、とてもきれいで居心地の良い会場でした。

会場で思ったこととしては、自作の名札などを持ってくれば良かったかなという点です。 当然誰も直接会ったことがなかったので、vim-jp で見る人であっても現実では誰だかわからず話しかけるのが難しいです。 何人か自作や他のイベントでもらった名札を持っている方がいらっしゃいましたが見習いたいなと思いました。

発表について

Rust で Neovim リモートプラグイン (スポンサーセッション) by Keita Iwatani さん

スポンサーセッションということで会社の紹介を中心にされるのかなと思っていましたが、がっつり vim の話でいい意味でびっくりしました。 nvim-rs という rust で neovim プラグインを実装するためのライブラリのお話で、rust でも プラグイン書けるんだなと興味深かったです。 自分はまだあまり rust 書いたことないですが、rust 勉強したい欲はあるので neovim プラグインを題材に書いてみるのもありかなと思ったりしました。

WSLは良いぞ by kyoh86 さん

Linux をいちからセットアップするのはデスクトップ環境の整備などもやらなくちゃいけなくて、そういうの好きじゃないと疲れるよね。Windows の WSL2 をあつかいやすい Linux 環境として使うのは悪くないよ」というお話でした。 一方で、WSL2 はネットワークまわりで罠にはまりやすいといった部分も指摘してらっしゃいました。 自分自身社給のPCが Windows で WSL2 を使おうとしたのですが、会社の VPN に阻まれて何もつながらなくなってしまうということで挫折しており共感する内容でした。

俺自身が無人島になることだ by kawarimidoll さん

自分にとって欠かせないプラグインを「無人プラグイン」と呼んだりすることがあり、この発表ではそのような「無人プラグイン」を自分でつくってみようという内容でした。 簡単なプラグインの作成から複雑なものにステップアップしていく過程が示されており参考になる内容でした。 有名プラグインの機能の自作も勧められていて、「仕組みを理解するための車輪の再発明」を好む自分としては結構刺さる内容だったなと思います。

vim を debug する by ぺりー さん

vimscript 組み込みの bufnr()デバッグをするために vim 本体のデバッグを試してみた際の過程を紹介した内容でした。 vim 本体の debug というとなかなかハードルが高く感じてしまいがちですが、デバッグするための様々なノウハウが紹介されていて、自分もやってみようかなと思える内容でした。 デバッグ手順については vim 以外のアプリケーションを debug する際にも使えそうだなと思いました。

vimを読もうとした話 by hakkadaikon さん

vim の膨大なソース量に、コード整形ツールや chat GPT を駆使して立ち向かった内容を紹介した内容でした。 まず、vim の規模 (約83万行) に改めて驚くとともに、プラットフォーム対応などで複雑化したソースはやはり読むのが大変そうだなという印象を受けました。 コード整形ツールでだいぶ読みやすくなる様子などを見て、#ifdef だらけのソースを読みやすく整形する手順などは他の C言語プロジェクトでも使えそうだと思いました。

vim と文字化け by rbtnn さん

vimscript で文字コード判別を行って文字化けに対処するお話でした。 UTF-8 の判別法は vim の外でも役に立つ知識だと思いました。 vim に bit 演算の組み込み関数があることなども少し驚きでした。 個人的に結構文字コードなど文書データの基盤的な話は好きなので楽しませていただきました。 スライドの共有が事前にあったのも手元でソースコードを参照しやすく良かったです。

vimで人生が豊かになりました by ryoppippi さん

もともと AtomVSCode を使っていたが、Neovim を使いはじめて設定に凝るようになって作業効率があがったし、編集作業が楽しくなったというお話でした。 vim をきっかけに自作キーボードまで手を出しはじめたところなど、作業環境が変わっていく様子を面白く聞くことができました。 vim-jp というコミュニティの良さについてもお話されており、vim-jp に入ったばかりの自分としても共感する内容でした。

懇親会

非公式ですが終了後懇親会がありました。 忘年会シーズンで空きが少ないなかスムースに店を見つけていただいて本当に助かりました。

全員が vim を使っているという共通の話題があり初対面でも vim の話で盛り上がることができました。 vim-jp のイベントや slack の話題、素の vim で実技試験を行う某学校の話、居酒屋の中で突如始まった SKK 実演布教会など飽きることなく楽しめました。 今回お話できなかった方とも話してみたいと思いました。

おわりに

今回は ゴリラ.vim の参加記を書きました。 オフラインイベントが初で不安はありましたが、全体を通して楽しめましたし参加して良かったと思いました。 また参加できるときは自身も参加してみたい、今度は登壇もしてみたいなと感じました。

Docker daemon のプロキシ設定における特殊文字の扱い

概要

自分の勤務先の環境では、社内のサーバーから外部のネットワークと通信する際プロキシを経由する必要がある。

各種アプリケーションを利用する際に個別にプロキシ設定が必要となる場合があるが、 Docker を利用する際も docker pull などで外部のネットワークから docker image を取得するためにプロキシの設定が必要である。 この設定の際に、プロキシ認証のユーザー名に含まれる特殊文字の扱いで詰まったので、対処法をメモしておく。

※ docker および systemd 等については詳しくないので、不正確な記述等あれば教えてください

環境

Ubuntu 20.04

プロキシ設定の基本

まずは、docker に限定せずコマンドラインツール一般でのプロキシ設定について確認しておく。

シェルから起動する多くのコマンドラインツール (curl, wget 等) は、http_proxyhttps_proxy という名前の環境変数からプロキシ設定を取得する。 たとえば、プロキシサーバーが proxy.example.com でプロキシ接続に利用するポートが 8080 である場合

export http_proxy=http://proxy.example.com:8080
export https_proxy=http://proxy.example.com:8080

のように設定すれば良い。

ユーザー名、password の認証がある場合には、

export http_proxy=http://<username>:<password>@proxy.example.com:8080

のような形式にする。

プロキシ設定における特殊文字

認証情報の中に、@ のような認証情報とサーバーのアドレスを区切るのに使われている文字や、その他 HTTP の URL 上で特殊な意味を持つ文字を含む場合は注意を要する。

例えば、弊社ではユーザー名にメールアドレスを使う設定になっているが、 たとえば、メールアドレスが foo@example.com, パスワードが 123 であるとして、

# 正しくない設定
export http_proxy=http://foo@exmaple.com:123@proxy.example.com:8080

のように書いてしまうと、ユーザー名に含まれる @ が認証情報とアドレス部分の区切り文字として解釈され、正しく設定できない。

正しく設定するには、@ を percent encoding して、

export http_proxy=http://foo%40exmaple.com:123@proxy.example.com:8080

のようにする必要がある。

Docker daemon のプロキシ設定

Docker のプロキシ設定と言っても、docker client 側と docker daemon 側それぞれについて設定が必要になる。 外部の registry から docker image をダウンロードしてくるための通信を発行したりするのは daemon 側であり、 今回はこの docker daemon 側のプロキシ設定について注目する。

公式ドキュメント含め、すでに詳しい解説は多くある。

Configure the daemon with systemd | Docker Documentation

プロキシのある環境でDockerを動かす方法 - Qiita

Set proxy on docker - Stack Overflow

簡単にまとめると、systemctl edit docker で docker の設定ファイルを開いて

[Service]
Environment="http_proxy=http://<username>:<password>@proxy.example.com:8080"

の内容で保存し、

systemctl daemon-reload
systemctl restart docker

でデーモンを再起動すれば良い。(sudo は必要に応じてつけること)

特殊文字に対する配慮

ユーザー名 foo@example.com, パスワード 123 で認証情報を設定する場合、一見

# 正しくない設定
[Service]
Environment="http_proxy=http://foo%40example.com:123@proxy.example.com:8080"

とすれば良さそうである。 しかし、実際この設定だと正常に動作しない。

docker の公式 documentをよく見ると、次のような注意書きがある。

Special characters in the proxy value, such as #?!()[]{}, must be double escaped using %%. For example:

[Service]
Environment="HTTP_PROXY=http://domain%%5Cuser:complex%%23pass@proxy.example.com:3128/"

したがって、以下のように設定するのが正しい

# 正しい設定
[Service]
Environment="http_proxy=http://foo%%40example.com:123@proxy.example.com:8080"

少しだけ深堀り

# 正しくない設定
[Service]
Environment="http_proxy=http://foo%40example.com:123@proxy.example.com:8080"

だと何がまずいのか、原因を探ってみる。 この状態で、

systemctl show docker --property=Environment

を叩いて設定を確認すると、Environment= と空になっている様子である。

/var/log/syslog (または /var/log/messages) を確認すると、以下のようなエラーが確認できる。(一部加工している)

Jul 28 10:39:12 <hostname> systemd[1]: /etc/systemd/system/docker.service.d/override.conf:2: Failed to resolve specifiers in http_proxy=http://foo%40example.com:123@proxy.example.com:8080, ignoring: Invalid slot

failed to resolve specifier と、specifier が解釈できないという趣旨のエラーが見れる。

調べてみると、systemd の設定ファイル内では % から始まる文字列が "specifier" として特別に解釈されるらしく、 % そのものの文字を埋め込みたい場合には、%% のように重ねて書く必要があるということらしい。 詳しい documentation は以下にある。

systemd.exec

systemd.unit

ほか参考記事

www.javacodegeeks.com

MacOS: binary release の clang を使う際のビルドエラーの解消

概要

デフォルトでインストールされている clang++ と違うバージョンのものを使う必要があり、 https://releases.llvm.org/ から tar.xz アーカイブでダウンロードして使っていたところ、ビルドの際にエラーが出ていた。

対処法などをメモしておく。

環境

https://github.com/llvm/llvm-project/releases/tag/llvmorg-16.0.0

から

https://github.com/llvm/llvm-project/releases/download/llvmorg-16.0.0/clang+llvm-16.0.0-arm64-apple-darwin22.0.tar.xz

をダウンロードして、/usr/local/以下にインストールした状態

$ sw_vers
ProductName:        macOS
ProductVersion:     13.3.1
ProductVersionExtra:    (a)
BuildVersion:       22E772610a
$ clang++ --version
clang version 16.0.0
Target: arm64-apple-darwin22.4.0
Thread model: posix
InstalledDir: /usr/local/bin

デフォルトの clang++ (/usr/bin/clang++) だと問題は生じない

問題

// main.cc
int main() {}

例えば、上のような空の main 関数だけのファイルを

$ clang++ main.cc

コンパイルするだけでも、以下のリンクエラーが発生

ld: library not found for -lSystem
clang-16: error: linker command failed with exit code 1 (use -v to see invocation)

さらに、標準ヘッダをなにか使うようなものをコンパイルしてみると...

// hello.cc
#include <iostream>

int main() {
    std::cout << "Hello, clang!" << std::endl;
}

以下のように大量にエラーが出てくる。

In file included from hello.cc:1:
In file included from /usr/local/bin/../include/c++/v1/iostream:43:
In file included from /usr/local/bin/../include/c++/v1/ios:220:
In file included from /usr/local/bin/../include/c++/v1/__ios/fpos.h:14:
In file included from /usr/local/bin/../include/c++/v1/iosfwd:100:
In file included from /usr/local/bin/../include/c++/v1/__mbstate_t.h:29:
/usr/local/bin/../include/c++/v1/wchar.h:143:77: error: use of undeclared identifier 'wcschr'
wchar_t* __libcpp_wcschr(const wchar_t* __s, wchar_t __c) {return (wchar_t*)wcschr(__s, __c);}
                                                                            ^
/usr/local/bin/../include/c++/v1/wchar.h:150:87: error: use of undeclared identifier 'wcspbrk'
wchar_t* __libcpp_wcspbrk(const wchar_t* __s1, const wchar_t* __s2) {return (wchar_t*)wcspbrk(__s1, __s2);}

/* ... */
                                                                                      ^
/usr/local/bin/../include/c++/v1/stdlib.h:150:34: error: unknown type name 'ldiv_t'
inline _LIBCPP_INLINE_VISIBILITY ldiv_t div(long __x, long __y) _NOEXCEPT {
                                 ^
/usr/local/bin/../include/c++/v1/stdlib.h:151:12: error: no member named 'ldiv' in the global namespace
  return ::ldiv(__x, __y);
         ~~^
/usr/local/bin/../include/c++/v1/stdlib.h:154:34: error: unknown type name 'lldiv_t'
inline _LIBCPP_INLINE_VISIBILITY lldiv_t div(long long __x,
                                 ^
/usr/local/bin/../include/c++/v1/stdlib.h:156:12: error: no member named 'lldiv' in the global namespace
  return ::lldiv(__x, __y);
         ~~^
fatal error: too many errors emitted, stopping now [-ferror-limit=]
20 errors generated.

stdlib.hwchar.h 等のC由来のヘッダファイルがうまく読み込まれていない?

対処法

リンクエラーについて、もう一回内容を見てみる。

ld: library not found for -lSystem
clang-16: error: linker command failed with exit code 1 (use -v to see invocation)

libSystem というライブラリが見つからないと言っているらしい。

探してみると...

$ fd 'libSystem' /
/Library/Developer/CommandLineTools/SDKs/MacOSX13.3.sdk/usr/lib/libSystem.tbd
/Library/Developer/CommandLineTools/SDKs/MacOSX13.3.sdk/usr/lib/libSystem.B_asan.tbd
/Library/Developer/CommandLineTools/SDKs/MacOSX13.3.sdk/usr/lib/libSystem.B.tbd
/Library/Developer/CommandLineTools/SDKs/MacOSX13.3.sdk/usr/lib/libSystem_asan.tbd
/Library/Developer/CommandLineTools/SDKs/MacOSX13.3.sdk/System/Library/Frameworks/CoreTelephony.framework/Support/libSystemDetermination.tbd
# more files...

どうやら /Library/Developer/CommandLineTools/SDKs/MacOSX13.3.sdk 以下などにあるらしい。

実際、

clang++ -L/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/lib main.cc

などで明示的にライブラリの search path に入れてやることでコンパイルが通るようになった。 (/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/ はバージョン番号付きの .sdk ディレクトリへのシンボリックリンクになっている。)

ヘッダファイルの方に関するエラーも

$ clang++ \
    -I/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/c++/v1 \
    -I/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include \
    -L/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/lib \
    hello.cc

のように明示的に include path を追加してやるとコンパイルが通った。

環境変数

コマンドラインでパスを明示してやることで一応の解決はするのだが、オプションが長すぎてやってられない。 これらのパスを常に探してくれるように指定するためには環境変数を使ってやればよい。

ライブラリのほうは LIBRARY_PATH、ヘッダのほうは CPATH を使えば良いようである。

設定の一例を以下に載せておく。

export SDK_ROOT="/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk"

export CPATH="${SDK_ROOT}/usr/include:${CPATH}"
export CPATH="${SDK_ROOT}/usr/include/c++/v1:${CPATH}"

export LIBRARY_PATH="${SDK_ROOT}/usr/lib:${LIBRARY_PATH}"

蛇足

環境変数について、man clang の ENVIRONMENT セクションにドキュメントされている。 ただし、この中に LIBRARY_PATH の記述は見当たらない。

man gcc のほうには記述があるので、clang 側が暗黙に gcc への互換性を保つような実装をしているということだろうか?

ライブラリの search path について、LDFLAGSLD_LIBRARY_PATH 等を使えば良いと書いてある記述も見かけたが、自分の環境ではうまく動かなかった。

また、一応以上のやり方で解決に至ったものの、MacOS まわりの開発環境に疎いのもあって結局 MacOSX.sdk が何者なのかとかはよくわからない。 詳しい方がいたらぜひ教えていただけたら幸いである。

Python: 引数に関する規則のまとめ

概要

Python における関数宣言や関数呼び出しにおける引数の規則についてまとめる。

Introduction

Python では、関数呼び出しの引数に関する規則がやや複雑である。

たとえば、可変長の引数を許すような書き方として

def func(*args,**kwargs):
    ...

をよく見かけるが、他の言語ではあまり見慣れない書き方に思われる。

さらに、やや複雑な例として、

def foo(a,b,/,c=10,*d,e,f=20,**g):
    pass

というような関数宣言があったときに、

foo(1,x=2,*(3,4,5,6),**{"e":7,"y":8})

のような呼び出しを行ったら、aからfまでの各引数の値が何になるかすぐにわかるだろうか? このような書き方について理解が曖昧であると感じるならぜひ最後まで読んでみてほしい。

上の問題の答えは以下である。

a = 1
b = 3
c = 4
d = (5, 6)
e = 7
f = 20
g = {'x': 2, 'y': 8}

Parameter と Argument

Python に限った話ではないが、「引数」には2種類の意味がある。 仮引数(parameter)と実引数(argument)である。

parameter というのは関数定義の中で使われる引数に対する名前のことである。 実際の呼び出しごとに値が変わるというような意味で parameter という語が使われているのではないかと思われる。

一方 argument というのは、関数呼び出しの式の中で実際に渡される値を言う。

これらの区別は重要であるので、以下の説明の中でも意識的に使い分ける。

関数呼び出し (function call)

6. Expressions — Python 3.11.4 documentation

Glossary — Python 3.11.4 documentation

Positional argument と Keyword argument

関数呼び出しにおける argument の渡し方について、

  • positional argument
  • keyword argument

の2種類がある。

def f(x,y):
    pass

f(1,2)     # positional argument
f(x=1,y=2) # keyword argument

positional argument は、対応する parameter を明示的に指定せず、宣言の順番から対応を決定するような渡し方である。

keyword argument は、対応する parameter を明示的に記述する渡し方である。

keyword argument は、positional argument よりも前に書くことはできない。 したがって、以下はエラーとなる。

f(x=1,2) # SyntaxError: positional argument follows keyword argument

Argument unpacking

関数に複数の値を渡すとき通常はカンマで区切って順番に書くが、 複数の引数を iterable や辞書でまとめて渡すこともできる。 これを argument unpacking といい、次の2種類がある。

  • positional argument unpacking
  • keyword argument unpacking

positional argument unpacking は、複数の positional argument を iterable に詰めてまとめて渡すもので、*arg のように * を前に書いて表す。 このとき、argはリストやタプルのような iterable である必要があり、arg からひとつずつ順番に値を取り出して順に positional argument として使う。

つまり、以下の2つの呼び出しは等価である。

f(1,2)
f(*(1,2)) # positional argument unpacking

keyword argument unpacking は、複数の keyword argument を辞書に詰めてまとめて渡すもので、**argのように * を前に書いて表す。 辞書のキーは string になっている必要があり、キーの値と parameter の名前が対応付けられる。

つまり、以下の2つの呼び出しは等価である。

f(x=1,y=2)
f(**{"x":1,"y":2}) # keyword argument unpacking

順番に関する細かい補足

(後の関数呼び出しの詳細を読んでからのほうがわかりやすいです。)

あまりやることはないとは思うが、positional argument unpacking は keyword argument より後に置くことができる。 ただし、keyword argument より positional argument unpacking のほうが先に処理される。 したがって、次のようなことが起こる。

def f(x,y):
    pass

f(y=1,*(2,)) # OK: x=2, y=1
f(x=1,*(2,)) # TypeError: f() got multiple values for argument 'x'

2つ目の呼び出しのエラーは、positional argument unpacking によって最初の positional parameter x2 が渡され、その後に keyword argument として x=1 が渡されたことで x に対する argument が重複していることにより起こっている。

また、keyword argument unpacking より後に positional argument unpacking を置くことはできない。

f(**{"y":2},*(1,)) # SyntaxError: iterable argument unpacking follows keyword argument unpacking

関数定義 (function definition) と parameter の宣言

8. Compound statements — Python 3.11.4 documentation

Glossary — Python 3.11.4 documentation

関数定義で使われる parameter には、以下の種類がある。

  • positional-or-keyword parameter
  • positional-only parameter
  • keyword-only parameter
  • var-positional parameter (*args)
  • var-keyword parameter (**kwargs)

以下、それぞれの種類について説明する。

positional-or-keyword parameter

def f(x,y):
    ...

のような普通の宣言で定義される parameter はこの種類である。

def f(x,y):
    pass

f(1,2)     # OK: x=1, y=2
f(x=1,y=2) # OK: x=1, y=2
f(y=2,x=1) # OK: x=1, y=2

positional-only parameter (Python 3.8 以降)

def f(x,y,/):
    ...

のように/で区切って parameter の宣言をすると、 / 以前の parameter は positional-only-parameter となる。

positional-only parameter に対しては、positional argument としてのみ引数を渡すことができて、 keyword argument として渡すことはできない。

def f(x,/):
    pass

f(1)   # OK: x=1
f(x=1) # TypeError: f() got some positional-only arguments passed as keyword arguments: 'x'

keyword-only parameter

def f(x,y,*,kw1,kw2):
    ...

のように、parameter を*で区切って宣言すると、 *以降の parameter は keyword-only parameter になる。

keyword-only parameter に対しては、keyword argument としてのみ引数を渡すことができて、 positional argument として渡すことはできない。

def f(*,x):
    pass

f(x=1) # OK: x=1
f(1)   # TypeError: f() takes 0 positional arguments but 1 was given

var-positional parameter

def f(*args):
    ...

のように parameter を*identifierの形で宣言すると、var-positional parameter になる。

var-positional parameter は、関数に対して余分に渡された positional argument をすべて吸収してタプルとして受け取る。 もし余分な positional argument がなければ空のタプルが渡される。

def f(x,*args):
    pass

f(1,2,3) # OK: x=1, args=(2,3)
f(1)     # OK: x=1, args=()

var-positional parameter より後に宣言された parameter は keyword-only である。

def f(*args,kw): pass # kw is keyword-only parameter

var-keyword parameter

def f(**kwargs):
    ...

のように、parameter を**identifierの形で宣言すると、var-keyword parameter になる。

var-keyword parameter は、関数に対して余分に渡された keyword argument をすべて吸収して辞書として受け取る。 余分な keyword argument がない場合は空辞書が渡される。

def f(**kwargs):
    pass

f(x=1,y=2) # OK: kwargs={'x': 1, 'y': 2}
f()        # OK: kwargs={}

Parameter のデフォルト値

var-positional, var-keyword 以外の種類の parameter については、 デフォルト値 (default value) を与えることができる。

default value がある場合、関数呼び出しの際に明示的に引数が渡されなければ default value が使われる。

def f(x=1):
    pass

f(100) # OK: x=100
f()    # OK: x=1 (default value)

デフォルト値をもつ parameter より後に、デフォルト値をもたない positional parameter を宣言することはできない。

def f(x=1,y): pass # SyntaxError: non-default argument follows default argument

デフォルト値は関数定義の際に一度だけ計算され、以降のすべての呼び出しで共有される。 したがって、mutable な値をデフォルト値とすることは十分に注意が必要である(基本的に推奨されない)。

関数呼び出しに関する詳細

ここでは、関数呼び出し時の parameter に対する値の割り当てに関する規則についてより詳細に説明する。

まず前提として、python では関数呼び出しに際してすべての parameter になんらかの値が割り当てられる必要がある。 明示的な方法あるいはデフォルト値によって値が割り当てられない限りエラーとなる。 また、var-positional, var-keyword paramter が存在しない状況で余分な引数が発生すればそれもエラーになる。

関数呼び出しにおける parameter への値の割り当ては、具体的には次の順で行われる。

  1. (positional argument unpacking を含む) 各 positional argument を、(*付きでない) positional parameter に前から順に割り当てる。positional argument の数が positional parameter よりも多く、かつ var-positional parameter (*identifier) が存在する場合には余った positional argument をタプルに詰めて var-positional parameter に割り当てる。
  2. (keyword argument unpacking を含む) 各 keyword argument について名前が一致する parameter を探して値を割り当てる。1 ですでに値が割り当てられていればエラーとなる。keyword argument に対して名前の一致する parameter がなく、かつ var-keyword parameter (**identifier) が存在する場合には対応するエントリを var-keyword parameter の値となる辞書に追加する。
  3. 1,2 で値が割り当てられていない parameter が存在し、かつその parameter に対してデフォルト値が存在すればそれを値として使う。値が明示的に割り当てられず、デフォルト値もない場合にはエラー。

最後に、問題として出した例を振り返ってみよう。

def foo(a,b,/,c=10,*d,e,f=20,**g):
    pass

foo(1,x=2,*(3,4,5,6),**{"e":7,"y":8})

parameter について整理すると、

a: positional-only parameter
b: positional-only parameter
c: positional-or-keyword parameter (with default value)
d: var-positional parameter
e: keyword-only parameter
f: keyword-only parameter (with default value)
g: var-keyword parameter

(*の付かない) positional parameter は a,b,c の3つである。

また argument については、argument unpacking した上で

positional argument: 1,3,4,5,6
keyword argument: x=2,e=7,y=8

関数呼び出しにおける parameter への値の割り当ては、

  1. positional argument について、先頭の positional parameter から順に a=1,b=3,c=4 を割り当てる。 余った5,6は var-positional parameter d に対して d=(5,6)のようにタプルで割り当てる。
  2. keyword argument について、
    • e は parameter として宣言されているので、e=7を割り当てる。
    • x,y は parameter としては宣言されていないので、var-keyword parameter g に対してg={'x': 2, 'y': 8}のように辞書で割り当てる。
  3. f は 1,2 で値が割り当てられていないが、デフォルト値20があるのでこれを使う。

以上で、全ての parameter について値が割り当てられた。 まとめると、以下のようになる。

a = 1
b = 3
c = 4
d = (5, 6)
e = 7
f = 20
g = {'x': 2, 'y': 8}

まとめ

この記事では、Python における関数宣言や関数呼び出しにおける引数の規則についてまとめた。

  • parameter, argument の区別
  • positional, keyword の区別
  • 複数の引数をまとめて渡す unpacking
  • parameter に対するデフォルト値

などいろいろあってなかなかややこしい仕様だが、引数の割り当てに関する規則を正確に理解しておくと混乱することが減るだろう。

Haskell を勉強して面白いと思ったところ

しばらく個人的に忙しい時期が続いたが,一時的に余裕が生まれたので以前から気になっていた Haskell を少し勉強した. LispOCaml関数型言語の経験はあったが,型クラスやモナドといった単語になんとなく興味があり Haskell に至った.

の2冊を中心に読んだ.

最初に"Haskell入門 関数型プログラミング言語の基礎と実践"のほうをあまり手を動かさずにざっと読み,後から"すごいH本"をコードを実行しながら読んだ. (読んだ後から思えば,本の内容などからするとむしろ逆の順番のほうが良さそうな気はする.)

勉強してみて1週間ぐらい経ったので、面白いと思ったところなどをまとめてみる.

Haskell に関しては本当に初心者なので,記述の正確性は保証しない. おかしいところがあれば遠慮なく指摘してほしい.

型システム

いわゆる Hindley-Milner 型システムに基づくもので,Ocaml のような ML 系の言語と基本的な使用感は同じである.

Haskell で特徴的なのは,型クラスによる型に対する制約の記述だろう. 型クラスはclassから始まる宣言によって記述し,そのクラスに属する型の満たすべきインターフェースをメソッドとして書き並べる. 例えば,次の例はモノイドを表す型クラスの(簡略化した)例である.

class Monoid m where
  mempty :: m
  (<>) :: m -> m -> m

mがモノイドであるとき,あるm型の値が単位元memptyとなり,モノイド上の(結合的な)演算を<>という演算子によって表現する.

具体的な型が型クラスの要求する制約を満たすことは,instance宣言でメソッドに対する具体的な実装を与えることで記述する. たとえば,リストは++による結合演算についてモノイドになり,単位元は空リスト([])である.

instance Monoid [a] where
  mempty = []
  (<>) = (++)

このように型クラスという形でインターフェースを記述し実装は具体型ごとに別々に与えることで自然な形でアドホック多相を実現できる.

型クラスという抽象化を通して多くの型に共通する本質的な部分を取り出すことに注意が向けられるし,具体型による特殊な実装に依存しすぎない再利用性の高いコードを記述する助けになるのではないかと感じた.

モナドという抽象化

Haskell の特徴としてよく語られるのはモナドという概念であると思う. モナドは"文脈付きの計算"のようなものを表す抽象化といえる.

上のような説明だけではあまり要領を得ないが,定式化は非常に単純であり,以下のたった3行の宣言に集約される.

class Monad m where
  return :: a -> m a
  (>>=) :: m a -> (a -> m b) -> m b

>>= (bind operator とも呼ばれる) が肝であり,

x >>= f

により,"xからモナド(にともなう文脈)を剥がして素の値だけを取り出し,この値に対する計算fを行って新しくモナド値(文脈付きの値)を生成する" という操作を表現する. これによって"文脈"を引き継ぎながら計算するということが表現できるのである.

この説明だけでは正直あまり嬉しさは伝わってこない思う. しかし,下のように様々な計算がモナドという抽象化の上で表現できるということを理解すると,ありがたみが感じられてくる.

特に,IO モナドは注目に値する. IO をともなう計算は本質的に外界とのやりとりをともなう"副作用"付きの計算であり,これをモナドというモデルの上で表現できるということは面白い発見である. また,IO による影響範囲が型のレベルではっきりと表現されることで可視化されるので,IO の影響範囲を局所化しようという動機も自然と発生するように思われる.

遅延評価

Haskell は"同じ関数を同じ引数で評価すれば必ず同じ値が得られる"という参照等価性を徹底した純粋関数型言語である. これはつまり関数をどのタイミングで評価しても結果が変わらないということになるから,必要になるギリギリまで評価を遅延させる遅延評価を可能にする.

実際には使われない引数が評価されたりすることがないので効率的である.

また,無限要素のリストのようなメモリ上に事前に展開することが不可能な対象を表現することが非常に自然かつ容易に行える. 次の例は無限リスト[1..]から最初の100要素を取り出している.

ghci> take 100 [1..]
[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100]

数学的な構造をその全体の大きさを気にすることなく素直に表現できるということは大きなメリットであると感じる.

構文的な簡潔さ

Haskell は実際にコードを記述するときの簡潔さにも大いに注意して設計されていると感じる. 例えば,先ほどの例でも登場したように無限リストは range により [1..]などのように非常に簡潔に書ける.

一風変わった演算子たちも特徴的である.

  • 関数合成: f . g = \x -> f (g x)
  • 関数適用: f $ x = f x ($を使うと結合順位が下がる)

セクション記法といって中置演算子(+など)の引数をひとつ固定した関数を書く記法も面白い. 例えば,(+3)\x -> x + 3 と等価である.

これらの記法は,関数型言語でありがちな括弧だらけの記述を簡潔にするのに役立つ. 例として,整数の無限リストから10要素取り出して2倍し,5より大きい値だけを取り出すという操作を考える. mapfilterを用いて次のように書けるが,やや冗長である.

ghci> filter (\x -> x > 5) (map (\x -> x*2) (take 10 [1..]))
[6,8,10,12,14,16,18,20]

.,$やセクション記法をうまく使うとだいぶ見やすくなる.

ghci> filter (>5) . map (*2) $ take 10 [1..]
[6,8,10,12,14,16,18,20]

モナドによる計算の記述を簡潔にするための構文要素としては do 式やリスト内包表記がある. ここでは特に do 式に注目する.

do 式は,>>=の連鎖に対する syntax sugar を提供する. 例えば,次は標準入力から2行入力を受け取り,それらの文字列を連結して出力する関数である.

getTwoLines :: IO ()
getTwoLines =
  getLine >>= \x -> getLine >>= \y -> putStrLn (x ++ y)

>>=の挙動に十分慣れていないと読むのが難しい. 一方,do 式を用いた場合は次のように書ける.

getTwoLines :: IO ()
getTwoLines = do
  x <- getLine
  y <- getLine
  putStrLn (x ++ y)

見た目はまるで命令型のプログラミングである. こちらのほうがだいぶ直観的に感じる人が多いだろう.

TikZ におけるスタイル指定と pgfkeys

概要

TikZ には,グラフィクスの見た目などを調整するための機構としてスタイルというものがある. この記事では,

  • グラフィクスの描画に特定のスタイルを適用する方法
  • 自分でスタイルを定義する方法
  • スタイルの機構そのもののベースとなっているpgfkeysライブラリの基本

について説明する.

  • 参考
    • texdoc tikz Section 12.4 (Using Graphic Options)
    • texdoc tikz Section 88 (Key Management)

スタイルを使う

TikZ にはグラフィクスの調整を行うためのスタイルが大量に定義されており,これを使うだけでかなりの部分の調整を行うことができる.

スタイルを使うには,\draw\nodeなど描画を行うコマンドに対して,\draw[<style name>]などのような形式で[...]内にスタイル名を指定する. 例えば,help linesというスタイルを指定すると,"幅0.2ptで灰色の線にする"という指定ができる.

\begin{tikzpicture}
    \draw             (0,1) -- (2,1);
    \draw[help lines] (0,0) -- (2,0);
\end{tikzpicture}

help linesを指定した下の線が通常よりも細くなっているのがわかる.

スタイルに対して引数を与えることができるものもある. その場合は,<style name>=<val>のような形式で指定を行う. 下の例では,line widthというスタイルに対して5ptという指定を行うことで線の太さを5ptにするという指定を行っている.

\tikz \draw [line width=5pt] (0,0) -- (1,0.1);

複数のスタイルを指定するには,スタイル名をカンマで区切って並べる.

自動で使われるスタイル

every pathevery nodeなど一部のスタイルは,path や node を描くとき明示的に指定しなくても自動で使われる.

スタイル指定を適用する範囲

スタイルは個別の描画コマンドに対して適用するだけではなく特定の範囲全体に対して適用したいという場合がある.

tikzpicture環境内でスタイルを適用する範囲を指定するためのscopeという環境が用意されている. scopeに対してスタイルを指定すると,範囲内のすべてのグラフィクスに対してこのスタイルが適用される.

tikzpicture環境全体にスタイルを適用するには,tikzpicutreに対してスタイルをオプション引数で渡せば良い.

また,プリアンブル中などで\tikzset{<style>,...}のように書くと,文書内のすべてのtikz環境に対してスタイルが適用される.

より広いスコープで指定したスタイルは,より狭いスコープにおける指定で上書きされる.

\begin{tikzpicture}[red]
    \draw (0,2) -- (3,2);
    \begin{scope}[blue]
        \draw             (0,1) -- (3,1);
        \draw[green,thin] (0,0) -- (3,0);
    \end{scope}
\end{tikzpicture}

スタイルの定義

標準で定義されているスタイルを用いるだけでも多くのことができるが,ある組み合わせを繰り返し使うような場合には,これを新しくスタイルとして定義すると便利である.

スタイルの定義は,スタイルの適用と同じようにコマンドのオプション引数に記述する. スタイルの定義は,次のような文法で行う.

<style name>/.style={<style>,...}

例えば,次の例では"太さ5ptで赤い点線にする"というスタイルをmy styleという名前で定義して使っている.

\begin{tikzpicture}[my style/.style={red,line width=5pt,dotted}]
    \draw [my style] (0,0) -- (3,0.1);
\end{tikzpicture}

/.append styleを使うことで,すでに定義されているスタイルに対してさらにオプションを追加することもできる.

\begin{tikzpicture}[help lines/.append style=blue!50]
    \draw              (0,0) grid +(2,2);
    \draw [help lines] (2,0) grid +(2,2);
\end{tikzpicture}

引数付きのスタイルの定義

.styleの定義の中に#1を含めると,引数を受け取ったとき#1が実際の引数で置き換えられる.

\begin{tikzpicture}[outline/.style={draw=#1,thick,fill=#1!50}]
    \node [outline=red] at (0,1) {red};
    \node [outline=blue] at (0,0) {blue};
\end{tikzpicture}

スタイル指定の仕組み

<key>=<value>のようにスタイル指定を行ったとき,実際には内部で\pgfkeys{<key>=<value>}が実行されている.(引数なしの<key>なら,\pgfkeys{<key>})

もう少し正確には,必要に応じて<key>/tikz/<key>/pgf/<key>のように補完してから\pgfkeysを実行する. また,与えられた<key>color,allows,shapeの有効なオプション値になっていればその意味で解釈される.

従って,たとえば

\tikzset{red,line width=5pt}

という指定は,

\pgfkeys{/tikz/color=red,/tikz/line width=5pt}

と等価である.

pgfkeys

pgfkeysは,key-value ペアの管理を行うパッケージである. \usepackage{tikz}などとしたとき,pgfkeysも自動で読み込まれる.

単体で用いる場合には\usepackage{pgfkeys}をプリアンブルに書く.

pgfkeysにおいて,各キーの名前はUnixのパス名のような構造をもつ.(/tikz/line widthなど) tikzで定義されているキーは,/tikz/...という名前になっている.

各キーには呼び出されたときに実行されるコードが対応付けられており,<key>に対するコードは<key>/.code=<code>という形式で指定する. <code>内に#1を含めると,その部分は呼び出し時に与えられた引数で置き換えられる.

\pgfkeys{<key>=<value>}のようにキーを呼び出すと,実際に<key>/.codeに設定されたコードが実行される.

例えば,

\pgfkeys{/my key/.code=The value is '#1'.}
\pgfkeys{/my key=hi!}

を実行すれば,/my keyというキーに対してhi!という値でコードの呼び出しが行われ,

The value is 'hi!'.

と出力される.

handler key

<key>/.codeのように,ドットを含むようなキーは特殊なはたらきをもつ. これらのキーを handler と呼ぶ.

以下,いくつか重要な handler キーを説明する.

.default handler

.default ハンドラにより,引数のデフォルト値を設定できる.

\pgfkeys{/my key/.code=(#1)}
\pgfkeys{/my key/.default=hello}
\pgfkeys{/my key=hallo,/my key}

出力

(hallo)(hello)

.cd handler

<key>/.cdを実行することで,続くキーを<key>に対する相対パスで書けるようになる. より正確には,/で始まらないキーを書いたとき,キーの前に補完される default path を変更する.

\pgfkeys{/foo/bar/.code=This is foo bar}
\pgfkeys{/foo/.cd,bar}

出力

This is foo bar

\tikzsetは,.cdハンドラを用いて次のように書くのとだいたい同じ働きである.

\def\tikzset#1{\pgfkeys{/tikz/.cd,#1}}

.style handler

.styleを用いると,あるキーの実行がさらに他のキーの実行を呼び出すようにすることができる.

\pgfkeys{/a/.code=(a:#1)}
\pgfkeys{/b/.code=(b:#1)}
\pgfkeys{/my style/.style={/a=foo,/b=bar,/a=#1}}
\pgfkeys{/my style=wow}

この例では/my style=wowの呼び出しによってさらに/a=foo,/b=bar,/a=wowの3つのキーの呼び出しが行われ,次が出力される.

(a:foo)(b:bar)(a:wow)

動作を考えれば,<key>/.style={<key>=<value>,...}は,<key>/.code=\pgfkeysalso{<key>=<value>,...}と同じである. (\pgfkeysalso\pgfkeysとほぼ同じだが,すでに.cd等で変更された default path を引きつぐという点が異なる)

まとめ

TikZ では,\drawなどの描画コマンドのオプション引数[...]などに特定のキーワードを指定することでグラフィクスの見た目を調整するスタイルという機構が備わっている. スタイルはmy style/.style=...のような記法により,スタイルを自分で定義することもできる.

これらの機構の裏側ではpgfkeysというライブラリが用いられており,TikZ におけるスタイル<key>=<value>の指定は内部で\pgfkeys{<key>=<value>}の実行に置き換えられる.

.stylepgfkeysにおいてhandlerと呼ばれる特殊なキーの1種であり,他のキーを用いて新しいキーを定義するのに利用できる. これは,pgfkeysのキーを TikZ のスタイルとして利用する上では,すでに定義されたスタイルから新しいスタイルを定義するということに読み替えられる.

Python の descriptor

概要

Python の descriptor は,属性 (attribute) へのアクセスをフックに関数呼び出しを行う機構である. descriptor の機構は,メンバ変数の getter や setter を簡潔に定義できるようにした property などに使われている.

ここでは,

について説明を行う.

descriptor の基本

あるクラスに対して,特別なメソッド

  • __get__()
  • __set__()
  • __delete__()

のいずれかが定義されているとき, そのクラスのオブジェクトを descriptor という.

descriptor が特別な振る舞いをするのは,descriptor のインスタンスがクラス変数として使われたときである. このとき,descriptor へのアクセスが次のように変換される.

  • 値の読み出し -> __get__()
  • 値の書き込み -> __set__()
  • 値の削除 -> __delete__()

ここでは,__get__()__set__()を中心に説明する.

基本的な例

class D:
    def __get__(self,obj,objtype=None):
        print("Called D.__get__()")

class A:
    x = D()

a = A()
a.x

出力

Called D.__get__()

クラスD__get__()メソッドを実装しているので,値の読み出しに関して descriptor として働く. クラスAはクラス変数としてクラスDのオブジェクトxを持っている. したがって,クラスAのオブジェクトaを通して descriptor xにアクセスしようとすると,D.__get__() が呼ばれる.

__get__() に自動で渡される引数

__get__()が呼ばれる際,引数objには呼び出し元のオブジェクト(上の例ではa),objtype にはaの型(上の例ではclass A)がそれぞれ渡される. A.dscのようにクラスから直接 descriptor へアクセスした場合は,引数objNone が渡される.

__set__() メソッド

x が descriptor であり,__set__() メソッドを実装しているとき,a.x = ... のような xへの書き込みは,__set__() メソッドの呼び出しに置き換えられる.

def __set__(self, obj, value):
    ...

のように__set__()メソッドが定義されているとき,呼び出し時に引数objには呼び出し元のオブジェクト,引数valueには書き込もうとした値が自動的に渡される.

descriptor でインスタンス変数を操作する

class D:
    def __get__(self,obj,objtype=None):
        print(f"Accessing 'val' giving {obj._val}")
        return obj._val
    def __set__(self,obj,v):
        print(f"Update 'val' to {v}")
        obj._val = v

class A:
    val = D()
    def __init__(self,v):
        self.val = v

a = A(123)
a.val

b = A(456)
b.val

a.val

出力

>>> a = A(123)
Update 'val' to 123
>>> a.val
Accessing 'val' giving 123
123
>>> b = A(456)
Update 'val' to 456
>>> b.val
Accessing 'val' giving 456
456
>>> a.val
Accessing 'val' giving 123
123

クラスAはクラス変数として descriptor val をもっている. Aのオブジェクトの初期化時に descriptor valへのアクセスが起こり,オブジェクトのインスタンス変数 _val がセットされる.

val 自体はクラス変数であるが,__set__()の呼び出し時に呼び出し元のオブジェクトがobjとして渡されているため,個別のオブジェクトに関するインスタンス変数を操作することができているという点に注意しておく.

複数のインスタンス変数を操作する

上の例でdescriptor を通してインスタンス変数を操作できることを確認したが,この例ではインスタンス変数が_valという名前で決め打ちになっていた. そのため,1つのクラスに複数の descriptor を持ちたい場合,上の方法では各 descriptor ごとに別々のクラスを用意しなければならないという問題が生ずる.

これを解決する方法として,__set_name__() という特殊なメソッドを用いることができる. __set_name__()はクラスの構築時にクラス変数としてもっている descriptor ごとに呼ばれる.

def __set_name__(self, owner, name):
    ...

という定義があったとき,owner にはその descriptor を保持するクラスが,name にはその descriptor がクラス内でもつクラス変数の名前が文字列として渡される. ソースコード中に記述したクラス変数名の文字列が直接文字列として渡されるという点が特徴的である.

class D:
    def __set_name__(self,owner,name):
        self.public_name = name
        self.private_name = "_" + name
    def __get__(self,obj,objtype=None):
        val = getattr(obj,self.private_name)
        print(f"Accesing {self.public_name!r} giving {val}")
        return val
    def __set__(self,obj,v):
        print(f"Update {self.public_name!r} to {v}")
        setattr(obj,self.private_name,v)

class A:
    x = D()
    y = D()
    def __init__(self,v1,v2):
        self.x = v1
        self.y = v2

a = A(123,456)
a.x
a.y

出力

>>> a = A(123,456)
Update 'x' to 123
Update 'y' to 456
>>> a.x
Accesing 'x' giving 123
123
>>> a.y
Accesing 'y' giving 456
456

__set_name__()メソッド内で public_nameprivate_name の2つの変数を定義し,public_name をログ表示用に,private_nameを実際のインスタンス変数操作用に使っている.

xyのそれぞれのクラス変数が__set_name__()メソッドを呼び出すので,変数名が動的に解決され,衝突することなく使うことができている.

参考

Descriptor HowTo Guide — Python 3.10.8 documentation

Descriptor についてまとまった公式のドキュメント

3. Data model — Python 3.10.8 documentation

リファレンス内の Data model の章に,descriptor の呼び出しなどに関する詳細な記述がある.