ラテアートを始めました

先日家庭用エスプレッソマシンを買いました。エスプレッソを入れつつラテアートの練習をしています。

進捗

主に「ハート」と「チューリップ」を練習しています。一貫して同じような形のアートが描けるようになりたいです。





エスプレッソマシン

スイスの「ソリス」というブランドの「ソリス バリスタ パーフェクタプラス」というマシンにしました。

マシンを選ぶ際は以下の条件を重視しました。

  • 省スペース(狭いキッチンでも大丈夫)
  • ラテアート向き(スチームが強い、スチームワンドが扱いやすい)

似た大きさのデロンギの「デディカ アルテ」と比較しましたが、デロンギの方が安くサイズは小さいものの、ソリスの方がポルタフィルターが大きく(コーヒーの粉の量の選択肢が多い)スチームも強いとのことなので、最終的にソリスを選びました。

購入に当たってはWeb上で色々情報収集をしましたが、特に以下の動画が購入の決め手になりました。

【最新型家庭用マシン】桁違いに凄い!ソリス バリスタ パーフェクタプラスを使ってみた - YouTube

【新型家庭用マシン】デロンギとソリス比べてみた - YouTube

使用感

実際にソリス バリスタ パーフェクタプラスを使って感じた事は以下の通りです。

  • 満足点
    • 大きさがちょうどいい。幅18.7cm×奥行37.1cm×高さ32.1cm、重量5.7kg。
    • スチームが十分強い。30秒くらいでスチームミルクが完成する。家庭用にしては強い方らしい。
    • スチームワンドが扱いやすい。ワンドの先を十分手前まで引き出せて位置もしっかり固定できる。
    • 抽出圧計が便利。どれくらい正確かは分からないが抽出具合の参考になる。
  • 不満点
    • 立ち上げてから抽出可能になるまで40秒程度だが、実際にマシンが十分に温まるまで5分程度かかる。ただし10万円以下の家庭用マシンならどれもこんな感じらしい。
    • エスプレッソの抽出とスチーム機能を同時に使うことが出来ない。これも10万円以下なので仕方ない。
    • 抽出圧を一定に出来ない(理想と言われている9気圧で固定したい)。やはり10万円以下なので仕方ない。
    • ポルタフィルターのサイズが54mmのため、ツールの選択肢が少ない(多くは業務用と同じ58mmフィルター向けに作られている)。

基本的には大満足で、特に大きな問題もなく毎日ラテアートの練習を楽しめています。上述の不満点を解消するのであれば、ハイエンドの大型家庭用マシンか業務用のものを買うしかないかなといった感じです。

Tune-A-Videoで動画をいらすとや風に変換させてみた話

↓こんなサーフィンの動画を

Tune-A-Videoでいらすとや風にすることができました。

また、「サーフィンする宇宙飛行士」のようなプロンプト(テキスト)を与えることで、動画の一部を変化させることもできました。

Tune-A-Videoとは

Tune-A-Videoとは機械学習の手法のひとつで、大雑把に言うと、Stable Diffusionのような「テキストから画像を生成する拡散モデル」を使って既存の動画をプロンプト(テキスト)で指定した内容に変化させることができます。

(Tune-A-VideoのGitHubページより引用)

手法

Tune-A-VideoのGitHubで公開されている実装はStable Diffusionに対応していますが、今回はいらすとや風の動画を作りたかったので、通常のモデルの代わりに以前自分が作った「いらすとや風画像生成モデル」を使用しました。

mickey24.hatenablog.com

動画の学習は以下の記事を参考にし、Google Colab Pro+でプレミアムクラスのGPU(Tesla A100、40GB)を使いました。

note.com

あとはひたすら学習率、学習ステップ数、プロンプト、シード等をいろいろ試していい動画が作れるまで粘りました。

生成できた動画いろいろ

せっかくなので、サーフィン動画を学習させたモデルに色々なプロンプト(テキスト)を与えてみました。

サーフィンする猫

不思議な色ですが、なかなか悪くなさそうです。

サーフィンする忍者

多少腕が消えたりしていますが、笑顔はいらすとや風になっています。

