Lumped tweets

Just marks

Better python code, better naming, better programming, better productivity

はじめに

この文章は社内の新人 Data Scientist (likewise a Machine Learning Researcher/Engineer) 向けに書いた文章をもう少し清書し、また社内向けの内容を削ったものです。

以下の区切り線以降がその内容です。


このドキュメントの目的

もしあなたがPythonのコードを「最低限読み書きできる」状態であれば、そこから「よりよいPythonを記述することを考えられる」ようになることです。

実際にできるかどうかはともかく、 よりベターなコードとは一体どういうものなのかを朧気ながら知り、考えることで、「今自分が書くべきコード」を意思と意図をもって書き下せるようになるのが目的です。

雑なコードであっても、「今は雑に書く」ことを「選択」しているのか「そうとしか書けない」のかでは雲泥の差と私は思っているからです。

このドキュメントの対象者

自分、他人の書いたソフトウェアを継続的にメンテナンスをしたことがない、あるいはその経験に乏しい人

1年もチーム開発していれば身につくようなことしか基本的には取り上げません。

エクスキューズ

間違っていることも在ると思います。にんげんだもの

タイトルについて (読み飛ばして良い)

なぜ Best code,... best productivity ではないのかというと、 ソフトウェアエンジニアリングにおいてBest wayは本当に限られたものしか無いためである。

全ての状態、全ての条件において 「たったひとつの冴えたやりかた」 は実際にはほとんど存在しない。Zen of Python ではそういっているが、そんなPythonも複数のやり方があるし、様々なProject, VirtualEnv環境がありもはや幻想である。

PEPが揃い、現在ではデファクトスタンダードがあるが

「この場合は、こういった理由により、こうすることがBetterなため、この選択をする」ということを繰り返していくことで、より高度な、よりBetterな、より複雑なことができるようになっていくと私は思う。

しかしそれは「この時の場合はこれがBest」という選択も事実上ある。Betterしかないというのもまた時と場合によるのだ。

しかしそれでも常により良い判断をするため、という目的のために Better code... というタイトルにした

PEP について

Python Enhancement Proposal の略称ことPEP

https://peps.python.org/pep-0001/#what-is-a-pep

Python Enhancement Proposals(PEP)は、Pythonに対する新しい機能の提案や、システムの変更、あるいはプロセスの改善など、Python言語に対する様々な側面の提案書である。

事実上RFCのようなもので、仕様書ではないにせよ、その仕様を提案するものである。

AcceptされたPEPはPythonにとって正式なDocumentとなり、そこに書いてあるものは「Pythonとして」のDocumentであり、事実上オフィシャルのアナウンスメントとして扱って良いものだと思う。

Example of PEP517

例えば https://peps.python.org/pep-0517Pythonプロジェクトにおけるビルドシステムアーキテクチャを提案している。

よって、現在では「プロジェクトのビルド」方法においては、このPEP517に従った方法で行うのが事実上のスタンダードになっているが、一方でこのPEP517は「仕様」を策定しているに過ぎないため、その方法に従ったツールは含まれておらず、またそのScope外のことについては異なるPEPで提案されるのが普通である。

具体的に言えば、PEP517はビルドシステムアーキテクチャを提案しており、その中身として例えば

  • pyproject.toml
    • このファイルは、パッケージのビルドに必要な設定や依存関係を定義する新しい標準的な方法として導入されました。setup.pyやsetup.cfgの代わりとして、より明確で簡潔な設定が可能になりました
  • ビルドバックエンドの指定
    • pyproject.toml内で、どのビルドバックエンドを使用するかを指定することができます。これにより、従来のsetuptools以外のビルドツールも選択できるようになりました
  • ビルドフロントエンドとバックエンドの分離
    • ビルドフロントエンド(例:pip)は、pyproject.tomlを参照して、適切なビルドバックエンドを呼び出してパッケージのビルドを行います。これにより、ビルドプロセスがより柔軟になりました

上記等が含まれている。

ビルドバックエンドとビルドフロントエンドはお互いに「お互いがPEP517に従って動いている」ことを前提にして構築されれば、そのどちらかを可変にでき、プログラマーが任意にそのツールを利用できる。

一方で、例えば python の version 管理(venvとか)についてはこのPEPに含まれておらず、PEP内部においてもそれは

