「うわっ…私のPythonコード、汚すぎ…」とならないためのリーダブルコード

「うわっ…私のPythonコード、汚すぎ…」とならないためのリーダブルコード

他人のコードを読んでいて、ふとしたときに思います。

「なんか読みにくいんだよな」

もちろん私だって、最初から読みやすいコードを書いていたわけではないですし、おそらく昔は他人に「うわ、こいつのコード読みにくっ!」と思われていたかもしれません。

とはいえ、Pythonを書いていて数年もすると、やはり宗派やら慣れやらでかなり読みやすいコードを書くように工夫するようになってきます。

そんな私が独断と偏見で「こういうふうにコードを書けばもっと読みやすくなるのに」と思えるようなコードをPythonで解説します。

(音符とか書いてあるあの水色のリーダブルコードという本を読めばもっと「〜に基づいたコードの書き方!」とか書けるのかもしれませんが、あいにく、私は今後コードを書く機会が仕事の都合上減る可能性がかなり高いので、読むに至っておりません)

文字列操作を読みやすくする

文字列操作には大きく4つの派閥があります。

足し算派、パーセント派、format派、そしてfstring派。

Pythonの時代の流れからすると左から右にいくにつれて最新バージョンでしか動かないようなものになります。

以下これら4つの派閥を説明します。

最初これら4つすべてを覚えることは大変かもしれませんが、他人のコードを読んで「なるほど、こういう意味だったのか」ということを理解することは大事なので、だんだんと覚えてゆくと良いです。

足し算派

誰もが最初に通る道だと思います。

私も最初はこの足し算派の人間でした。

この派閥のメリットとしては、なんと言っても、初心者にとってわかりやすい。

例えばこれを見てください。

directory_name = '/Users/tester/Desktop/'
file_name = directory_name + 'myWorkSpace.txt'

このように、あるディレクトリー'/Users/tester/Desktop/'の中にあるファイルmyWorkSpace.txtを取り出すという処理がかなり「わかりやすく」書いてあるように見えます。

しかし、この派閥のデメリットとしては、なんと言っても慣れると読みにくい。

例えば、先ほどのディレクトリー配下に1.txtから9.txtというファイルがあるとします。そのファイルのすべてにアクセスをfor文でする場合、

directory_name = '/Users/tester/Desktop/'
for i in range(1, 10):
    file_name = directory_name + str(i) + '.txt'
    # ファイルを読み込む処理

慣れた者からしたら「わざわざ数字をstrに変えて足し算するなんて、めんどくさいったら!」となります。

この問題を解決してくれるのが、下の3つの派閥です。

パーセント派

C言語/C++に触れていれば少しはわかるかもしれません。

例えばこういった書き方が挙げられます。

print('this is a %s.' % 'sample') # 'this is a sample.'
print('%03d' % 25) # 025

意外にもこのような書き方はpipでインストールできるようなサードパーティーライブラリーの中身にもよく見られます。

この書き方を使うと、例えば001.txtから120.txtまでのファイルにアクセスしたい場合にも、

directory_name = '/Users/tester/Desktop'
for i in range(1, 121):
    file_name = '%s/%03d.txt' % (directory_name, i)
    # ファイルを読み込む処理

という書き方ができるようになります。

format派

formatというものを使えば、先ほどのパーセント派よりももう少しエレガントに書けます。

desktop_directory_name = '/Users/tester/Desktop'
for i in range(1, 121):
    file_name = '{directory_name}/{file_number:03}.txt'.format(
            directory_name = desktop_directory_name,
            file_number = i
        )
    '%s/%03d.txt' % (directory_name, i)
    # ファイルを読み込む処理

これも先ほどと同様にサードパーティーライブラリーの中身にもよく見られます。

fstring派

最近流行りの「文字列の中に埋め込んじゃえば良くない!?」というものです。

エディターによっては{}で囲まれた変数の部分の色をきれいにしてくれたりするので、読みにくさは特に感じないかもしれません。

また、もう少し細かく分けると、fstring派にはバージョンの違いをちゃんと考慮してくれる穏健派と最新バージョンこそ正義だと勘違いしている過激派の2種類がいます。

穏健派のコード例としては、

a = 309
print(f'a={a}'} # a=309
li = [301, 209, 501, 116]
print(f'sum of li is {sum(li)}.') # sum of li is 1127.
charms = 'I love Python.'
print(f'charms={charms}') # charms=I love Python.

そして過激派のコード例としては、

a = 309
print(f'{a=}'} # a=309
li = [301, 209, 501, 116]
print(f'{sum(li)=}') # sum(li)=1127
charms = 'I love Python.'
print(f'{charms=}') # charms='I love Python.'

この過激派のコードは3.8以降でなければ動きません。そのため古いバージョンを切り捨てることになります。

ちなみに私はこのfstring派の過激派に近いかもしれない人間です。

