pythonのsetattrを使って、CSVヘッダーをModelクラスの属性名として動的に追加する


pythonにはcsvを読み込む便利なモジュールがありますよね。あれを使ってDBのテストレコードを挿入を試してみました。その際にModelにCVSの各カラム値を、下記のように属性名を記述して代入をしていると、カラム値・Modelクラスの変更に伴いソースコードの変更が発生します。これをなくすためのリファクタリングをしていきます。

setattrのメリット、使い道はリフレクトでも分かるかもしれませんが、このサンプルのほうが分かりやすいかと思います。

実行環境はPython 3.4.3を想定しています。

```python
import csv

class Model():
    pass

if __name__ == '__main__':
    models = []
    with open('person.csv', 'r') as f:
        reader =  csv.reader(f)
        # 1行だけ読み込む（ヘッダー）
        Model._columns = next(reader)
        for row in reader:
            model = Model()
            #動的にmodelに属性をセット
            for i in range(len(row)):
                model.coulmn1 = row[0]
                model.coulmn2 = row[1]
            models.append(model)
```

### 属性のセットにsetattrを使う

先に今回使うcsvファイルを掲示しておきます。

```csv
id,name,age
1,Taro,19
2,Ziro,18
3,Mikio,40
4,Mineko,38
```

pythonにはsetattrという組み込み関数があります。`setattr(オブジェクト,属性名,値）`として使います。これの何がいいと言うと、属性名を変数で指定することができます。`obj.var1`にするとobjの中にvar1がないか探してしまいます。もしvar1が変数で展開して欲しい場合は`setattr()`を使いましょう。

```python
import csv

class Model():
    pass

if __name__ == '__main__':
    models = []
    with open('person.csv', 'r') as f:
        reader =  csv.reader(f)
        # 1行だけ読み込む（ヘッダー）
        columns = next(reader)
        for row in reader:
            model = Model()
            #動的にmodelに属性をセット
            for i in range(len(row)):
                setattr(model, columns[i], row[i])
            models.append(model)

    for model in models:
        print(model)

# =>
# <__main__.Model object at 0x1053169b0>
# <__main__.Model object at 0x105316a58>
# <__main__.Model object at 0x105316b00>
# <__main__.Model object at 0x105316ba8>
```

### 属性一覧の順番を維持する

今のままだとオブジェクトの属性が出力されないので、dirを使わないでもデフォルトで出力されるようにします。pythonではprint文で出力する際に`__str__`という特殊メソッドが実行されます。このメソッドで表示したい文字列を返してあげれば良いです。

接頭辞が_（アンダーバー）ではないものは表示から除くようにしています。これによりModelに内部処理のためのメソッドなどを定義したい場合は、_（アンダーバー）をつけてあげれば良いです。

```python
import csv

class Model():
    # 要素の長さと比較するときに、属性数に含まれてしまうため
    # アンダーバーをつける
    _columns = []

    def __init__(self):
        if len(self._columns) == 0:
            raise Exeption("Please set the columns of %s" % self)

    def __str__(self):
        attribute_str = ""
        # 接頭辞が_のものを除外
        attributes = list(filter(
                            lambda attribute_name: not(attribute_name[0].startswith("_")),
                            dir(self)))

        if len(attributes) == len(self._columns):
            # selfにcolumnsはないため、クラス変数を参照
            for attribute in self._columns:
                attribute_str += "%s = %s \n" % (attribute,
                                                 getattr(self, attribute))
        else:
            raise Exception("Incorrect the number of %s.columns" % self)

        return attribute_str

if __name__ == '__main__':
    models = []
    with open('person.csv', 'r') as f:
        reader =  csv.reader(f)
        # 1行だけ読み込む（ヘッダー）
        Model._columns = next(reader)
        for row in reader:
            model = Model()
            #動的にmodelに属性をセット
            for i in range(len(row)):
                setattr(model, Model._columns[i], row[i])
            models.append(model)

    for model in models:
        print(model)

# =>
# id = 1 
# name = Taro 
# age = 19 
# 
# id = 2 
# name = Ziro 
# age = 18 
# 
# id = 3 
# name = Mikio 
# age = 40 
# 
# id = 4 
# name = Mineko 
# age = 38
```