Python🐍でリファクタするならこうしてみよう集

こんにちは。本業フロントエンド副業パイソニスタの[ぬこすけ](https://twitter.com/nuko_suke_dev)です。

Python エンジニアの皆さん、元気にリファクタリングしていますか？
最近、昔作ったサイトで Python で書いたバックエンドのコードをリファクタリングしているのですが、負債まみれで吐きそうです😇

https://nuko-programming.com/ranking/Python/page/1

この記事では Python のコードをリファクタリングする際の参考になる例をいくつか紹介します。
ぜひ普段の Python での開発の参考になればと思います！

### 注意事項

* 対象読者は **Python 初心者から中級者** を想定しています。
* Python のバージョンは記事執筆時点(2023/6/15)で最新の `3.11` を前提にお話します。
* **必ずしも紹介した方法が最適解とは限りません**。というのも、プロジェクト毎で状況が違ったり、コードに対する思想というのも人それぞれだからです。
「こういう方法もありますよ！」などあればコメントいただけると嬉しいです！

## list だけでなく tuple や set も使おう

Python で配列を扱う時はとりあえず `list` を使えばなんとかなりますが、 **特性に応じて `tuple` や `set` を使い分けると良い** でしょう。

まず、 **配列に対して特に変更を加えない場合は `tuple`** が使えます。
`tuple` を使うことで予期しない変更によるバグを防いだり、メモリの節約にもなります。

```python
# 値を追加したり削除はできない
fruits = ('apple', 'banana', 'orange',)
```

逆に **配列に対して追加や削除、変更を行う場合は `list`** を使うべきでしょう。

```python
fruits = ['apple', 'banana', 'orange',]
fruits.append('lemon')
fruits.pop()
```

その他、 **重複の削除や大量のデータに対する要素の存在確認といった用途では `set`** を使うと良いでしょう。

```python
fruits = ['apple', 'banana', 'orange', 'apple', 'orange']
# 重複が削除される
fruits_set = set(fruits)
```

このように用途に応じて **`list` だけでなく `tuple` や `set` も使い分ける** のもリファクタの 1 つです。

## 繰り返し文にはなるべく内包表記を使おう

配列で繰り返し文を書く場合は `for ~ in` でも書けますが、 **内包表記を使った記述の方が簡潔で、かつ処理速度も早い** です。

```python
fruits = ['apple', 'banana', 'orange',]

for fruit in fruits:
    print(fruit)

# 内包表記
[print(fruit) for fruit in fruits]
```

また、 **内包表記から辞書も作成できます** 。

```python
fruits = ['apple', 'banana', 'orange',]

fruits_len_dict = {fruit: len(fruit) for fruit in fruits}
print(fruits_len_dict)
# => {'apple': 5, 'banana': 6, 'orange': 6}
```

## できるだけイミュータブルにしよう

**データの書き換えが不可能な、イミュータブルなオブジェクトにすることで予期せぬバグを防ぐことができます**。
普段の開発でサクッとイミュータブルに変更できるのであればリファクタしてみるのも良いでしょう。
例えば、次のようなリファクタ例が挙げられます。

* `list` を `tuple` に
* `set` を `fronzenset` に
* `dict` を `MappingProxyType` に

それぞれ具体的なコードを紹介します。

### `list` を `tuple` に

```python
# 変更可
mutable_fruits = ['apple', 'banana', 'orange']

# 変更不可
immutable_fruits = ('apple', 'banana', 'orange',)
```

### `set` を `fronzenset` に

```python
# 変更可
mutable_fruits = {'apple', 'banana', 'orange'}

# 変更不可
immutable_fruits = frozenset(['apple', 'banana', 'orange'])
```

### `dict` を `MappingProxyType` に

```python
from types import MappingProxyType

# 変更可
mutable_fruit_color = {
    'apple': 'red',
    'banana': 'yellow',
    'orange': 'orange'
}

# 変更不可
immutable_fruit_color = MappingProxyType({
    'apple': 'red',
    'banana': 'yellow',
    'orange': 'orange'
})
```

## dataclass を使おう

Python には組み込みで `dataclass` というデコレータが使えます。

https://docs.python.org/ja/3.11/library/dataclasses.html

これは **クラスを定義する時に便利** です。
通常、クラスを定義する時は次のようになるでしょう。

```python
class Person:
    def __init__(first_name: str, last_name: str)
        self.first_name = first_name
        self.last_name = last_name
```

このクラスは `dataclass` を使った場合は次のようになります。

```python
from dataclasses import dataclass

@dataclass
class Person:
    first_name: str
    last_name: str
```

このように **簡潔にクラスを書くことができます**。

また、 `dataclass` には **オプションを指定することができます**。
例えば、「[できるだけイミュータブルにしよう](#できるだけイミュータブルにしよう)」の例以外にも `frozen=True` を指定することで **手軽にイミュータブルなインスタンスを作成できます** 。

```python
from dataclasses import dataclass

@dataclass(frozen=True)
class Person:
    first_name: str
    last_name: str

person = Person('taro', 'yamada')
# エラー！！
person.first_name = 'hogeo'
```

## ゲッターやセッターを活用しよう

Java などのプログラミング言語にはゲッターやセッターがありますが、 Python にもあります。
ゲッターやセッターを使うことには賛否両論があったりしますが、リファクタの 1 つの手段として理解しておくのが良いでしょう。

例えば、次のように姓名を半角スペースを空けて出力するコードがあります。

```python
from dataclasses import dataclass

@dataclass
class Person:
    first_name: str
    last_name: str

person = Person('Taro', 'Yamada')
print(f"{person.first_name} {person.last_name}")
```

コードの至るところで姓名を出力する場合、このコードだとしんどそうです。

Python では `property` デコレータを使ってゲッターを定義できます。

```python
from dataclasses import dataclass

@dataclass
class Person:
    first_name: str
    last_name: str

    @property
    def full_name(self) -> str:
        return f"{self.first_name} {self.last_name}"

person = Person('Taro', 'Yamada')
print(person.full_name)
```

`person.full_name` というプロパティアクセスの形で姓名を参照できるようになりました。
これであればコードの至るところで `f"{person.first_name} {person.last_name}"` を書かなくて済みそうです。

## データはプライベートにしよう

Python では「_」(アンダースコア)を使ってプライベートを表現します。
よくある例がクラス定義でしょう。

```python example.py
from dataclasses import dataclass

@dataclass
class Person:
    __first_name: str
    __last_name: str

    @property
    def full_name(self) -> str:
        return self.__first_name + self.__last_name

person = Person('Taro', 'Yamada')
print(person.full_name)
```

この例では `__first_name` と `__last_name` がプライベートなインスタンス変数と定義され、外部からは基本的には参照できなくなります。

クラスの例を挙げましたが、 **モジュールレベルでも「_」を使ってプライベート化できます** 。

先ほどの例では `person` が定義されていますが、実はこの変数は外部から参照できてしまいます。

```python
# 他の py ファイルから参照できる!!
from exmple import person

print(person.full_name)
```

**本来は外部には公開しないデータを、他ファイルから参照してデータを書き換えられてしまうとバグの元** になります。
また、 PyCharm などの **IDE で import する候補にサジェストされるのも鬱陶しい** です。

次のように `example.py` を修正することで他のモジュールに対し「参照するな」ということを明示化できます。

```python example.py
from dataclasses import dataclass

@dataclass
class Person:
    __first_name: str
    __last_name: str

    @property
    def full_name(self) -> str:
        return self.__first_name + self.__last_name

_person = Person('Taro', 'Yamada')
print(_person.full_name)
```

OSS でもたまにこのような「_」　の記述　が見られたりします。

https://github.com/PyCQA/flake8/blob/main/src/flake8/discover_files.py#L15

また、  **py ファイル名の先頭に「_」を使うことで、全体に公開せず特定のパッケージのみで使うことを明示する** やり方もあります。

例えば、 Python で有名な `requests` というライブラリでは `_internal_utils.py` というファイルが用意されています。
これは `requests` 内で使うことを明示し、 `requests` ライブラリのユーザー側に参照して欲しくないことを明示しています。

https://github.com/psf/requests/tree/main/requests

## 抽象クラスを活用しよう

Java といった言語には抽象クラスがありますが、 Python でも **`ABC` を使うことで抽象クラスを定義できます**。
抽象クラスを活用することで、継承した子クラスに対して実装を強制できます。

```python
from abc import ABC, abstractmethod
from typing import Type

class Animal(ABC):
    def __init__(self, name: str):
        self.name = name

    # 子クラスでメソッドの実装をしないとエラーになる
    @abstractmethod
    def talk(self) -> str:
        pass

class Cat(Animal):
    def talk(self) -> str:
        return f"吾輩は{self.name}である。"

class Dog(Animal):
    def talk(self) -> str:
        return f"{self.name}だワン！"

def get_random_animal(animal_name: str) -> Animal:
    return random.choice([Cat, Dog])(animal_name)

animal: Animal = get_random_animal("トム")
print(animal.talk())
```

## デコレータを定義しよう

「[抽象クラスを活用しよう](#抽象クラスを活用しよう)」で紹介した `abstractmethod` や「[ゲッターやセッターを活用しよう](#ゲッターやセッターを活用しよう)」で紹介した `property` のように、 Python には **関数やクラスを機能拡張するデコレータ** という機能があります。

共通化できるようなロジックがあれば、デコレータとして実装するのも 1 つの手でしょう。
次のコード例は、関数の実行時間を計測するようなデコレータです。

```python
import time

def check_perf(func):
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        end = time.time()
        print(f"{func.__name__} took {end - start} seconds to run.")
        return result

    return wrapper

@check_perf
def print_numbers_1():
    for i in range(100):
        print(i)

@check_perf
def print_numbers_2():
    for i in range(100):
        print(i)

print_numbers_1()
print_numbers_2()
```

## 特殊メソッドを活用しよう

`__init__` に代表されるように Python には特別なメソッドが用意されています。
この **特殊メソッドを実装することでより使いやすいオブジェクトを作ることができます** 。

例えば `__eq__` という特殊メソッドがあります。
これは `==` で比較した時に呼び出されるメソッドです。

次の例を見てください。

```python
class Person:
    def __init__(self, name: str, email: str):
        self.name = name
        self.email = email

person1 = Person('Taro Yamada', 'yamada@gmail.com')
person2 = Person('Taro Yamada', 'yamada@gmail.com')
print(person1 == person2)
# => False
```

`person1` と `person2` は同じ名前とメールアドレスの `Person` ですが、別の `Person` として判定されます。
同じ `Person` と判定するためには次のロジックに変更する必要があるでしょう。

```python
person1 = Person('Taro Yamada', 'yamada@gmail.com')
person2 = Person('Taro Yamada', 'yamada@gmail.com')
print(person1.name == person2.name and person1.email == person2.email)
# => True
```

ただし、同様のロジックを至るところで使うとなると実装がしんどそうです。
`__eq__` を使って次のようにリファクタします。

```python
class Person:
    def __init__(self, name: str, email: str):
        self.name = name
        self.email = email

    def __eq__(self, other):
        if not isinstance(other, Person):
            return False
        return self.name == other.name and self.email == other.email

person1 = Person('Taro Yamada', 'yamada@gmail.com')
person2 = Person('Taro Yamada', 'yamada@gmail.com')
print(person1 == person2)
# => True
```

このように特殊メソッド `__eq__` を使って `==` で期待通りのオブジェクト比較ができるようになりました。
他にも `__new__` や `__str__` など色々な特殊メソッドがあります。

https://docs.python.org/ja/3/reference/datamodel.html#special-method-names

## モダンなライブラリを使おう

時代は移りゆくものです。
「このライブラリがデファクタだ」と思っていたものが、いつの間にか代替のライブラリが出現していたりします。

例えば、 Python で HTTP リクエストを扱いやすくするライブラリとしては [`requests`](https://github.com/psf/requests/tree/main) が有名ですが、代替のライブラリとして `httpx` というのがあります。

https://www.python-httpx.org/

`httpx` は `requests` と同じようなインターフェースを保ちつつも、非同期処理や JavaScript の `axios` のようなクライアントインスタンス生成機能があったりします。

```python
import httpx

example_api_client = httpx.AsyncClient(base_url="https:/example.com/api")
try:
    await example_api_client.get('/users')
    await example_api_client.get('/articles')
finally:
    client.close()
```

## 非同期にしよう

Python は同期的に処理が動きます。
例えば、 HTTP リクエストが外部の API へ通信を走らせたときには、レスポンスが返ってくるまで他の処理を動かせません。

これはこれでデバッグしやすいなどのメリットはありますが、もし **処理速度を早めたいのであれば非同期化** するのが良いでしょう。

非同期化するには例えば `asyncio` を使った実装が挙げられるでしょう。
「モダンなライブラリを使おう」で紹介した `httpx` と組み合わせて HTTP リクエストの処理を非同期化できます。

```python
import asyncio
import httpx

async def main():
    async with httpx.AsyncClient() as client:
        response = await client.get('https://example.com')
        print(response)

asyncio.run(main())
```

## 型定義を書こう

TypeScript のように **Python も型定義をすることができます** 。

```python
def get_full_name(first_name: str, last_name: str) -> str:
    return f"{person.first_name} {person.last_name}"
```

Python における型定義は実行時には無視されるものの、型定義の恩恵を十分に受けることができます。

例えば、 PyCharm などの IDE は型定義を元に賢く警告を出してくれたりします。
![スクリーンショット 2023-06-15 13.30.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/164159/e251b4df-55fc-367a-38f7-251e9f1b1ef8.png)
`mypy` を使えば型定義のチェックも行うことができます。

https://mypy.readthedocs.io/en/stable/

また余談ですが、「[できるだけイミュータブルにしよう](#できるだけイミュータブルにしよう)」に関連して Python では定数は大文字で変数名を書くという慣習がありますが、型定義を使ってでも再代入不可能な変数を表すことができます。

```python
# 再代入不可
CONST_VAL: Final[int] = 1
```

## リンターを導入しよう

JavaScript の世界では `eslint` というのがありますが、 Python でも同様に `pylint` や `flake8` といった、コーディング規約に遵守したコードかをチェックするリンターが存在します。

JavaScript 界隈では `eslint` が主流ですが、 Python 界隈ではいくつか選択肢が存在します。

どのリンターを採用すべきかはプロジェクト毎で違うと思いますが、何か 1 つのリンターに合わせて一貫性を保ったコードを書くのが本質的でしょう。

## コードフォーマッタを導入しよう

JavaScript の世界では `prettier` というのがありますが、 Python でも同様に `black` や `autopep8` といったコードフォーマッタが存在します。

「リンターを導入しよう」でお話した内容と同じにはなりますが、何か 1 つのコードフォーマッタに合わせて一貫性を保ったコードを書くのが本質的です。

## パッケージマネージャの特性を理解しよう

Python プロジェクトのパッケージマネージャはいくつかありますが、代表的なのは `poetry` や `pipenv` でしょう。
この **パッケージマネージャの特性を理解することもリファクタリングに繋がります** 。

例えば `pipenv` では Python 実行時に `.env` ファイルを環境変数として自動で読み込んでくれる機能があります。

https://pipenv-ja.readthedocs.io/ja/translate-ja/advanced.html#automatic-loading-of-env

なので、あえて [`python-dotenv`](https://github.com/theskumar/python-dotenv) のようなライブラリを使って `.env` を読み込む実装をしなくても良かったりします。
（アプリケーションが `pipenv` での実行を前提とした作りになってしまいますが）

## 最後に

Python で具体的なリファクタに使えそうな例を挙げました。

この他にも「こういうリファクタテクニックがある」「こういう書き方の方が良い」といったことがあればコメントいただけると嬉しいです！

ここまでご覧いただきありがとうございました！ by [ぬこすけ](https://twitter.com/nuko_suke_dev)