機械工学で学ぶオブジェクト指向プログラミング

# はじめに

オブジェクト指向プログラミング (object-oriented programming, OOP) とは、システムをオブジェクトの集合としてモデル化し、オブジェクトの構成とその動作を決定したうえでプログラミングを行う手法です。保守性や再利用性を向上させながら大規模で複雑なプログラムを作る際には、オブジェクト指向プログラミングの考え方は非常に重要となります。

しかしながら、初学者がオブジェクト指向プログラミングを学ぶときに使われる例は複雑なシステムを伴っていないことが多いです。対象とするオブジェクトは「車」や「動物」などの万人にとってイメージしやすいものの計算の実用性に乏しかったり、クラスに設計されるメソッドの機能は標準出力の些細な表現の違いのみにとどまっていたりするのが現状です。

そこで本記事では、オブジェクト指向プログラミングを実践する適度に複雑な具体例として、機械工学のいち分野である材料力学で登場する計算を取り上げます。材料力学におけるはりの曲げ問題の解法をオブジェクト指向プログラミングによって計算することを試み、オブジェクト指向プログラミングを深く理解し身につけることを目的とします。

# はりの曲げ問題の概要

はり (梁; beam) とは、断面の寸法と比較して長さが長い構造部材のことで、建築物の梁、橋などが例にあげられます。はりに荷重 (Load) が作用するとはりは曲がります。これをはりの曲げ変形といい、この変形を力学的に考えるのがはりの曲げ問題です。

ミオソテスの方法とは、はりの問題の基本問題を利用することで、微分方程式を解かずに複雑な問題が容易に解ける方法のことをいいます。

はりの基本問題について説明します。次の図のように、長さ $l$ のはりに曲げモーメントや荷重が作用することにより、はりは曲がります。この場合のはり先端の変位を $\delta$, 先端の傾きを $\theta$ としこれらを求めます。なお図中の記号 $EI$ は、はりのヤング率が $E$, 断面二次モーメントが $I$ であることを表しています。

![はりの問題の図](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2394550/a3e301b2-ea3a-db46-05a6-833a9248e91c.png)


はりの基本問題は、つぎの図の3つにまとめられます。

![はりの基本問題](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2394550/8568c9d8-bf6a-83f6-e002-29cc24a49cf5.png)

(1) は曲げモーメント $M$ が、 (2) は集中荷重 $P$ がそれぞれ片持はりの先端に作用しています。(3) は片持はりの全体に、はりの単位長さ当たりに分布する荷重 (分布荷重) $q$ が作用しています。

# プログラムの全体像

下記に、最終的に作成するプログラムのクラス図を示します。これらのクラスとその役割について次に説明します。

```mermaid
classDiagram
    Load <|-- ConcentratedLoad
    Load <|-- DistributedLoad
    
    class Load{
      +float a
      +displacement()
    }
    class ConcentratedLoad{
      +float P
    }
    class DistributedLoad{
      +float q 
    }
	class CantileverBeam{
	  +float l
	  +float E
	  +float I
	  +displacement()
	}
```

## Load クラス

荷重を表す基本クラスです。属性 `a` は、荷重の適用位置を表す浮動小数点数で、単位は mm です。メソッド `displasement()` は、派生クラス (`ConcentratedLoad` クラス、`DistributedLoad` クラス) でオーバーライドされる、はりの変位を計算するための内部メソッドです。

## ConcentratedLoad クラス

`Load` クラスの派生クラスです。集中荷重を表します。属性 `P` は集中荷重の大きさを表す浮動小数点数で、単位は kN です。`ConcentratedLoad` クラスにおいて、属性 `a` は次の図のように寸法を取ります。

![集中荷重](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2394550/9cb9a41d-bb4a-03c6-e5a5-acb7f52ee4ad.png)

## DistributedLoad クラス

`Load` クラスの派生クラスです。分布荷重を表します。属性 `q` は、はりの単位長さ当たりに分布する荷重を表す浮動小数点数で、単位は kN/mm です。`DistributedLoad` クラスにおいて、属性 `a` は次の図のように寸法を取ります。

![分布荷重](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2394550/5841eafe-879c-89ef-a77e-91c197afacf9.png)

## CantileverBeam クラス

片持はりを表すクラスです。下記にそれぞれの属性を説明します。

- `l`: はりの長さ (mm)
- `E`: はりのヤング率 (GPa)
- `I`: はりの断面二次モーメント ($\mathrm{mm^4}$)

メソッド `displasement()` は、はりの変位 (mm) を計算します。

## 使用例