もちろん、クライアントとか上司とかに頼まれたら他の派閥でも書きますが、何も言われなかったら、基本的にこの過激な書き方をします。

関数で何が入力されて何が出力されるのか見やすくする

Pythonはわざわざ変数の型を与えなくても良いということが利点として挙げられていますが、逆に最近のトレンドでは「やっぱ、型が定まっていないと、何を入れて良いのかわからないし、結局ちゃんと型をチェックしないといけない」というものになっています。

個別変数の1つ1つにまで型を指定することはしなくても良いかもしれませんが、自分が作った関数くらいには型をアノテーションしておいたほうが後々の保守の観点からも楽だと思います。

def myadd(val1: int, val2: int) -> int:
    return val1 + val2

ただし、以下の

def mysum(li: list[int]) -> int:
    res = 0
    for val in li:
        res += val
    return res

のように新しすぎるコードを書くと動かなくなってしまうことがあるので、注意が必要です。

この場合はlistはsubscriptableではないというエラーが出てきます。

変数名の規則を決めておく

これはよく言われていることなので、あまり説明はしませんが、要は「あれ、これってintだったっけ?」とか「これってなんのための変数だったっけ?」とかとなるのを防ぐために重要です。

もちろんこれは「逆ハンガリアンなんてFワードだ」とか「スネークケースなんてCワードだ」とか宗教戦争が起きそうなものもたくさんあるのであまり多くは言えませんが笑、組織やプロジェクトによって策定された変数名の規則に従っておくのが無難だと思います

import re

num = 0
while True:
    num_str = input('0以上の整数を入力してください: ')
    if re.search('^[0-9]+$', num_str):
        num = int(num_str)
        break
    print('不正な入力がありました')

iterableなものはiterableにしておく

例えば、リストの中身を出力する際に、

li = [302, 102, 403, 109]
for ix in range(len(li)):
    print(li[ix])

と書いてあるよりも、

li = [302, 102, 403, 109]
for val in li:
    print(val)

と書いてあるほうが読みやすいです。

ただし、以下の

li = [302, 102, 403, 109]
for ix in range(len(li)):
    li[ix] = li[ix] * 2
    # li[ix]を使ったような何かしらの処理

のように要素の中身を完全に書き換えたい場合は、この限りではありません。

できるだけ仲間になりそうな要素はまとめる

さっきのiterableなものは〜のところで、「いやいや、そうは言ってもさ、複数のリストの同じインデックスの要素にアクセスしたいんだわ」となることがあるかもしれません。

が、これは要素の構築の仕方次第で解決できます。

li1 = [103, 325, 450, 670]
li2 = [341, 504, 220, 118]
# あまり良くない例
for ix in range(len(li1)):
    print(li1[ix], li2[ix])

こう書きたくなるかもしれませんが、

li1 = [103, 325, 450, 670]
li2 = [341, 504, 220, 118]
# より良い例
for val1, val2 in zip(li1, li2):
    print(val1, val2)

こう書いたほうが読みやすいです。

できるだけ機能が同じものはまとめる

例えば、「名前、値段、エネルギー」という要素を持つ「昼食」と「デザート」がある場合は、

class Lunch(object):
    def __init__(self, name: str, price: int, energy: int):
        self.name   = name
        self.price  = price
        self.energy = energy


class Dessert(object):
    def __init__(self, name: str, price: int, energy: int):
        self.name   = name
        self.price  = price
        self.energy = energy

と書くよりも、継承を使って

class Dish(object):
    def __init__(self, name: str, price: int, energy: int):
        self.name   = name
        self.price  = price
        self.energy = energy


class Lunch(Dish):
    pass


class Dessert(Dish):
    pass

と書いてしまったほうが、よりエレガントです。

もちろん、ここにクラス関数を含める場合も同様です。

importでfromが使える場合はできるだけfromを使う

まあ、これはステークホルダーの問題や、実際のライブラリーの仕様上の都合から、使えないこともあるので、一部ライブラリーに関してですが、

from time import sleep, time
from matplotlib import pyplot as plt

といったような書き方をすると読みやすくなると思います。

withが使えるものにはwithを使う

昔のPythonではwithというものがなかったため、少々面倒な書き方をしないといけないことがありましたが、今はそんなことはありません。

ファイルの読み込みは

fname = 'sample.txt'
with open(fname, mode='r') as f:
    f.read()

でするのが格段に楽ですし、

ウェブ上からhtmlを取り出すのは

import requests

url = 'https://example.com'
with requests.get(url) as req:
    print(req.text)

と書くほうが圧倒的に楽です。

とはいえインターネット上には過去の文献やサイトがかなりの数あるので、これらを賞味してゆくのはなかなかに慣れてゆく必要があると思います。

自分が作ったクラスの出力に責任を持つ

自作のクラスにより生成されたインスタンスにはどのような変数が含まれているのかということをわざわざ以下の

class Dish(object):
    def __init__(self, name: str, price: int, energy: int):
        self.name   = name
        self.price  = price
        self.energy = energy

