初心者データサイエンティストの備忘録

調べたことは全部ここに書いて自分の辞書を作る

オブジェクト指向設計実践ガイド【第2章】まとめ

オブジェクト指向を使ってアプリケーションを作るために、オブジェクト指向の設計方法を学んでいます。 今回は、「オブジェクト指向設計実践ガイド」の第2章:単一責任のクラスを設計するを読んでまとめていきます。
なお、使用する言語はPythonです。

aisinkakura-datascientist.hatenablog.com

2.1 クラスに属するものを決める

この節ではTRUEなコードを書くべきとの主張がされています。自分の理解を深めるために、TRUEでない状態も併せて考えました。

要件 意味 満たしていない状態
見通しが良い(Transparent) 変更するコードにおいても、そのコードに依存する別の場所のコードにおいても、変更がもたらす影響が明白である。 コードを変更する際に、どこを変更すべきかわからない。
合理的(Reasonable) どんな変更であっても、かかるコストは変更がもたらす利益にふさわしい。 例えば、1,000万の利益を得るための改修に2,000万かかってしまった。
利用性が高い(Usable) 新しい環境、予期していなかった環境でも再利用できる。 例えば、PythonからRに文法を変えたら全く動かなくなった。
模範的(Exemplary) コードに変更を加える人が、上記の品質を自然と保つようなコードになっている。 コードが変更に耐えられない状態になっており、変更が生じるたびにスパゲティ化していく。

このTRUEなコードを書くための最初の一歩が、それぞれのクラスが単一責任になるように設計することだと本書では述べられています。

2.2 単一の責任を持つクラスをつくる

本節以降では、自転車を例にしてコードの説明がされています。私の場合、自転車の各部品の用語が覚えられなかったので、まずは用語を説明します。

図1:自転車の各用語の説明

色々用語が出てきましたが、最終的には表1のような2つのクラスを作成することを目指します。

表1:各クラスの変数とメソッド

次に本節の要点を箇条書きでまとめておきます。

  • 設計を最初から完璧にすることはできない。
  • したがって、変更に強いコードを書かなければならない。
  • 変更に強いコードにするための工夫の一つとして、クラスやメソッドを単一責任にするべき。
  • クラスが単一責任であるかの確認方法は2つある。
    • 1つ目は、あたかもクラスに知覚があるかのように問うてみること。例えば、「Gearクラスさん、ratioを教えてください」のような違和感のない問いだけで構成されるクラスは単一責任である。一方で、「Gearクラスさん、rimを教えてください」のような違和感がある問いが混ざっていたら単一責任ではない。
    • 2つ目は、一文でクラスを表現するこできるか確かめること。今回の場合、「Gearクラスは、Gearに所属するcog, chainringから、ratioを計算すること」と説明できる。(gear_inchesは今回は実装するが、単一責任の観点からはGearクラスに本来入れるべきではないと考えられる。)なお、一文で説明できないとは文章中に「それと」や「または」が入ってしまうときを指している。
  • ただし、改めて繰り返すが、最初から完璧な設計はできない。したがって、単一責任ではないクラスを設計してしまうこともあるかもしれない。そのため、単一責任を完璧に果たせない場合でも、変更に強い設計変更に対応可能なコードを書くべき。

正直、このあたりは十分に理解できていません。まず、単一責任の定義が本書では曖昧で、人によってとらえ方が異なるのではないかと思います。また、なぜ、今回はGearクラスでgear_inchesを実装してしまうのかもよくわかりませんでした。本当に単一責任にするなら、Gear、Wheelクラスに加えて、Gear、Wheelクラスの間を取り持つクラスが必要になるのではと私は考えています。

2.3 変更を歓迎するコードを書く

本節では、変更に強いコードを書くテクニックが紹介されます。

インスタンス変数の隠蔽

インスタンス変数に変更が生じても、コード全体に変更が生じないコードを書く工夫です。
アンチパターン(良くない例)とデザインパターン(良い例)を示して説明します。

# アンチパターン
class AntiPattern:
    def __init__(self, a, b):
        self.a = a
        self.b = b
        
    def func1(self):
        return self.a+self.b
    
    def func2(self):
        return self.a-self.b
    
    def func3(self):
        return self.a*self.b
    
    def func4(self):
        return self.a/self.b

アンチパターンが悪い理由は、self.aとself.bに変更が生じたときに、func1~func4の中身に変更が必要になることです。例えば、下記のコードでは8か所変更しています。

