SOLID原則:インターフェース分離の原則

インターフェース分離の原則とは?

  • インターフェースクラス(抽象クラス)を使う人が使うものだけ書いておきましょう という原則
  • インターフェースクラスに何でもかんでも書いてはいけません ✋
  • 難しく言うと、不必要な依存関係をなくす ということ
依存関係について詳しく
  • Aを修正するとBも修正が必要になる つまり、BはAに依存している と言える(BはAと依存関係にある)

  • この場合、Aがインターフェース、Bがインターフェースを使う側のソースコードというイメージを持つといいのかもしれません

よくない例

文章だけでは理解できないので、Pythonで書いたコードで例を挙げてみる

動物クラスを作る

以下は、動物クラスのインターフェースとして使う予定

動物であれば、走る, 食べる, 寝る 動作はできますよね 🐈

class Animal:
    """動物クラス."""
    
    def run(self):
        

    def eat(self):
        

    def sleep(self):
        

動物クラスを継承して猫クラスを作る

以下は、具体的にコードを実装して猫クラスを作ってみました

そして、この猫クラスをAさんが使っているとしましょう

class Cat(Animal):
    """ネコのクラス."""

    def run(self):
        print('ネコが走る')

    def eat(self, food: str):
        print(f'ネコが{food}を食べる')

    def sleep(self):
        print('ネコが眠る')

雀クラスを作りたくなった

  • Animalクラスに fly メソッドを追加することにした
class Animal:
    """動物クラス."""
    
    def run(self):
        

    def eat(self):
        

    def sleep(self):
        

    def fly(self): <- 追加したメソッド

以下が雀クラスです

ここだけ見ると良さそうです

class Suzume(Animal):
    """雀のクラス."""

    def run(self):
        print('雀が走る')

    def eat(self,):
        print('雀が食べる')

    def sleep(self):
        print('雀が眠る')

    def fly(self):
        print('雀が飛ぶ')

が、 Animal クラスを変えてしまったので、Aさんが使っているソースコードでエラーが出るようになってしまいました

fly メソッドが追加された と聞いたAさんは、とりあえず実装することにしました

class Cat(Animal):
    """ネコのクラス."""

    def run(self):
        print('ネコが走る')

    def eat(self, food: str):
        print(f'ネコが{food}を食べる')

    def sleep(self):
        print('ネコが眠る')

    def fly(self):
        # ネコは飛ばないので、このメソッドは呼ばない!!!  <- なんであるのかわからないメソッドが追加された
        raise 

Aさんが↑のように、一旦 fly メソッドを追加したことでエラーは起こらなくなりましたが、これはよくないソースコードの書き方です

存在しているのに使ってはいけないメソッドがあるって、なんか気持ち悪くないですか・・・ 🥶 ?

それに、人間はミスをする生き物なので、今後、誰かが間違って使ってしまう可能性もなきにしもあらずですね。 なので、よくないです。

参考: https://kanoe.dev/blog/interface-segregation-principle/

良い例

では、どう修正するのが良いのか

ポイントは、 共通するところはまとめる、共通しないところは分離する です

まず共通するところをまとめる

動物であれば、走る・食べる・寝る は共通

class Animal:
    """動物クラス.

  動物において共通の動作をまとめたクラス.
  """
    
    def run(self):
        

    def eat(self):
        

    def sleep(self):

鳥類は共通して、飛べる という動作があるので、鳥類でまとめる

(ペンギンとかダチョウは、とりあえず見逃してください 🙇‍♀️)

また鳥類は動物なので、Animalクラスで定義した動作もできる → Animalクラスを継承する

class Bird(Animal):
    """鳥類クラス.

  鳥類において共通の動作をまとめたクラス.
  """

    def fly(self):

今回は鳥類でまとめているので、猫クラスを定義するためにも哺乳類クラスも作っておく

哺乳類も動物なので、Animalクラスで定義した動作もできる → Animalクラスを継承する

class Mammal(Animal):
    """哺乳類クラス.

  哺乳類において共通の動作をまとめたクラス.
  """

    def cry(self):
        

