Pythonの_(アンダースコア)の扱いまとめ

この記事では、Pythonでの_(アンダースコア)の扱いをまとめます。

Pythonでは、アンダースコアが関数やメソッドの前後に付与されていたり、変数として使用されています。慣例的な用法であったり、システム的な制約を付与する場合があるのですが、たまに忘れてしまってあれ何だっけとなる時があるので、備忘用にまとめていきます。

アンダースコアの位置

アンダースコアは色々な位置で使用されています。
アンダースコアが慣例的、システム的に使われている位置は以下の通りです。

アンダースコアの位置
アンダースコアのみ_
先頭に1つのアンダースコア_foo
末尾に1つのアンダースコアfoo_
先頭に2つのアンダースコア__foo
先頭と末尾に2つずつのアンダースコア__foo__
途中に1つ以上のアンダースコアfoo_bar_baz, 10_000_000

では、順に説明していきます。

アンダースコアのみの場合

アンダースコアのみは慣例的な用法で、必要のない値(または一時的な変数)の代入先として使用されます。

例えば、以下のようにfor文でインデックスへのアクセスが不要な場合や

for _ in range(47):
    print('Hello, world')

必要のない値や返り値の代入先として使われています。

temp_list = [0, 1, 2, 3]
a, b, _, _ = temp_list
print(a)
# 0
print(b)
# 1
print(_)
# 3

# 3次元座標を取得する関数でx軸だけ必要な場合
x, _, _ = get_coordinate(area)  

先頭に1つのアンダースコアの場合

先頭に1つのアンダースコアを付与する対象は、以下のパターンがあります。

  • クラス内の変数やメソッド:慣例的な意味合い
  • 関数:システム的な制約を受ける場合がある

クラス内の変数やメソッドの先頭にアンダースコアを1つ付与

クラス内の変数やメソッドの先頭にアンダースコアを1つ付与する場合は、慣例的な意味として、「クラス内でのみで参照・使用されるもの」を示します。つまり、クラス外からアクセスされることを意図されていない変数・メソッドということです。と言っても慣例的な意味合いなので、アクセス自体は可能ですので、あくまでも意思表示(注意喚起)というような形です。

class Test:
    def __init__(self):
        # _fooはクラス外から直接呼ばれることを意図していない
        self._foo = 0

    # barメソッドはクラス外から直接呼ばれることも意図している
    def bar()
        self._foo += 1
        return self._foo
    # _bazメソッドはクラス外から直接呼ばれることを意図していない
    def _baz()
        self._foo = 0
        return self._foo

t =  Test()
# 慣例的にアクセスしてほしくはないが、クラス外からアクセス可能
print(t._foo)
# 0

# クラス外からアクセスされることを想定されており、アクセス可能
print(t.bar())
# 1

# 慣例的にアクセスしてほしくはないが、クラス外からアクセス可能
print(t._baz())
# 0

関数の先頭にアンダースコアを一つ付与

関数の先頭にアンダースコアを一つ付与する場合は、システムな制約を受けます。関数が含まれるモジュールからワイルドカードでインポートする場合に、アンダースコアで始まる関数は読み込まれません。

# test_module.py

def foo():
    return 0
def _bar():
    return 1
from test_module import *

foo() # test_moduleからインポートされる
_bar() # _が先頭にあるためインポートされない

といってもワイルドカードによるインポートは、何をインポートしているか分からなくなる(名前空間が分からなくなる)ため、使用を推奨されていないので、この事例に当たることは少ないかもしれません。

末尾に1つのアンダースコアの場合

利用したい変数名がPythonの予約キーワード(class, del 等)と同じ場合に、末尾に1つのアンダースコアを付与することで、名前が衝突することに避けます。これも慣例的なアンダースコアの使い方です。

def foo(class_):
    print(class_)

なお、pythonの予約キーワードは以下で確認できます。

import keyword
print(keyword.kwlist)

# ['False', 'None', 'True', 'and', 'as', 'assert', 'async', 'await', 'break', 'class', 'continue', 'def', 'del', 'elif',  'else', 'except', 'finally', 'for', 'from', 'global', 'if', 'import', 'in', 'is', 'lambda', 'nonlocal', 'not', 'or', 'pass', 'raise', 'return', 'try', 'while', 'with', 'yield']

先頭に2つのアンダースコア

先頭に2つのアンダースコアを付与する対象は、クラスの変数やメソッドです。
上記の場合、対象に対してネームマングリング(Name Mangling)が適用されます。

ネームマングリング

ネームマングリングとは、クラス内の先頭に__がついた変数・メソッドに対して、その名前を「_クラス名」+変数名(or メソッド名)に変換します。
その結果、先頭に1つのアンダースコアの場合と違い、定義した変数名・メソッド名でクラス外からアクセスできなくなります。