# 要件がa→a+1、b→b-1に変更になってしまった場合、アンチパターンだと...
class AntiPatternChange:
    def __init__(self, a, b):
        self.a = a
        self.b = b
        
    def func1(self):
        return (self.a+1)+(self.b-1) # 変更した!
    
    def func2(self):
        return (self.a+1)-(self.b-1) # 変更した!
    
    def func3(self):
        return (self.a+1)*(self.b-1) # 変更した!
    
    def func4(self):
        return (self.a+1)/(self.b-1) # 変更した!

これの改善案として、コンストラクタで変えてしまう手もあります。

# アンチパターンの改善案1
class AntiPatternBetterChange1:
    '''
    a, b:数値
    コンストラクタで対応する
    '''
    def __init__(self, a, b):
        self.a = a+1 # 変更した!
        self.b = b-1 # 変更した!
        
    def func1(self):
        return self.a+self.b
    
    def func2(self):
        return self.a-self.b
    
    def func3(self):
        return self.a*self.b
    
    def func4(self):
        return self.a/self.b

しかし、これはコンストラクタに変数の値を変更する機能を持たせることになります。単一責任の原則にしたがえば、コンストラクタは「インスタンス作成時に動く」という機能だけを持っているべきです。そこで、変数の値を変更する機能は分離します。

# アンチパターンの改善案2
class AntiPatternBetterChange2:
    '''
    a, b:数値
    '''
    def __init__(self, a, b):
        self._a = a
        self._b = b
        
    def a(self): 
        """
        a:数値
        aの変更を管理する
        """
        return self._a
    
    def b(self):
        """
        b:数値
        bの変更を管理する
        """
        return self._b
        
    def func1(self):
        return self.a()+self.b()
    
    def func2(self):
        return self.a()-self.b()
    
    def func3(self):
        return self.a()*self.b()
    
    def func4(self):
        return self.a()/self.b()

このようにすれば、self.a、self.bに変更が生じた際にも下記のように2か所の変更で済みます。

class AntiPatternBetterChange2:
    '''
    a, b:数値
    '''
    def __init__(self, a, b):
        self._a = a
        self._b = b
        
    def a(self): 
        """
        a:数値
        aの変更を管理する
        """
        return self._a+1 # 変更した!
    
    def b(self):
        """
        b:数値
        bの変更を管理する
        """
        return self._b-1 # 変更した!
        
    def func1(self):
        return self.a()+self.b()
    
    def func2(self):
        return self.a()-self.b()
    
    def func3(self):
        return self.a()*self.b()
    
    def func4(self):
        return self.a()/self.b()

これでほとんど完成なのですが、func1~func4の中では毎回self.a()、self.b()とカッコがついておりスマートではありません。そこで、@propertyを使います。
@propertyについては、こちらの過去記事を参照してください。

aisinkakura-datascientist.hatenablog.com

@propertyを使ったスクリプトはこちらです。

# デザインパターン
class DesignPattern:
    '''
    a, b:数値
    '''
    def __init__(self, a, b):
        self._a = a
        self._b = b
    
    @property
    def a(self): 
        """
        a:数値
        aの変更を管理する
        """
        return self._a
    
    @property
    def b(self):
        """
        b:数値
        bの変更を管理する
        """
        return self._b
        
    def func1(self):
        return self.a+self.b
    
    def func2(self):
        return self.a-self.b
    
    def func3(self):
        return self.a*self.b
    
    def func4(self):
        return self.a/self.b

これで、インスタンス変数の変更に強いスクリプトを作成することができました!

データ構造の隠蔽

これは簡単にいえば、データの構造と意味を分けましょうという話です。
まず、悪い例を挙げます。

# アンチパターン
import math
class ObscuringReferences:
    def __init__(self, data):
        self._data = data
        
    @property
    def data(self):
        return self._data
    
    def diameters(self):
        return self.data[0]+(self.data[1]*2)

    def area_of_innner_ring(self):
        return (self.data[0]/2)**2*math.pi

このようなコードの場合、dataの構造が変わるとself.data[インデックス]となっている箇所を全て変更しなければなりません。さすがにそれは面倒すぎるので、構造と意味を分けるメソッドを挟み込みます。

# デザインパターン
# 構造と意味を分離する!
import math
import pandas as pd
class DesignPatternOfObscuringReferences:
    def __init__(self, data):
        self._data = data
    
    @property
    def data(self):
        return self.wheelify(arg_data=self._data)
    
    # 構造と意味を分離するメソッド
    def wheelify(self, arg_data):        
        return pd.Series(arg_data, index=["rim", "tire"])

    def diameters(self):
        return self.data["rim"]+(self.data["tire"]*2)
    
    def area_of_innner_ring(self):
        return (self.data["rim"]/2)**2*math.pi