本プログラムでは、設定したはりに、設定した一つの荷重が作用したときに生じるたわみによる変位 (displasement) を計算します。本記事では Python を用いてこれらのクラスを作成していきます。次のコードは実際の使用例です。

```python
beam = CantileverBeam(length=5000, youngs_modulus=205, moment_of_inertia_of_area=8_333_333)
con_load = ConcentratedLoad(position=4000, magnitude=1)
dis_load = DistributedLoad(position=3000, magnitude_per_unit_length=1e-3)
print("集中荷重による変位:", beam.displacement(con_load))
print("分布荷重による変位:", beam.displacement(dis_load))
```

出力結果は下記のようになります。

```
集中荷重による変位: 17.17073239414637
分布荷重による変位: 34.53658674731713
```

# 実装方法の説明

この節では、単純にクラスを定義するコードから始め、オブジェクト指向で登場する概念の説明に合わせて少しづつコードを書き加えながら使用できるコードを完成させます。

## クラスの定義方法

Python でクラスを宣言するには、 `class` キーワードを使用します。中身のない空の集中荷重クラス `ConcentratedLoad` クラスを作成するには、次のようなコードになります。

```python
class ConcentratedLoad:
    pass
```

`ConcentratedLoad` オブジェクトは、クラス名を呼び出して次のように作ることができます。

```python
con_load = ConcentratedLoad()
```

## 属性

属性とは、そのオブジェクトが持つ変数のことです。本記事の例で言うと、片持はりオブジェクトは「長さ」「ヤング率」「断面二次モーメント」という3つの属性を持ちます。Python では、オブジェクトを作成している間あるいは作成したあとに、それに属性を与えることができます。

先ほどと同様に、空の片持はり `CantileverBeam` クラスを作成し、そのオブジェクトを作ります。

```python
class CantileverBeam:
    pass

beam = CantileverBeam()
```

このオブジェクトに属性を追加するには、次のようなコードを書くことができます。

```python
beam.l = 5000
beam.E = 205
beam.I = 8_333_333
```

定義した属性にアクセスすることも可能です。

```python
print(beam.l)
print(beam.E)
print(beam.I)
```

```
5000
205
8333333
```

## 初期化

前節の方法では、情報を持った片持はりを作るために逐一属性を追加するコードを書かなければならず、非効率でミスが発生しやすいです。オブジェクトを作成するときにその属性に値を代入できるのが望ましいですが、それを実現するのが Python のオブジェクトの初期化です。 `CantileverBeam` オブジェクトを作成するときに長さ、ヤング率、断面二次モーメントの値を渡せるようにするには、次のように `__init__()` メソッドを使用します。

```python
class CantileverBeam:
    def __init__(self, length, youngs_modulus, moment_of_inertia_of_area):
        self.l = length # 長さ (mm)
        self.E = youngs_modulus # ヤング率 (GPa)
        self.I = moment_of_inertia_of_area # 断面二次モーメント (mm^4)
```

これにより、 `length`, `youngs_modulus`, `moment_of_inertia_of_area` 引数にそれぞれ値を渡して、 `CantileverBeam` オブジェクトを作ることができます。第一引数の `self` は、作られた個別のオブジェクト自体を参照することを示します。

値を渡した属性は直接読み書きすることができます。

```python
beam = CantileverBeam(length=5000, youngs_modulus=205, moment_of_inertia_of_area=8_333_333)

print(beam.l)
print(beam.E)
print(beam.I)
```

```
5000
205
8333333
```

同様にして、 `ConcentratedLoad` クラスと `DistributedLoad` クラスも、属性を持たせるために次のように定義します。

```python
class ConcentratedLoad:
    def __init__(self, position, magnitude):
        self.a = position # 左端からの位置 (mm)
        self.P = magnitude # 力の大きさ (kN)

class DistributedLoad:
    def __init__(self, position, magnitude_per_unit_length):
        self.a = position # 分布荷重がかかり始める位置 (mm)
        self.q = magnitude_per_unit_length # はりの単位長さあたりに分布する荷重 (kN/mm)
```

## 操作の追加

はりの変位を計算するために `CantileverBeam` クラスにメソッド `displacement()` を定義します。そのためには、次の表の要素が必要になると考えられます。

| メソッド | displacement                             |
| ---- | ---------------------------------------- |
| 引数   | `ConcentratedLoad` または `DistributedLoad` |
| 結果   | 変位                                       |
| 処理内容 | 荷重の種類に応じて、変位を計算する                        |

この内容を `CantileverBeam` クラス内に記述したものが、次のコードになります。