We do not require that any particular “virtual environment” mechanism be used; a build frontend might use virtualenv, or venv, or no special mechanism at all. But whatever mechanism is used MUST meet the following criteria:

特定の“仮想環境”メカニズムを使用することを要求していません。ビルドフロントエンドはvirtualenvやvenvを使用するか、特別なメカニズムをまったく使用しないかもしれません。しかし、どのメカニズムが使用されるにしても、以下の基準を満たす必要があります

と、その要求を示すに過ぎない。

このように、ある要素において、Pythonとして、一定の品質とReviewをもって提示されるのがPEPであり、できるだけPEPにおいて推奨されていることは実施していくべき内容である。

他にも、標準Formatを指定する PEP8 があり、これがもっとも知られているPEPであると言っても過言ではないだろう。

コーディングスタイル, PEP8

全Pythonistaが読んだことがあるはずの、The Zen of python の中にはいくつかStyleに関係するようなこともある。一応列挙する

Beautiful is better than ugly.
醜いより美しいほうがいい。
Explicit is better than implicit.
暗示するより明示するほうがいい。
Simple is better than complex.
複雑であるよりは平易であるほうがいい。
Complex is better than complicated.
それでも、込み入っているよりは複雑であるほうがまし。
Flat is better than nested.
ネストは浅いほうがいい。
Sparse is better than dense.
密集しているよりは隙間があるほうがいい。
Readability counts.
読みやすいことは善である。
Special cases aren't special enough to break the rules.
特殊であることはルールを破る理由にならない。
Although practicality beats purity.
しかし、実用性を求めると純粋さが失われることがある。
Errors should never pass silently.
エラーは隠すな、無視するな。
Unless explicitly silenced.
ただし、わざと隠されているのなら見逃せ。
In the face of ambiguity, refuse the temptation to guess.
曖昧なものに出逢ったら、その意味を適当に推測してはいけない。
There should be one-- and preferably only one --obvious way to do it.
何かいいやり方があるはずだ。誰が見ても明らかな、たったひとつのやり方が。
Although that way may not be obvious at first unless you're Dutch.
そのやり方は一目見ただけではわかりにくいかもしれない。オランダ人にだけわかりやすいなんてこともあるかもしれない。
Now is better than never.
ずっとやらないでいるよりは、今やれ。
Although never is often better than right now.
でも、今"すぐ"にやるよりはやらないほうがマシなことが多い。
If the implementation is hard to explain, it's a bad idea.
コードの内容を説明するのが難しいのなら、それは悪い実装である。
If the implementation is easy to explain, it may be a good idea.
コードの内容を容易に説明できるのなら、おそらくそれはよい実装である。
Namespaces are one honking great idea -- let's do more of those!
名前空間は優れたアイデアであるため、積極的に利用すべきである。

実はこのThe Zen of Python もPEP20である。このPEP20は具体的なものもあるが抽象的なこともあるし、なによりジョークまで入っている。

しかし、PEP8はより具体的に、「Python のコードはこう書く」といったようなガイドが記述されている。

一部を引用すると

1レベルインデントするごとに、スペースを4つ使いましょう

https://pep8-ja.readthedocs.io/ja/latest/

とかである。 if 式の書き方や 関数の定義の方法、 class の書き方や前後の改行数等も結構細かく定義されている。

「いや別に好きに書いたらいいじゃん」

と思うこともあるだろう。しかし、コードは「書かれる時間」よりも「読まれる時間」のほうが長い。

書くのは一瞬であっても、そのコードが数時間、数日、数年と残っていくと考えれば、「一定のルール」に従って記述されていることで、その読み手へのコストを下げることができる。

たとえ発注者やPMが「いや、PoCだから書き捨てでさくっと作ってよ」と言っても、実際にそれが本当に書き捨てであるかどうかは捨てられるまでわからない。

なにより、お昼休みにご飯を食べて返ってきた1時間後の自分でさえ、乱雑に書いた自分コードを読んで理解できないこともあるだろう。その可能性を減らすための方法である。

さらにはコードを書く時も「どのように書いたらいいか」と考えるコストを減らすことで、よりロジックやデザイン/設計へ脳内リソースのフォーカスを与えることができる。

PEP8 以外のスタイルガイド

pep8 とは異なるが大きな企業だとStyleGuideを公開していることもある。有名なところで言えば