上記のようなwheelifyを使うことで、もしdataの構造に変更が生じても、diametersやarea_of_innner_ring中のself.dataを変更せずに済みます。

あらゆる箇所を単一責任にする

ここでは、ちょっと細かいですが各メソッドの責任を単一にしていくことを学びました。

繰り返し処理のパターン

まず、アンチパターンのメソッドを書きます。

# アンチパターン
def diameters(wheels):
    return [wheel["rim"]+wheel["tire"]*2 for wheel in wheels]

ここに記載したdiametersというメソッドは下記の2つの機能を持っています。

  • 各タイヤの直径を計算する
  • 各タイヤを繰り返し計算する

これにより、このメソッドは単一責任になっていません。
これを下記の2つのメソッドに分解します。

# デザインパターン
# 各責任に分ける

# 各タイヤの直径を計算する
def diameter(wheel):
    return wheel["rim"]+wheel["tire"]*2

# 各タイヤを繰り返し処理する
def diameters(wheels):
    return [diameter(wheel) for wheel in wheels]

これにより、単一責任なメソッドを作成できました。

意味でメソッドを分ける

一つ上のパターンでは機能でメソッドを分けました。しかし、ここでは意味でメソッドを分けていきます。 まずは、アンチパターンです。

# アンチパターン
def gear_inches(ratio, rim, tire):
    return ratio*(rim+(tire*2))

このgear_inchesメソッドは、まず最初に直径=rim+tire×2を計算しています。そして、計算された直径に対し、ratioを掛け算することで最終的にギアインチ=ratio×直径を計算しています。
このように、意味を2つ持っているメソッドも分解して単一責任にしていきます。それによりできるメソッドはこちらです。

# デザインパターン
def diameter(rim, tire):
    return rim+(tire*2)


def gear_inches(ratio, rim, tire):
    return ratio*diameter(rim, tire)

クラス内の余計な責任を隔離する

最後に当初の設計が不十分で責任を十分に分離できないときから、後々責任を分離しやすくする方法を記載しておきます。
方法は単純で、クラスの中に分離できそうな箇所を分離しておくです。
ここでは、最初化からデザインパターンスクリプトを書いておきます。

# Gearの要素として、chainring, cog, ratio, gear_inchesを持ちたい
# しかし、そのためには、wheelの要素を持っている必要がある
# 一旦は、Gearクラスの中にwheelも実装してしまう
# ただし、wheelに関する部分はGearの中でも隔離させておく

class Gear:
    def __init__(self, chainring, cog, rim, tire):
        self._chainring = chainring
        self._cog = cog
        self._wheel = self.Wheel(rim, tire)
    
    @property
    def chainring(self):
        return self._chainring
    
    @property
    def cog(self):
        return self._cog  
    
    def ratio(self):
        return self.chainring/self.cog
    
    def gear_inches(self):
        return self.ratio()*self._wheel.diameter()
    
    # wheelに関するスクリプト(なんらかの理由で別クラスを作りたくない場合にクラスの中で単一責任を持ったクラスを作成する)
    class Wheel:
        def __init__(self, rim, tire):
            self._rim = rim
            self._tire = tire
        
        def diameter(self):
            return self._rim + (self._tire*2)

これにより、Wheelクラスを分離したいときにそのままコピペして外に出すだけで済みます。
実際にWheelクラスを分離し、新たにcircumferenceメソッドを実装したときのスクリプトがこちらです。

# GearクラスとWheelクラスを独立させ、単一責任にする
# Whhelクラスにはcircumferenceメソッドを追加する

class Gear:
    def __init__(self, chainring, cog, wheel):
        self._chainring = chainring
        self._cog = cog
        self._wheel = wheel
        
    @property
    def chainring(self):
        return self._chainring
    
    @property
    def cog(self):
        return self._cog
    
    @property
    def wheel(self):
        return self._wheel
    
    def ratio(self):
        return self.chainring/self.cog
    
    def gear_inches(self):
        return self.ratio()*self.wheel.diameter()
    
class Wheel:
    def __init__(self, rim, tire):
        self._rim = rim
        self._tire = tire
        
    @property
    def rim(self):
        return self._rim
    
    @property
    def tire(self):
        return self._tire
    
    def diameter(self):
        return self.rim + (self.tire*2)
    
    def circumference(self):
        from math import pi
        return self.diameter()*pi

