結論
可読性を損なわない程度の利用にとどめるべきだなと思いました。
なんでもそうですが、得られるものよりも失うもののの方が多いなら不適切ですね。
getattr とは
オブジェクトの属性を動的に取得できる組み込みの関数で次のような構文です。
詳細は公式資料をご覧ください。
getattr(オブジェクト, 属性名, 属性がない場合に返却する値)
具体的な使い方
まず、食品の栄養素情報のデータクラスがあるとします。
@dataclass(frozen=True)
class FoodInformation:
name: str
energy: float
protein: float
fat: float
carbohydrates: float
食品の情報を動的に取得したい場合、次のように記述できます。
def get_nutrient_value(food: FoodInformation, nutrient: str) -> float | str:
return getattr(food, nutrient, f"{nutrient} 属性は存在しません")
boiled_egg = FoodInformation(
name="ゆでたまご",
energy=134,
protein=12.5,
fat=10.4,
carbohydrates=0.3
)
nutrient = "protein"
value = get_nutrient_value(boiled_egg, nutrient)
print(value) # 出力値:12.5
nutrient = "fiber"
value = get_nutrient_value(nutrient)
print(value) # 出力値:fiber 属性は存在しません
次に getattr を利用しない場合のコードを見てみます。
def get_nutrient_value(food: FoodInformation, nutrient: str) -> float | str:
if nutrient == "energy":
value = food.energy
elif nutrient == "protein":
value = food.protein
elif nutrient == "fat":
value = food.fat
elif nutrient == "carbohydrates":
value = food.carbohydrates
else:
value = f"{nutrient} 属性は存在しません"
return value
boiled_egg = FoodInformation(
name="ゆでたまご",
energy=134,
protein=12.5,
fat=10.4,
carbohydrates=0.3
)
nutrient = "fat"
value = get_nutrient_value(boiled_egg, nutrient)
print(value) # 出力値:10.4
想像してみてください、食物繊維の属性が増えたら?ビタミン B の属性が増えたら?アミノ酸スコアの属性が増えたら…。
繰り返しとなりますが、getattr は動的に属性を取得できるので便利だということです。
使うべきではないと思った場面
冷凍バナナを持ったら何でも叩きたくなるのと同じで、getattr の存在を知ってしまったらなんでもかんでも getattr したくなるのは当然です🐵
しかしながら、最終的に次のコードに戻ってくる話をします。始点であり現時点での終点です。
これは見てのとおり、min か max かによって比較演算子が変わるだけです。
if min_max == "max":
self.problem += (nutrient_value <= value, problem_name)
elif min_max == "min":
self.problem += (nutrient_value >= value, problem_name)
一応説明しますが、このコードはたんぱく質、脂質、炭水化物の比率を求めるプログラムで制約条件を設定するコードの一部です。
たんぱく質は体重の 2 倍のグラム数にしたり、脂質は 20% 以下にしたりする等、制約を与えています。
getattr を無理やり適用した結果は次のようなものになります。
import operator
min_max = "max"
operator_name = "ge" if min_max == "min" else "le"
comparison_operator = getattr(operator, operator_name)
self.problem += (comparison_operator(nutrient_value, value), problem_name)
もともと operator を利用する前提がなく、min や max から ge(greater equal) や le(less equal) に変換しているのもわかりづらい要因かと思いますが、その変換が無かったとしても結局わかりにくい気がします。
値を取得するだけならまだしも、関数を取得することで難易度が少し上がっているのだろうと思います。
getattr を使わずにまた別の書き方をする場合は辞書とラムダでしょうか。
comparison_operators = {
"max": lambda x, y: x <= y,
"min": lambda x, y: x >= y
}
comparison_operator = comparison_operators[min_max]
self.problem += comparison_operator(nutrient_value, value), problem_name
先述の operator と getattr でやっていたことを目に見える範囲でやっただけと言い換えることができるかもしれません。
これはそんなに悪くない気がしているので、気が変わったらこれを採用するかもしれないのですが、結局単純な if else に戻りました。
あらためて結論
getattr が便利なのはわかりましたが、できるかどうかではなく、やるべきかどうかは考えものだなあと思いました。
いろんなことに共通することかもしれませんが、数ある選択肢の 1 つとして持っておくのが大事で、何でもかんでも何も考えずに適用してしまうのは好ましくないなと思いました。