サーフィンするキノコ

何故か赤いキノコばかり生成されました。

サーフィンする脳みそ

さすがに厳しかったようです。

流れ星を眺める女の子

試しにサーフィンと関係ないプロンプトを与えてみましたが、やはり無理でした。フレーム間の一貫性がなくランダムな感じの動きになってしまいました。

苦労したこと

上ではさらっと「粘りました」と書きましたが、うまくいくハイパーパラメーターの組み合わせやシード値の探索には骨が折れました。

シード値の探索

例えば、以下の動画は同じ学習後のモデルを使いシード値だけを変えて出力したものです。

ご覧のように、一番左の動画はきれいな一方、他の動画は途中で髪型が変わったり首から上が一瞬消えたりしています。 同じ学習後のモデルでもシード値次第でここまで違う結果になるので、ハイパーパラメーターを変えてモデルを作る度に、数十個のシード値に対して動画生成をさせて本当にいい動画が出ないモデルかどうかを確かめていたため手間がかかりました。

元動画の実写スタイルの過学習

また、Tune-A-Videoでモデルを学習する際に、学習元の動画のスタイル(実写)を過学習してしまい、いらすとや風のスタイルで出力できなくなる場合もありました。 以下は異なる学習率や学習ステップ数でサーフィン動画を学習させた後に「パーティーハットをしながらサーフィンする人」の動画を出力させたもので、右に行くほど学習が進んだモデルの出力になっています。

今回の元動画は実写なので、学習率を上げたり学習ステップ数を増やしたりすればするほど実写風の動画が生成されやすくなる傾向がありました。一方で、学習率を下げ過ぎたり学習ステップ数を減らしたりすると、今度は元動画の特徴(例:正面に向かってサーフィンする)を捉えられなかったり、フレーム間の一貫性(例:波が少しずつ変化する)を保てなかったりといった問題が出てきてしまいました。

この点に関しては「学習率を下げて頻繁にvalidationの出力をさせ、丁度いい学習ステップ数を探し当てる」という対策を取って対処しました。

動画学習時と動画生成時に与えるプロンプトの調整

Tune-A-Videoの動画学習時のプロンプトと動画生成時のプロンプトに共通の単語が多く含まれている場合にも、上と同じく元動画の実写スタイルに引っ張られる問題が発生しやすいようです。

検証のために「サーフィンする男性(A man surfing)」というプロンプトを使って実写のサーフィン動画をモデルに学習させた後、「サーフィンする男性(A man surfing)」(左)と「サーフィンする男の子(A boy surfing)」(右)というプロンプトを与えて動画を生成させてみました。

左の「男性(man)」という単語を与えた場合の動画は若干実写風であるのに対して、右の「男の子(boy)」はいらすとや風で動画を生成することに成功しています。このことから、「サーフィンする男性」というプロンプトで実写のサーフィン動画を学習させると、「サーフィン」と「男性」という概念に動画のスタイルの情報(実写)も結び付けられてしまっているのではないかと推測できます。

一方、動画学習時と生成時のプロンプトの単語を一切被らないようにした場合、今度は元動画の構図を完全に無視した動画が生成されてしまうようです。例えば動画学習時のプロンプトを「実写」に、生成時のプロンプトを「サーフィンする男性」にしたところ、学習率が高かったとしても、元動画にはない木が背景に出てきたりフレーム間の一貫性がなくなって体の一部が突然消えたりといった問題が発生しました。

そのため、動画学習時と動画生成時のスタイルが異なる場合、「学習時と生成時のプロンプトが過度に被らないようにしつつも多少は共通の単語を入れる」といった工夫が必要です。

おわりに

昨年Stable Diffusionが公開されてからの技術の進歩が非常に早く、毎日が驚きの連続です。VALL-Eのような優れた音声合成技術も揃いつつあるため、動画自動生成が実用レベルになるのも時間の問題かもしれません。

Stable Diffusionを使って「いらすとや風画像生成モデル」を作った話

今話題の画像生成モデル「Stable Diffusion」をいらすとやの画像でfinetuneしてみたところ、任意のテキストに対していらすとやっぽい画像を作れるモデルが出来上がりました。