Google Python Style Guide だろう https://google.github.io/styleguide/pyguide.html

詳細は省くが、PEP8よりもぶっちゃけより思想が強い。好みで採用するべきだろう。

いずれにせよ

こういったコード規約は エディターやIDE にLinter/Formatterを組み込んだりして「考えずに適応される」状態であると良いだろう。

例えば、 pep8 であれば autopep8 というツールが存在し、 代表的なエディターである VSCode などでもそれを利用するプラグインなどがある。

これはpep8に準拠するコードへ保存時に自動的に整形したり、準拠していないコードへ警告を出したりする。

こういったツールを使うことで、怒られたら治す、というか勝手に治ってる、という状況が作れるようになり、より生産的になるだろう。

google のStyle Guide へ従う場合は https://github.com/google/pyink などが利用できる。

命名、名前、型Hint

私の尊敬する先輩の言葉がある。

「名前が決まればプログラミングの8割は終わったようなもの」

さて、本ドキュメントの本懐である。ここまでは全部素振りである。

このドキュメントのタイトルが Better python code, better naming, better programming, better productivity となっているが、実際には Better Name, Better Name, Better Name, Better type eating the better Name. である。

ディレクトリ名、ファイル名、クラス名、関数名、メソッド名、変数名……

これらに対して一貫して必要なことが名前付けであり、名前付けの本質はそのモノへの理解と表現である。

ある要素に名前がつけられたとき、人間はその名前でその要素を認識する。

例えば、以下のようなコードがあるとする。

class Employee:
    name: str
    id: int
    def __init__(self, name: str, id: int)
      self.name = name
      self.id = id

class Company:
    employee_list: []Employee = []

    def __init__(self, employee_list: []Employee):
      self.employee_list = employee_list


company = Company([Employee('Taro', 150)])
print(company.employee_list) #=> [Employee(name="Taro", id=150)]

良さそうである。

が、 Employee.id は一体なんだろうか? Employeeの Identifier として int 型の値は、例えばなんだろうか?

DB上の Auto Increment されるレコードの作成順番も表す PrimaryKey だろうか? それとも従業員番号だろうか? あるいはそれとは関係ない何らかの一意な数字だろうか?

一般に、ベターな答えは、一番最後の「何ら関係のない一意な数字」であるだろう。 Ruby on Rails の標準が AutoIncrement-edなPrimaryKeyだったが、実際には特定の関係ない一意な数字が振られるのがベターな場合が多いだろう。

ただしこれはMySQLがAutoIncrement-edなPrimaryKeyに対して強力に作用するため、場合によってはそれがBestになる場合もある たとえばこれがGoogle Cloud Spanner のようなデータベースの場合はAuto Increment-ted なInt型のIDではパフォーマンスが出ない。 よって、IDは ID として抽象化してモデリングを行ったほうが良い事がおおい。

しかし例えば、 150 は Taro の従業員番号であるとする。なので、IDと言いながら実際には従業員番号である。

なので、このインスタンス変数名は id よりも employee_number のほうが適切だろう。

class Employee:
    employee_number: int
    # 省略

おっと、ここだけではない! よく見てみると name は “Taro“ と入力されている。

これは名、あるいはFirstNameを表現しているが、では果たして「firstname」という名前にインスタンス変数名を変えるべきだろうか?

名前は大変難しい。少なくとも、「last name」と「family name」だけで区切られた名前は「ミドルネームはどうするねん」となる。

そして fullname とする場合、氏名が入ってくることを期待しそうだが、システムにおいて本名が必要だろうか?

この場合のシステムではおそらく従業員を管理する必要がある関係上、本名が必要だろう。

翻って、では Employee クラスの name 変数は一体何が入ってくるべきだろうか? 日本においては法律上利用される氏名が入ってくるべきだろう。

であるならば、 「氏名」だが、氏名は name である。ならば name で良い可能性が高いが、例えば法律的にValidな名前、例えば戸籍上の名前、とするならば official_name というのはどうだろう。

それに、近年では通称を利用するケースもある。通称は a.k.a (as known as) という言葉を使う言葉がある。よし、では nameofficial_name として、ついでに aka_name も追加すれば本名と通称を登録できるようになるだろう。