dessert = Dish('プリンアラモード', 320, 400)
print(dessert.name)   # 'プリンアラモード'
print(dessert.price)  # 320
print(dessert.energy) # 400

のように書いて出力するのは大変です。

コードをミスったり、リファクタリングで発生する無駄な作業を減らすためにも、

class Dish(object):
    def __init__(self, name: str, price: int, energy: int):
        self.name   = name
        self.price  = price
        self.energy = energy
        
    def __str__(self):
        name   = self.name
        price  = self.price
        energy = self.energy
        return f'<{self.__class__.__name__}: {name=}, {price=}yen, {energy=}kcal>'


dessert = Dish('プリンアラモード', 320, 400)
print(dessert) # <Dish: name='プリンアラモード', price=320yen, energy=400kcal>

のように特殊メソッドを使って見やすくすることをおすすめします。

エラーが起きても起きなくても想定外の挙動が起きたらとりあえず出力する

これはほんの一例ですが、UNIX系で書いたものをWindowsに移行するときに発生するファイル名が/で取得されるのか\\で取得されるのか問題に付随して発生します。

例えば、あるディレクトリーsampleの中にsub1からsub8までのサブディレクトリーが入っており、さらにその中にそれぞれraw1.csvからraw5.csvというファイルが入っている場合、UNIX系で実行すると

import os

for fname in os.walk('./sample'):
    print(fname)
# ('./sample', ['sub1', 'sub6', 'sub8', 'sub7', 'sub5', 'sub2', 'sub3', 'sub4'], [])
# ('./sample/sub1', [], ['raw2.csv', 'raw3.csv', 'raw1.csv', 'raw4.csv', 'raw5.csv'])
# ('./sample/sub6', [], ['raw2.csv', 'raw3.csv', 'raw1.csv', 'raw4.csv', 'raw5.csv'])
# ('./sample/sub8', [], ['raw2.csv', 'raw3.csv', 'raw1.csv', 'raw4.csv', 'raw5.csv'])
# ('./sample/sub7', [], ['raw2.csv', 'raw3.csv', 'raw1.csv', 'raw4.csv', 'raw5.csv'])
# ('./sample/sub5', [], ['raw2.csv', 'raw3.csv', 'raw1.csv', 'raw4.csv', 'raw5.csv'])
# ('./sample/sub2', [], ['raw2.csv', 'raw3.csv', 'raw1.csv', 'raw4.csv', 'raw5.csv'])
# ('./sample/sub3', [], ['raw2.csv', 'raw3.csv', 'raw1.csv', 'raw4.csv', 'raw5.csv'])
# ('./sample/sub4', [], ['raw2.csv', 'raw3.csv', 'raw1.csv', 'raw4.csv', 'raw5.csv'])

と出力されますが、Windowsでは\\を含んだ別な文字列が出てきます。

これを/を用いたreplace()でディレクトリー名を書き換えようとすると、出力先が不明になってしまうことがあるかもしれません。

このようなとき、実際に動かすときにprint()を使うことで、「なぜこのコードが適切に動かないのか」ということを考察できます。

コメントは書く

当たり前すぎるので最初のほうで言うのを忘れていましたが、何をやっているのかということを必ずコメントに書きましょう。

基本的にコードを書くというのは1人ですることではありません。

(中にはLinusのように1人で世界的なOSを作ってしまう人もいるが)

多くのステークホルダーにとって、そして未来の自分にとってわかりやすい注釈を入れることは、その後の作業量に大きく影響します。

さいごに

コードも恋人もきれいなほうが良い。

Pythonを作ったGuido van Rossumもそう言ってました(言ってたような気がするのですが、日本語の文献では見つからなかった)

Pythonをそれなりに書けるようになったのは良いことだとは思いますが、もしその段階に進んだのなら、コードをきれいに書けるように心がけましょう。

きっと今よりもさまざまな人とより良い関係を築けるし、より良いプロダクトを作れるようになると思います。

なんでこのページを書いたのか

「私めっちゃコード書けると思うんだよね、たぶんうちの代でいちばんだと私は私自身で思う!」と言っていたある先輩のコードが文字列処理周りであまりにも汚すぎたということと加えて、「うちの代でいちばんコード書けるのお前じゃなくて俺だから」と言っていた同級生がfor文周りであまりにもひどい処理を書いていたからです。

あのさ、そう自負するのは良いし、そもそも私はコードなんてほとんど、いや全く書かないような仕事をするから何を言われても直接的に言い返す価値もないと思っているけど、そういうふうに言いたいならさ、もうちょっと読みやすいコードを書いてくれないか?

私は君たちや私自身よりも圧倒的にきれいなコードを書く人や、圧倒的にエレガントな処理をするコードを書く人を幾多と知っているんだから、君たちがマウントをとっているのが、運動会のかけっこで同級生に勝ち誇っている小学1年生くらいにかわいく見えるのよ。