Stable Diffusionとは

Stable Diffusionは、指定されたテキスト(文字列)に対応する画像を生成する機械学習モデルのひとつです。ソースコードと学習済みモデルは無償で公開されていて、誰でも利用できるようになっています。

(Stable DiffusionのGitHubページより引用)

今回は、この画像生成モデルをいらすとやの画像でfinetune(微調整)することで、入力テキストに対応する画像をいらすとやのようなスタイルで出力させることを試みました。

開発環境

開発環境はGoogle Colab Pro+で、主にプレミアムGPU(NVIDIA A100)を使いました。Stable Diffusionモデルが非常に大きいため、標準GPUだとメモリがあふれて学習も画像生成もできませんでした。

うまく生成できた画像の例

いらすとや画像でfinetuneしたStable Diffusionモデルを使い、シード値を変えつつ様々なテキストで画像生成を試してみました。以下は生成した画像の中でよさげなものの例です。

白馬に乗る宇宙飛行士

これはOpen AIの画像生成モデル「DALL·E 2」のプロジェクトページにも載っている非現実的な画像の例です。DALL·E 2の場合は写真や鉛筆画のような画像でしたが、いらすとや画像でfinetuneしたモデルはしっかりいらすとやのスタイルで画像を生成できました。

宇宙で猫と一緒にバスケする宇宙飛行士たち

ボールが多すぎたり、一部人や猫の顔が変だったりしていますが、割とそれっぽく出来た方だと思います。

ラーメンを食べる子犬

笑顔の子犬は特にいらすとやっぽい感じがします。

買い物するクマ

子グマがカートに詰め込まれていたりと程よくツッコミどころがあります。

手から吹雪を出す人

うまくできた例は吹雪というよりは氷塊といった感じでしょうか。明らかに別の紫色の魔法や銃のようなものを使ったりする人の画像も出てきました。

砂漠でスキーする人

一番右の画像の「STOC DELIT HAE」が何なのかは全く分かりませんが、全体的によく出来ていると思います。

アボカドの形をした椅子

いらすとやの雰囲気を残しつつ、アボカドのような椅子の画像が生成できましたが、中にはアボカドが椅子に乗っているだけの画像もありました。

アボカドの形をした椅子に座る人

いい画像も生成できましたが、割と難易度が高かったようです。人とアボカドのフュージョンが生まれた時は戦慄を覚えました。

うまくいかなかった例

一方で、シード値を変えてもそれらしい画像が出てこなかったテキストもありました。

チュチュを着た赤ちゃん大根が犬の散歩をしている

なかなか複雑なテキストですが、これもDALL·Eのプロジェクトページにある例です。赤ちゃん大根の妖精っぽい画像やチュチュを着た子が犬の散歩をしている画像は生成できたのですが、残念ながら全ての条件を満たす画像はできませんでした。

虹色のスミを吐くイカ

全体的に綺麗なのですが、いらすとやっぽさのある画像は見つかりませんでした。

人にマッサージをする犬

人が犬にマッサージをしたり、人が人にマッサージをしたり、人が得体の知れないモノにマッサージをする画像はできましたが、犬がマッサージをする画像は見つかりませんでした。

ベッドで眠るティラノサウルス

全体的に何か残念な感じです。

人の鉛筆画

遺影になってしまいました。

火災避難訓練

これは訓練ではない。

学習過程で画像がいらすとや化していく様子

シード値を固定しつつ、finetuning過程の各チェックポイントを使って「白馬に乗る宇宙飛行士」の画像を生成し、アニメーションにしてみました。最初は写真のような画像が生成されていたのに対して、学習が進むにつれていらすとや風の画像出力に変わっていく過程が見て取れます。

シード値によっては割と早い段階からいらすとや風になっている画像もある一方で、途中で馬や人の向きが変わったり馬全体が宇宙飛行士に変わったりと不安定なものもあるのが興味深いです。

おわりに

以前自分が「いらすとや風人間画像生成モデル」の記事を書いてから5年が経ちました。

mickey24.hatenablog.com

