When using a Python decorator, especially one defined in another library, they seem somewhat magical. Take for example Flask’s routing mechanism. If I put some statement like @app.route("/")
above my logic, then poof, suddenly that code will be executed when I go to the root url on the server. And sure, decorators make sense when you read the many tutorials out there that describe them. But for the most part, those tutorials are just explaining what’s going on, mostly by just printing out some text, but not why you might want to use a decorator yourself.
I was of that opinion before, but recently, I realized I have the perfect use for a decorator in a project of mine. In order to get the content for Product Mentions, I have Python scrapers that go through Reddit looking for links to an Amazon product, and once I find one, I gather up the link, use the Amazon Product API to get information on the product. Once that’s in the database, I use Rails to display the items to the user.
While doing the scraping, I also wanted a web interface so I can check to see errors, check to see how long the jobs are taking, and overall to see that I haven’t missed anything. So along with the actual Python script that grabs the html and parses it, I created a table in the database for logging the scraping runs, and update that for each job. Simple, and does the job I want.
The issue I come across here, and where decorators come into play, is code reuse. After some code refactoring, I have a few different jobs, all of which have the following format: Create an object for this job, commit it to the db so I can see that it’s running in real time, try some code that depends on the job and except and log any error so we don’t crash that process, and then post the end time of the job.
def gather_comments(): scrape_log = ScrapeLog(start_time=datetime.now(), job_type="comments") session.add(scrape_log) session.commit() try: rg = RedditGatherer() rg.gather_comments() except Exception as e: scrape_log.error = True scrape_log.error_message = e.message scrape_log.end_time = datetime.now() session.add(scrape_log) session.commit() def gather_threads(): scrape_log = ScrapeLog(start_time=datetime.now(), job_type="threads") session.add(scrape_log) session.commit() try: rg = RedditGatherer() rg.gather_threads() except Exception as e: scrape_log.error = True scrape_log.error_message = e.message scrape_log.end_time = datetime.now() session.add(scrape_log) session.commit()
If you know a bit about how decorators work, you can already see how perfect an opportunity using this concept is here, because decorators allow you to extend and reuse functionality on top of functions you already use. For me, I want to log, time, and error check my scraping, and reusing the same code is not ideal. But a decorator is. Here’s how to write one.
Decorator Time
First thing to do, is write a function, that takes a function as parameter and call that function at the appropriate time. Since the work of the functions above is done with the same format, this turns out really nice.