387
284

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

なぜPythonにはsetter/getterメソッドがないのか?

Last updated at Posted at 2021-03-23

概要

JavaやC++などの他のオブジェクト指向言語ではsetter/getterメソッドの使用が推奨されているが、Pythonには存在しない。
もともとJavaやC++に触れていた僕には「なぜPythonにはsetter/getterメソッドがないのか?」がすごく疑問だった。
さらに、Javaなどの言語の資料では
クラス内部の変数はprivateにして隠蔽せよ
と書いてあるが、逆にPythonの資料では
privateはなるべく使わずpublicを使え
と書いてあることが多い。
矛盾しているのでは...?

気になったのでいろいろ調べて見た結果、そこには2つの理由があることがわかった。
さらに、Pythonでsetter/getterを使いたい場合の1つの代替案があるということもわかった。
この記事では

  • オブジェクト指向とカプセル化について
  • Pythonにsetter/getterメソッドが存在しない2つの理由
  • setter/getterメソッドの代替案

の順に説明する。
最初のオブジェクト指向についての説明については基礎的な内容なので、「そんなの知ってるぜ!」という人はすっ飛ばしてしまって大丈夫。

オブジェクト指向とクラス

最近の言語ではオブジェクト指向言語と呼ばれるものが多く、クラスという概念がある。
Pythonで適当なクラスの例に考える。
以下のようなポケモンのクラスがあって名前と体重を設定可能とする。

# クラスの定義
class Pokemon:
    def __init__(self, name='フシギダネ', weight=7.0):
        self.name = name
        self.weight = weight

# 1匹目 (デフォルトの名前)
pokemon = Pokemon()
print("このポケモンの名前は", pokemon.name, "だよ")
print("重さは", pokemon.weight, " kgだよ")

# 2匹目 (名前と体重を後から代入)
pokemon2 = Pokemon()
pokemon2.name = 'ピカチュウ'
pokemon2.weight = 6.0
print("2匹目のポケモンの名前は", pokemon2.name, "だよ")
print("重さは", pokemon2.weight, " kgだよ")

# 3匹目(コンストラクタでset)
pokemon3 = Pokemon('ポッポ',10)
print("3匹目のポケモンの名前は", pokemon3.name, "だよ")
print("重さは", pokemon3.weight, " kgだよ")
出力
このポケモンの名前は フシギダネ だよ
重さは 7.0  kgだよ
2匹目のポケモンの名前は ピカチュウ だよ
重さは 6.0  kgだよ
3匹目のポケモンの名前は ポッポ だよ
重さは 10  kgだよ

Pokemon()クラスから具体的に3匹のポケモンのオブジェクト (Pythonではインスタンスと言う)を生成している。

__init__がコンストラクタの部分。
この中でデフォルトの名前をフシギダネにしてるので、特に名前を指定しなければフシギダネになる。
2匹目は名前と体重を代入してるのでピカチュウになっている。
3匹目のようにコンストラクタの引数で直接設定することもできる。

しかし、このような書き方はいろいろと問題がある。
例えば、クラスの変数に外からいくらでもアクセス可能なため、体重を変な値に書き換えることも簡単にできちゃう

pokemon2 = Pokemon()
pokemon2.name = 'ピカチュウ'
pokemon2.weight = '6.0'
pokemon2.weight = pokemon2.weight -9999 

print("重さは", pokemon2.weight, " kgだよ")
# 出力: 重さは -9993.0  kgだよ

そこでsettergetterメソッドを使ったカプセル化の概念が出てくる。

カプセル化とは

コードを使わずに少し抽象的な話をする。
例えば僕らが洗濯機を使う時を考える。
僕らは「スタート」のボタンを押せば洗濯が始まるということは知ってるけど、中身の配線や細かい原理がどうなってるかはよく知らない。
というか、知る必要がない。
ボタンを押す」という僕らの動作と洗濯機内部の「洗濯する」という機能は完全に分離されているということだ。
実はこれはとても大事なことだ。
もし新しいバージョンの洗濯機が出てきて内部の構造は変わっても、僕たちはそれと関係なくいつものように「スタート」ボタンを押せばいい。
もし洗濯機の内部の配線が剥き出しになっていて中身を触り放題になっていたらとても危険だ。
間違った使い方をしてケガをしてしまうかもしれないし、中身の構造が変わるたびにユーザーも直接影響を受けることになってしまうからだ。

このように、必要な機能を1つにまとめた上で内部を隠蔽して外から見えなくすることをカプセル化という。
カプセル化はオブジェクト指向の大事な概念の一つだ。

ユーザーは正しい(限定された)使い方でしか内部の機能、データにはアクセスできないようにすることが望ましい。
洗濯機の例だと「スタートボタンを押す」以外の方法で勝手に洗濯機が起動しないようにしなければいけない。