あれから機械学習技術や開発環境が大きく進歩し、個人レベルで今回のようなクオリティの画像生成モデルが作れるようになってしまった事にとても驚いています。5年前には想像できなかった時代がやってきていることを身を以て知りました。

もちろん、今回のモデルでも毎回いい画像が生成できるわけではありません。この記事のうまくいった例は何度もテキストとシード値を変えつつ画像生成を繰り返した結果であって、その裏にはたくさんの失敗した画像たちがいます。

しかし、今回は5年前のモデルと違い、前情報なしでは本家と見間違えそうな画像が生成できることもあるため、念のため今回のモデルはTwitter botも含め公開しないでおこうと思います。

ところで、いらすとやさんのようなイラストレーターがこういった機械学習技術を活用して作画を効率化することはもはや可能な域に達してしまったかもしれませんね。

PythonでクラスC内の型ヒントにC自身を指定するとNameErrorが発生する問題の対処法

問題

以下のようにあるクラスCのメソッドfの型ヒントにC自身を使いたいとします。

class C:
    def f(self) -> C:  # causes NameError!
        return C()

このコードをそのまま実行すると以下のようにNameErrorが発生します。

$ python foo.py
Traceback (most recent call last):
  File "/Users/mickey/foo.py", line 1, in <module>
    class C:
  File "/Users/mickey/foo.py", line 2, in C
    def f(self) -> C:
NameError: name 'C' is not defined

原因

メソッドfが定義される時点でクラスC自体の定義が完了していないため、この時点で型名Cを解決できないことがNameErrorの原因です。

解決策

以下のように型ヒントのCをクォートで括ると解決します。

class C:
    def f(self) -> 'C':  # quoted, causes no error
        return C()

これはForward Referencesと呼ばれるもので、型名を文字列リテラルで指定することで型名の解決を後回しにすることができます。

参考資料

「FIRE 最強の早期リタイア術 最速でお金から自由になれる究極メソッド」を読んだ

FIREとはFinancial Independence, Retire Earlyの略で、簡単に言うと十分な不労所得を確保することで経済的自立し、仕事を早期リタイアして自由な時間を手に入れることです。自分は今のところ早期リタイアするつもりはないのですが、実際に早期リタイアした人の話を知ることでお金について新しい考え方を身につけられるかもしれないと思い、本書を手に取ってみました。

本書の内容

