🏷️

識別子(変数名や関数名など)の命名ガイドライン

2022/01/14に公開約8,900字2件のコメント

識別子とは、変数名や関数名などの、プログラム中に現れる名前のことです。適切な識別子を考案することは、熟練者でも難しいものです。

この記事では、プログラミング言語の文法を一通り学んだ初学者に向け、識別子の命名に関する暗黙的なルールを体系化した資料を提供することを試みます。筆者の主観に基づいて書かれていますので、多少の誤りが含まれている可能性があります。お気付きの際はぜひご指摘ください。

サンプルコードはPythonで記述します。下に示す記号を用いてサンプルコードの望ましさを記載しています。

記号 意味
望ましいです
⚠️ 代案が存在するのであれば避けるべきです
🚫 避けるべきです

共通事項

そもそも、「よい識別子の命名」とは一体何なのでしょうか。これについては明らかで、「素早くコードの意図を把握することができる命名」となります。また、プログラミング言語は自然言語である英語をベースに設計されています。このため、これから書き連ねていくルールは、すべて「プログラムを英文として読むことができるか」を意識しています。

表記方法のルールを確認する

多くのプログラミング言語では、識別子はその種類によって標準的な命名規則が決まっています。通常、その識別子の利用方法に応じて、下の5種類の中から選んで使用することになります。

  • キャメルケース (camelCase)
  • パスカルケース(PascalCase)
  • 小文字のスネークケース(snake_case)
  • 大文字のスネークケース(SNAKE_CASE)
  • ケバブケース(kebab-case)
# 🚫 クラス名がスネークケースで、メソッド名がパスカルケースで記述されています
class sample_code:
  def Format(self):
    pass

# ✅ クラス名はパスカルケース、メソッド名はスネークケースで記述しましょう
class SampleCode:
  def format(self):
    pass

英文法を意識する

単語の選び方や語順、スペルが誤っていませんか。知らない単語がある場合は辞書を引くようにしましょう。直訳が正しいとは限りません。

# 🚫 registという動詞は存在しません
def regist_user():
  pass

# ✅ registerを使用しましょう
def register_user():
  pass

略語は用いない

現代のIDEでは、長い名前も補完機能を使用すれば簡単に入力できます。可能な限り略語は用いず、正式な名前を使用するようにしましょう。一般的な英和辞典に略称が登録されている程度の略語であれば使用しても問題ありません。

# ⚠️ errやmsgは何を表しているのでしょうか
err_msg = "エラーです"

# ✅ 単語は省略せずに最後まで記述しましょう
error_message = "エラーです"

ただし、チーム内で使用する略語に関して共通のルールがあるのであればこの限りではありません。

過不足がないか確認する

識別子は、その定義を確認せずともコードの意味が理解できるように命名する必要があります。

calculate_total_score は「合計得点を計算する」関数に見えますが、実装を見ると output.txt に合計得点を出力しています。このままでは、関数の使用者が意図せずファイルを書き換えてしまうかもしれません。export_total_score のように、挙動を過不足なく説明できるようにしましょう。

# 🚫 計算しているだけでしょうか?
def calculate_total_score(subjects):
  total_score = sum(subject.score for subject in subjects)
  with open("output.txt", "w") as f:
    f.write(total_score)

# ✅ ファイルの出力まで行っていることを明示しましょう
def export_total_score(subjects):
  total_score = sum(subject.score for subject in subjects)
  with open("output.txt", "w") as f:
    f.write(total_score)

ただし、利用者から隠蔽されている挙動まで説明する必要はありません。次の例で、 squared 関数は新しい値が入力されると cache に計算結果をキャッシュしておきますが、利用者側はキャッシュの存在を意識することなく squared を利用できるので、キャッシュの存在を命名によって明示する必要はありません。

cache = dict()

# ✅ キャッシュに書き込まれていることは利用者が意識する必要はありません
def squared(n):
  if not n in cache:
    cache[n] = n ** 2
  return cache[n]

同じ語は同じ意味で使う

次の例において、load_file 関数は location 引数が https:// から始まっていればインターネット上からデータを取得し、そうでなければローカルファイルを開いて File クラスのコンストラクタに渡します。次のコードの問題は、load_file という関数名に含まれる file と、クラス名である File が別のものを表しているという点です。

ネットワークリソースとローカルファイルをあわせて Resource と呼ぶことにより、この問題を回避することができました。

# 🚫 Fileとは何でしょう?
def load_file(location):
  if location.startswith("https://"):
    return requests.get(location)
  else:
    return File(open(location, "r"))

# ✅ FileとResourceのように区別しました
def load_resource(location):
  if location.startswith("https://"):
    return requests.get(location)
  else:
    return File(open(location, "r"))

コンテキストに含まれる語は名称に含める必要がない

ある識別子がスコープをつくるとき、そのスコープ内の識別子の名称はスコープの名称を含める必要はありません。