では猫クラスを書いてみる

class Cat(Mammal):
    """ネコのクラス."""

    def run(self):
        print('ネコが走る')

    def eat(self, food: str):
        print(f'ネコが{food}を食べる')

    def sleep(self):
        print('ネコが眠る')

    def cry(self):
        print('ネコが泣く')
    
    def purr(self):
        """鳴くメソッド."""
        print('ニャー')
  • 鳴くメソッドに加えて、Animalクラスのメソッドと、Mammalクラスのcryメソッドが猫クラスでは実行できる

雀クラスは、Birdクラスを継承すると良さそう 😊

class Suzume(Bird):
    """雀クラス."""

    def run(self):
        print('雀が走る')

    def eat(self,):
        print('雀が食べる')

    def sleep(self):
        print('雀が眠る')

    def fly(self):
        print('雀が飛ぶ')

    def sing(self):
        """さえずるメソッド"""
        print('ちゅん♪')
  • さえずるメソッドに加えて、AnimalクラスのメソッドとBirdクラスのflyメソッドが雀クラスでは実行できる

こんな感じでクラスを作っていくと、共通部分は使い回せて、独自の部分も作れる構造になる

参考:https://qiita.com/k-penguin-sato/items/86b8262bfbe189fc72c3

インターフェース分離の原則を守るメリットは?

  • インターフェースを使う側に親切 ✨
  • つまり、インターフェースを使う側が、自分に必要な部分だけ考えればいい

  • 上記の例だと、この原則を破っていたソースコードでは、猫クラスを実装したいだけなのに、flyメソッドが存在しているせいで、

    「猫は飛ばないからこのメソッドは呼び出したらエラーになるようにしなきゃ」 

    みたいな無駄な考えとソースコードを生んでしまう

  • インターフェース分離の原則を守った修正後のソースコードでは、Mammalクラスが用意されているので猫のことだけ考えて、インターフェースを使うことができる

他の原則にも言えますが、バグも少なくなりますね 😄

あとは機能拡張やソースコードの修正など、メンテナンスもしやすくなりそう。

どの原則も極論、プログラムが正常に動く可能性を高める 原則とも言えそうですね。

CORSとは何か(解決編)

CORSを使う

の前に、CORSには二つのリクエストがあるという話を入れる

CORSのリクエストは二つ

  1. 単純リクエス
  2. プリフライトリクエス

単純リクエストとは?

  • GET, HEAD, POST のリクエス
  • Content-Type ヘッダーに、 application/x-www-form-urlencodedmultipart/form-datatext/plain が入るリクエス

参考:https://developer.mozilla.org/ja/docs/Web/HTTP/CORS#simple_requests

  • 普通のリクエストだと思えば良さそう
  • 普通というのが、難しいですが、リクエストがあったらすぐに、サーバー側にリクエストが飛ぶという感覚(後述のプリフライトリクエストを読むとわかるかと・・)

プリフライトリクエス

  • リクエストがあったら(実際のリクエスト と呼ぶことにする)、サーバー側に実際のリクエストがすぐに飛ぶのではなく、その前に OPTIONS というHTTPリクエストが飛ぶ
  • OPTIONSリクエストは、実際のリクエストをサーバー側に送信しても安心であるかを確かめるリクエストで、実際のリクエストが、ユーザーデータに影響を与える場合に、OPTIONS リクエストを送る
  • ユーザーデータに影響を与える場合は、例えば、 POST, PATCH, DELETEなどのリクエス

参考:https://developer.mozilla.org/ja/docs/Web/HTTP/CORS#preflighted_requests

  • OPTIONSリクエストをPassするには、レスポンスヘッダーに POST, PATCH, DELETEなどのリクエストを含めなければいけない
  • レスポンスヘッダーなので、サーバー側での設定が必要となる

実際、どういうエラーが出るのか

  • 実際にfrontend ⇒ backendにPOSTリクエストを送ったときのエラーはこちら