実際に手を動かすとわかりやすのですが、本当に手早くWheelクラスを新たに実装することができました。

まとめ

本章の前半では、コードの設計はコードを書く最初の時点で完璧に決めることはできない要件の仕様変更は顧客の都合などによりいくらでも起こりうるという現場の事情を踏まえて、いつでも変更しやすいコードを書くべきとしつこく説明しています。
また、それを実現するための手段の一つとして単一責任の考え方が導入されました。
本章の後半では、単一責任なコードを書くためのテクニックが紹介されました。

私としては、単一責任を十分に理解できたとは言えません。しかし、一旦は機能と意味を分けていくという意識とここで学んだテクニックを活かして実装していきたいと思います。

pandas.mergeできないときは型を意識する

すごい基本的なことで、かつ調べればすぐわかることですがメモしておきます。

下記のような2つのデータフレームをカラムkeyで結合します。結合の仕方はなんでも良いです。

図1:結合対象のDatFrame

結合するときのスクリプトは下記の通りです。

import pandas as pd
pd.merge(left_df, right_df, on=["key"], how="outer")

ところが結合したら、left_dfとright_dfのkeyを同じものと認識してくれず、図2のようになってしまいました。

図2:意図しない結合結果

理由を調べてみると、keyの型が違うことが原因でした。具体的には、下記のスクリプトを書いて確かめました。 (スクリプト中の -> は出力結果を表しています)

print(type(left_df["key"][0]))
# -> <class 'str'>


print(type(right_df["key"][0]))
# -> <class 'int'>

keyの型が違うことがわかったので、型を揃えて結合させればOKですね。今回は、right_dfのkeyの型をstrにして結合させます。

right_df["key"] = right_df["key"].astype(str)

無事に意図したとおりの出力ができました。

図3:意図通りの結合

Pythonの@propertyの振る舞い

Pythonで@propertyがよくわからなかったので、色々調べてみた。
正直、完璧に理解した感はないが、一旦分かった範囲でまとめてみる。

@propertyとは?

3.4. プロパティ - ゼロから学ぶ Python」よると、@propertyを下記のように説明している。

クラスのメンバで変数のように参照することのできる関数のことをプロパティといいます

ということで、ごく簡単なスクリプトを書いて確認してみる。

class Test:
    def __init__(self, a):
        self._a = a
        
    @property
    def a(self):
        return self._a
    
    def func1(self):
        return self.a

test = Test(a=1)
print(test.func1())

# -> 1 が表示される

func1の中でメソッドaをself.aで呼び出していることがポイント。本来、aはメソッドなので、カッコをつけたself.a()として呼び出す必要がある。しかし、@propertyのお陰で変数のような形式で呼び出すことができている。

@propertyを使う場面

オブジェクト指向設計実践ガイド(下記の本)によれば、すべてのインスタンス変数は隠蔽すべきとのこと。 したがって、変数を直接扱うのではなく、メソッドaをはさみ、隠蔽することが必要だ。その際、@propertyを設定することで、a()ではなくa単体を書けばよいのでコードが少しシンプルになるということらしい。

注意

上記と似たようなコードだが、下記は動かない。

class Test:
    def __init__(self, a):
        self.a = a # 変数aに_がなくなった
        
    @property
    def a(self):
        return self.a
    
    def func1(self):
        return self.a

test = Test(a=1)
print(test.func1())

# エラー:can't set attributeを出力する

これは、変数aとメソッドaの名前が同じだからエラー:can't set attributeを出力してしまう。
なので、propertyを使うときは、必ず_を前に一つつけた変数を用意しましょう。
※_を前にひとつつけた変数は、慣習的にそのクラスの外では使わない変数を指すようです。

【読書記録】オブジェクト指向設計実践ガイド 第1章まとめ

体系的にオブジェクト指向の設計方法を学ぶため、本書を読み始めました。
本書を読んで、過去に自分が作成したダメコードの拡張性を高めたいと思っています。

今回は第1章「オブジェクト指向設計」の要点をまとめようと思います。第1章はポエム的な感じで、コードは出てきません。
私も箇条書きで要点をまとめておくくらいにとどめておこうと思います。

要点まとめ

原則・デザインパターン・メトリクスについて書かれた記事

【読書記録】アダルト・チャイルドが自分と向きあう本

本書を手に取った理由

アダルト・チャイルドからの回復法を調べているうちに、この本にたどり着きました。