class Test():
    def __init__(self, name):
        self.name = name
        self._name = name
        self.__name = name

t = Test('foo')

# クラス外からアクセス可能
a = t.name

# 慣例的にアクセスしてほしくはないが、クラス外からアクセス可能
b = t._name

# クラス外からアクセス不可(そもそも存在しない)
c = t.__name

# マングリング後の名前ならばクラス外からアクセス可能
d = t._Test__name

つまり、ネームマングリングが実施されることで、擬似的にではありますが、プライベート変数及びメソッドのような挙動となります。
もちろん、クラス内からであれば、マングリングされる前の名前でアクセス可能です。

マングリングの注意点

マングリングは便利ですが、気をつけなればいけないこともあります。それは継承時の挙動です。

マングリングされた変数の継承時の挙動

以下のように、継承元クラスでマングリングされた変数に継承先でアクセスしようとすると、エラーになります。これは継承元で'アリス'という文字列が_Base__nameに代入され
ますが、継承先では、_Child__nameを参照しているため、発生します。

class Base():
    def __init__(self):
        self.__name = 'アリス'

# Baseクラスを継承
class Child(Base):
    def print_name():
        print(self.__name)

c = Child()
c.print_name()
# AttributeError: 'Child' object has no attribute '_Child__name'    

マングリングされたメソッドの継承時の挙動

以下のスクリプトは__get_titleメソッドで取得されたタイトル名をprint_titleメソッドで出力しています。Childクラスで__get_titleをオーバライドしているので、オーバライド後のメソッドが呼ばれることを期待しています。
ですが、__get_title メソッドはマングリングされているおり、そもそもBaseクラスのprint(self.__get_title())で参照されている__get_titleの実態が_Base__get_titleのため、期待した挙動になりません。

class Base():
    def print_title(self):
        print(self.__get_title()) # _Base__get_titleの返り値を出力している

    def __get_title(self): # _Base__get_titleを定義している
        return '不思議の国のアリス'

# Baseクラスを継承
class Child(Base):
    def __get_title(self): # _Child__get_titleを定義している
        return '鏡の国のアリス'

c = Child()
# 以下は'鏡の国のアリス'を出力してほしいが、実際にはBaseクラスの'不思議の国のアリス'を出力してしまう
c.print_title()
# 不思議の国のアリス

先頭と末尾に2つずつのアンダースコア

先頭と末尾に2つずつのアンダースコアがある場合は、pythonで特別な用途で使用するためのマジックメソッドを意味します。なお、マングリングの対象外です。
__init____len__などがありますが、既にオブジェクトに対する用途が決まっていますので、用途を理解せずに定義や上書きすることはやめたほうが良いです。
dir関数でオブジェクトの属性が分かるので、オブジェクトごとにどんなマジックメソッドがあるか確認できます。

foo = 1
dir(foo)
# ['__abs__', '__add__', '__and__', '__bool__', '__ceil__', '__class__', '__delattr__', '__dir__', '__divmod__', '__doc__', '__eq__', '__float__', '__floor__', '__floordiv__', '__format__', '__ge__', '__getattribute__', '__getnewargs__', '__gt__', '__hash__', '__index__', '__init__', '__init_subclass__', '__int__', '__invert__', '__le__', '__lshift__', '__lt__', '__mod__', '__mul__', '__ne__', '__neg__', '__new__', '__or__', '__pos__', '__pow__', '__radd__', '__rand__', '__rdivmod__', '__reduce__', '__reduce_ex__', '__repr__', '__rfloordiv__', '__rlshift__', '__rmod__', '__rmul__', '__ror__', '__round__', '__rpow__', '__rrshift__', '__rshift__', '__rsub__', '__rtruediv__', '__rxor__', '__setattr__', '__sizeof__', '__str__', '__sub__', '__subclasshook__', '__truediv__', '__trunc__', '__xor__', 'as_integer_ratio', 'bit_length', 'conjugate', 'denominator', 'from_bytes', 'imag', 'numerator', 'real', 'to_bytes']

途中に1つ以上のアンダースコア

数字を見やすくする場合や関数名やメソッド名で単語を接続する場合に使います。

# 3桁ずつ見やすいように区切る
value = 100_000_000
print(value, type(value))
# 100000000, <class 'int'>

# 関数名の単語を_で繋ぐ
def get_final_params():
    pass

終わりに

今回はPythonでのアンダースコアの扱いについてまとめました。理解すると便利な使い方や分かりやすいコードを書くことができるので、定期的に復習しながら、使いこなせていければなと思います。

PythonPython

Posted by mako