# ⚠️ resource_nameは冗長です
class Resource:
  resource_name: str

# ✅ nameフィールドはResourceクラスのメンバなので、Resourceのものであることは明らかです
class Resource:
  name: str

リスト構造はすべての要素が同格であるときにのみ用いる

タプルは複数の値を手軽に管理できますが、要素へのアクセスの際にインデックスの利用を強要されるため、可読性が著しく下がります。

# ⚠️ タプルの要素番号が意味を持ってしまっています
tanaka = "田中", True, 30
print(tanaka[2])

# ✅ 値にはきちんと名前を与えましょう
@dataclass
class Person:
  name: str
  is_male: bool
  age: int

tanaka = Person(name="田中", is_male=True, age=30)
print(tanaka.age)

ただし、座標を表すベクトルなど、要素同士の位置関係自体に意味がある場合はこの限りではありません。

ルールの厳密さはスコープの大きさに比例する

識別子のスコープが大きく、長生きであるほど、利用者としてはその定義を確認することが難しくなるため、より厳密な名づけが必要です。ファイルをまたいで使用される可能性のある識別子は、特に配慮して命名する必要があるでしょう。

# ✅ ループ内でしか使用されない変数に短い名前を割り当てました
for i in range(len(projects)):
  print(projects[i])

# ✅ スコープが大きくなる場合は情報量を増やしましょう
for current_project_index in range(len(projects)):
  do_something()
  do_something()
  do_something()
  do_something()
  do_something()
  do_something()
  print(projects[current_project_index])

# ✅ 複雑な式を用いる場合は一時変数の利用も有効です
sphere_radius = 5
height = 10

r = sphere_radius
h = height
volume = math.pi * r**2 * h / 3

変数・プロパティ

名詞句が基本

ほとんどの変数やプロパティの名前は、英文中でそのまま名詞句として使用できる形になっています。特に、形容詞の修飾・被修飾の関係は間違いを犯しやすいので、重点的にチェックしましょう。

# 🚫 createが動詞になっています
create_user = User(name="Taro", age=30)

# ✅ 受け身の形にしました
created_user = User(name="Taro", age=30)

名詞で終わる場合が多い

前項で説明したように、多くの変数やプロパティの名前は、名詞句であるのと同時に、その名詞句の核となる名詞で終わるように命名されています。修飾語が長くなりすぎる場合は後置する場合もありますが、通常は避けるようにしましょう。変数の切り出し方が適切でない可能性があります。

# 🚫 前置詞句が入っており冗長です
name_of_user = "Taro"

# ✅ 核となる名詞が変数名の末尾に来ました
user_name = "Taro"

# ⚠️ 修飾語が長すぎる場合は後置する場合もありますが、避けるほうが無難です
user_created_when_application_started = User("Taro")

反復可能(iterable)なオブジェクトには単数を表す名詞の複数形を用いる

iterableなオブジェクト(Pythonでは for in 構文の in に指定可能なオブジェクト)には、反復される各要素を表す名前の複数形を用います。ただし、key-value 構造を意識するオブジェクトにも複数形が用いられる場合があります。

# 🚫 user_name はリストなので、複数形にすることが望ましいです
user_name = ["Taro", "Hanako"]

# ✅ 複数形にしました
user_names = ["Taro", "Hanako"]

# 🚫 Membersクラス自体はiterableではありません
@dataclass
class Members:
  names: List[str]
  addresses: List[str]

# ✅ 単数形にしました
@dataclass
class Group:
  member_names: List[str]
  member_addresses: List[str]

# ⚠️ dictが直接実装しているイテレータは辞書のキーに対しての反復ですが、この形式もよく見られます
http_headers = {
  "Content-Type": "application/json",
  "Authorization": "Bearer Token",
}

前置詞で終わることもある

前置詞で終わる名称を使用することもあります。この場合は、入っている値をその識別子に続けて記述したとき、ちょうど句として成り立つように命名します。この記法は、多くの場合「過去分詞+前置詞」の形で用いられます。

# ✅ 前置詞で終わる場合もあります
class User:
  created_at: datetime
  last_updated_at: datetime

class Room:
  used_by: User

真理値型の場合

そもそも真理値型というのは、英文法で考えてみれば Yes/No で答える疑問文の回答に相当する概念です。このため、命名においても Yes/No 疑問文を意識する必要があります。

is~ や can~ の形が多い

is や can から始まる形が最も一般的で、「きれいな」コードになります。これは、「if 主語」に続けて識別子を記述した際に自然な英文になるからなのですが、世の中のプログラマが is や can が先頭にある形に慣れすぎたためか、主語を明示する場合は敢えて英文法と異なる「is S C」「can S V」といった形をとることが多かったりします。

flag (もっと悪質なコードだと flg)という接尾辞がついているコードを見かけることがありますが、真偽を逆に解釈されかねないので使用すべきではありません。