以前記事にした本との比較

以前記事にした本は、こちらです。

(前回記事) aisinkakura-datascientist.hatenablog.com

こちらの本は、アダルト・チャイルドの特徴を知るには良かったです。
しかし、回復の方法を知るには、今回紹介する本の方が適切な気がします。 アダルト・チャイルドの特徴は既に知っており、回復法を知りたい方は今回記事にした本を読むと良いと思います。

本書の内容

本書には、アダルト・チャイルドから回復するためのワークが複数記載されております。
図1は、アダルト・チャイルドの特徴とワークの目的、問いをまとめたものです。
各特徴を改善するためのワークに線を引いています。
また、「代表的な問い」とは各ワークで問いかけられる質問です。
この質問に答えることをきっかけとして、私たちアダルト・チャイルドは自分と向き合うことができ、それが回復につながります。

図1:ACの特徴とワーク

感想

本書の良いところは、ACからの回復に焦点を当てて書かれていることです。
ACに限らず、よくやってしまう間違いは、自分をカテゴライズすることで満足してしまい、そこからの改善を図ろうとしないことだと私は思っています。
ACについてもそれは起きがちで、ACの特徴を見て「私は(僕は)ACなのだ!」とカテゴライズして安心してしまう方もいるのではないでしょうか?
その点、本書はACの特徴や各人のステータスに応じたワークが紹介されており、非常に良かったです。

私事になってしまいますが、このワークに取り組む前に、図1中の「特定したパターンから得られるメッセージを意識的に否定すること」が偶然できたことがあります。
それは音楽を聴くことに関してです。
私の親はなんでも禁止するタイプで、高校生まで読書・テレビ視聴・音楽を聴くこと・友人と遊ぶことなどは親の許可が必要でした。
そのためなのか、私は今でもテレビ視聴・音楽を聴くこと・遊ぶことに罪悪感を持っています。これらのことをしようとすると、精神的な圧迫感を感じたり、口が乾き、心拍数が上がるという身体的な症状が現れたりします。 しかし、先日電車に乗っているときに、周囲の音楽を聴いている人たちを見て、「そうか音楽を聴くことは悪いことではないんだ!!」と思う瞬間がありました。 そのとき、強烈な解放感を得ることができましたし、親から受けていた間違ったメッセージを否定することができたという確信がありました。
それ以降、何か罪悪感を感じるときは「別に悪いことをしているわけではない」と意識的に親から受けたメッセージを否定するようにしています。
そのお陰か、最近は強いうつ状態にならずに済むようになってきました。だから、本書を読み、ACから回復するワークの一つとして、「特定したパターンから得られるメッセージを意識的に否定すること」が存在することを知った時には、非常に腹落ちする感覚がありました。

私もそうですが、ACの方々は過去置かれていた環境で培われた、歪んだ価値観をどうしても引きずってしまう傾向にあると思います。それを意識的に断ち切ることが、ACからの回復の礎になるのだと思いました。

おまけ

ワークは一人で取り組むよりも、仲間や専門家とともに取り組んだ方が効果的で安全だと本書に書かれていました。そこで、自分が調べた専門機関を箇条書きで記載しておきます。

【読書記録】生きづらさの正体はアダルトチルドレン。 - その根本原因を解消し、楽な生き方を手に入れる。

本書を手に取った理由

私はうつ状態になり、日常生活に支障が出ることが多々あります。 そんな折、友人が「自分はアダルトチルドレンかもしれない」と言い出したため調べたところ、自分がアダルトチルドレンの特徴に良くあてはまることに気がつきました。 そこで、アダルトチルドレンについての知識を増やすため、本書を手に取りました。

対象の読者

本書は、アダルトチルドレンの基礎知識を得るには良い本だと思います。逆に言えば、アダルトチルドレンの知識を既に持っている人にとっては、物足りないかもしれません。私はアダルトチルドレンについて、ほとんど知識を持っていなかったので非常に役に立ちました。
私の場合、アダルトチルドレンの特徴を知ることが、自分の過去を振り返る契機となり、特に役に立ったと感じています。

アダルトチルドレンの特徴を知り、自分の過去を振り返る

アダルトチルドレンの特徴として「完璧主義である」という特徴が挙げられていました。この特徴を私に当てはめると図1のようになります。

図1:理想と現実

