Advice Criticism on Python App
MRAB
python at mrabarnett.plus.com
Tue Mar 23 21:36:31 EDT 2010
Jimbo wrote:
> I have made a Python App(really script) that will check a stocks
> current values from a website & save that data to a SQLite 3 database.
>
> I am looking for any suggestions & criticisms on what I should do
> better or anything at all but mainly in these areas:
> [QUOTE]
> - Correct Python Layout of code
> - Correct error checking: Am I catching all my errors or are my
> exceptions not specific enough? Should I be using more try excepts
> inside my functions?
> - Are there any areas where huge errors, bugs etc could occur that I
> have not compensated for?
> - I am also looking for suggestions on how to write a function better,
> so if you think that function is really bad & should be rewritten
> completely or something I would really like to hear it.
> - Anything else that you see
> - Is python meant to be used in the way I have used it? To make a
> 'semi detailed' app?[/QUOTE]
>
> Any advice criticism would be really helpful.
>
> App:
> [CODE]"""
> *Stock Data Builder*
> Algorithm:
> - Search website for stock
> - Get website HTML source code
> - Search code for target stock data(price,dividends,etc..)
> - Add data to text file
> """
>
> import sys
> import os
> import sqlite3
> import datetime
> import time
> import urllib2
>
> ### Global Variables ###
> menu = "***Stock Program*** \n\n1. Add a Stock to track \n2. Get
> Todays Tracking Data \n3. Exit \nEnter decision: "
> target = '<th scope="row" class="row"><a href="/asx/research/
> companyInfo.do?by=asxCode&asxCode=%s">%s</a>'
> ASXurl = 'http://www.asx.com.au/asx/markets/priceLookup.do?
> by=asxCodes&asxCodes='
>
> class stock:
> code = ""
> purchasePrice = 0
> purchaseQuantity = 0
> price = [] # list of recent prices
> recentBid = [] # list of recent bids for stock
> recentOffer = [] # list of recent offers for stock
> stockVol = [] # list of stock quantity available on
> market
This will be variables belonging to the class itself, not instances of
it.
> def __init__(self):
> """ Default Constructor """
> self.code = ""
> self.purchasePrice = 0
> self.purchaseQuantity = 0
>
> def constructor(self, stockCode, purPrice, purQuant):
> """ Constructor """
> self.code = stockCode
> self.purchasePrice = purPrice
> self.purchaseQuantity = purQuant
>
> def setData(self, stockCode, purPrice, purQuant, priceList,
> reBidList, reOffList, popList):
> """ Defines & implements the objects' public variables """
> self.code = stockCode
> self.purchasePrice = purPrice
> self.purchaseQuantity = purQuant
> self.price = priceList
> self.recentBid = reBidList
> self.recentOffer = reOffList
> self.stockVol = popList
>
> self.printStats()
>
> def updateData(self, priceEle, bidEle, offerEle, populEle):
> """ Adds data to stock object's lists """
> self.price.append(priceEle)
> self.recentBid.append(bidEle)
> self.recentOffer.append(offerEle)
> self.stockVol.append(populEle)
>
> def printStats(self):
> """ Output Stock attributes """
>
> print("Stock Code: "+self.code)
In Python 2 'print' is a statement, so it doesn't need its arguments to
be enclosed in (). You could also use Python's string formatting:
print "Stock Code: %s" % self.code
> print("Stock Purchase Price: "+str(self.purchasePrice))
> print("Stock Quantity Owned: "+str(self.purchaseQuantity))
> print("***Initial Investment Value:
> "+str(self.purchasePrice*self.purchaseQuantity))
> if not(len(self.price) <= 0):
'not' has a lower priority than '<=', and this can be simplified anyway:
if len(self.price) > 0:
or even:
if self.price:
because empty containers (eg lists) are treated as False, non-empty ones
as True, by 'if' and 'while' statements.
> print("Stock Current Price: "+str(self.price[-1]))
> print("Recent Bid: "+str(self.recentBid[-1]))
> print("Recent Offer: "+str(self.recentOffer[-1]))
> print("Total Stock Volume in market:
> "+str(self.stockVol[-1]))
> print("***Present Investment Value:
> "+str(self.price[-1]*self.purchaseQuantity))
> print("\n")
>
>
> ### Functions ###
> def connectDatabase(dbLocation, dbName, tableName):
> """ Establish & Return connection to SQLite Database """
>
> try:
> if not (os.path.exists(dbLocation)):
> os.mkdir(dbLocation) # create folder/dir
>
> os.chdir(dbLocation) # change directory focus to
> dbLocation
It's normally easier to use absolute paths instead of changing the
current directory.
> conn = sqlite3.connect(dbLocation+dbName)
It's better to join paths using os.path.join() because that will insert
any directory separators for you.
> cur = conn.cursor()
> try:
> createTableQ = "CREATE TABLE IF NOT EXISTS "+tableName
> +" (code varchar PRIMARY KEY, purchase_price float, purchase_quantity
> float, purchase_date varchar);"
> cur.execute(createTableQ)
> conn.commit()
> except:
> pass
DON'T USE BARE EXCEPTS, especially if you're just going to ignore the
exception. Exceptions can be raised for a variety of reasons, including
one you didn't expect due to bugs, so catch only what you expect.
> return conn
> except IOError or OSError:
The result of:
IOError or OSError
is:
IOError
What you want is:
except (IOError, OSError):
Note: the parentheses are necessary here, otherwise it it would mean
"catch an IOError exception and then bind it to the name 'OSError'".
> print "Connection to database failed"
> return False
>
Some times you return the connection, and other times False. A more
usual value than False in such cases is None (unless you're returning
True or False).
> def retrieveStockDatabase(conn, tableName):
> """ Read SQLite3 database & extract stock data into StockList """
>
> stockList = []
> stockQuery = "select recent_price, recent_offer, recent_bid,
> stock_volume from ? ;"
> cur = conn.cursor()
> cur.execute("select code, purchase_price, purchase_quantity from
> "+tableName+";")
>
> for row in cur.fetchall():
> newStock = stock()
> newStock.code = row[0]
> newStock.purchasePrice = row[1]
> newStock.purchaseQuantity = row[2]
> cur.execute(stockQuery,[newStock.code])
> for rw in cur.fetchall():
> newStock.price.append(rw[0])
> newStock.recentOffer.append(rw[1])
> newStock.recentBid.append(rw[2])
> newStock.stockVol.append(rw[3])
> stockList.append(newStock)
>
> return stockList
>
> def getDate():
> """ Return todays date in format DD:MM:YYYY """
> time = datetime.datetime.now()
> date = time.strftime("%d:%m:%Y") # string format time (%y)
> return date
>
> def newStockDatabase(conn, stockTable, stock):
> """ Add a new stock to SQLite database if not already there
> We save the stocks code, purchase price, quantity purchased
> & date of purchase. """
> cur = conn.cursor()
> try:
> createTableQ = "create table "+stock.code+" (date varchar
> PRIMARY KEY, recent_price float, recent_offer float, recent_bid float,
> stock_volume double);"
> stockQuery = "insert into "+stockTable+"
> values(?, ?, ?, ?);"
> cur.execute(createTableQ)
> cur.execute(stockQuery,
> [stock.code,stock.purchasePrice,stock.purchaseQuant,getDate()])
> conn.commit()
> except IOError or OSError:
> print "Table may already exist or bad SQLite connection."
> return False
>
> def webFormat(URL):
>
> if (URL.startswith("http://")==False):
> URL = "http://"+URL
>
Better as:
if not URL.startswith("http://"):
The conditions of 'if' and 'while' statements don't need to enclosed in
parentheses like in C, Java, etc.
> return URL
>
> def getSource(URL):
> """ Retrieve HTML source code from website URL &
> save in sourceBuffer """
>
> try:
> URL = webFormat(URL) # make sure URL contains essential
> "http://"
> sourceBuffer = urllib2.urlopen(URL)
> print '\nResponse code = ',sourceBuffer.code
> print 'Response headers = ',sourceBuffer.info()
> print 'Actual URL = ',sourceBuffer.geturl()
> sourceCode = sourceBuffer.read()
> sourceBuffer.close()
> return sourceCode
>
> except IOError: # URLError
> print "Function Failed: Reasons could be invalid URL name \nOR
> \nHTML protocol message transfer failure."
> return False # function failed
>
> def getTargetText(targetStrtData, targetEndData, dataBuffer):
> """ Grabs target text that lies inside 'dataBuffer' string
> between targetStrtData & targetEndData """
>
> try:
> result = dataBuffer.split(targetStrtData)
> result.pop(0)
> result = result[0].split(targetEndData)
> result.pop(1)
> print result
> return result
> except IOError:
> print "Function Failed: Reasons could be targetStrtData and/or
> targetEndData is not present in dataBuffer."
>
You haven't given a return statement for when there's an error, so it'll
return None. It's probably clearer if you write a function in Python
either as a _function_ which _always_ returns a value or as a
_procedure_ which _never_ returns a value.
In Python the preference is for a function to raise an exception to
indicate an error instead of returning a flag.
> def getStockData(htmlText, selectedStock):
> """ Extract stock data(stock code,price,etc) from htmlText """
> try:
> # Here I extract my number data from HTML text
> tempList = []
> for string in htmlText:
> for i in string.split('>'):
> for e in i.split():
> if ('.' in e and e[0].isdigit()):
> tempList.append(float(e))
> elif (',' in e and e[0].isdigit() ):
> # remove ',' chars
> e = e.replace(',','')
> tempList.append(float(e))
>
>
> selectedStock.updateData(tempList[0],tempList[2],tempList[3],tempList[7])
>
> except IOError: # is this the correct error I should be trying to
> catch here??
> print "Function Failed: Reasons could be: sites HTML data has
> changed. Consult author of program."
> return False
>
> def createStockTracker(stockCode,stockPrice,stockQuant, stockList):
> """ Add a new stock to the database to track """
> newStock = stock()
> newStock.constructor(stockCode,stockPrice,stockQuant)
> stockList.append(newStock)
> return stockList
>
> def writeStockToDatabase(conn, stock):
> """ Write ONLY this Stock's attributes to SQLite Database """
>
> cur = conn.cursor()
> date = getDate()
>
> tableName = stock.code
> stockquery = "insert into "+tableName+" values(?, ?, ?, ?, ?);"
> cur.execute(query,[date,stock.price[-1], stock.recentOffer[-1],
> stock.recentBid[-1], stock.stockVol[-1]])
> conn.commit()
>
> def writeAllToDatabase(conn, stockList):
> """ Enter recent Stock attributes into SQLite Database """
>
> cur = conn.cursor()
> date = getDate()
>
> for stock in stockList:
> tableName = stock.code
> stockquery = "insert into "+tableName+"
> values(?, ?, ?, ?, ?);"
> cur.execute(query,[date,stock.price[-1],
> stock.recentOffer[-1], stock.recentBid[-1], stock.stockVol[-1]])
> conn.commit()
>
> ### Input Functions ###
> def inputNewStock():
> """ """
> print "*Please note only an Australian Securities Exchange(ASX)
> listed stock can be tracked in Version 1.0."
> badInput = True
>
> while (badInput == True):
Better as:
while badInput:
> try:
> code = raw_input("Please enter the ASX code for the
> stock you wish to track: ")
If you assign to a name in a function then that name will be treated as
a local name (variable) unless you say it's global in the function,
therefore 'code', etc, won't been seen outside this function.
> price = input("Please enter the individual stock value
> for "+code+": ")
> quantity = input("Please enter the number/quantity of
> stocks purchased: ")
> if (len(code)>3):
> badInput = True
> else : badInput = False
> return True
This will check the length, setting badInput accordingly, and then
return True.
> except IOError:
I don't see anything in the 'try' block that will raise IOError.
> if (raw_input("Incorrect input. Note: ASX code cannot be
> more than 3 chars. Press 'x' to exit or anything else to try
> again")=='x'):
> return False
> badInput = True # this I am not sure if necessary to loop
> correctly
>
You're not explicitly returning anything from the function when the loop
exits, so it would return None.
>
> ### Main program loop ###
> def main():
> programEnd = False;
> dbLocation = "C:\Users\Sam\Desktop\StockApp/"
If your string contains literal backslashes then use raw strings
instead. Raw strings can end in a backslash, but that shouldn't matter
in the script if you're joining paths together using os.path.join().
> dbName = "stockData.db"
> stockTable = "stocks"
>
> conn = connectDatabase(dbLocation,dbName,stockTable)
> stockList = retrieveStockDatabase(conn,stockTable)
>
> for s in stockList:
> s.printStats()
>
> while (programEnd == False):
Better as:
while not programEnd:
>
> decision = input(menu) # Print Menu
input() is often considered a bad function to use because it's
equivalent to eval(raw_input()), so the user could enter any expression.
It's better to use int(raw_input()) in this case and then catch the
ValueError if what was entered isn't an int.
>
> if (decision==1):
>
> if not(inputNewStock()==False):
A double-negative, equivalent to:
if inputNewStock() != False:
It would be more Pythonic if you had inputNewStock() return the code,
etc, or raise an exception if there was an error. You could then catch
that exception in a 'try' block.
> stockList =
> createStockTracker(acode,price,quantity,stockList)
> newStockDatabase(conn,stockTable,stockList[-1])
> print("\n** New Stock **")
> stockList[-1].printStats()
> print "The stock "+code+" was successfully added to
> our database. \nNow every time the program runs it will automatically
> track this stock & obtain its stock attributes\n\n"
> # TO DO:
> # get new stock recent Data from internet etc.
> # add stock data to data base;
> elif (decision==2):
> if (len(stockList)>0):
Better as:
if stockList:
> for Stock in stockList:
> URL = ASXurl+Stock.code
> sourceCode = getSource(URL)
> targetData = getTargetText(target %
> (Stock.code,Stock.code),"</tr>",sourceCode)
> getStockData(targetData,Stock)
> Stock.printStats()
> #writeStockToDatabase(conn,Stock)
> else:
> print "You must first identify a stock to follow.
> \nPlease select option 1."
> elif (decision==3):
> print "Thank you for using Stock Program. Goodbye.\n"
> programEnd = True; # Exit program
>
> conn.close()
> return 0 # End program
>
'main' is called by the main program and its return value is never used,
so just omit this return statement.
> main()[/CODE]
The recommended standard in Python is for names of classes to be
CamelCase, names of constants to be UPPERCASE_WITH_UNDERSCORES, and
names of everything else to be lowercase_with_underscores.
More information about the Python-list
mailing list