箇条書きで軽くまとめてみます。

  • 基本として貯蓄・節約によって貯めた資産を元手にインデックスファンドへ投資し、資産を増やして経済的自立を目指すことになる。
  • POTスコア=(給与の中央値−最低賃金の差額)÷ 学位にかかる総費用
  • POTスコアが高い(学位が収入に与える費用対効果が大きい)キャリアを選ぶことが大切。
  • 家は様々なコストがかかるので買わない。可能な限り賃貸にする。
  • 車も買わない。
  • 借金はしない。既に借金がある場合は最優先で完済する。複利の力を敵に回すと危険。
  • インデックスファンドは手数料が安く、85%のアクティブファンドを上回る運用成果を出す。
  • 現代ポートフォリオ理論に従ってポートフォリオを設計する。
    • アセットアロケーション(株式や債券などの割合)を決める。例:株式6割、債券4割
    • 投資対象のインデックスを決める。S&P500、FTSE Global All Cap Indexなど。特定の国・地域(特に自国)にオーバーウェイトしすぎないように気をつける。
    • ファンドを選ぶ。投資信託かETF。手数料を抑える。
  • リバランシング:市場の高騰や暴落によって資産の割合があらかじめ決めておいたアセットアロケーションから大きく逸脱し始めた場合、割合が大きくなりすぎた資産を売却し、割合が少なくなりすぎた資産を購入する。
    • 例:「株式6割、債券4割」のポートフォリオが株価の下落によって「株式3割、債券7割」になった場合、債券を売却し、株式を購入することで元の「株式6割、債券4割」に戻す。下落時に株式を安く買い増すことで後の回復期に早く資産が復活することが期待できる。
    • リバランシングは機械的に行う。感情に流され下落時に株を売って回復期に利益を逃すようなことがあってはならない。
    • 株式100%のアセットアロケーションだとリバランシングができない。株式以外の資産もポートフォリオに入れておく。
    • 個別株をポートフォリオに入れてはいけない。インデックスファンドと違い、個別株はゼロ(倒産)になり得るため、株価下落時のリバランシングで個別株を買い続けると資産がなくなる可能性がある。
  • 4%ルール:運用しているポートフォリオの4%の資金で1年の生活費を賄えれば、毎年4%取り崩しても95%の確率で貯蓄が30年以上持続する、という研究結果がある。
    • ポートフォリオの4%の資金で1年生活できるようになるために、25年分の生活費相当のポートフォリオを作り上げる必要がある。
  • 早期リタイアまでの年数は年収ではなく貯蓄率によって決まる。貯蓄率が上がれば投資金額も増え、リターンの複利の力によって早く目標の金額に到達できる。
    • 貯蓄率を上げるために収入だけでなく節約も重要となる。
    • 数パーセントの貯蓄率の変化だけでリタイアまでの年数が数年も変わり得る(10%等低い貯蓄率の場合に顕著)。
  • シークエンス・オブ・リターン・リスク:4%ルールは5%の確率で失敗する。退職後の最初の数年に下落相場が発生すると、資産が大きく目減りしていく中でも生活費のために資産を売却しなければならず、ポートフォリオを大きく毀損してしまい、相場が回復局面に入っても資金を取り戻せなくなる。
    • 対策として「現金クッション」と「利回りシールド」で備える。
  • 現金クッション:下落相場になった時に生活費として使う現金。下落相場時に生活費のためにポートフォリオを大きく取り崩す必要がなくなる。
    • 過去に株式市場が暴落から立ち直るまで最大5年(世界恐慌時)かかっている。
    • そのため5年分の生活費の現金クッションがあると安心。
    • しかしポートフォリオとは別に5年分の現金を用意するのはかなり大きな金額となる。対策として「利回りシールド」を利用する。
  • 利回りシールド:一時的にポートフォリオの資産の中心を高利回り資産に置き換え、分配金を増やす。
    • 投資信託やETFの分配金を現金クッションの一部として使うことで、現金クッションのために用意する資金を抑えられる。
    • 高利回り資産には、優先株、REIT(不動産投資信託)、社債、高配当株のインデックスファンドなどがある。
    • ボラティリティが上がるという欠点がある。
  • 地理的アービトラージ:物価が高い国に住んでいる人は、東南アジアなど物価が安い国に移住したり一年中旅行している方が年間の支出を抑えられる。
  • 子育てしている人でも経済的自立したり年中旅行したりしている人達もいる。
  • 早期リタイア後も副業を始めたりパートタイムで仕事に戻るという選択肢も考慮する。フルタイムの仕事よりも自由な時間を確保しつつ資産にも余裕ができる。

感想など

  • 著者がカナダ在住のため税金などの面で日本と違う点があるが、アイディア自体は日本でも概ね実行可能だと思う。
  • 節約重要。早期リタイアを目指すかどうかはともかく自分の支出を詳細に把握してみようと思った。
  • フルタイムで働いている間は分配金を受け取るよりも投資信託で分配金を自動再投資しつつ資産の増加に期待したほうがいいと思う。しかし退職時に高分配のETFや投資信託へ鞍替えすると投資信託の売却時に課税されてしまうため悩ましい。
  • カナダではETFの方が手数料や経費率の面で投資信託よりも有利らしくETF一択とのことだが、日本だと海外ETFの外国税額控除の手続きが必要だったり為替手数料を考慮する必要があったりするため一概にどちらがいいか言えないと思う。
  • 利回りシールドは生活費用の現金(の一部)を分配金に頼る戦略だが、下落相場時に投資信託やETFの分配金が著しく下がる、または支払われない可能性はどれくらいあるだろうか?
  • 前半の著者の育った環境の話(中国で1æ—¥44セント生活)やお金に関する持論(お金は世界で最も大切)は単純に興味深かった。

終わりに

