user1880836 user1880836 - 25 days ago 5
Python Question

Values from a class instance's attribute being added to a different instance of the same class

I'm parsing pdfs to extract table data using my PdfTable class. When I create a class instance then create another class instance it seems that the first class instance file_1.cells are being prepended to the second class instance file_2.cells. I cannot figure out why this is happening as I don't think I'm creating class variables but only instance variables. For some reason the data from set_cells is persisted when another class instance is instantiated. What is happening?

from pdfminer.pdfdocument import PDFDocument
from pdfminer.pdfpage import PDFPage
from pdfminer.pdfparser import PDFParser
from pdfminer.pdfinterp import PDFResourceManager, PDFPageInterpreter
from pdfminer.converter import PDFPageAggregator
from pdfminer.layout import LAParams, LTTextBox, LTTextBoxHorizontal, LTTextLineHorizontal
from tabulate import tabulate
from utils import clean_string
from collections import namedtuple


class PdfTable(object):

def __init__(self, file_name):
self.file_name = file_name
self.table_headers = None
self.cells = None
self.rows = None

def process_file(self, file_name):
pages = []
with open(file_name, 'rb') as fp:
parser = PDFParser(fp)
doc = PDFDocument(parser)
rsrcmgr = PDFResourceManager()
laparams = LAParams()
device = PDFPageAggregator(rsrcmgr, laparams=laparams)
interpreter = PDFPageInterpreter(rsrcmgr, device)

for page in PDFPage.create_pages(doc):
interpreter.process_page(page)
pages.append(device.get_result())

return pages

def set_table_headers(self, page_obj, table_headers={}):
values = ('NAME', 'VALUE', 'REFERENCE RANGE')
Header = namedtuple('Header', 'name, x0, y0')

for obj in page_obj:
if isinstance(obj, LTTextLineHorizontal):
text = clean_string(obj.get_text())
if text in values:
table_headers[text] = Header(text, obj.bbox[0], obj.bbox[1])
elif isinstance(obj, LTTextBoxHorizontal):
self.set_table_headers(obj, table_headers)

return table_headers

def set_cells(self, page, headers, cells=[]):
Cell = namedtuple("Cell", "col, text, x0, y0")
col = None
text = None

for obj in page:
if isinstance(obj, LTTextLineHorizontal):
obj_x0 = obj.bbox[0]
obj_y0 = obj.bbox[1]

if obj_y0 < headers['NAME'].y0 and (obj_x0 == headers['NAME'].x0 or
obj_x0 == headers['VALUE'].x0 or obj_x0 == headers['REFERENCE RANGE'].x0):

if obj_x0 == headers['NAME'].x0:
col = 'NAME'
elif obj_x0 == headers['VALUE'].x0:
col = 'VALUE'
elif obj_x0 == headers['REFERENCE RANGE'].x0:
col = 'REFERENCE RANGE'

text = clean_string(obj.get_text())
if text:
cells.append(Cell(col, text, obj_x0, obj_y0))

elif isinstance(obj, LTTextBoxHorizontal):
self.set_cells(obj, headers, cells)

return cells

def set_rows(self, cells):
Cell = namedtuple("Cell", "col, text, x0, y0")
Row = namedtuple('Row', 'test, value, ref_range, y0')

name_col = [cell for cell in cells if cell.col == 'NAME']
value_col = [cell for cell in cells if cell.col == 'VALUE']
ref_col = [cell for cell in cells if cell.col == 'REFERENCE RANGE']

# normalize val col with blank cells to match name col length
values_y0 = [cell.y0 for cell in value_col]
missing_val_cells = [cell.y0 for cell in name_col if cell.y0 not in values_y0]
value_col += [Cell('VALUE', None, None, y) for y in missing_val_cells]

rows = [Row(name_cell.text, value_cell.text, ref_cell.text, name_cell.y0)
for name_cell in name_col for value_cell in value_col
for ref_cell in ref_col
if name_cell.y0 == value_cell.y0 == ref_cell.y0]

return rows

def parse_pages(self):
pages = self.process_file(self.file_name)
self.set_metadata(pages[0])

for page in pages:
self.table_headers = self.set_table_headers(page)
self.cells = self.set_cells(page, self.table_headers)

self.rows = self.set_rows(self.cells)


if __name__ == "__main__":
file_1 = PdfTable("RawData/pdfs/3768958-2.pdf")
file_1.parse_pages()

print("file_1 cells")
print tabulate(file_1.cells, headers="keys", showindex="always")

file_2 = PdfTable("RawData/pdfs/3768959.pdf")
file_2.parse_pages()

print("\nfile_2 cells")
print tabulate(file_2.cells, headers="keys", showindex="always")


file_1.cells

col text x0 y0
-- --------------- --------------- -------- -------
0 NAME TP 42.8571 570.887
1 NAME RIN 42.8571 554.172
2 VALUE 13.5 221.716 570.887
3 VALUE 1.0 221.716 554.172
4 REFERENCE RANGE 11.8-14.2 (SEC) 412.555 570.887
5 REFERENCE RANGE 0.8-1.2 412.555 554.172


file_2.cells

col text x0 y0
-- --------------- -------------------- -------- -------
0 NAME TP 42.8571 570.887
1 NAME RIN 42.8571 554.172
2 VALUE 13.5 221.716 570.887
3 VALUE 1.0 221.716 554.172
4 REFERENCE RANGE 11.8-14.2 (SEC) 412.555 570.887
5 REFERENCE RANGE 0.8-1.2 412.555 554.172
6 NAME RSW 42.8571 570.887
7 NAME BCW 42.8571 554.172
8 VALUE 8.7 221.716 570.887
9 VALUE 25.6 221.716 554.172
10 REFERENCE RANGE 4.5-12.5 412.555 570.887
11 REFERENCE RANGE 14.0-30.0 412.555 554.172


expected file_2.cells

col text x0 y0
-- --------------- -------------------- -------- -------
0 NAME RSW 42.8571 570.887
1 NAME BCW 42.8571 554.172
2 VALUE 8.7 221.716 570.887
3 VALUE 25.6 221.716 554.172
4 REFERENCE RANGE 4.5.-12.5 412.555 570.887
5 REFERENCE RANGE 14.0-30.0 412.555 554.172


Not only are file_1.cells prepended to file_2.cells but after processing file_2, file_1.cells are the combination of cells from both instances.

Answer

You have mutable default arguments table_headers={} and cells=[] which is probably the issue, or at least can cause other issues. These values are shared across calls to the methods so that changes in one call are reflected elsewhere.