利用字典和列表实现数据表格(一)

实现数据表格的目的

经常要在手机上通过pythonista练习python,因为Pythonista不支持pandas, 无法使用数据表格。很多时候需要用到数据表格,反复用字典和列表的组合显得很麻烦,所以就想按照pandas的思路,自己写一个简单的数据表格对象,把它当作一种学习。源代码公布在Github:
https://github.com/radiantbk/datasheet/blob/master/dsheet1.0.0.py

设计思路

作为一个数据表格,最基本的元素是row和col。利用字典和列表的特性,可以把列表作为row,每个列表元素作为一行,列的信息以字典的形式存在,每一个字典作为列表的元素,代表一行的数据。字典的键为字段名,键对应的值作为一个单元格的值。

dic1 = {'name':'a','age':20,'gender':'male'}
dic2 = {'name':'b','age':21,'gender':'female'}
dic3 = {'name':'c','age':18,'gender':'female'}
list1 = [dic1,dic2,dic3]

基于以上代码,每一个字典代表一行的数据,字典里的每一个键代表一个字段名,这构成了一个表格的雏形。作为一个表格对象,它需要有表格名、字段名,索引,数据;需要具备添加一列、添加一行、删除一行、删除一列等方法;字典列表的形式不便于阅读,最好能显示得想表格一样。所以这个对象应该有name(表格名)、colname(字段名)、表格内容等属性,索引在形成表格时作为表格的第一列添加。涉及的对象方法包括生成表格、更新字段名、增加一行、增加一列、截取部分表格内容、显示表格等方法。