これをクラスで考えると、クラス内部の変数は外からは隠蔽されていて、決められた方法でしかアクセスできないのが理想だ。
そしてこれを「決められた方法」を実現するのがsetter/getterメソッドだ。

↓ちなみに、昔ツイッターでバズってた似たような例
(厳密にはこの考え方=オブジェクト指向ではないというツッコミはある)

(ここでのカプセル化の説明の仕方についてコメントをもらったので最後に追記してます)

setterとgetterの役割

Javaなどの他のオブジェクト指向言語ではクラス内部の変数を隠蔽して「setter/getterメソッドを使う」のが一般的だ。
Pythonで書くとこんな感じになる。

class Pokemon:
    def __init__(self, name='フシギダネ', weight=7.0):
        self.__name = name
        self.__weight = weight

    def set_name(self, name):
        self.__name = name

    def set_weight(self, weight):
        self.__weight = weight

    def get_name(self):
        return self.__name

    def get_weight(self):
        if self.__weight <= 0:
                raise ValueError(f'体重がマイナスになっちゃってるよぉ(>_<)')
        return self.__weight

pokemon = Pokemon()
pokemon.set_name('ピカチュウ')
pokemon.set_weight(6.0)

print("このポケモンの名前は", pokemon.get_name(), "だよ")
print("重さは", pokemon.get_weight(), " kgだよ")

print("重さを不正な値に変更しちゃうぞ!")
pokemon.set_weight(-999)
print("重さは", pokemon.get_weight(), " kgだよ")
出力
このポケモンの名前は ピカチュウ だよ
重さは 6.0  kgだよ
重さを不正な値に変更しちゃうぞ!
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-40-7exxxxxxxxxx> in <module>
     27 print("重さを不正な値に変更しちゃうぞ!")
     28 pokemon.set_weight(-999)
---> 29 print("重さは", pokemon.get_weight(), " kgだよ")

<ipython-input-40-7exxxxxxxxxx> in get_weight(self)
     15     def get_weight(self):
     16         if self._weight <= 0:
---> 17                 raise ValueError(f'体重がマイナスになっちゃってるよぉ(>_<)')
     18         return self._weight
     19 

ValueError: 体重がマイナスになっちゃってるよぉ(>_<)

まず、クラス変数をpublicではなくprivateにして外部から隠蔽する。
これで先ほどの例のように直接pokemon.nameに値を代入することができなくなる。
ちなみにPythonでは変数の頭に__をつけることで変数をprivateにすることができる
これらの変数にアクセスするためにはクラスメソッドであるset_name()get_name()などを使う必要がある。
set_name()で名前をセットし、get_name()で値を取得する。
そして、get_weight()の中で不正な値をチェックできるようにする。

これによりクラス内部の変数のカプセル化に成功し、setter/getterメソッドを使ってのみアクセスできるようになった。
めでたしめでたし。

ではなかった。

Pythonでこの書き方が推奨されないのには2つの理由がある。

理由1:Pythonicでないから

Pythonic(パイソニック)とはPythonでコードを書く際の考え方、指針のことだ。
『Effective Python』という書籍によると

Pythonプログラマは、明示すること、複雑さよりは単純さを選ぶこと、可読性を最大化することを好みます

とある。要は「Pythonを書くなら読みやすくて単純なコードを書こうぜ!」ってことですね。
setter/getterを使った書き方は確かに安全に見えるけど、変数を取得するたびに毎回

pokemon.get_name()

みたいに書くのめんどくさいし読みづらいよねってことだ。
確かに最初に紹介した

pokemon.name

という書き方の方が簡単だし直感的だ。
最近ではIDEを使えばsetter/getterを勝手に補完してくれたりはするものの、やはりコードの量が多くなってしまうことには変わらない。
Pythonを書く人(Pythonistaとか呼ばれる)からすると
こんな書き方はけしからん
ということだ。

この考え方は「privateよりもpublicを使え」という言葉にも当てはまる。
privateで制約を与えるんじゃなくて、publicで自由にやろうよ。みんな大人なんだから。」
といった感じだ。
ちなみにEffective Pythonによるとこの「大人なんだから」というのはPythonのモットーらしい。
PythonにはPEP8というコードを書く際のスタイルガイドがあり、それを守ってみんなが行儀良くコード書けば
厳しい制約なんていらないよね?という考えがベースにある。

PEP8では具体的に
インデントの時はスペース4個分あけよう
一行は79文字より長くなりすぎないようにしよう
みたいな決まり事がまとめられている。
https://pep8-ja.readthedocs.io/ja/latest/