```python
class CantileverBeam:
    def __init__(self, length, youngs_modulus, moment_of_inertia_of_area):
        self.l = length # 長さ (mm)
        self.E = youngs_modulus # ヤング率 (GPa)
        self.I = moment_of_inertia_of_area # 断面二次モーメント (mm^4)
    def displacement(self, load):
        if isinstance(load, ConcentratedLoad):
            # 変位δ (mm) を計算する
            a = load.a
            b = self.l - a
            P = load.P
            E = self.E
            I = self.I
            delta_A = (P * a ** 3) / (3 * E * I)
            theta_A = (P * a ** 2) / (2 * E * I)
            return delta_A + theta_A * b
        elif isinstance(load, DistributedLoad):
            a = load.a
            b = self.l - a
            M = (load.q * b**2) / 2
            P = load.q * b
            delta_A = (M * a**2) / (2 * self.E * self.I) + (P * a**3) / (3 * self.E * self.I)
            theta_A = (M * a) / (self.E * self.I) + (P * a**2) / (2 * self.E * self.I)
            delta_B_dash = (load.q * b**4) / (8 * self.E * self.I)
            delta_B = delta_A + theta_A * b + delta_B_dash
            return delta_B
```

`self.l` のようにすることで、メソッド内部の処理で自分自身の属性にアクセスすることができます。Python 組み込みの `isinstance()` 関数によって、第一引数に指定したデータ型が、第二引数に指定したデータ型と等しいかどうかを判定しています。

## 継承

継承とは、使いたい既存のクラスを指定し、追加・変更したい一部の属性やメソッドだけを新たに定義した新しいクラスを作ることです。集中荷重と分布荷重はどちらも荷重 (Load) であり、同じ属性 `a` を持っています。したがって、 `Load` という基底クラスを定義してから、 `ConcentratedLoad` と `DistributedLoad` を `Load` の派生クラスとして定義することができそうです。これを実現するには、次のようなコードを書くことができます。

```python
class Load:
    def __init__(self, position):
        self.a = position

class ConcentratedLoad(Load):
    def __init__(self, position, magnitude):
        super().__init__(position)
        self.P = magnitude

class DistributedLoad(Load):
    def __init__(self, position, magnitude_per_unit_length):
        super().__init__(position)
        self.q = magnitude_per_unit_length
```

派生クラスは、 `ConcentratedLoad(Load)` のように括弧内に基底クラスの名前を入れて定義します。このコードでは、荷重適用位置を表す属性 `a` が `Load` クラスから継承されています。

派生クラスのメソッドが基底クラスのメソッドを置き換えることをオーバーライドといいます。継承したクラスで `__init__()` をオーバーライドする場合、通常は基底クラスの `__init__()` も呼び出す必要があります。 `super()` を使用することで、明示的に親クラスの初期化子を呼び出すことができます。

## 多態性

多態性とは、厳密には異なる複数のオブジェクトを同一視して、まとめて処理できることをいいます。いま、変位を計算する `displacement()` メソッドを実装するのに、受け取る荷重の種類ごとに条件分岐をして変位を計算しています。もし将来新たな種類の荷重が増えると、そのたびに条件分岐を増やさなければならず、コードのメンテナンスが大変になります。そこで、荷重の種類ごとに異なる挙動を持つメソッドをそれぞれのクラスに実装し、共通のメソッドを提供することで、条件分岐を解消することが可能です。

次のコードのように、各荷重クラスに `displacement()` メソッドを実装し、 `CantileverBeam` クラス内で呼び出します。

```python
class Load:
    def __init__(self, position):
        self.a = position
    
    def displacement(self, beam):
        pass

class ConcentratedLoad(Load):
    def __init__(self, position, magnitude):
        super().__init__(position)
        self.P = magnitude
    
    def displacement(self, beam):
        a = self.a
        b = beam.l - a
        P = self.P
        E = beam.E
        I = beam.I
        delta_A = (P * a ** 3) / (3 * E * I)
        theta_A = (P * a ** 2) / (2 * E * I)
        return delta_A + theta_A * b

class DistributedLoad(Load):
    def __init__(self, position, magnitude_per_unit_length):
        super().__init__(position)
        self.q = magnitude_per_unit_length

    def displacement(self, beam):
        a = self.a
        b = beam.l - a
        q = self.q
        M = (q * b**2) / 2
        P = q * b
        E = beam.E
        I = beam.I
        delta_A = (M * a**2) / (2 * E * I) + (P * a**3) / (3 * E * I)
        theta_A = (M * a) / (E * I) + (P * a**2) / (2 * E * I)
        delta_B_dash = (q * b**4) / (8 * E * I)
        delta_B = delta_A + theta_A * b + delta_B_dash
        return delta_B

class CantileverBeam:
    def __init__(self, length, youngs_modulus, moment_of_inertia_of_area):
        self.l = length # 長さ (mm)
        self.E = youngs_modulus # ヤング率 (GPa)
        self.I = moment_of_inertia_of_area # 断面二次モーメント (mm^4)
    def displacement(self, load):
        return load.displacement(self)
```

