{ "metadata": { "name": "Pickle and Redis" }, "nbformat": 3, "nbformat_minor": 0, "worksheets": [ { "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "* Author: Tim Hopper\n", "* Twitter: [@tdhopper](http://twitter.com/tdhopper/)\n", "* Email: tdhopper@gmail.com\n", "\n", "Content of talk given at [PyCarolinas](http://blog.pycarolinas.org/) 2012.\n", "\n", "This material is on my github page: https://github.com/tdhopper/Pickle-and-Redis." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# Persistent Data in Python\n", "\n", "## Pickle\n", "\n", "[](https://twitter.com/hmason/status/257906982063861760)\n", "\n", "Pickle is a Python module for \"serializing and de-serializing a Python object structure.\"\n", "\n", "Basically, Python lets you save objects to disk." ] }, { "cell_type": "code", "collapsed": false, "input": [ "import pickle" ], "language": "python", "metadata": {}, "outputs": [], "prompt_number": 64 }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Examples\n", "\n", "Dump the text string \"abcdefg\" to a file called \"pickle_test.\"" ] }, { "cell_type": "code", "collapsed": false, "input": [ "pickle.dump(\"abcdefg\", open(\"pickle_test\", \"wb\"))" ], "language": "python", "metadata": {}, "outputs": [], "prompt_number": 2 }, { "cell_type": "markdown", "metadata": {}, "source": [ "Pickle dumps are binary files. They're not designed to be read as text." ] }, { "cell_type": "code", "collapsed": false, "input": [ "print open(\"pickle_test\", \"r\").read()" ], "language": "python", "metadata": {}, "outputs": [ { "output_type": "stream", "stream": "stdout", "text": [ "S'abcdefg'\n", "p0\n", ".\n" ] } ], "prompt_number": 3 }, { "cell_type": "heading", "level": 4, "metadata": {}, "source": [ "Pickling other built in classes (and combinations thereof)" ] }, { "cell_type": "code", "collapsed": false, "input": [ "data1 = {'a': [1, 2.0, 3, 4+6j],\n", " 'b': ('string', u'Unicode string'),\n", " 'c': None}\n", "\n", "pickle.dump(data1, open('data.pkl', 'wb'))\n", "\n", "data2 = pickle.load(open('data.pkl', 'rb'))" ], "language": "python", "metadata": {}, "outputs": [], "prompt_number": 4 }, { "cell_type": "code", "collapsed": false, "input": [ "data1 == data2" ], "language": "python", "metadata": {}, "outputs": [ { "output_type": "pyout", "prompt_number": 5, "text": [ "True" ] } ], "prompt_number": 5 }, { "cell_type": "markdown", "metadata": {}, "source": [ "What can be pickled?\n", "\n", "* __None__, __True__, and __False__\n", "* integers, long integers, floating point numbers, complex numbers\n", "* normal and Unicode strings\n", "* tuples, lists, sets, and dictionaries containing only picklable objects\n", "* functions defined at the top level of a module\n", "* built-in functions defined at the top level of a module\n", "* classes that are defined at the top level of a module\n", "* instances of such classes whose __dict__ or __setstate__() is picklable\n", "\n", "(From the [official documentation](http://docs.python.org/library/pickle.html))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Pickling Custom Classes\n", "\n", "Pickle can handle much more than built in classes:" ] }, { "cell_type": "code", "collapsed": false, "input": [ "class PicklePerson(object):\n", " def __init__(self, name, age, location):\n", " self.name = name\n", " self.age = age\n", " self.location = location\n", " \n", " def __repr__(self):\n", " return \"name: \" + self.name + \"\\n\" + \"age: \" + self.age + \\\n", " \"\\n\" + \"location: \" + self.location" ], "language": "python", "metadata": {}, "outputs": [], "prompt_number": 68 }, { "cell_type": "code", "collapsed": false, "input": [ "todd = PicklePerson(\"Todd\", \"30\", \"Raleigh\")\n", "print todd" ], "language": "python", "metadata": {}, "outputs": [ { "output_type": "stream", "stream": "stdout", "text": [ "name: Todd\n", "age: 30\n", "location: Raleigh\n" ] } ], "prompt_number": 8 }, { "cell_type": "code", "collapsed": false, "input": [ "pickle.dump(todd, open(\"pickle_todd\", \"wb\"))\n", "recovered_todd = pickle.load(open(\"pickle_todd\",\"r\"))" ], "language": "python", "metadata": {}, "outputs": [], "prompt_number": 10 }, { "cell_type": "code", "collapsed": false, "input": [ "recovered_todd" ], "language": "python", "metadata": {}, "outputs": [ { "output_type": "pyout", "prompt_number": 11, "text": [ "name: Todd\n", "age: 30\n", "location: Raleigh" ] } ], "prompt_number": 11 }, { "cell_type": "heading", "level": 4, "metadata": {}, "source": [ "What can't be pickled?" ] }, { "cell_type": "code", "collapsed": false, "input": [ "def f(x): return x+1\n", "pickle.dump(f, open(\"pickle_good\",\"wb\"))" ], "language": "python", "metadata": {}, "outputs": [], "prompt_number": 13 }, { "cell_type": "code", "collapsed": true, "input": [ "try: \n", " with open(\"pickle_bad\",\"wb\") as f:\n", " \n", " pickle.dump(lambda x: x+1, f)\n", "\n", "except pickle.PicklingError:\n", " print \"Can't pickle :-(\"" ], "language": "python", "metadata": {}, "outputs": [ { "output_type": "stream", "stream": "stdout", "text": [ "Can't pickle :-(\n" ] } ], "prompt_number": 14 }, { "cell_type": "markdown", "metadata": {}, "source": [ "Also, from http://stackoverflow.com/a/11685634/982745:" ] }, { "cell_type": "code", "collapsed": true, "input": [ "class NotPickable(object):\n", " def __init__(self, x):\n", " self.attr = x\n", "\n", "o = NotPickable(open('Pickle and Redis.ipynb', 'r+w'))\n", "\n", "try: \n", " with open(\"pickle_bad\",\"wb\") as f:\n", " \n", " pickle.dumps(o)\n", "\n", "except TypeError:\n", " print \"Can't pickle :-(\"" ], "language": "python", "metadata": {}, "outputs": [ { "output_type": "stream", "stream": "stdout", "text": [ "Can't pickle :-(\n" ] } ], "prompt_number": 19 }, { "cell_type": "markdown", "metadata": {}, "source": [ "Pickling errors can cause problems when using the multiprocessing module for parallelization. I have an example [here](https://gist.github.com/3091122) and [here](http://stackoverflow.com/questions/8804830/python-multiprocessing-pickling-error)'s some discussion on StackOverflow." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Pickle Security\n", "\n", "[](http://docs.python.org/library/pickle.html#pickle-python-object-serialization)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## cPickle\n", "\n", "\"cPickle can be up to 1000 times faster than pickle because the former is implemented in C. \"" ] }, { "cell_type": "code", "collapsed": false, "input": [ "import cPickle, os" ], "language": "python", "metadata": {}, "outputs": [], "prompt_number": 20 }, { "cell_type": "code", "collapsed": false, "input": [ "%timeit pickle.dump([data1 for x in range(1000)], open(\"pickle_todd\", \"wb\"))" ], "language": "python", "metadata": {}, "outputs": [ { "output_type": "stream", "stream": "stdout", "text": [ "100 loops, best of 3: 7.8 ms per loop\n" ] } ], "prompt_number": 21 }, { "cell_type": "code", "collapsed": false, "input": [ "%timeit cPickle.dump([data1 for x in range(1000)], open(\"pickle_todd\", \"wb\"))" ], "language": "python", "metadata": {}, "outputs": [ { "output_type": "stream", "stream": "stdout", "text": [ "100 loops, best of 3: 2.76 ms per loop\n" ] } ], "prompt_number": 22 }, { "cell_type": "markdown", "metadata": {}, "source": [ "For reference, the size of the pickle, in bytes, is:" ] }, { "cell_type": "code", "collapsed": false, "input": [ "os.path.getsize('/Users/tdhopper/Dropbox/PyCarolinas 2012/pickle_todd')" ], "language": "python", "metadata": {}, "outputs": [ { "output_type": "pyout", "prompt_number": 23, "text": [ "4112" ] } ], "prompt_number": 23 }, { "cell_type": "markdown", "metadata": {}, "source": [ "However, \"in the cPickle module the callables Pickler() and Unpickler() are functions, not classes. This means that you cannot use them to derive custom pickling and unpickling subclasses.\"" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Redis\n", "\n", "### Basics\n", "\n", "\"Redis is an open source, advanced key-value store. It is often referred to as a data structure server since keys can contain strings, hashes, lists, sets and sorted sets.\" (http://redis.io/)\n", "\n", "Some advantages:\n", "\n", "* Unlike memcached, redis can save its state to the disk.\n", "* Data from any Redis server can replicate to any number of slaves. A slave may be a master to another slave.\" (Wikipedia)\n", "* Optionally durable.\n", "* Holds data set in memory.\n", "* Fast, fast, fast.\n", "\n", "\n", "[](http://redis.io/topics/whos-using-redis)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Installation\n", "\n", "Redis is easy to install on *nix systems:\n", "\n", " $ wget http://redis.googlecode.com/files/redis-2.4.17.tar.gz\n", " $ tar xzf redis-2.4.17.tar.gz\n", " $ cd redis-2.4.17\n", " $ make\n", "\n", "(There's an unofficial Windows port.)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Start a redis server with:\n", "\n", " $ redis-server\n", "\n", "" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The Redis server can be accessed directly from the Redis Command Line Interface (CLI):\n", "\n", " $ redis-cli\n", "\n", "" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Setting and getting keys is easy:\n", "\n", " redis> set foo bar\n", " OK\n", " redis> get foo\n", " \"bar\"" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The Redis website provides helpful documentation on all the \"redis-cli\" commands:\n", "\n", "[](http://redis.io/commands)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Data Types\n", "\n", "#### Strings\n", "\n", "\"Strings are the most basic kind of Redis value. Redis Strings are binary safe, this means that a __Redis string can contain any kind of data__....\"\n", "\n", "Including:\n", "\n", "* An image\n", "* A Pickled Python object!\n", "\n", "A string must be less than 512 megabytes." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "##### Counters\n", "\n", " SET mykey \"10\"\n", " OK\n", " redis> INCR mykey\n", " (integer) 11\n", " redis> GET mykey\n", " \"11\"\n", "\n", "(\"__Note:__ this is a string operation because Redis does not have a dedicated integer type.\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "##### Appends\n", "\n", " redis> EXISTS mykey\n", " (integer) 0\n", " redis> APPEND mykey \"Hello\"\n", " (integer) 5\n", " redis> APPEND mykey \" World\"\n", " (integer) 11\n", " redis> GET mykey\n", " \"Hello World\"\n", "\n", "This gives [fast way to store a time series](http://redis.io/commands/append). \n", "\n", "Also see DECR and INCRBY." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "##### Slices\n", "\n", "Slicing strings is easy:\n", " \n", " redis> SET mykey \"This is a string\"\n", " OK\n", " redis> GETRANGE mykey 0 3\n", " \"This\"\n", " redis> GETRANGE mykey -3 -1\n", " \"ing\"\n", " redis> GETRANGE mykey 0 -1\n", " \"This is a string\"\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Lists\n", "\n", "* \"Redis Lists are simply lists of strings, sorted by insertion order.\"\n", "* \"It is possible to add elements to a Redis List pushing new elements on the head (on the left) or on the tail (on the right) of the list.\"" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ " redis> RPUSH mylist \"hello\"\n", " (integer) 1\n", " redis> RPUSH mylist \"world\"\n", " (integer) 2\n", " redis> RPUSH mylist \"HELLO\" \"PyCarolinas\"\n", " (integer) 4\n", " redis> LRANGE mylist 0 -1\n", " 1) \"hello\"\n", " 2) \"world\"\n", " 3) \"HELLO\"\n", " 4) \"PyCarolinas\"" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The maximum list size is $2^{32}-1\\approx\\mbox{}4\\text{ billion}$.\n", "\n", "Combine RPUSH, LPUSH, RPOP, and LPOP to create your favorite queue!" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Sets\n", "\n", "* \"Redis Sets are an unordered collection of Strings.\"\n", "* \"...you can do unions, intersections, differences of sets in very short time.\"\n", "* Like lists, the maximum size is about 4 billion.\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "\tredis> SADD myset \"Hello\"\n", "\t(integer) 1\n", "\tredis> SADD myset \"World\"\n", "\t(integer) 1\n", "\tredis> SADD myset \"World\"\n", "\t(integer) 0\n", "\tredis> SMEMBERS myset\n", "\t1) \"World\"\n", "\t2) \"Hello\"\n", "\n", "Get a random set item with SPOP or SRANDMEMBER." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Hashes\n", "\n", "\n", "\"Redis Hashes are maps between string fields and string values.\"\n", "\n", "\tHMSET myhash field1 \"Hello\" field2 \"World\"\n", "\tOK\n", "\tredis> HGET myhash field1\n", "\t\"Hello\"\n", "\tredis> HGET myhash field2\n", "\t\"World\"\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Sorted Sets\n", "\n", "\"...every member of a Sorted Set is associated with score, that is used in order to take the sorted set ordered, from the smallest to the greatest score.\"\n", "\n", " redis> ZADD myzset 1 \"one\"\n", "\t(integer) 1\n", "\tredis> ZADD myzset 1 \"uno\"\n", "\t(integer) 1\n", "\tredis> ZADD myzset 2 \"two\"\n", "\t(integer) 1\n", "\tredis> ZADD myzset 3 \"two\"\n", "\t(integer) 0\n", "\tredis> ZRANGE myzset 0 -1 WITHSCORES\n", "\t1) \"one\"\n", "\t2) \"1\"\n", "\t3) \"uno\"\n", "\t4) \"1\"\n", "\t5) \"two\"\n", "\t6) \"3\"\n", "\tredis> ZRANGE myzset 0 -1\n", "\t1) \"one\"\n", "\t2) \"uno\"\n", "\t3) \"two\"\n", "\n", "Notice that \"two\" only appears once. When _ZADD myzset 3 \"two\"_ is called, the score of \"two\" is updated from 2 to 3.\n", "\n", "\"While members are unique, scores may be repeated.\"" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Redis and Python\n", "\n", "A Python interface is redis is available at https://github.com/andymccurdy/redis-py\n", "\n", " $ sudo pip install redis\n", "\n", "Using redis from Python is as easy as importing the package and connecting to a server:" ] }, { "cell_type": "code", "collapsed": false, "input": [ "import redis\n", "r = redis.StrictRedis(host='localhost', port=6379, db=0)" ], "language": "python", "metadata": {}, "outputs": [], "prompt_number": 24 }, { "cell_type": "markdown", "metadata": {}, "source": [ "Setting and getting keys is easy:" ] }, { "cell_type": "code", "collapsed": false, "input": [ "r.set('foo', 'bar')" ], "language": "python", "metadata": {}, "outputs": [ { "output_type": "pyout", "prompt_number": 25, "text": [ "True" ] } ], "prompt_number": 25 }, { "cell_type": "code", "collapsed": false, "input": [ "r.get('foo')" ], "language": "python", "metadata": {}, "outputs": [ { "output_type": "pyout", "prompt_number": 26, "text": [ "'bar'" ] } ], "prompt_number": 26 }, { "cell_type": "code", "collapsed": false, "input": [ "%timeit r.set('foo', 'bar')" ], "language": "python", "metadata": {}, "outputs": [ { "output_type": "stream", "stream": "stdout", "text": [ "10000 loops, best of 3: 146 us per loop\n" ] } ], "prompt_number": 28 }, { "cell_type": "code", "collapsed": false, "input": [ "%timeit r.get('foo')" ], "language": "python", "metadata": {}, "outputs": [ { "output_type": "stream", "stream": "stdout", "text": [ "10000 loops, best of 3: 160 us per loop\n" ] } ], "prompt_number": 27 }, { "cell_type": "markdown", "metadata": {}, "source": [ "Times are in the order of 100 nanoseconds. According to Wolfram Alpha:\n", "\n", "[](http://www.wolframalpha.com/input/?i=160+us)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "In general, the StrictRedis class implements commands identically to the redis-cli commands.\n", "\n", "Let's create a set of words from a paragraph in the [Wikipedia page on redis](http://en.wikipedia.org/wiki/Redis):" ] }, { "cell_type": "code", "collapsed": false, "input": [ "import string\n", "\n", "text = \"\"\"\n", " Redis typically holds the whole dataset in RAM. Versions up to 2.4 could be configured \n", "to use virtual memory but this is now deprecated. Persistence is reached in two different \n", "ways: One is called snapshotting, and is a semi-persistent durability mode where the dataset \n", "is asynchronously transferred from memory to disk from time to time, written in RDB dump format. \n", "Since version 1.1 the safer alternative is AOF, an append-only file (a journal) that is written \n", "as operations modifying the dataset in memory are processed. Redis is able to rewrite the \n", "append-only file in the background in order to avoid an indefinite growth of the journal.\"\"\"\n", "\n", "# Strip punctuation: http://stackoverflow.com/a/2402306/982745 \n", "text_list = [word.translate(None, string.punctuation) for word in text.split()] \n", "\n", "for word in text_list: r.delete(word) # In case these words are already in redis, delete them." ], "language": "python", "metadata": {}, "outputs": [], "prompt_number": 65 }, { "cell_type": "markdown", "metadata": {}, "source": [ "Create a set of the words:" ] }, { "cell_type": "code", "collapsed": false, "input": [ "for word in text_list:\n", " r.sadd(\"persistence\", word)\n", " \n", "print [r.srandmember('persistence') for i in range(10)] # Get ten random words\n", "print [r.srandmember('persistence') for i in range(10)] # Get ten more random words" ], "language": "python", "metadata": {}, "outputs": [ { "output_type": "stream", "stream": "stdout", "text": [ "['of', 'avoid', 'semipersistent', 'from', 'rewrite', 'RAM', 'Persistence', 'memory', 'file', 'Since']\n", "['a', 'virtual', 'alternative', 'where', 'semipersistent', 'modifying', 'in', 'an', 'asynchronously', 'rewrite']\n" ] } ], "prompt_number": 38 }, { "cell_type": "markdown", "metadata": {}, "source": [ "Count all the word frequency in this text:" ] }, { "cell_type": "code", "collapsed": false, "input": [ "for word in text_list:\n", " r.incr(word)\n", "\n", "# Print most used words in this document:\n", " \n", "for word in set(text_list):\n", " if int(r.get(word)) > 2:\n", " print word\n", " print \"\\t\\t\", r.get(word)" ], "language": "python", "metadata": {}, "outputs": [ { "output_type": "stream", "stream": "stdout", "text": [ "dataset\n", "\t\t3\n", "in\n", "\t\t6\n", "to\n", "\t\t6\n", "memory\n", "\t\t3\n", "is\n", "\t\t8\n", "the\n", "\t\t7\n" ] } ], "prompt_number": 44 }, { "cell_type": "markdown", "metadata": {}, "source": [ "The best part is that all this data will persist across your Python sessions!" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Storing Python Objects in Redis\n", "\n", "##### Direct Picklin'" ] }, { "cell_type": "code", "collapsed": false, "input": [ "bob = pickle.dumps(PicklePerson(\"bob\",\"50\",\"durham\"))\n", "print bob" ], "language": "python", "metadata": {}, "outputs": [ { "output_type": "stream", "stream": "stdout", "text": [ "ccopy_reg\n", "_reconstructor\n", "p0\n", "(c__main__\n", "PicklePerson\n", "p1\n", "c__builtin__\n", "object\n", "p2\n", "Ntp3\n", "Rp4\n", "(dp5\n", "S'age'\n", "p6\n", "S'50'\n", "p7\n", "sS'name'\n", "p8\n", "S'bob'\n", "p9\n", "sS'location'\n", "p10\n", "S'durham'\n", "p11\n", "sb.\n" ] } ], "prompt_number": 66 }, { "cell_type": "code", "collapsed": false, "input": [ "r.set(\"bob\", bob)" ], "language": "python", "metadata": {}, "outputs": [ { "output_type": "pyout", "prompt_number": 49, "text": [ "True" ] } ], "prompt_number": 49 }, { "cell_type": "code", "collapsed": false, "input": [ "pickle.loads(r.get(\"bob\"))" ], "language": "python", "metadata": {}, "outputs": [ { "output_type": "pyout", "prompt_number": 50, "text": [ "name: bob\n", "age: 50\n", "location: durham" ] } ], "prompt_number": 50 }, { "cell_type": "markdown", "metadata": {}, "source": [ "##### Redisco\n", "\n", "Redisco is a library build on redis-py that allows you to store objects in Redis. " ] }, { "cell_type": "code", "collapsed": false, "input": [ "import redisco\n", "from redisco import connection_setup, models\n", "redisco.connection_setup(host='localhost', port=6379, db=0)" ], "language": "python", "metadata": {}, "outputs": [], "prompt_number": 57 }, { "cell_type": "code", "collapsed": false, "input": [ "class Person(models.Model):\n", " name = models.Attribute(required=True)\n", " age = models.Attribute(required=False)\n", " location = models.Attribute(required=False)\n", " \n", "for x in Person.objects.filter(name=\"Tim\"):\n", " x.delete()" ], "language": "python", "metadata": {}, "outputs": [], "prompt_number": 58 }, { "cell_type": "code", "collapsed": false, "input": [ "tim_hopper = Person(name=\"Tim\",age=\"26\",location=\"Morrisville\")\n", "tim_smith = Person(name=\"Tim\",age=\"75\",location=\"Chapel Hill\")" ], "language": "python", "metadata": {}, "outputs": [], "prompt_number": 59 }, { "cell_type": "code", "collapsed": false, "input": [ "tim_hopper.save()\n", "tim_smith.save()" ], "language": "python", "metadata": {}, "outputs": [ { "output_type": "pyout", "prompt_number": 60, "text": [ "True" ] } ], "prompt_number": 60 }, { "cell_type": "code", "collapsed": false, "input": [ "Person.objects.filter(name=\"Tim\")" ], "language": "python", "metadata": {}, "outputs": [ { "output_type": "pyout", "prompt_number": 61, "text": [ "[, ]" ] } ], "prompt_number": 61 }, { "cell_type": "code", "collapsed": false, "input": [ "Person.objects.filter(name=\"Tim\", age=\"26\")[0] == tim_hopper" ], "language": "python", "metadata": {}, "outputs": [ { "output_type": "pyout", "prompt_number": 62, "text": [ "True" ] } ], "prompt_number": 62 }, { "cell_type": "markdown", "metadata": {}, "source": [ "Redisco is in version 0.1.4 and hasn't been updated recently. Nevertheless, it gives you an idea of what redis-py is capable of. " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "------------------\n", "\n", "* Author: Tim Hopper\n", "* Twitter: [@tdhopper](http://twitter.com/tdhopper/)\n", "* Email: tdhopper@gmail.com\n", "\n", "Content of talk given at [PyCarolinas](http://blog.pycarolinas.org/) 2012.\n", "\n", "This material is on my github page: https://github.com/tdhopper/Pickle-and-Redis." ] } ], "metadata": {} } ] }