個人的にはこのような考え方の違いは「校風」みたいなものだと思っている。
例えば偏差値の低い学校の校風は厳しい
制服の着こなしにルールがあったり髪を染めるのが禁止だったりする。
これらの厳しいルールがあるおかげで不良が暴れ回ったりしづらくなり治安が保たれるが、
ちゃんとルールを守ってる優等生にとっては息苦しい。

逆に進学校の校風はかなり自由で緩かったりする。
進学校の生徒はみんなルールを守るので私服OKだったり髪も自由に染めれたりする。

個人的には前者がJava、後者がPythonなどの言語のスタイルで、Pythonの**"自由な校風"を一言
で表したのが
Pythonic**なのではないかなと思う。
(一応誤解のないように言っておくと、どちらが優れているということを言いたいわけではない。)

理由2:Pythonで完全なカプセル化は実現できないから

理由1での言語ごとのスタイルの違いは校風の違いのようなものだという話をした。
とはいえ進学校の生徒にも変わった奴がいて

自由な校風なんて知らねぇ!俺は明日から毎日制服で登校するんだ!

とか言い出す人がいるかもしれない。
その場合きっとクラスメイトはこう言うだろう。

そもそもうち制服ないけど何着るつもりなの?スーツ?それって厳密には制服じゃないから意味なくない?
と。
この例えがどれだけ的を射ているかはわからないが、実はPythonでsetter/getterメソッドを使っても
Javaのような完全なカプセル化を実現することはできない
これが2つ目の理由だ。

