こんにちは。本業フロントエンド副業パイソニスタのぬこすけです。
Python エンジニアの皆さん、元気にリファクタリングしていますか?
最近、昔作ったサイトで Python で書いたバックエンドのコードをリファクタリングしているのですが、負債まみれで吐きそうです😇
この記事では Python のコードをリファクタリングする際の参考になる例をいくつか紹介します。
ぜひ普段の Python での開発の参考になればと思います!
注意事項
- 対象読者は Python 初心者から中級者 を想定しています。
- Python のバージョンは記事執筆時点(2023/6/15)で最新の
3.11
を前提にお話します。 -
必ずしも紹介した方法が最適解とは限りません。というのも、プロジェクト毎で状況が違ったり、コードに対する思想というのも人それぞれだからです。
「こういう方法もありますよ!」などあればコメントいただけると嬉しいです!
list だけでなく tuple や set も使おう
Python で配列を扱う時はとりあえず list
を使えばなんとかなりますが、 特性に応じて tuple
や set
を使い分けると良い でしょう。
まず、 配列に対して特に変更を加えない場合は 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
だけでなく tuple
や set
も使い分ける のもリファクタの 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}
できるだけイミュータブルにしよう
データの書き換えが不可能な、イミュータブルなオブジェクトにすることで予期せぬバグを防ぐことができます。
普段の開発でサクッとイミュータブルに変更できるのであればリファクタしてみるのも良いでしょう。
例えば、次のようなリファクタ例が挙げられます。
-
list
をtuple
に -
set
をfronzenset
に -
dict
をMappingProxyType
に
それぞれ具体的なコードを紹介します。
list
を tuple
に
# 変更可
mutable_fruits = ['apple', 'banana', 'orange']
# 変更不可
immutable_fruits = ('apple', 'banana', 'orange',)
set
を fronzenset
に
# 変更可
mutable_fruits = {'apple', 'banana', 'orange'}
# 変更不可
immutable_fruits = frozenset(['apple', 'banana', 'orange'])
dict
を MappingProxyType
に
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 では「_」(アンダースコア)を使ってプライベートを表現します。
よくある例がクラス定義でしょう。
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
を修正することで他のモジュールに対し「参照するな」ということを明示化できます。
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
person1
と person2
は同じ名前とメールアドレスの 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
というのがあります。
httpx
は requests
と同じようなインターフェースを保ちつつも、非同期処理や 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 は型定義を元に賢く警告を出してくれたりします。
mypy
を使えば型定義のチェックも行うことができます。
また余談ですが、「できるだけイミュータブルにしよう」に関連して 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
ファイルを環境変数として自動で読み込んでくれる機能があります。
なので、あえて python-dotenv
のようなライブラリを使って .env
を読み込む実装をしなくても良かったりします。
(アプリケーションが pipenv
での実行を前提とした作りになってしまいますが)
最後に
Python で具体的なリファクタに使えそうな例を挙げました。
この他にも「こういうリファクタテクニックがある」「こういう書き方の方が良い」といったことがあればコメントいただけると嬉しいです!
ここまでご覧いただきありがとうございました! by ぬこすけ