实现过程

  1. 定义dsheet类,设置name,colname,ds(数据表)属性,并在创建dsheet对象时自动生成ds。代码如下:
 class dsheet():
  def __init__(self,data,col_name=''):
    self.name=''
    self.ds=[]
    self.colname=col_name
    self.__setds(data,col_name)
  1. 定义__setds(self,data,col_name) ,data是要创建表格的值,可以是普通列表,也可以是二维列表。col_name是字段名,支持不整字段名,当字段名小于要创建的字段数量时,超出的部分字段名为空。无返回值,但是会更新ds内容,并触发字段名更新检查。代码如下:
  def __setds(self,data,col_name):#不建议从外部调用
    dic={}
    dic_li=[]
    if type(data) != list: #data必须为列表,否则创建失败,返回空值
      print('data shoud be list')
      return None
    else:
      for i in range(len(data)):
        dic={'row':i}
        dic_li.append(dic)
    maxlen=1
    for ea in data:
      if type(ea)==list: #data可以是二维列表,当是二维列表时,生成多个字段
        if len(ea)>maxlen:
          maxlen=len(ea)
    for i in range(len(dic_li)):
      for ci in range(maxlen):
        if type(data[i])==list:
          try:
            col = col_name[ci]
          except:
            col ='' #获取字段名失败时,将字段名相应的设置为空,确保字段列表不完成,仍然可以生成数据表
          try:
            coldata = data[i][ci] #获取某一个数据单元值失败时,将数据单元值设置为None,确保二维列表元素不完整时仍然可以生成数据表。
          except:
            coldata = None
          dic={col:coldata}
          dic_li[i].update(dic)
        else:
          try:
            col = col_name[ci]
          except:
           col =None
          dic={col:data[i]}
          dic_li[i].update(dic)
    self.ds = dic_li
    self.__updatecol() #生成数据表后,检查字段名,更新字段名
  1. 更新字段名。当DS有变化时,应该调用该方法,更新字段名。
  def __updatecol(self):
    if len(self.ds) == 0:
      print('ds is empty, can not update column')
    else:
      collist = []
      for ea in self.ds[0].keys():
        collist.append(ea)
    self.colname=collist
  1. 在列的末端增加一列,addcol(self, name,data)。name为要增加的字段名,data是增加列的内容。data不需要是列表,如果不是列表,该列所有行的值都是一样是data的值。name不能为空,既然是增加,还是要给人家一个名分嘛。需要强调的是,data是列表时,如果长度小于行数,剩下的行的值会被设置为None。返回值为self,因此这个方法执行以后,会返回一个新的ds。代码如下:
  def addcol(self,name,data):
    dic={}
    if type(data) != list:
      for i in range(len(self.ds)):
        dic={name:data}
        self.ds[i].update(dic)
    else:
      for i in range(len(self.ds)):
        try:
          dic_data = data[i]
        except:
          dic_data = None
        dic={name:dic_data}
        self.ds[i].update(dic)
    self.__updatecol()
    return self
  1. 在行的末端增加一行,addrow(self, data)。data是要增加的行数据,data 可以是列表也可以是一个单值。是单值时,增加行的个字段值均为data;是列表时,如果列表的元素个数小于字段数时,剩下的字段会被赋值为None。返回值为self,因此这个方法执行以后,会返回一个新的ds。代码如下:
  def addrow(self,data):
    dic={}
    n=len(self.ds)
    dic['row']=n
    if type(data) != list:
      for i in range(1,len(self.colname)):
        dic[i]=data
    else:
      for i in range(1,len(self.colname)):
        try:
          dic_value = data[i-1]
        except:
          dic_value=None
        dic[self.colname[i]]=dic_value
    self.ds.append(dic)
    return self
  1. loc(row,col=’’),类似于pandas dataframe的loc方法,支持部分截取数据表内容,row代表的时要截取的行范围,":"代表所有行(默认值),"0:3"代表从第一行到第三行(以0为起点,右边不包含)。col代表要截取的字段范围,默认为空(代表截取所有字段的内容);如果col的值不为空,它必须是要截取的字段范围的列表(如:col=[‘name’,‘age’,‘gender’])。需要注意的是,如果col列表里的字段名有不存在与数据表的情况,会提示错误,返回None. 方法会返回一个新的ds,原来的ds不会改变。代码如下:
  def loc(self,row,col=''):
    row_range = ''
    row_num=0
    col_num=0
    return_li=[]
    return_info = 'row number showed:'+str(row_num)+', col number showed:'+str(col_num)
    if col!='': #如果字段名的范围是部分,检查传入的字段名
      if type(col)==list:
        for ea in col:
          if ea not in self.colname: #发现有字段名在数据表不存在,返回None
            print('%s not in colname list'%ea)
            print(return_info)
            return None
      else: #如果字段名不为空,又不是列表,提示错误返回None.
        print('colname should be list, not %s'%(type(col)))
        print(return_info)
        return None
    if row == ':':
    #行的范围是全部时,确定字段范围,返回对应字典键的值,并将它存于一个新的列表里
      if col =='':
        col=self.colname
      for ea in self.ds:
        dic_loc={}
        for ek in ea.keys():
          if ek in col:
            dvalue=ea[ek]
            dic_loc[ek]=dvalue
        return_li.append(dic_loc) 
    else:
    #行的范围是部分,先选择行的范围,再确定字段范围,返回对应字典键的值,
    #并将它存于一个新的列表里
      row_range=row.split(':')
      row_start = int(row_range[0])
      row_end = int(row_range[1])
      if row_start >=row_end:
        print('start of row should be smaller than end of row')
        return None
      else:
        if col=='':
          col=self.colname
        for n in range(row_start,row_end):
          data = ''
          dic_loc={}
          for ek in col:
            dic_dsn=self.ds[n]
            dvalue = dic_dsn[ek]
            dic_loc[ek]=dvalue
          return_li.append(dic_loc)
    new_data=[]
    new_col=[]
    for ec in return_li[0].keys():
      new_col.append(ec)
    for ed in return_li:
      new_value=[]
      for ek in ed:
        new_value.append(ed[ek])
      new_data.append(new_value)
    new_ds = dsheet(new_data,new_col)
    return new_ds
  1. show(row,col=’’)方法,参数及代码和loc方法类似,它仅仅返回选定的数据表范围包含多少行和多少列。它的作用是通过print函数显示数据表的选定内容。代码如下:
 def show(self,row,col=''):
    row_range = ''
    row_num=0
    col_num=0
    return_li=[]
    return_info = 'row number showed:'+str(row_num)+', col number showed:'+str(col_num)
    if col!='':
      if type(col)==list:
        for ea in col:
          if ea not in self.colname:
            print('%s not in colname list'%ea)
            return return_info
      else:
        print('colname should be list, not %s'%(type(col)))
        return return_info
        
    col_range= len(self.colname)
    if row == ':':
      if col =='':
        col_num=len(self.colname)
        head = ','.join(self.colname)
        col=self.colname
        print(head)
      else:
        col2=col.copy()
        col_num=len(col)
        head=','.join(col2)
        print(head)
      for ea in self.ds:
        data=''
        for ek in ea.keys():
          if ek in col:
            dvalue=ea[ek]
            data += str(dvalue)+','
        data=data[:-1]
        row_num += 1
        print(data)
    else:
      row_range=row.split(':')
      row_start = int(row_range[0])
      row_end = int(row_range[1])
      if row_start >=row_end:
        print('start of row should be smaller than end of row')
        return None
      else:
        if col=='':
          col_num=len(self.colname)
          head = ','.join(self.colname)
          col=self.colname
          print(head)
        else:
          col_num=len(col)
          col2=col.copy()
          head=','.join(col2)
          print(col2)
          print(head)
        for n in range(row_start,row_end):
          data = ''
          for dkey in col:
            data += str(self.ds[n][dkey])+','
          data=data[:-1]
          row_num+=1
          print(data)
    return_info = 'row number showed:'+str(row_num)+', col number showed:'+str(col_num)
    return return_info