# ✅ is~、can~の形がきれいです
class User:
  is_active: bool  # Userは主語です
  can_write_articles: bool

# ✅ この命名に従うとif文が非常に読みやすいです
if current_user.is_active:
  pass

# ✅ 主語を明示する場合は is/can + S の語順になります (逆も見かけますがこちらのほうが一般的です)
is_user_active = True
can_user_write_articles = False

# ✅ should、didも見かけます
class Article:
  should_update: bool  # Articleは目的語です
  did_update: bool

# ⚠️ その他はあまり用いられません
are_all_users_active = True
have_checked = False

# 🚫 Trueはアクティブを表すのか非アクティブを表すのかが曖昧です
active_flag = True

動詞の場合は三人称単数現在形、もしくは原形

状態を表す動詞の場合は三人称単数現在形、それ以外の場合は原形を用いることが多いです。主語を明示する場合は先頭に置く素直な語順となります。

# ✅ 状態動詞を用いる場合は三人称単数形を用いられる場合が多いです
class Employee:
  exists: bool
  has_clients: bool

# ✅ この命名に従うとif文が非常に読みやすいです
if employee.exists:
  pass

# ✅ 状態を表す動詞以外は原形で用いられる場合が多いです
class HttpClient:
  use_proxy: bool
  throw_on_failure: bool

# ✅ 主語を明示する場合は S + V の語順になります
user_exists = True
employee_has_clients = False

否定形を使用しない

notを使って否定形を作ることは避け、反対の意味を持つ単語を探すか、入れる値を逆転させましょう。

# 🚫 形容詞を否定形では使わないようにしましょう
is_not_active = False

# ⚠️ 動詞を使う場合も避けたほうが無難です
do_not_use_proxy = False

# ✅ 入れる値を逆転させました
is_active = True

# ✅ 反対の意味を持つ単語を使いました
is_disabled = False
is_inactive = False

関数・メソッド

動詞で始める

ほとんどの関数やメソッドは、動詞から始まります。副詞句が必要な場合は、動詞より後ろ側に記述するようにしましょう。

# 🚫 動詞が入っていません
def user_creation():
  pass

# ⚠️ 動詞で始まっていません
def securely_send_message():
  pass

# ✅ 動詞で始まっています
def create_user():
  pass

def send_message_securely():
  pass

メソッドのレシーバーオブジェクトは主語または目的語になる

メソッドの呼び出しは、レシーバーとなるオブジェクトに対してはたらきかける操作です。このため、メソッド名の主語か目的語のいずれかがレシーバーオブジェクトとなっていることが望ましいです。なお、このルールはbool型の変数やプロパティにも有効です。

# 🚫 read_csv_fileはUserのメソッドである必要がありますか?
class User:
  def read_csv_file(self):
    pass

# ✅ Userはcreateの目的語、change_passwordの主語です
class User:
  def create():
    return User()
  def change_password(self):
    pass

引数が目的語になっている場合がある

引数が関数やメソッドの名前の目的語となる形は、英文としての読みやすさが増すため、しばしば用いられます。

# ✅ メソッドの目的語が引数になっていると、利用する際の英文がより自然な形になります
class User:
  def equals(self, other: User):
    return self.name == other.name and self.password == other.password
  def take_part_in(self, tour: Tour):
    pass

if user1.equals(user2):
  pass

user1.take_part_in(tour)

副作用を伴わない値を返す関数は、プロパティと同じ命名をしてもよい

値を計算して返すことのみを目的とする関数やメソッドは、動詞を含まなくても構いません。

# ✅ full_nameはオブジェクトの状態から値を計算して返すだけのメソッドです
class User:
  def full_name(self):
    return f"{self.first_name} {self.last_name}"

# ⚠️ 副作用を伴う場合は動詞を用いるべきです
def weather(date):
  return requests.get(f"https://weather.example.com/{date}").json()

# ✅ 動詞を加えました
def get_weather(date):
  return requests.get(f"https://weather.example.com/{date}").json()

おわりに

冒頭にも述べたように、識別子の命名は、すべてのプログラマにとって永遠の課題です。この記事が、もし読者の方のお悩みを解決できたのであれば筆者としては嬉しい限りです。

ちなみに、識別子の命名を考える際は GitHub の検索機能が非常に便利です。検索キーワード欄に識別子を入力して、「Code」の欄の検索結果(ログインしていないと表示されません)のヒット件数が、その命名が本当に正しいかどうかのおおよその指標になります。

userExists vs existsUser

Discussion

これは識別子についての記事ではないのではないか?

https://zenn.dev/koyakei/articles/09cc2138952b5c

コメントありがとうございます!
リンク先の記事の話題も、本記事の話題もどちらも識別子という認識で問題ないと思います。
本記事において識別子 (identifier) は、プログラムの構文木のノードとしての識別子という意味で用いています。少々混乱を招く表記になってしまっているかもしれません💦

ログインするとコメントできます