Python(Tableクラスの継承その1)

今回は以前に作成したTableクラスを継承して、新たな機能を持ったExtableクラスを作成しました。(前回作成したTableクラスについてはPython(Tableクラス設計)
継承先で追加したい機能は以下のようになります。

(1) 特定の行または列を削除するメソッドを持たせる。
→ 行(列)を削除すると行(列)数が変化することに留意
(2) 表の内容をリストのリストで返すメソッドを持たせる。
→ 省略された要素をどのように返すか
(3) 表の内容を転置するメソッドを持たせる。
(4) 特定のデータ型だけを要素とする機能を持たせる
→ isinstanceを使う

今回の継承クラスを作成する前に親となるTableクラスにいくつかの機能を追加した方が、継承先で同じような処理を何度も書く必要がないので、以下のような変更をしました。

①表の最大列数と最大行数をインスタンス変数として保存し、継承先でも利用できるようにする。
→ 行や列を削除したり、転置したりした時の更新処理用
②引数のインデックスが表の範囲外ではないか、値が表の範囲外でないかを確認するメソッドを追加。
③可読性を上げ、再利用可能にするように、内部関数をstaticmethodとして記述。
④ forループ 記述を できるだけリスト内包表記やジェネレータ式に変更。

class Table:
    def __init__(self,xss=[[]]):
        """リストを表として整形する
           @param xss  表示したい2次元リスト(行の要素ごとをリストとする)"""        
        if not all(isinstance(xs,list) for xs in xss):
            Table.err("要素がリスト型ではありません")
        
        self._axis_y = len(xss)
        self._axis_x = max(len(xs) for xs in xss) if xss else 0

        tmp_dict = {}            
        if xss != [[]]:
            for _y,xs in enumerate(xss):
                for _x,x in enumerate(xs):
                    tmp_dict[_y,_x] = x

        self._table_dict = tmp_dict

    def _validate_indexes(self, index, strict=False):
        try:
            _y,_x = index
        except:
            raise IndexError(f"{index}は不正なインデックスです")
            
        self._validate_axis_x_index(_x, strict)
        self._validate_axis_y_index(_y, strict)
        
    def _validate_axis_x_index(self, i, strict):
        Table.__validate_idx(i, self._axis_x, strict)
 
    def _validate_axis_y_index(self, i, strict):
        Table.__validate_idx(i, self._axis_y, strict)
 
    @staticmethod
    def __validate_idx(idx, maxnum, strict):
        """要素へアクセスするインデックスが利用可能かどうか調べる
           @param idx    辞書の要素へアクセスするインデックス
           @param max    登録されている要素の最大値
           @param strict 最大値の確認をするかどうかの真偽値"""
        if not isinstance(idx, int):
            raise IndexError(f"{idx}: インデックスは整数でなければなりません")
        if idx < 0 or (strict and idx >= maxnum):
            raise IndexError("out of range")

    def __make_string(self):
        """辞書に登録されている要素から表の文字列を作成する関数
           @param d 2次元配列の座標をタプルのキーとする辞書
           @return  辞書の座標に要素を入れて作成した表の文字列"""

        def check(x,y):
            """辞書内の要素をチェックして値を返す関数
               @param x 座標x
               @param y 座標y
               @return  該当する要素があればその値の文字列、
                        なければ' '(空白1つ)の文字列"""
            if (y,x) in self._table_dict:
                if self._table_dict[y,x] in ( None,[],(),{},"" ):
                    return " "
                else:
                    return str(self._table_dict[y,x])                    
            else:
                return " "
            
        tmp_cell_list = list(self._table_dict.keys())     
        if len(tmp_cell_list)==0:
            return "++\n++"
        
        tmp_list=[[check(x,y) for x in range(self._axis_x)] for y in range(self._axis_y)]            
        tmp_ps_list = [[len(tmp_list[y][x]) for y in range(self._axis_y)]
                       for x in range(self._axis_x)]            
        ps=[max(i) for i in tmp_ps_list]            
        str_line = Table.separator(("-" * i for i in ps),"+")
        str_table_list = [Table.separator(("%*s"%v for v in zip(ps,tmp_list[n])),"|")
                                                    for n in range(self._axis_y)]
        return Table.join_with_newline(str_line, str_table_list)

    @staticmethod
    def err(message=""):
        raise SyntaxError(message)

    @staticmethod
    def separator(s,sep):
        """リスト内の文字列を指定した文字列で区切り、結合する関数
        @param s   文字列
        @param sep 区切りに使用する文字列
        @return    指定した文字列で区切り、要素を結合した文字列"""
        return sep + sep.join(s) + sep

    @staticmethod
    def join_with_newline(delim, strs):
        """リスト内の文字列を指定した文字列で区切り、結合する関数
        @param delim 上段に付足す文字列
        @param strs  文字列のリスト
        @return      文字列を上段に追加し、要素を結合した文字列"""
        d = delim + '\n'
        return d + d.join(s+'\n' for s in strs) + delim

    def __str__(self):
        return self.__make_string()

    def __contains__(self,value):
        return value in self._table_dict.values()

    def __getitem__(self,index):
        self._validate_indexes(index, True)
        if index in self._table_dict:
            return self._table_dict[index]

    def __setitem__(self,index,value):
        self._validate_indexes(index)
        _y,_x = index
        self._axis_y = max(_y+1,self._axis_y)
        self._axis_x = max(_x+1,self._axis_x) 
        self._table_dict[index] = value

ではExTableクラスについて実装していきます。

仕様の(4)特定の型のみ入力可能 が最も簡単。該当箇所はコンストラクタ__init__と__setitem__の二箇所なので、Tableクラスのメソッドをオーバーライドすることで対応します。

from basetable01 import Table

class ExTable(Table):
    def __init__(self,xss=[[]],*,type=object):
        Table.__init__(self,xss)
        if not all(all(isinstance(x,type) for x in xs if x != None) for xs in xss):
            raise ValueError(f"データ型が{type}ではないデータが含まれています")
        self._type = type
        self.get_list()
def __setitem__(self, index, value):
        if not isinstance(value,self._type):
            raise ValueError(f"データ型が{self._type}ではありません")        
        super().__setitem__(index, value)
        self.get_list()

コンストラクタで特定の型を指定できるようにtypeを引数に追加し、デフォルトではint,float,str,list,Noneなど全てのオブジェクトの規定クラスとなるobject型を登録しています。
allメソッドとジェネレータ式で不適切な型がないかをチェックするようにしています。
※ ジェネレータ式内でif x != Noneと記述しているのは型を指定したときに空白部分をNoneと登録してもエラー判定されないようにするため。

__setitem__ではコンストラクタで登録した型と入力値をisinstanceメソッドでチェックして親クラスのメソッドに値を渡すだけです。

self.get_list()は仕様(2)表の内容をリストのリストで返すメソッド です。Tableクラスには値を辞書の形でしか持たせていないので、継承先のExTableクラスで表のデータを2次元配列として管理できるようにしました。

def get_list(self):
        """親Tableクラスのリストから2次元配列を得る関数"""
        def check(x,y):
            """辞書内の要素をチェックして値を返す関数
               @param x 座標x
               @param y 座標y
               @return  該当する要素があればその値、なければNone"""
            if (y,x) in self._table_dict:
                return self._table_dict[y,x]                    

        tmp_dict={}
        for index,value in self._table_dict.items():
            if value != None:
                tmp_dict[index] = value            
        self._table_dict = tmp_dict

        tmp_cell_list = list(self._table_dict.keys())
        n = range(len(tmp_cell_list))           
        tmp_y,tmp_x = 0,0
        for i in n:
            if tmp_y < tmp_cell_list[i][0]:
                tmp_y = tmp_cell_list[i][0]
            if tmp_x < tmp_cell_list[i][1]:
                tmp_x = tmp_cell_list[i][1]
                
        self._axis_x, self._axis_y = tmp_x+1, tmp_y+1
        self._table_list = [[check(x,y) for x in range(self._axis_x)]\
             for y in range(self._axis_y)]        
        return self._table_list

処理はTableクラスで実装済みの __make_string メソッドと同様の処理で、登録されている辞書の要素を元に表の要素をリストのリストとして登録して値を返すようにしています。辞書に登録されていない部分はNoneとして2次元配列には保管されています。

コアな部分は上記の処理ですが、今回は仕様(1)行または列の削除、仕様(3)転置によって辞書の変更をする必要があり、辞書の変更にget_listメソッドで作成した2次元配列を利用する場合、辞書にはNoneとして要素を追加する部分ができます。
そのため、2次元リストを作成する要素を返す内部関数checkメソッドを呼ぶ前に、辞書に登録されている値がNoneの要素は除外し、行と列の最大値をリサイズする処理を入れています。

この処理がない場合の実行結果は、下図のように空白の行や列が残った状態になります。

記事が長いので、 仕様(1)行または列の削除、仕様(3)転置 についての説明とExTableの全コードについては次回投稿分にまとめます。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

This site uses Akismet to reduce spam. Learn how your comment data is processed.