しかし戸籍上の名前は機密度が高い可能性がある。そもそも「戸籍上の名前」という「戸籍上」とPrefixがつくあたり、そもそも戸籍を表現する必要があるのではないか? と考える事ができる。

class FamilyRegistration:
  uuid: str
  fullname: str

class Employee:
  aka_name: str # 通称
  family_registration: FamilyRegistration #正式名称は別クラス
  # 省略

このように戸籍情報を別のクラスとして、そこで法律上Validな情報を保存し、Employee単位では通称だけを利用するようにするのはどうだろうか?

いやしかし、日本の「戸籍」は「一つの戸籍の中に複数の人間の情報」が入っているため、「戸籍の中の特定の誰か」を示す必要があるのでないか?

そうであるならば

class FamilyRegistration:
  class Status(enum):
    ALIVE = 'alive'
    DEATH = 'death'
  class PersonRegistration:
    id: str
    state: Status
    fullname: str
    dob: datetime

A family registration has many person registration のような状態にし、戸籍上死亡しているかどうかや date of birthday を取れるようにして……

いや、dob は Data of Birth の略だ。わかりにくので data_of_birthborn_datetime にしようとする。

ここで考えるのはまたしても名前、そして型だ。

日本の戸籍上では「日時」は永続化されない。よって、 time の情報は不要のはずだ。よって

class PersonRegistration:
    born_date: date

生年月日、というふうにして、「時」はいらんだろう、ということで born_date として型も date 型にして「日付」までしか持っていないことを表現するとしよう……

いかがだろうか。「この名前で良いのだろうか」と考えることは「こう理解しているが正しいだろうか」と考えることである。

つまり名前付けを終える、ということは「この理解とする」と現時点において確定させることである。

特にシステムのユースケースやコンテクストなどを決まっていないため↑の話は無限に深掘って無限に複雑にしていくことができる。

例えば戸籍情報はとてもセンシティブであり、取り扱うのは難しいだろう。よって、雇用に当たって必要な情報を考え切り出して、その属する境界(Boundary)に名前をつけていくのが良いだろう。

つまり、 Employee には名前は持たず、 PersonalInformationPaymentInformation あたりに分けて、用途に応じて名前を保存しておくとかである。

このように、「今このシステムにおいての目的、制約において、あるデータ、操作、情報がどんな性質を示す、持つ、表すのかを表現する」ことが「名前を決めること」である。

よって、たとえ間違っても、 list[Person()] を受け取る変数(Personの配列)が plist にならないことがこれでわかるだろう。

ここまで読んだあなたであるならば、その変数名は person_list, persons, or people になるはずだ。(ただし、 people はより「人々」といった抽象概念を表現することがあるので、 日本人がよく読むコードなら、 person_list か persons が良いだろう。実際には persons は……ここで英語の授業は終了とする)

型の入口

何気なく「型」の話をした。Python3では変数にTypeHintをつけることができる。これはHintであり、実行時エラーにならない。例えば以下のようなコードは実行できる。

>>> def plus(i: list[int]) -> list[int]:
...     return [] + i
...
arg: list[str] = ['hoge']
>>> plus(arg)
['hoge']

plus 関数の 引数 iint の配列であることを期待するが、実行時には 文字列の配列(list[str]) を投入できる。

例えばエディタ上で書いてみると(私のこれはIntellIJというIDEですが、VSCodeでも大体ちゃんとやってくれるはず)以下のように警告してくれる

ide

エディタやIDE上にこういった機能がなくても、例えば以下のように mypy などを使うと警告を抽出してくれる

$ poetry run mypy testing.py
testing.py:6: error: Argument 1 to "plus" has incompatible type "list[str]"; expected "list[int]"  [arg-type]
Found 1 error in 1 file (checked 1 source file)

(ポエム) 型、という言葉について

型といったときに、様々なコンテクストがある。プログラミングにおける型はおよそ計算機数学における型と近しいが、実際にはそれを更に蒸留したものと認識できる。

殆どの場合、特に私のような平凡なプログラマーが型といったときにはデータ型を指しており、ラムダ計算におけるKindを指しているわけではない。

型システムというものそのものが一つの研究分野として成立している世界であり、それはとても超大な理論体系を持っている。