分配金の下落の可能性など、実践する上でいくつか疑問点は残りますが、早期リタイアへの道を可能な限り再現性のあるやり方で紹介している点は評価できると思います。FIREに興味がある方は読んでみて損はないのではないでしょうか。

ちなみに著者は「お金は世界で最も大切」と言及していましたが、「金は命より重い・・・!そこの認識をごまかす輩は、生涯地を這う・・・!!」とは言っていませんでした。

最近の自分のC++スタイル

何だかんだでC++を書き続けています。今やC++20です。C++0xで騒いでいた頃が懐かしいです。

今回は最近自分がC++を書く時に気にしていることについて簡単に紹介していこうと思います。

基本方針

同じコードを触る開発者が多いため、実装者にとっての利便性よりも共同開発者にとっての可読性や保全性を重視しています。そのため、可読性を下げる機能や扱いが難しい機能は扱わない事が多いです。

auto

autoは書き手にとっては便利ですが、多用すると(特に共同開発者にとっての)コードの可読性が下がるためあまり使いません。

// Preferred: articlesの型がひと目で分かる
std::optional<std::vector<CrawledArticle>> articles = CrawlArticles(...);

// CrawlArticles()のシグネチャーを調べない限りarticlesの型が分からない
auto articles = CrawlArticles();

ただし、コンテナのイテレーターは型名が無駄に長く、また変数の型が周辺のコードから明らかなことが多いためautoを使っています。 同様の理由で、std::make_uniqueの戻り値を受ける変数に対してもautoを使っています。

// Preferred: std::make_uniqueの戻り値の型は自明なのでautoを使っても可読性は失われない
auto crawler = std::make_unique<ArticleCrawler>(...);

// autoを使わないとArticleCrawlerが二度出てくるため冗長に見える
std::unique_ptr<ArticleCrawler> crawler = std::make_unique<ArticleCrawler>(...);

std::pairとstd::tuple

operator<などが定義されているため便利ですが、first、secondやget<3>はdescriptiveな名前ではなく、可読性が下がるため極力使わないようにしています。可能であればstructを定義した方が可読性が高いコードになります。

// secondが何を保持する変数なのかひと目で分からない
if (ProcessArticle(article_info.second)) {...}

// (0オリジンで)2番目の要素が何か調べないとコードを把握できない
if (ProcessArticle(std::get<2>(article_info))) {...}

継承

ポリモーフィズムのための継承はほぼ使っていません。たまにStrategyパターンのために使うことがある程度ですが、多用すると他の開発者にとっての可読性が下がる(オブジェクトがインスタンス化されている箇所まで辿らないと仮想関数呼び出しがどの実装を呼んでいるのか分からなくなる)ため気をつけています。Template Methodパターン等、実装を共有する目的での継承は使っていません(共同開発者で使っている人はいます)。

Dependency Injectionでユニットテストをしやすくする目的での継承は使っています。Abstract Factoryも稀に使います。ただし、具象クラスを使ったテストで済む場合(ネットワークやディスクアクセスを起こさず、動作も十分速いクラスに依存するコードの場合)はなるべくDependency Injectionを使わずにテストしています。

テンプレート

テンプレートは強力ですが、共同開発者がコードを読む時に関数テンプレートやクラステンプレートがインスタンス化されている箇所まで辿らないと動作の確認ができない場合があるため多用しないようにしています。ただし、他の人も色々な型の変数で使いたくなるようなライブラリを設計する場合は話が変わってくると思います。

std::unique_ptr

非常によく使います。生のポインタをnewすることはほぼありません。std::unique_ptrをインスタンス化する時はstd::make_uniqueで行います。std::unique_ptrのコンストラクタはほぼ使いません。

std::unique_ptrは関数のシグネチャー上でオブジェクトの所有権の移動を明確化するためにも使えます。可読性や保全性の向上が期待できます。

