「うわっ 私の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幎生くらいにかわいく芋えるのよ。