はじめに
オブジェクト指向プログラミング (object-oriented programming, OOP) とは、システムをオブジェクトの集合としてモデル化し、オブジェクトの構成とその動作を決定したうえでプログラミングを行う手法です。保守性や再利用性を向上させながら大規模で複雑なプログラムを作る際には、オブジェクト指向プログラミングの考え方は非常に重要となります。
しかしながら、初学者がオブジェクト指向プログラミングを学ぶときに使われる例は複雑なシステムを伴っていないことが多いです。対象とするオブジェクトは「車」や「動物」などの万人にとってイメージしやすいものの計算の実用性に乏しかったり、クラスに設計されるメソッドの機能は標準出力の些細な表現の違いのみにとどまっていたりするのが現状です。
そこで本記事では、オブジェクト指向プログラミングを実践する適度に複雑な具体例として、機械工学のいち分野である材料力学で登場する計算を取り上げます。材料力学におけるはりの曲げ問題の解法をオブジェクト指向プログラミングによって計算することを試み、オブジェクト指向プログラミングを深く理解し身につけることを目的とします。
はりの曲げ問題の概要
はり (梁; beam) とは、断面の寸法と比較して長さが長い構造部材のことで、建築物の梁、橋などが例にあげられます。はりに荷重 (Load) が作用するとはりは曲がります。これをはりの曲げ変形といい、この変形を力学的に考えるのがはりの曲げ問題です。
ミオソテスの方法とは、はりの問題の基本問題を利用することで、微分方程式を解かずに複雑な問題が容易に解ける方法のことをいいます。
はりの基本問題について説明します。次の図のように、長さ $l$ のはりに曲げモーメントや荷重が作用することにより、はりは曲がります。この場合のはり先端の変位を $\delta$, 先端の傾きを $\theta$ としこれらを求めます。なお図中の記号 $EI$ は、はりのヤング率が $E$, 断面二次モーメントが $I$ であることを表しています。
はりの基本問題は、つぎの図の3つにまとめられます。
(1) は曲げモーメント $M$ が、 (2) は集中荷重 $P$ がそれぞれ片持はりの先端に作用しています。(3) は片持はりの全体に、はりの単位長さ当たりに分布する荷重 (分布荷重) $q$ が作用しています。
プログラムの全体像
下記に、最終的に作成するプログラムのクラス図を示します。これらのクラスとその役割について次に説明します。
Load クラス
荷重を表す基本クラスです。属性 a
は、荷重の適用位置を表す浮動小数点数で、単位は mm です。メソッド displasement()
は、派生クラス (ConcentratedLoad
クラス、DistributedLoad
クラス) でオーバーライドされる、はりの変位を計算するための内部メソッドです。
ConcentratedLoad クラス
Load
クラスの派生クラスです。集中荷重を表します。属性 P
は集中荷重の大きさを表す浮動小数点数で、単位は kN です。ConcentratedLoad
クラスにおいて、属性 a
は次の図のように寸法を取ります。
DistributedLoad クラス
Load
クラスの派生クラスです。分布荷重を表します。属性 q
は、はりの単位長さ当たりに分布する荷重を表す浮動小数点数で、単位は kN/mm です。DistributedLoad
クラスにおいて、属性 a
は次の図のように寸法を取ります。
CantileverBeam クラス
片持はりを表すクラスです。下記にそれぞれの属性を説明します。
-
l
: はりの長さ (mm) -
E
: はりのヤング率 (GPa) -
I
: はりの断面二次モーメント ($\mathrm{mm^4}$)
メソッド displasement()
は、はりの変位 (mm) を計算します。
使用例
本プログラムでは、設定したはりに、設定した一つの荷重が作用したときに生じるたわみによる変位 (displasement) を計算します。本記事では 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
クラスを作成するには、次のようなコードになります。
class ConcentratedLoad:
pass
ConcentratedLoad
オブジェクトは、クラス名を呼び出して次のように作ることができます。
con_load = ConcentratedLoad()
属性
属性とは、そのオブジェクトが持つ変数のことです。本記事の例で言うと、片持はりオブジェクトは「長さ」「ヤング率」「断面二次モーメント」という3つの属性を持ちます。Python では、オブジェクトを作成している間あるいは作成したあとに、それに属性を与えることができます。
先ほどと同様に、空の片持はり CantileverBeam
クラスを作成し、そのオブジェクトを作ります。
class CantileverBeam:
pass
beam = CantileverBeam()
このオブジェクトに属性を追加するには、次のようなコードを書くことができます。
beam.l = 5000
beam.E = 205
beam.I = 8_333_333
定義した属性にアクセスすることも可能です。
print(beam.l)
print(beam.E)
print(beam.I)
5000
205
8333333
初期化
前節の方法では、情報を持った片持はりを作るために逐一属性を追加するコードを書かなければならず、非効率でミスが発生しやすいです。オブジェクトを作成するときにその属性に値を代入できるのが望ましいですが、それを実現するのが Python のオブジェクトの初期化です。 CantileverBeam
オブジェクトを作成するときに長さ、ヤング率、断面二次モーメントの値を渡せるようにするには、次のように __init__()
メソッドを使用します。
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
は、作られた個別のオブジェクト自体を参照することを示します。
値を渡した属性は直接読み書きすることができます。
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
クラスも、属性を持たせるために次のように定義します。
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
クラス内に記述したものが、次のコードになります。
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
の派生クラスとして定義することができそうです。これを実現するには、次のようなコードを書くことができます。
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
クラス内で呼び出します。
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
クラスにプロパティと名前マングリングを適用しています。
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
は、まるで属性のようにアクセスすることができます。
beam = CantileverBeam(5000, 205, 8_333_333)
print(beam.E)
205
しかし、 E
に新たに値を代入しようとするとエラーが発生します。
beam.E = 200
beam.E = 200
^^^^^^
AttributeError: property 'E' of 'CantileverBeam' object has no setter
今回のカプセル化の意図とは反しますが、もし @E.setter
というデコレータをつけたセッターメソッド (非公開属性の値を編集するメソッド) E()
を定義すれば、このエラーを回避することができます。
__init__()
メソッド内の self.E
などを self.__E
のように変えたおかげで、 __E
属性にはアクセスできなくなります。
print(beam.__E)
print(beam.__E)
^^^^^^^^
AttributeError: 'CantileverBeam' object has no attribute '__E'
このように、Python は先頭に2つのアンダースコア (__
) をつけた属性の命名方法を使用することで、外部のコードからは見えないようにすることができます。
まとめ
最終的に完成する全体のコードは次のようになります。
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版). オライリー・ジャパン.