これまでは HaskellOCaml、F# といった一部のプログラミング言語においては超強力な型システムがあった関係で、この一部のプログラマー言語を活用したソフトウェア開発においては生産性へ寄与していた。さらに近年ではTypeScriptやなどの登場により高度ながらもHaskellなどと比較すれば簡便化され、より広く普及した型システムが登場したことにより、その深淵を除く機会が増えてきた。

もちろん、近年のC++(over C++11) などでも型推論などが使えるため、日夜その重要性は増しているといっても過言ではないだろう。

一方で、未だ殆どのプログラマーにとって 型 はただのデータ型であり、そのデータとその性質を内包する境界を宣言するものに過ぎない。が、たったそれだけでも我々プログラマーに対して与える影響は大きく、またその価値は絶大である。

もし計算機数学、計算機における型そのものへ興味があるのであれば、「型システム入門」を読むのが良いだろう。え? 僕? 諦めました。むずかしすぎるよぅ……

型システム入門 −プログラミング言語と型の理論− | Benjamin C. Pierce, 住井 英二郎, 遠藤 侑介, 酒井 政裕, 今井 敬吾, 黒木 裕介, 今井 宜洋, 才川 隆文, 今井 健男 |本 | 通販 | Amazon

型と仲良くする

簡潔に、そしてわかりやすい範囲で述べれば、型は 「ある値(データ)がどのような値を取りうる範囲を決め、それがどのような性質であり、どのように振る舞うか」でる。

Python3 には組み込み型と User定義型 ≒ class がある。

例えば int 型と float 型を考えてみよう。

integer_value: int = 1
str_value: string = "My name is HaiTo"

例えば、 integer_valueint 型であり、これは 符号付き整数 を表現している。同様に str_value は文字列を示す

つまり、

  • integer_value
    • どのような値の範囲か(※事実上無限長拡張できるが、64bit整数値とする)
      • 63bit 整数 〜 63bit整数 (-9,223,372,036,854,775,807 ~ 9,223,372,036,854,775,807)
    • どのような性質か
      • 符号(正負)付きの整数値であり、小数の値は存在せず、また int はPython3においてはメモリが許すだけの桁を維持できる
    • どのように振る舞うか
      • +, -, *, - といったオペレーターに反応できる
  • str_value
    • どのような値の範囲か
    • どのような性質か
      • 文字の列(シーケンス)であり、またリテラルでありつまりImmutableである。内部的にはUTF-16,32のいずれかだが事実上任意の文字を表現できる。byte等にも等価変換できる
    • どのように振る舞うか
      • split, strip, replace, …. といった振る舞いを持っている

当たり前のことを当たり前にいっているが、これは PrimitivePythonの組み込みクラスだからだ。例えば以下のようなデータクラスを宣言したとしよう。

@dataclass は説明のため利用しません

class Temperature:
    kelvin_value: float
    def __init__(self, kelvin_value: float):
        self.kelvin_value = kelvin_value

kelvin 、つまり絶対零度を0としたSI単位系のうち温度を表すもので、それを Temperature 、つまり「気温」として表現しているに過ぎないコードである。

このClassがあろうが無かろうが、以下のコードでもよいはずだ。

temprature = 309

さすがに 309 は意味がわからないかもしれないので、もう少し丁寧にする

temprature_kelvin = 309

これでおよそ人肌(36度くらい)の気温であることがわかった。

例えば、二日間の気温の差をとるとしよう。

yesterday_temprature_kelvin = 309
today_temprature_kelvin = 300
print(yesterday_temprature_kelvin - today_temprature_kelvin) #=> 9

素晴らしい。ではこれをファーレンハイトで表示してほしいという話があったとする。

ファーレンハイトを計算するには {摂氏}×1.8+32 だ。その定義より便宜上 9 kelvin の差は摂氏9度の差と ほぼ 等価なので、流用できる。

yesterday_temprature_kelvin = 309
today_temprature_kelvin = 300
diff_temprature_kelvin = yesterday_temprature_kelvin - today_temprature_kelvin
diff_temprature_fahrenheit = diff_temprature_kelvin * 1.8 + 32
print(diff_temprature_fahrenheit) #=> 48.2

素晴らしい。ファーレンハイトという単位の是非はともかく、現実には一部地域で常用されている単位なので、変換ロジックも簡単だ。

ただ、 1.8 とか 32 とか、このコードが短く、受け取る変数名がわかりやすいので、「Kelvinとファーレンハイトの変換なんだな」と理解でき、そのために必要な変数なんだなと理解できる。