図1で言いたいことは、自分は元来完璧主義ではないのに、家庭環境に適応するために完璧主義になってしまったことです。
私のような元来完璧主義ではない人間は、何事も完璧にこなそうという強い意志もなく、そこに対するメリットを見出すことができません。しかし、完璧主義的発想を後から植え付けられてしまうと、できないのに、理想だけは完璧を目指そうとします。そのため、理想と現実にギャップが生まれます。そのギャップが、今のうつ状態を引き起こす要因の一つであると私は考えました。
このように、私はうつ状態を引き起こす要因の一つを言語化することができました。そういった意味で、アダルトチルドレンの特徴を知ることは私にとって有益でした。

今後の取り組み

本書には、アダルトチルドレンから回復するための方法もいくつか記載されていました。 私は、下記2点に取り組もうと思います。

1.カウンセリングを受ける
本書に下記のように書いてありました。

アダルトチルドレンの改善に最も効果的な方法が、カウンセリングを受けることです。根本的な回復を目指すなら ば、セルフケアだけではどうしても限界があるからです。

これについては、ちょうど勤め先で社外カウンセリング制度が始まったので、利用してみようと思います。カウンセリングはお金がかかるイメージがあったので、非常に助かります。また、カウンセリングの怖いところはエセ心理士に騙されてしまうことです。しかし、会社が契約しているということで、安心感があります。

2.子どもに今までよりも丁寧に接する
本書に記載があったぬいぐるみ療法からヒントを得ました。
ぬいぐるみ療法は、自分が人からして欲しかったことを、ぬいぐるみにしてあげることで癒しを得る療養法だそうです。私の場合、ぬいぐるみではなく子どもに対して色々してあげることが、子どものためにも自分のためにもなると思いました。
では、自分が親からして欲しかったことは何か?それは、子どもをもっとよく見て子育てをすることです。 そのため、私は子どもをよく観察し、子どもが親にして欲しいことを感じ取り、子育てをしていこうと思います。
例としては、子どもを抱っこしてあげる時間を増やそうと思います。我が子は抱っこされることが本当に好きで「すぐに抱っこして」とアピールします。基本的にはすぐに抱っこしているのですが、家事をしている最中はどうしても後回しになってしまうこともあります。ですが、子どもが生まれて1年半の時間が経過し、親も片手でできることが増えてきました。片手でできる家事をしながら、子どもを抱っこするということを意識していこうと思います。
「子どもが親にしてもらいたいことは何か?」ということを子育てのベースにしていこうと思います。

まとめ

本書を読むことで、アダルトチルドレンの特徴や改善の方向性を知ることができます。本書だけでは、根本的な回復は難しいと思いますが、「自分がアダルトチルドレンかも」と思う人にはとても良いと思います。

継承できないメソッド・変数

車輪の再開発ですが、Pythonにおいて継承できないメソッド・変数をまとめます。

継承できないメソッド・変数

結論からいうと、__(アンダースコアが2個)が冒頭についているメソッド・変数は継承できません。

次のスクリプトを見てみましょう。

class Parent:
    def __init__(self, v):
        self.v = v
        
    def func(self):
        print(self.v)
        
    def _func(self):
        print(self.v)
        
    def __func(self):
        print(self.v)
        
class Child(Parent):
    def do(self):
        self.func()
        self._func()
        self.__func()
        
child = Child(v=1)
child.do()

このスクリプトは「'Child' object has no attribute '_Child__func'」というエラーが出力されて動きません。 Parentで定義した__funcメソッドにマングリング機構が働き、メソッド名が_Child__funcに変更してしまったためアクセスできなくなりました。

変数についても同じです。

class Parent:
    def __init__(self, v, _v, __v):
        self.v = v
        self._v = _v
        self.__v = __v
        
    def func(self):
        print(self.v)
        print(self._v)
        print(self.__v)
                
class Child(Parent):
    def do(self):
        self.func()
        
child = Child(v=1, _v=2, __v=3)
child.do()

このときは、「__init__() got an unexpected keyword argument '__v'」というエラーを出力してしまいました。

使いどころ

__(アンダースコア2個)の変数は、継承できないことを利用して、名前衝突を防ぐことができます。一番分かりやすいのはコンストラクタ。

class Parent:
    def __init__(self, v):
        self.v = v

class Child(Parent):
    def __init__(self, b):
        self.b = b

Parentの__init__にマングリング機構が働くことで、Childの__init__とは別のメソッドとなるのですね。(今まで、子クラスのコンストラクタで親クラスのコンストラクタをオーバライドしていると思っていましたが、勘違いでした)

まとめ

メソッド・変数ともに__(アンダースコア2個)が付いている場合は、継承できない。