Access to XMLHttpRequest at 'http://localhost:8000/api/v1/calc/calc_kakeibo_from_csv' 
from origin 'http://localhost:3000' 
has been blocked by CORS policy: 
No 'Access-Control-Allow-Origin' header is present on the requested resource.
  • developerツールのNetworkでも確認できますね(すいません、拡大してください 🙏)

f:id:YukiMatsu88:20210622194221p:plain
corsエラー_1

  • Consoleにエラーメッセージが出てます(すいません、拡大してください🙏)

f:id:YukiMatsu88:20210622194303p:plain
corsエラー_2

注目すべきは・・・

No 'Access-Control-Allow-Origin' header is present on the requested resource.
  • Access-Control-Allow-Origin ヘッダーがないと言われたので、設定する

FastAPIの設定

  • FastAPIでは、 CORSMiddleware を使用して、CORSの設定ができる
  • こんな感じで設定
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware

app = FastAPI(
    title="calc_csv_kakeibo_backend",
    version="1.0",
)

origins = [
    "http://localhost:3000",
]

app.add_middleware(
    CORSMiddleware,
    allow_origins=origins,
    # オリジン間リクエストでCookieをサポートするのでTrue
    allow_credentials=True,
       # 今作ってるアプリでは、POSTしかしないので一旦POSTのみ
    allow_methods=["POST"],
    # 全て許可
    allow_headers=["*"],
    # 全て許可
    expose_headers=["*"],
)

日本語で書いてあるので、ここを読めばどういうふうに設定すればわかりそう

https://fastapi.tiangolo.com/ja/tutorial/cors/#corsmiddleware

許可されたか確認してみる

f:id:YukiMatsu88:20210622194631p:plain

  • ちゃんと、Access-Control-Allow-Originlocalhost:3000 が設定されてそう

開発してても、既にこの辺は設定済みなので、アプリ自作しなかったら、こんなに調べなかったので、良い機会でした ☀️

参考:https://qiita.com/10mi8o/items/2221134f9001d8d107d6

CORSとは何か

きっかけ

  • frontend ⇒ javascript, backend ⇒ pythonで個人的にWebアプリを作成してて、frontendからbackendにpostリクエストした時に CORSエラー が発生
  • 名前は聞いたことあるが、結局どういう時に発生するのかわからなかったので、ちゃんと調べてみた

(まず)読み方

  • 一般的には”シーオーアールエス”もしくは”コルス”と読みます。 とのこと

https://tech.jstream.jp/blog/cdn/cors_cdn/

CORSとは何か?

  • 同一生成元ポリシー というポリシーで設けられた制限を緩めるためのもの

同一生成元ポリシーとは?

  • クロスサイトリクエストフォージェリ(CSRF) などといったセキュリティ攻撃を防止するためのポリシー
  • 同じオリジン間でないとアクセスできない という制約

・・・・・またいろいろ新しい単語が出てきたので、それぞれ軽く説明をかく

クロスサイトリクエストフォージェリ(CSRF)とは?

  • Webアプリケーションに対する攻撃方法の一種
  • 攻撃手順は以下
  • 攻撃対象者をあらかじめ用意しておいた攻撃用Webページに誘導する(アクセスさせる)
  • 攻撃対象者が↑のWebページにアクセスしたときに、不正なリクエストが実行される
  • 攻撃用Webページで不正なリクエストが処理され、攻撃対象者が意図していない処理が行われる

以下のページがわかりやすかったです

クロスサイトリクエストフォージェリ(CSRF) | トレンドマイクロ

オリジンとは?

f:id:YukiMatsu88:20210618193455p:plain

スキーマ、ホスト名、ポートをまとめて、オリジン と呼ぶ

オリジンが同じ場合

http://www.test.com/
http://www.test.com:80/
http://www.test.com/test.html

オリジンが違う場合

https://www.test.com => スキーマが違うので、http://www.test.com/ へはアクセスできない

http://www.example.com => ホスト名が違うので、http://www.test.com/ へはアクセスできない

http://www.test.com:8080 => ポート番号が違うので、http://www.test.com:8000 へはアクセスできない