ただこれが以下のようなコードならどうだろうか。

prev = 309
current = 300
diff = prev - current
diff_2 = diff * 1.8 + 32
print(diff_2) #=> 48.2

何をやっているのか はわかるが なんでこうなっているのか が全くわからないだろう。

1.832 とは一体何者だろうか? 数値に意味はあるのか? てか diff_2 ってなんだ? なんで diff を print しないんだ?

変数名がわかりやすければ、まだわかるのだ。直近の例のように変数名がわかりにくかったり、数字に名前がついていないとその意味もわからないのだ。

では元に戻り、改めてデータクラスを見てみよう。

class Temperature:
    kelvin_value: float
    def __init__(self, kelvin_value: float):
        self.kelvin_value = kelvin_value

例えば、このデータクラスを以下のように拡張する

あらかじめ言っておくが、これは間違った拡張である

class Temperature:
    BASE = 273.15

    def __init__(self, kelvin_value: float):
        self.kelvin_value = kelvin_value

    def __add__(self, other: 'Temperature') -> 'Temperature':
        if not isinstance(other, Temperature):
            raise TypeError("Unmatched type")
        return Temperature(self.kelvin_value + other.kelvin_value)

    def __sub__(self, other: 'Temperature') -> 'Temperature':
        if not isinstance(other, Temperature):
            raise TypeError("Unmatched type")
        return Temperature(self.kelvin_value - other.kelvin_value)

    def to_celsius(self) -> float:
        return self.kelvin_value - self.BASE

    def to_fahrenheit(self) -> float: 
        return self.to_celsius() * 1.8 + 32

add だとか sub だとかは何かというと、Python演算子というのは事実上メソッド呼び出しであり、たとえば

1 + 2

としたときには、 (1).__add__(2) という形に書き下せる(実際にはちょっと違うけどまぁ許して)

つまり、 __add__ を実装すれば + に、 __sub__ を実装すれば - に反応できるようになる。なので、このクラスを使うと以下のように表現できる

yesterday: Temperature = Temperature(309)
toady: Temperature = Temperature(300)
diff: Temperature = yesterday - today
print(diff.to_celsius()) #=> 9

なんとわかりやすいコードだろうか。昨日の気温、今日の気温があり、差の気温を取って、それをセルシウス度として表現する。

型情報がついきているため、変数名を短くしても十分にわかりやすいだろう。もちろん yesterday_temperature としてもよいだろう。

kelvin という情報は変数名には不要だろう。その意味は Temperature 内部に隠蔽され、四則演算のロジックはそのクラスの内部で処理されるので、Kelvinであるかどうかはインスタンス化されたあとであれば知らなくてよいだろう。

ここで条件が変わったことを知ったとしよう。見ての通り、Kelvinは我々にとって直感的な値ではない。入力値として摂氏の値が入ってくることになった。

内部的にはKelvinで扱っておこう。なにせSI単位系なので……しかしこれは直感的には正しそうだが実際には正しくない場合が多い。前述の通り、セルシウス度とKelvinの互換性は高いが完全ではない。しかし今回はこのまま内部的には引き続き Kelvin を保持するとする。

なので、以下のようにすると良いだろう。

class Temperature:    
    BASE = 273.15
    kelvin_value: float

    @classmethod
    def from_celsius(cls, celsius_value: float) -> Temperature:
        return cls(celsius_value + cls.BASE)

    def __init__(self, kelvin_value: float):
        self.kelvin_value = kelvin_value
#  .. 省略

ではこれを利用する

yesterday: Temperature = Temperature.from_celsius(24)
toady: Temperature = Temperature.from_celsius(20)
diff: Temperature = yesterday - today
print(diff.to_celsius()) #=> -269.15

このように、「気温どうし」を足したり引いたりすると、直感的に間違っているような値が出てくる。

しかし、内部計算をみればこれは正しい。 つまり 297.15 - 293.15 = 4.0 kelvin ==> -269.15℃ なのだ。

「差がしりたいんだ」となるだろう。なので、そもそも Temperature - TemperatureTemperature を返却していることがおかしいのだ。

同様に + でもそうだ。つまり、