class Notifier {
 public:
  // 引数がstd::unique_ptrの場合は呼び出し元がオブジェクトの所有権を譲渡することを意味する
  explicit Notifier(std::unique_ptr<MailSender> mail_sender)
    : mail_sender_(std::move(mail_sender)) {}
  ...
 private:
  std::unique_ptr<MailSender> mail_sender_;
};
...
auto mail_sender = std::make_unique<MailSender>(...);
...
// std::unique_ptrの指すオブジェクトの所有権をstd::moveで譲渡する
auto notifier = std::make_unique<Notifier>(std:move(mail_sender));
// 戻り値がstd::unique_ptrのため、呼び出し元が戻り値の所有権を受け取ることを意味する
std::unique_ptr<ArticleCrawler> CreateArticleCrawler();

ちなみにstd::shared_ptrは今のところ滅多に使っていません。

Designated initializers

structの初期化で便利かつ可読性も向上するのでよく使います。日本語では指示付き初期化と言うらしいです。

struct Person {
  std::string name;
  int age = 0;
};

Person person = {.name = "John Doe", .age = 42};

指示付き初期化は元々C99で導入された機能ですが、C++20でも利用可能になりました。ただしいくつかC++20特有の制約があります(こちらのページの"仕様"を参照してください)。

他にも、指示付き初期化は引数が多いorデフォルト引数の一部だけを変えて呼び出すことが多い関数に使うと便利です。 abseil / Tip of the Week #173: Wrapping Arguments in Option Structsに載っている例を少し簡略化したものを以下に紹介します。

struct PrintDoubleOptions {
  std::string_view prefix = "";
  int precision = 8;
  char thousands_separator = ',';
  char decimal_separator = '.';
  bool scientific = false;
};

void PrintDouble(double value,
                 const PrintDoubleOptions& options = {});

// 変更したいオプションだけ指定する
PrintDouble(5.0, {.prefix = "value=", .scientific = true});

structと指示付き初期化なしだと以下のように扱いづらいコードになります。

void PrintDouble(double value, std::string_view prefix = "", int precision = 8,
                 char thousands_separator = ',', char decimal_separator = '.',
                 bool scientific = false);

// 引数リストの後半にあるものだけ変えたい場合、その引数よりも前のデフォルト引数を全て指定しなければならない
// 同じ型の引数が隣り合っていると順番を間違えてもコンパイルが通ってしまう
//(この場合thousands_separatorとdecimal_separator)
PrintDouble(5.0, "value=", 8, ',', '.', true);

終わりに

他にもプロジェクト独自の規約や自分なりの基準がありますが、最近よく気にしているのはだいたいこの辺りです。

pytype:型ヒントなしでも使えるPython用静的型検証ツール

github.com

PytypeはGoogle製のPython用静的型検証ツールです。型ヒントなしのコードでも型推論を行って検証し、潜在的なバグを指摘してくれます。

インストール

pip install pytype

使用例

例えば以下のようなfoo.pyというファイルがあったとします。

def increment(x):
    return x + 1

increment('0')

このコードは実行するとTypeErrorをraiseします。

pytypeでこのコードを検証してみます。

$ pytype foo.py

すると、以下の通り当該コードに対してunsupported operand type(s) for +: 'str' and 'int'というエラーを出力します。実引数と関数本体のコードを組み合わせた検証もしてくれることが分かります。

File "/Users/mickey/depot/pytype_examples/foo.py", line 2, in increment: unsupported operand type(s) for +: 'str' and 'int' [unsupported-operands]
  Function __add__ on str expects str
Called from (traceback):
  line 4, in current file

pytypeは型推論以外にも、未定義属性の参照など様々なプログラムエラーをレポートしてくれます。

class Person(object):
    def __init__(self, name):
        self.name = name
        # self.ageを定義していない

    def greet(self):
        # 未定義のself.ageを参照している
        print("Hi, I'm {}. I'm {} years old.".format(self.name, self.age))
$ pytype foo.py
File "/Users/mickey/depot/pytype_examples/foo.py", line 8, in greet: No attribute 'age' on Person [attribute-error]

その他の使い方はGithubのプロジェクトページを参照してください。例えば、pytypeが解析した型情報を型ヒントとして出力し、ソースコードにmergeすることも可能です。

まとめ

pytypeは型ヒントなしのコードでも型推論を行い検証してくれます。実際にコードを実行する前に潜在的なバグを素早く確認できるため、ユニットテストの前に実行させるようにしておくと便利です。