(参考)

同一生成元ポリシー(Same-Origin Policy) - とほほのWWW入門

CORS & Same Origin Policy 入門 | yamory Blog

改めて同一生成元ポリシーとは?

  • 外のサイトは危ないので、自分が今いる領域から外に出ることはできないようにする

みたいな(あくまで)イメージですね。

改めてCORSとは?

  • 外のサイトは危険だけど、それでもアクセスしたい場合がある
  • そういう時は、意図的に、制限をゆるくして、外のサイトであってもアクセスできるようにする

というイメージですね。

まとめ

つまり、Webクライアントとサーバーを分けて実装している場合は、異なるオリジン間で通信を行うことになる(今回、自作しているアプリは、まさにこの事例)

ので、 CORS 設定が必須となる


概要がわかったが、具体的にどうすればいいのかって話とかは別の記事で書こうと思います😶

SOLID原則:リスコフの置換原則

リスコフの置換原則とは?

  • Liskov Substitution Principle = LSP
  • あるクラスを別のクラスが継承する 文脈で関係してくる原則
  • 親の決まりを子供は破ってはいけないの原則
  • (大半が難しい文章で説明されていて、個人的には上の文章がしっくりきた ✨)
  • クラスを継承するとき、

    継承元 = 親クラス

    継承する側 = 子クラス

という場合があると思いますが、この時に、継承する側である子クラス内で、親クラスで定められているルールを破るコードを書いてはいけない というのがリスコフの置換原則

別の言い方をすると、

親クラスを子クラスで置き換えても問題なく動作する とも言える

よくない例

  • 実際にサンプルコードを書いてリスコフの置換原則を破ってみる
  • Pythonで書いてます
  • 以下はとあるチームでの開発を例として取り上げてみた

〜とあるチームの開発初期の話〜

  • ある店舗に並べる商品を定義するクラスが必要になり以下のクラスを作成
# 店舗に並ぶ商品を宣言したクラス
class Goods:
    name: str
    # 仕入れ原価
    price: int

    def get_price(self) -> int:
        """商品の値段を返すメソッド."""
        return self.price

    def set_price(self, price: int) -> none:
        """商品に値段をつけるメソッド."""
        self.price = price

〜消費税を導入〜

  • 拡張機能で消費税10%を考慮する必要が出てきた
  • もともと店舗に並ぶ商品を宣言したクラスを継承して、新しいクラスを作ることにした
class NewGoods(Goods):
    
    def set_price(self, price: int) -> None
        """商品に値段をつけるメソッド."""
        self.price = int(price * Decimal('1.10'))
  • 親クラスである Goods クラスの値段設定メソッドは、引数で渡された値をそのまま商品の値段として設定するだけの機能だった
  • しかし、子クラス(NewGoodsクラス)では、引数で渡された値より10%増の値段が設定される機能になってしまった・・・・
  • またprice 変数は仕入れ原価なのに、仕入れ原価でもない変数になってしまった(そもそも変数名分かりづらい問題もある)

=>子クラスが親クラスのルールを守っていない

〜買い物カゴに入っている商品の合計を算出する機能を追加する〜

  • しばらくして、買い物カゴに入っている商品の合計値を算出して請求金額を出してほしいとの要望があり、新しくコードを実装することになった

  • 商品の宣言は、 NewGoods クラスだから、このクラスにある price 変数を商品ごとに取得してきて、全て足し算すればいいね

  • と教えてもらった担当者は早速以下のコードを実装。
class CalcGoods:

    def calc(self, goods_list: list[NewGoods]) -> int:
        """買い物カゴに入っている商品の合計金額を算出する."""
        total_price: int = 0
        
        # わかりやすくfor文で書いておく
        for goods in goods_list:
            # NewGoodsはGoodsクラスを継承しているので、get_priceメソッドを使用できる
            # 消費税考慮 <--- ❌
           total_price += int(goods.get_price() *  Decimal('1.10'))
        
        return total_price
  • CalcGoods クラスのcalc メソッドは、二重で消費税をかけることになり、テストかもしくは本番環境で期待した値段よりも高い値段が算出されてしまう

  • NewGoods クラスで price には商品原価を登録しておく というルールを無視したことが原因で起こってしまったバグ