PythonとJavaで比較してみる。
Javaにはpublic, private, protectedなど様々なアクセス演算子がある。

  • public: どこからでもアクセス可能
  • private: 同じクラスからのみアクセス可能
  • protected: クラスが違ってもサブクラス(継承したクラス)からならアクセス可能
  • デフォルト(アクセス演算子なし): 同じクラスのパッケージからはアクセス可能(= package private

一方でPythonにはprivatepublicの2種類しかない。
ではPythonのprivate演算子でどれくらい隠蔽が可能なのか確かめてみる。
以下のようにPokemon()クラスの上位クラスとしてIkimono()クラスを考える。
そしてprivateなクラス変数である__nameIkimono()クラス側で定義し、get_name()Pokemon()
側で定義する。
この時以下のコードはどのような挙動を示すだろうか?
上位クラスでprivateで定義した変数なので、それを継承したPokemon()クラスからはアクセスできないようになっているはずだ。

class Ikimono:
    def __init__(self, name='フシギダネ', weight=7.0):
        self.__name = name
        
    def set_name(self, name):
        self.__name = name
        
class Pokemon(Ikimono):
    def get_name(self):
        return self.__name

pokemon = Pokemon()
print("このポケモンの名前は", pokemon.get_name(), "だよ")

上のコードを実行すると確かに以下のエラーが出て失敗する。
一見すると隠蔽はできてそうだ。

AttributeError: 'Pokemon' object has no attribute '_Pokemon__name'

ところで、このエラーはどういう意味だろうか?
同じコードの最後で以下を実行してみよう。

print("このポケモンの名前は", pokemon._Ikimono__name, "だよ")
出力
このポケモンの名前は フシギダネ だよ

あれ、出力できちゃった。

実はIkimono()クラスで定義したprivate変数は内部では _Ikimono__nameという名前で保存される。
これを継承先であるPokemon()クラスのgetterメソッドからアクセスしようとすると _Pokemon__nameを呼び出そうとして、無事失敗する。
ところが、これってprivateと言いつつただ頭にクラスの名前を付けてるだけなので、pokemon._Ikimono__nameすれば本来隠蔽されて
取得できないはずの上位クラスの変数が取得できてしまった。

このようにPythonにおける隠蔽は言語仕様上かなりガバガバなのである。
もちろんこれは理由1で述べた「Pythonは自由で制約の少ない言語にしよう」とマッチしている。
privateと言いつつ間違った使い方をすれば外部からでも値を取得できるようになっているのだ。

「でもみんなそんな変な使い方しないよね?大人なんだから」
という声が聞こえそうだ。
他にも属性アクセスという仕組みを使って内部の変数を簡単に壊せちゃったりする。
(Effective Python第二版、項目42参照)

要するに言語思想からしてsetter/getterは使うべきじゃないし、そもそも言語仕様上純粋なカプセル化は不可能なのだ。

代替案:@propertyを使え

以上長々とPythonでgetter/setterメソッド定義すべきじゃない理由を述べてきた。
とはいえ全部publicでopenな変数しか使えないのは不便だ。
例えば上のsetterとgetterの役割のコードでは「体重が不正な値の時にはエラーを出す」という仕組みをsetterメソッドで定義
していた。
Pythonでこのような処理をしたい場合は以下のように@propertyを使えば良い。
(出力は上のsetter/getterの役割のコードと同じ)

class Pokemon:
    def __init__(self, name='フシギダネ', weight=7.0):
        self._name = name
        self._weight = weight

    @property
    def name(self):
        return self._name
    
    @property
    def weight(self):
        return self._weight

    @name.setter
    def name(self, name):
        self._name = name

    @weight.setter
    def weight(self, weight):
        if weight <= 0:
                raise ValueError(f'体重がマイナスになっちゃってるよぉ(>_<)')
        self._weight = weight

pokemon = Pokemon()
pokemon.name = 'ピカチュウ'
pokemon.weight = 6.0

print("このポケモンの名前は", pokemon.name, "だよ")
print("重さは", pokemon.weight, " kgだよ")

print("重さを不正な値に変更しちゃうぞ!")
pokemon.weight = -999
print("重さは", pokemon.weight, " kgだよ")

このように書けばsetter/getterメソッドとほぼ同じ内容を実装することができる。
ここで注目したいのが変数を代入するときの処理がpokemon.nameに直接代入していることだ。
これは見た目上はクラス変数に直接値を代入しているように見えて中ではsetter, getterの処理が走っているということだ。
これにより毎回getter/setterを長々と書く煩わしさから開放されつつも同様の処理を行うことができる。
このようなpropertyを使った方法はPythonに限らず最近はいろんな言語で実装されている
https://ja.wikipedia.org/wiki/%E3%83%97%E3%83%AD%E3%83%91%E3%83%86%E3%82%A3_(%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0)

ちなみにクラス内部の変数の頭に_(アンダースコア)が一つ付いてるのはPEP8で分かりやすいようにそうしましょうねと言われているだけで、
それ自体に意味はない。(前述のように2つ付けるとprivate変数になる)

最後に

pythonにsetter/getterメソッドがないことや、private変数が推奨されていないことはちょっと調べてみるといくらでも出てくる。
でも「なんでそうなってるのか?」「無理やりそういう書き方をするとどうなるのか?」について突っ込んだ記事はあまりなかったので、
調べてまとめてみた。
結局setter, getter使いたい時は@property使おうねで全て終わってしまう記事なんだけど、この記事で少しでも理解が深まるといいなと
思う。

余談

僕が社会人一年目で新卒研修を受けていた時、研修にJavaがあった。
同期の一人が
「Javaってこんなめんどくさい書き方するんだ。Pythonなら一行で書けるのに。Javaはクソ」
とぶつぶつ言っていた。
それを聞いた他の同期が
「僕はJavaみたいな堅牢な言語、嫌いじゃないけどなぁ」
と返していた。
僕はその時
「堅牢?どういう意味だろう?ムズカシイ。」
と思ったのだけど、今回いろいろ調べてみてその意味が少しわかった気がした。

追記

コメントでカプセル化の説明について以下のようなコメントを貰った。

そのまま洗濯機をモデルとしてクラスを作成するとした場合に、
洗濯機クラスはstart()メソッドだけがあり、それ以外に洗濯機を起動するmethodがないようにするのが、
理想だと思われます。
(中略)
setter,getterを通して洗濯機の内部のデータにアクセスすることを推奨しているように見えてしまいます。

洗濯機を例に出した時に「スタートボタン以外で起動しないようにする」と説明していたのにsetter/getterで中の変数にアクセスできることを推奨するのはおかしいのでは?という指摘。
確かにその通りで、この記事ではJavaにおいてsetter/getterメソッドを使った上で完全なカプセル化が実現できるという前提で話を進めていたけど、setter/getterメソッドを使って直接データにアクセスできている時点で実は完全に内部を隠蔽できてはいない。
記事の中ではset_weight()メソッドの中では不正な値を弾く処理をしているからまだいいけど、get_name()に至っては内部変数nameをそのまま持ってきているので、厳密には隠蔽できてない。
このあたりを詰めていくと、Pythonでsetter/getterメソッドを使えないという話を超えて
「そもそもsetter/getterメソッドはいるのか?」
という論点になってしまいこの記事の要旨から外れてしまうため触れなかった。
ただ、洗濯機の例からsetter/getterメソッドを使ってカプセル化を実現するという説明になってしまってるのは確かに話が飛躍してしまっていておかしい。
(ごめんなさい。もっとうまい説明が見つかったらこっそり修正します。)

この記事では触れてこなかったのだけど、Javaなどの言語においてもsetter/getterメソッドなんてそもそも必要ないという人は一定数いるみたいだ。
例えばこの記事で触れている:https://qiita.com/katolisa/items/6cfd1a2a87058678d646

387
284
15

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
387
284

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?