3
\$\begingroup\$

How can I improve this code (elegance, best practices, etc)?

""" Simple in-memory database as a response to the Thumbtack coding challenge. """
class SimpleDb(object):
 """ Simple in-memory database as a response to the Thumbtack coding challenge. """
 def __init__(self):
 """ Initialize SimpleDb instance. """
 self.__db = {}
 self.__num_equal_to_cache = {}
 self.__transactions = [] # Stores previous values to allow rollback
 def assign(self, name, value):
 """ Sets value of name to value. Inserts name into database if it doesn't already exist. """
 current_value = self.get(name)
 if current_value == value:
 return
 self.__update_num_equal_to(current_value, value)
 self.__update_current_transaction(name, current_value)
 self.__db[name] = value
 def get(self, name):
 """ Returns value of name if it exists in the database, otherwise returns None. """
 return self.__db[name] if name in self.__db else None
 def get_num_equal_to(self, value):
 """ Returns number of entries in the database that have the specified value. """
 return self.__num_equal_to_cache[value] if value in self.__num_equal_to_cache else 0
 def unset(self, name):
 """ Removes name from database if it's present. """
 current_value = self.__db.pop(name, None)
 if current_value is None:
 return
 self.__update_num_equal_to(current_value)
 self.__update_current_transaction(name, current_value)
 def begin(self):
 """ Opens transaction block. """
 self.__transactions += [{}]
 def rollback(self):
 """
 Reverts database to its state before most current transaction.
 Returns True on success, returns False if there aren't any open transactions.
 """
 if not self.__transactions:
 return False
 for name, value in self.__transactions.pop().iteritems():
 current_value = self.get(name)
 if current_value == value:
 continue
 self.__update_num_equal_to(current_value, value)
 if value is None:
 del self.__db[name]
 else:
 self.__db[name] = value
 return True
 def commit(self):
 """
 Commits all transactions to database. Returns True on success,
 returns False if there aren't any open transactions.
 """
 if not self.__transactions:
 return False
 self.__transactions = []
 return True
 def __update_num_equal_to(self, current_value, new_value=None):
 """
 Decrements by one the number items present with current_value (if current_value
 is not equal to None) and increments by one the number present with new_value
 (if new_value is not equal to None).
 """
 for amount_to_add, value in [(-1, current_value), (1, new_value)]:
 if value is not None:
 self.__num_equal_to_cache.setdefault(value, 0)
 self.__num_equal_to_cache[value] += amount_to_add
 def __update_current_transaction(self, name, value):
 """
 Stores current value of name if not already stored to most recent transaction
 (if any transactions open) to enable restoration of previous state on rollback.
 """
 if self.__transactions and name not in self.__transactions[-1]:
 self.__transactions[-1][name] = value
def display(value, default=None):
 """
 Prints value to stdout. If value is None and a default value is
 specified (and not None), then the default value is printed instead. Otherwise
 the None value is printed.
 """
 if value is None and default is not None:
 value = default
 print value
OPS = {
 'GET': (2, lambda db, name: display(db.get(name), "NULL")),
 'NUMEQUALTO': (2, lambda db, value: display(db.get_num_equal_to(value))),
 'UNSET': (2, lambda db, name: db.unset(name)),
 'BEGIN': (1, lambda db: db.begin()),
 'ROLLBACK': (1, lambda db: db.rollback() or display("NO TRANSACTION")),
 'COMMIT': (1, lambda db: db.commit() or display("NO TRANSACTION")),
 'END': (1, lambda db: False),
 'SET': (3, lambda db, name, value: db.assign(name, value)),
}
def process_command(simpleDb, command):
 """
 Parses string commands and applies them to the database.
 Returning False indicates that no more commands should be passed in.
 """
 command = command.split()
 opcode = command.pop(0).upper() if len(command) > 0 else None
 if opcode is None or opcode not in OPS or len(command) != (OPS[opcode][0] - 1):
 print "INVALID COMMAND"
 elif 'END' == opcode:
 return False
 else:
 OPS[opcode][1](simpleDb, *command)
 return True
def run():
 """ Reads database command from the command line and passes it through for processing. """
 # BEGIN \n SET a 30 \n BEGIN \n SET a 40 \n COMMIT \n GET a \n ROLLBACK \n END
 simpleDb = SimpleDb()
 while process_command(simpleDb, raw_input()):
 pass
run()
Jamal
35.2k13 gold badges134 silver badges238 bronze badges
asked Feb 18, 2014 at 2:08
\$\endgroup\$
2

1 Answer 1

6
\$\begingroup\$

I find your treatment of transactions odd. I would expect that when a transaction is in progress, data modifications commands get buffered; queries would consult the state stored in the current transaction for any relevant data, then any stacked transactions, then finally consulting the "real" database. Instead, you immediately "auto-commit" each command, and add an entry to the undo list; a commit simply discards the undo list. That's not a realistic model of a database: one would expect that a commit either succeeds atomically or has no effect at all on the database.

answered Feb 18, 2014 at 3:20
\$\endgroup\$
5
  • \$\begingroup\$ I was thinking just that. I've re-written the code such that no changes are made until the user sends the commit command. \$\endgroup\$ Commented Feb 18, 2014 at 6:49
  • 1
    \$\begingroup\$ You might want to post the revised code as a second question, referencing this one. \$\endgroup\$ Commented Feb 18, 2014 at 6:55
  • \$\begingroup\$ some databases do indeed work like this -- optimistically committing, and writing a transaction log at the same time. it makes rollback more expensive, but commit much cheaper as you don't need to re-read the destination data to check that it's changed while the the transaction was running, before finally committing (and writing the rows). (eg: microsoft sql server works this way, as does oracle) \$\endgroup\$ Commented Jul 8, 2015 at 7:06
  • \$\begingroup\$ @AndrewHill No matter how the transaction is implemented, it should still be atomic. \$\endgroup\$ Commented Jul 8, 2015 at 7:53
  • \$\begingroup\$ yes, row/page locks in sql server and temp storage of old versions for rollback purposes in oracle both prevent partially complete transactions from becoming visible; these dbms's are atomic, they just rely on more sophisticated ; how should the database behave when when 2 transactions try to modify the same row / or read a row written but not commited? \$\endgroup\$ Commented Jul 8, 2015 at 23:47

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.