参考:http://marupeke296.com/OOD_No7_LiskovSubstitutionPrinciple.html

リスコフの置換原則を守るメリットは?

  • 今回の例からも言えるが、バグの可能性が減る
  • 子クラス(サブクラス、継承元クラス)を使うときに、意識すべきことが少ない

    =>今回の場合だと、 NewGoods クラスを使うときに、set_price メソッドを使う時は、消費税が既に反映されている」という事実を意識して、実装しなければならない という状態が起こっている



うむ、なんとなく理解できた気がする

調べてると、契約による設計 みたいな概念も出てきましたが、
これはまたの機会にしようかなと思います 🌻

SOLID原則:開放閉鎖の原則

  • 調べていると、 開放閉鎖 と書いている人と 解放閉鎖 と書いている人がいたが、 開放閉鎖 と書いている人の方が多そうな印象だったので、ここでは 開放閉鎖 にしておく
  • オープン・クローズドの原則 とも言ったりするのかな?

開放閉鎖の原則とは?

  • Open Closed Principle = OCP
  • 拡張できて修正不要の原則

    • そもそも

      既存のクラスやシステムが機能拡張可能である状態 = Open:開いている

      機能拡張の際に修正を要しない状態 = Close:閉じている というらしい

  • Open Closed = 拡張できて修正不要の原則(そのまま読める

機能拡張の方法には2つ存在する

  1. 既存のコードを修正する
  2. コードを新しく追加する ← 今回の原則では、こちらで機能拡張するという前提。

開放閉鎖の原則では、機能拡張する際に、 既存のコードを修正することなく、コードを追加することができるプログラムにしましょうという原則


最近、よく思うのですが、使われているソフトウェアは、基本的に常に仕様変更や機能拡張が入ると思っていた方が良さそう

逆に、1回作って、何も変更や機能拡張がないソフトウェアは、必要ではないソフトウェアで、使われていないソフトウェアとも言えるのかなと思います

新卒の頃から、仕様変更で自分が書いたコードが無駄になってしまう現象に虚しさと悲しみとで、😑 ってなった時も何度かありましたが、どこの業界の開発部隊でも開発している限り、そういうものなんだなと思い始めました

よくない例

class Payment:
    
    def pay(self, type: str) -> int:
        # type別の決済をする
        if type == 'credit':
            # クレジットカードで決済する
            return XXXXXXX

        if type == 'applePay':
            # ApplePayで決済する
            return XXXXXXX
        

payment = Payment()

# ApplePayで支払う
payment.pay(type='applyPay')

PayPayも対応できるようにしてほしい との要望があった

  • 修正案は・・・
if type == 'PayPay':
    # PayPayで決済する
    return XXXXXXX

Payment クラスに追加してしまうと・・・・

開放閉鎖の原則は守られていないコードだ

と言えてしまいますね

既存のコードである Payment クラスに変更を加えてしまっているので 🙅‍♀️ ですね

開放閉鎖の原則を守るコードに書き換えてみる

Class Payment:

    def pay(self) -> int:
        # 決済する
        pass

# クレジットカードのクラスを作成
Class Credit(Payment):
    """クレジットカードのクラス."""

# ApplePayのクラス
Class ApplePay(Payment):
    """ApplePayのクラス."""
    

# PayPayのクラス
Class PayPay(Parment):
    """PayPayのクラス."""

paypay = PayPay():
price = paypay.pay()

決済の大元となるクラスを作成し、それぞれの決済がPaymentクラスを継承するように修正

このようにすると、既存のコードは書き換えることなく、PayPayクラスを追加するだけで機能拡張することができ、開放閉鎖の原則は守られているコードになった

参考:https://kanoe.dev/blog/open-closed-principle/

開放閉鎖の原則のメリットは?

既存のコードに手を加えていないので、バグるリスクが小さくなる(少なくとも既に動いているコードはバグらなさそう)

オブジェクト指向に限らず守りたい最重要原則 "open/closed principle - OCP - 開放/閉鎖原則"(SOLID原則より) - Qiita


この前書いた、単一責任の原則とメリットは似てますね 🤔

メリットが薄っぺらくなってしまいましたが、概要については理解できたので終わり

こちらも徐々に学んでいきたいところですねぇ・・・

SOLID原則:単一責任の原則

単一責任の原則とは?

まずはざっくり理解してみる。

  • Single Responsibility Principle = SRP
  • クラスや関数、メソッドなどがもつ責任は1つにする ということ
  • もう少し分かりやすい表現にすると、クラスや関数などを 変更する理由は1つだけ持つようにしましょう という原則
  • クラスや関数のコードを変更する理由が2つ以上ある場合は良くないということ

良くない例

  • 以下のEmployeeクラスは変更する理由を3つ持つのでよくない
class Employee:
    def calculatePay(self):
        # 従業員の給与を計算するメソッド
        # 給与計算に関するビジネスルールが変更するたびに変更が入るメソッド
        pass

    def reportWorkingHours(self):
        # 従業員の勤怠をまとめるメソッド
        # 勤怠に関するビジネスルールが変更するたびに変更が入るメソッド
        pass

    def save(self):
        # 従業員に関する情報をDBに保存するメソッド
        # 従業員に関する情報が変わるたびに変更が入るメソッド
        pass
  • ↑のクラスは変更する理由を3つも持っており、不安定なクラス と言えるらしい

修正案

  • それぞれの変更理由ごとにクラスを分ける という修正
# 従業員の給与を計算するクラス
# 従業員の給与に関するビジネスルールが変わったらここだけ修正すれば良い
class EmployeePay():
    def calculatePay(self):
        pass

# 従業員の勤怠を管理するクラス
# 従業員の勤怠に関するビジネスルールが変わったらここだけ修正すれば良い
class EmployeeWorkingHour():
    def reportWorkingHours(self):
        pass

# 従業員のDBを操作するクラス
class EmployeeRepository():
    def save(self):
        pass

    def get(self)

https://プログラマが知るべき97のこと.com/エッセイ/単一責任原則/

https://zenn.dev/k_sato/articles/98f4b15747e191

単一責任の原則メリットを考える

単一責任の原則の概要を理解したので、今度は、メリットを考えてみる

単一責任の原則を守らないとどうなるのか?

わざとこの原則を破ってみる。

  • 複数の機能を持たせたクラスである Sample クラスを例に挙げてみる
  • Sample クラスには、A機能とB機能があると仮定する
  • イメージはこんな感じでしょうか・・・
  • (この時点でちょっとミスってるコードだが、気にしない、とりあえず)
class Sample:

    number: int

    def __init__(self, number: int):
        self.number = number
    
    def a(self):
        # A機能
        return self.number * 100

    def b(self):
        # B機能
        # B機能では、10以下の数値が来ることしか想定していない
        if self.number < 10:
            return self.number - 1
        else:
            raise ValueError
  1. 複数の機能(A機能、B機能)を一つのクラスや関数に持たせちゃう

    つまり、責任が一つではない、修正する理由が一つではないという状態

  2. A機能を動作させたところ正常に終了した

  3. 次にB機能を動作させたところ正常に終了した

    ⇒ ここまでは良さそう

  4. ある日、A機能に仕様が追加された。

    A機能では、100以上の数字しか扱わないとの仕様になった。そこで・・・

class Sample:

    number: int

    def __init__(self, number: int):
        
        # 100以上の数値しか扱わない
        if 100 <= number:
            self.number = number
        else:
            raise ValueError
    
    def a(self):
        # A機能
        return self.number * 100

    def b(self):
        # B機能
        if self.number < 10:
            return self.number - 1
        else:
            raise Error

numberは100以上じゃないと例外に飛ぶようにした

  1. 仕様変更したA機能を動作させたところ、正常に終了した

  2. 続いてB機能を動作させたところ、 ValueError になってしまった・・・!

これまで動作していた以下のコードでエラーになってしまう事態が起きた模様・・・

sample = Sample(number=3) ←ここ

result = sample.b()

新A機能を実現しようとしたら、既存のB機能が動かなくなってしまった

ソフトウェアにおける責任を全うさせるために存在すると理解

  • ソフトウェアというのは、とある機能をエラーなく実行したいという思いの元、作成されるはず。

  • つまり、エラーなく正常に処理し、正常に終了するという期待(責任)がソフトウェアにはある

  • この、正常にエラーなく動作するという責任を全うする可能性をなるべく上げるために、単一責任の原則は存在している (と一旦理解。)

  • 今回、例に挙げたA機能、B機能もクラスが分かれていれば、A機能の仕様変更がB機能の挙動に影響を与えることもなかった => つまり、B機能が ValueError になる事態は避けられたはず。

https://qiita.com/MinoDriven/items/76307b1b066467cbfd6a

単一責任の原則について、勉強してみたが、設計や設計思想って本当に奥が深いし、語れたら、本当にすごいエンジニアなのではないかと思う、今日このごろ。。

それに↑のQiitaの記事によると、ビジネスの関心がないと困難とのこと。。。

時間をかけて設計については勉強していきたいですね 🤕

from __ future __ import annotationsとは?

※ コードは説明に特化したコードであくまで例です。。 🙇‍♀️

調べるきっかけ

  • 開発していてたまたま見かけたコードで単純になんだろうと思ったので調べた

from future import annotationsとは何か?

  • Python で型ヒントを書くとき、型定義するときに必要
  • Pythonのバージョンによっては書かなくてもよい
__ future __とは?
  • Pythonが用意してくれているモジュール
  • (ちょっと詳しくはよく理解できなかったので公式ドキュメントだけ貼っときます・・)

future - Future statement definitions - Python 3.9.5 documentation

どういうときに使うのか?

1.  Python3.8以前でリストやタプルの型定義をするとき

Python3.8以前のリストやタプルの型定義のやり方

# パターン1: typing パッケージを使う
# 'List' を使う
from typing import List

number_lists: List[int] = [1, 2, 3]

# パターン2: from __future__ import annotationsを先頭に書くパターン
# 'list' を使う
from __future__ import annotations

number_lists: list[int] = [1, 2, 3]
⭐ ポイント
  • Python3.6では __future__ を使えないっぽいので、パターン1しか書けないようです
  • Python3.9以降では、 from __future__ import annotations を書かなくてもパターン2を使用することができるようです

2021年版Pythonの型ヒントの書き方 (for Python 3.9) | フューチャー技術ブログ

2. Python3.7~3.9で引数に複数の型を設定したいとき

from __future__ import annotations

def display_id(name: int | str) -> str:
  • Python3.6では、typingの Union を使用して↓のように書きます
from typing import Union

def display_id(name: Union[int, str]) -> str:

2021年版Pythonの型ヒントの書き方 (for Python 3.9) | フューチャー技術ブログ

3. Python3.7以降で自作関数を戻り値の型定義としたい時

例えば以下のようなクラスを返したい時

class User():
    def __init__(self, first_name: str, name: str, age: int):
        
        self.first_name = first_name
        self.name = name
        self.age = age

    @classmethod
    def func(cls, fullname: str, age: int) -> User:
        # fullnameとageからUserオブジェクトを返すメソッド
        # 処理は省略.
# パターン1
# from __future__ import annotationsを使う時
# 戻り値の型に、User とかけます
from __future__ import annotations

class User():
        省略

    @classmethod
    def func(cls, fullname: str, age: int) -> User:

# パターン2
# typing を使う時
# ' ' (シングルクォーテーション)が必要

class User():
        省略

    @classmethod
    def func(cls, fullname: str, age: int) -> 'User'

pythonの型ヒントで自作クラスを指定したい|teratail

他にもありそうですが、ここまでで、なんとなく存在意義を理解したのでここまで。

他は都度学んだら書くようにしようかなと思います。