试运行代码

  1. 运行创建代码,并运行show()。
  l1=['beijing','shanghai','guangzhou','shenzhen']
  ds1 = dsheet(l1,['city'])
  print(ds1)
输出结果如下:
<__main__.dsheet object at 0x000001F7789823C8>
>>> 
>运行如下代码:
>if __name__=='__main__':
  l1=['beijing','shanghai','guangzhou','shenzhen']
  ds1 = dsheet(l1,['city'])
  ds1.show(':')
输出结果如下:
  row,city
0,beijing
1,shanghai
2,guangzhou
3,shenzhen
>>> 
  1. 运行addrow,addcol方法,看看增加行和列的效果
if __name__=='__main__':
  l1=['beijing','shanghai','guangzhou','shenzhen']
  ds1 = dsheet(l1,['city'])
  l2=[1,2,3,4]
  ds1=ds1.addcol('rank',l2)
  ds1.show(':')
 输出结果如下:(增加了一列rank)
 row,city,rank
0,beijing,1
1,shanghai,2
2,guangzhou,3
3,shenzhen,4

再增加一行,代码如下:
if __name__=='__main__':
  l1=['beijing','shanghai','guangzhou','shenzhen']
  ds1 = dsheet(l1,['city'])
  l2=[1,2,3,4]
  ds1=ds1.addcol('rank',l2)
  l4=['chengdu','5']
  ds1=ds1.addrow(l4)
  ds1.show(':')
  输出结果如下:
row,city,rank
0,beijing,1
1,shanghai,2
2,guangzhou,3
3,shenzhen,4
4,chengdu,5
>>> 

运行loc方法,部分选择数据表内容,检查loc方法是否会生成一个新的ds

再增加一列GDP,通过loc方法选择第二行和第三行的GDP和City.同时检查loc方法是否改变了ds1的内容。
if __name__=='__main__':
  l1=['beijing','shanghai','guangzhou','shenzhen']
  ds1 = dsheet(l1,['city'])
  l2=[1,2,3,4]
  ds1=ds1.addcol('rank',l2)
  l4=['chengdu','5']
  ds1=ds1.addrow(l4)
  l5=[2400,2900,2100,2200,1200]
  ds1=ds1.addcol('GDP',l5)
  ds2=ds1.loc('1:3',['city','GDP'])
  ds2.show(':')
  ds1.show(":")
 输出的结果如下:
 row,city,GDP
0,shanghai,2900
1,guangzhou,2100

row,city,rank,GDP
0,beijing,1,2400
1,shanghai,2,2900
2,guangzhou,3,2100
3,shenzhen,4,2200
4,chengdu,5,1200
>>> 发现ds2是一个独立于ds1的表格,它不会改变ds1的内容。

总结

以上代码初步实现了数据表的基本功能,但是还有很多功能需要进一步完善,例如:删除行或列(当然可以通过loc方法来实现,但最好能有一个独立简单的方法)、分类统计(类似于dataframe groupby),等将在下一篇博文里分享。


版权声明:本文为weixin_42101791原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。