## カプセル化

カプセル化とは、属性の読み書きやメソッドの呼び出しを制限する機能です。Python は非公開属性というものを持っていませんが、プロパティと名前マングリングを使用することで、外部コードが偶然属性名を当てたりしないよう非公開のように扱うことができます。

次のコードは、 `CantileverBeam` クラスにプロパティと名前マングリングを適用しています。

```python
class CantileverBeam:
    def __init__(self, length, youngs_modulus, moment_of_inertia_of_area):
        self.__l = length # 長さ (mm)
        self.__E = youngs_modulus # ヤング率 (GPa)
        self.__I = moment_of_inertia_of_area # 断面二次モーメント (mm^4)

    @property
    def l(self):
        return self.__l

    @property
    def E(self):
        return self.__E

    @property
    def I(self):
        return self.__I

    def displacement(self, load):
        return load.displacement(self)
```

プロパティとして定義した `l`, `E`, `I` は、まるで属性のようにアクセスすることができます。

```python
beam = CantileverBeam(5000, 205, 8_333_333)
print(beam.E)
```

```
205
```

しかし、 `E` に新たに値を代入しようとするとエラーが発生します。

```python
beam.E = 200
```

```
    beam.E = 200
    ^^^^^^
AttributeError: property 'E' of 'CantileverBeam' object has no setter
```

今回のカプセル化の意図とは反しますが、もし `@E.setter` というデコレータをつけたセッターメソッド (非公開属性の値を編集するメソッド) `E()` を定義すれば、このエラーを回避することができます。

`__init__()` メソッド内の `self.E` などを `self.__E` のように変えたおかげで、 `__E` 属性にはアクセスできなくなります。

```python
print(beam.__E)
```

```
    print(beam.__E)
          ^^^^^^^^
AttributeError: 'CantileverBeam' object has no attribute '__E'
```

このように、Python は先頭に2つのアンダースコア (`__`) をつけた属性の命名方法を使用することで、外部のコードからは見えないようにすることができます。

# まとめ

最終的に完成する全体のコードは次のようになります。

```python
class Load:
    def __init__(self, position):
        self.__a = position
    
    @property
    def a(self):
        return self.__a
    
    def displacement(self, beam):
        pass

class ConcentratedLoad(Load):
    def __init__(self, position, magnitude):
        super().__init__(position)
        self.__P = magnitude
    
    @property
    def P(self):
        return self.__P
    
    def displacement(self, beam):
        a = self.a
        b = beam.l - a
        P = self.P
        E = beam.E
        I = beam.I
        delta_A = (P * a ** 3) / (3 * E * I)
        theta_A = (P * a ** 2) / (2 * E * I)
        return delta_A + theta_A * b

class DistributedLoad(Load):
    def __init__(self, position, magnitude_per_unit_length):
        super().__init__(position)
        self.__q = magnitude_per_unit_length
    
    @property
    def q(self):
        return self.__q

    def displacement(self, beam):
        a = self.a
        b = beam.l - a
        q = self.q
        M = (q * b**2) / 2
        P = q * b
        E = beam.E
        I = beam.I
        delta_A = (M * a**2) / (2 * E * I) + (P * a**3) / (3 * E * I)
        theta_A = (M * a) / (E * I) + (P * a**2) / (2 * E * I)
        delta_B_dash = (q * b**4) / (8 * E * I)
        delta_B = delta_A + theta_A * b + delta_B_dash
        return delta_B

class CantileverBeam:
    def __init__(self, length, youngs_modulus, moment_of_inertia_of_area):
        self.__l = length # 長さ (mm)
        self.__E = youngs_modulus # ヤング率 (GPa)
        self.__I = moment_of_inertia_of_area # 断面二次モーメント (mm^4)

    @property
    def l(self):
        return self.__l

    @property
    def E(self):
        return self.__E

    @property
    def I(self):
        return self.__I

    def displacement(self, load):
        return load.displacement(self)
```

本記事では、機械工学で登場する計算を例に挙げ、継承・多態性・カプセル化といったオブジェクト指向プログラミングで重要な概念を実践しました。今後は、より高品質なクラス設計にするために、デザインパターンに当てはめることを検討しています。

# 参考文献

- 村上敬宜. (2021). *材料力学* (新装版). 森北出版.
- Lubanovic, B. (2021). *入門Python 3* (鈴木駿, Trans.; 第2版). オライリー・ジャパン.