やってみる

アウトプットすべく己を導くためのブログ。その試行錯誤すらたれ流す。

Pythonで共通の任意な親クラス、関数、変数名を持った、別名定義クラスをDRYに書きたい。(クラスインスタンスでなくクラスオブジェクト)

タイトルが伝わりにくい。試行錯誤しまくったログ。

成果物

GitHubPython.Class.201709191248

やりたかったこと

  • 再代入禁止にしたい
    • そのための実装を個別クラスに分けたい: type.__setattr__, metaclass
  • 共通処理を1箇所だけで実装したい: ふつうにclass定義する
  • 上記2件を実装しつつ、同一変数名をもった別名の型をつくりたい: type(name, bases, dict)
  • これらが同一型であると判定できるようにしたい: isinstance(obj, Type)

ソースコード

再代入禁止

class ConstMeta(type):
    class ConstError(TypeError): pass        
    def __init__(self, name, bases, dict):
#        print('__init__', name, bases, dict, self, type(self))
        super(ConstMeta, self).__init__(name, bases, dict)
        import sys
        sys.modules[name]=self()#ConstMetaを継承したクラスのモジュールに、そのクラスのインスタンスを代入する        
    def __setattr__(self, name, value):
        if name in self.__dict__.keys(): raise self.ConstError('readonly。再代入禁止です。')
        super(ConstMeta, self).__setattr__(name, value)

共通処理

class OctaveClass(metaclass=ConstMeta):
    #絶対値octave(国際式,YAMAHA)をOctaveClassに変換する
    # 国際式     :-1〜9
    # YAMAHA式   :-2〜8
    # OctaveClass:0〜10 (16進数表記は使えない。key(A〜G)と被るから)
    @classmethod
    def Get(cls, octave:int) -> int:
        cls.Validate(octave)
        if cls.Min < 0: return octave + abs(cls.Min)
        elif 0 < cls.Min: return octave - abs(cls.Min) # 最低値が0より大きい方式は現在存在しない
        else: return octave
        
    @classmethod
    def Validate(cls, octave:int):
        if not(isinstance(octave, int)): raise TypeError(f'引数octaveはint型にしてください。: type(octave)={type(octave)}')
        if octave < cls.Min or cls.Min+10 < octave: raise ValueError(f"引数octaveは{cls.Min}〜{cls.Min+10}までの整数値にしてください。: octave={octave}")

個別クラス作成

class_datas = (
{'name':'SPN', 'members': {'Min':-1, 'Names':{'ja': '国際式'}, 'Descriptions':{'ja': '88鍵盤の最低音がA0になる。'}}},
{'name':'YAMAHA', 'members': {'Min':-2, 'Names':{'ja': 'YAMAHA式'}, 'Descriptions':{'ja': '根拠不明。'}}},
{'name':'ZERO', 'members': {'Min':0, 'Names':{'ja': 'ゼロ式'}, 'Descriptions':{'ja': 'OctaveClassと同値。最低周波数のオクターブを0とする。'}}}
)

OctaveTypes = []
for cd in class_datas:
    Type = type(cd['name'], (OctaveClass,), cd['members'])
    OctaveTypes.append(Type)

同一名の変数を持たせて、値だけ変えたクラスオブジェクトを作る。

OctaveClassクラスオブジェクトだけ作って、そのクラスインスタンスを生成し、各種変数を上記のようにするなら簡単にできるはず。しかし今回は、インスタンス変数でなくクラス変数を対象としているため、面倒なことになった。

動作確認

print(OctaveTypes) for t in OctaveTypes: print(issubclass(t, OctaveClass)) print(t.Min) print(t.Names) print(t.Descriptions) OctaveTypes[0].Min = 100

* `SPN`, `YAMAHA`, `ZERO`, 3つとも `OctaveClass`のサブクラスである
* 3クラスとも`Min`, `Names`, `Descriptions`のクラス変数を持っている
* クラス変数は再代入禁止である(例外発生)

# 気づいたこと

## 難解なコードになってしまった

今回やりたかったことは、大したことではないはず。同じことをC#やJavaでやるなら継承するだけで済むはず。Pythonでは言語仕様や実行までのプロセスを自分で作りこまねばならないイメージ。

それがメタプログラミングの強みなんだろう。それ自体はいいけど、オブジェクト指向言語と謳うならカプセル化”も”できるようにしてくれたら嬉しい。どうしてもできることを期待してしまう。

### メタコードと本筋コードの区別がつきにくい

今回は1ファイルに全部書いたせいもある。

別ファイルに分けたとしても、プロジェクトごとに定数実装ファイルをコピペするのが面倒。

### クラス属性の操作に対するプログラミングが複雑化の主原因

インスタンス属性を扱うだけなら簡単にできたはず。今回はクラス属性の操作に対してプログラミングする必要があったので、metaclassを使わねばならなかった。すると一気にコードが煩雑になる。可読性も下がる。コード量も増える。

`__`,`cls`,`self`,`super(..., self).`などの頻発や使い分けの必要だけをみても冗長。

### 2種類あるクラス生成方法

クラスオブジェクトの作成に`class`構文と`type()`関数の2種類の方法を使ったので紛らわしい。どちらもクラスオブジェクト生成する方法であるが、統一した方法で実装したかった。とくに`type()`による生成はコードからクラス定義だとわかりにくい。

しかし、実装方法が分からず、試行錯誤していたらたまたまできたので今回の形に落ち着いた。

# 所感

こんな方法でいいのか?もっとスマートにできないの?