class Temperature:
    BASE = 273.15

    @classmethod
    def from_celsius(cls, celsius_value: float) -> 'Temperature':
        return cls(celsius_value + cls.BASE)

    def __init__(self, kelvin_value: float):
        self.kelvin_value = kelvin_value

    def __add__(self, other: 'Temperature') -> float:
        if not isinstance(other, Temperature):
            raise TypeError("Unmatched type")
        return self.kelvin_value + other.kelvin_value

    def __sub__(self, other: 'Temperature') -> float:
        if not isinstance(other, Temperature):
            raise TypeError("Unmatched type")
        return self.kelvin_value - other.kelvin_value

    def to_celsius(self) -> float:
        return self.kelvin_value - Temperature.BASE

    def to_fahrenheit(self) -> float:
        return self.to_celsius() * 1.8 + 32

これで、ただ float を返すようになった。その和/差をまた気温として使いたければ、改めてそれでInstance化するとよいだろう。

素晴らしい。この拡張について考えていけば、 from_fahrenheit() クラスメソッドがあってもいいだろう。いっそ、 __init__ においては呼び出し元を限定して from... からしか呼び出されたときに動作しないようにし、 from_kelvin, from_fahrenheit, from_celsius のFactoryClassMethodを経由してのみ使えるようにしてもよいだろう。

さて、ここまでにおいてこの Temperature は事実上ただの float 型の値であるが、それを包んだ Temperature 型としている。

このTemperature 型は

  • どのような値を取るか
    • floatの値だが、Kelvin値において地球上ではおよそ200.0 kelvin - 350.0kelvin 程度のRangeである
  • どのような性質か
    • 「気温」を表している。
  • どのように振る舞うか
    • 足し引きができ、また様々な単位の値へ変換できる

といえる。

考えれば、 これはただの float よりもその表現力が「狭くなっている」ことがわかるだろう。

私はわざと割り算や掛け算を宣言しなかった。

「気温」で割り算や掛け算をするだろうか? 殆どの場合しない。しないのであれば、提供 しないことができる

つまり、事実上ただの float を扱っているのだが、 float よりもその取り合える振る舞いが狭まっているのだ。

これは興味深いことである。同じ 300.0 という値であっても、それを float として使うか、気温として使うかで性質が変わり、取り得る値や振る舞いが変わっているかのように見える。

これが型を付けることの強いモチベーションである。

名前と同じように「こいつが一体なにものなのか」を表現する方法が型である。

このように、値に型をつけて、名前をつけて、「こいつは一体なにもので、どういった振る舞いをするのか」を確定させていくのがプログラミングにおいてもっとも重要な要素の一つであり、そしてたったひとつの冴えたやりかたが存在しないものなのである。

なぜ私が Jupyter notebook の外の世界において pandas や polars, dict を消したがるのか

別記事予定

実践編

Dropped

おわりに: よりBetterなコードを目指すために

名前付けや型のセクションで何度も言っているように、「今の考えていること」を表現するのがコードである。

「こういったデータが入ってくる(input)」「こういった処理をする」「こういったデータを返却する(output)」

殆どのプログラミングというのはこの3ステップを実現するためのものである。

InputがFileなのか、API Call なのか、はたまたS3を通じたファイル連携なのか……Inputも色々あり、Outputも同様に色々ある。

それぞれにおいて、「色々」に具体的な名前をつけたり、型を付けて値の範囲を制限したり、Validationしたりしていく。

つまり、プログラミングをするのに実際に必要なのはそのプログラミング言語に対する理解と、そのプログラミング言語の外の知識や経験である。

あるビジネスや機能を表現するのに対して必要なのは、そのビジネスや機能の知識なのだ。プログラミングをうまくなるためには、プログラミングだけをしているのではなく、あなたが立ち向かうビジネスや機能について知り、自分なりにそのビジネスをプログラムとして表現し、向き合い、改善し続けていくことだ。

すべての時間軸において最高のコードはない。ビジネスは常に変化するし、環境も変化するし、我々が理解するビジネスもまた日々変わっていく。

すべての時間軸において最高のコードはないが、「今考えうるBetter than elseだと自分が思うコード」は間違いなくある。

しかし一方で、「今は時間がないので」、このコードである、というのも理由として十分ではある。

どこを何を、どこまで何をするのか、その判断もまたプログラミングなのだ。

ぜひ今後もプログラミングをしていこう。

References