20
30

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

お題は不問!Qiita Engineer Festa 2023で記事投稿!

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

Last updated at Posted at 2023-06-15

こんにちは。本業フロントエンド副業パイソニスタのぬこすけです。

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

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

注意事項

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

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

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

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

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

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

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

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

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

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

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

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

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

for fruit in fruits:
    print(fruit)

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

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

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

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

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

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

  • listtuple
  • setfronzenset
  • dictMappingProxyType

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

listtuple

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

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

setfronzenset

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

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

dictMappingProxyType

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 というデコレータが使えます。

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

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

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

from dataclasses import dataclass

@dataclass
class Person:
    first_name: str
    last_name: str

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

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

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 つの手段として理解しておくのが良いでしょう。

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

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 デコレータを使ってゲッターを定義できます。

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 が定義されていますが、実はこの変数は外部から参照できてしまいます。

# 他の 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 でもたまにこのような「_」 の記述 が見られたりします。

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

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

抽象クラスを活用しよう

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

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 つの手でしょう。
次のコード例は、関数の実行時間を計測するようなデコレータです。

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__ という特殊メソッドがあります。
これは == で比較した時に呼び出されるメソッドです。

次の例を見てください。

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

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

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__ を使って次のようにリファクタします。

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__ など色々な特殊メソッドがあります。

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

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

例えば、 Python で HTTP リクエストを扱いやすくするライブラリとしては requests が有名ですが、代替のライブラリとして httpx というのがあります。

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

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 リクエストの処理を非同期化できます。

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 も型定義をすることができます

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
mypy を使えば型定義のチェックも行うことができます。

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

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

リンターを導入しよう

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

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

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

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

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

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

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

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

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

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

最後に

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

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

ここまでご覧いただきありがとうございました! by ぬこすけ

20
30
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
20
30

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?