{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "[Oregon Curriculum Network](http://www.4dsolutions.net/ocn)
\n", "[Discovering Math with Python](Introduction.ipynb)\n", "\n", "\n", "# Chapter 3: A FIRST CLASS\n", "\n", "We're still unpacking the Executive Summary of what Python is. Python is an interpreted, object-oriented, high-level programming language with dynamic semantics. \n", "\n", "What does \"object-oriented\" mean? Have we looked at that yet?\n", "\n", "Yes and no.\n", "\n", "We've been making use of the built-in types that Python provides, including the function type, and several data structures, each with its own bag of tricks. However, we've yet to define our own types. That's what this chapter is about, how to do that.\n", "\n", "The function we didn't finish at the end of Chapter 2, is supposed to take a cyclic representation of a permutation, and return a dict. By this chapter's end, we'll have an implementation, but as a method of a class rather than as a free-standing function.\n", "\n", "Here's what we ended with:" ] }, { "cell_type": "code", "execution_count": 1, "metadata": { "collapsed": false }, "outputs": [ { "data": { "text/plain": [ "function" ] }, "execution_count": 1, "metadata": {}, "output_type": "execute_result" } ], "source": [ "def inv_cyclic(cycles : tuple) -> dict:\n", " \"\"\"stub function: doesn't do anything yet\"\"\"\n", " output = {} # empty dict\n", " pass # more code goes here\n", " return output\n", "\n", "type(inv_cyclic)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Lists know how to append. Sets know how to participate in union and intersection operations. A numpy matrix will invert itself, in the linear algebra sense, if we use the right type of matrix (not just an ordinary numpy array). A dict may be updated with another dict.\n", "\n", "The idea of a type of object, is that to each type there corresponds a set of methods, characteristic of that type.\n", "\n", "This style of thinking was not meant to seem \"out of the blue\" but is rather meant to imitate what we might consider everyday human language grammar. We know that Dog type animals have characteristic behaviors and attributes, such as a propensity to bark, wag a tail, eat.\n", "\n", "## Mathematical Types\n", "\n", "Mathematics provides a rich set of types which, like animals, come with their own characteristic behaviors. Some add and multiply, others only multiply, or only add. Some may be inverted, though what that means exactly will vary with the type.\n", "\n", "In Python, we get various operators and other syntax that we see many types of objects use. \n", "\n", "For example not only integers can add. \n", "\n", "Strings can too, but for them, using the plus symbol results in concatenation. \"ABC\" + \"DEF\" gives the string \"ABCDEF\". \n", "\n", "What's true, though, is both numbers and strings have a use for \"add\" as designated by the + (plus sign).\n", "\n", "When it comes to mathematical concepts such as \"multiply\" and \"inverse\" these often go together, as we explore in this chapter. \n", "\n", "An object times its inverse, if defined, typically yields what we call the multiplicative identity element, such that p \\* I = I \\* p = p i.e. multiplying anything by I leaves that anything unchanged. \n", "\n", "That's what we *mean* by \"identity element\" (or \"neutral element\" in some texts).\n", "\n", "We have an identity element associated with addition as well, named zero. p + I = I + p = p when I is zero, and p plus the additive inverse of p (e.g. 3 + (-3)) is likewise I (0).\n", "\n", "A matrix times its own inverse gives the identity matrix, which if you know linear algebra looks like this:" ] }, { "cell_type": "code", "execution_count": 2, "metadata": { "collapsed": false }, "outputs": [ { "data": { "text/plain": [ "array([[ 1., 0., 0., 0., 0.],\n", " [ 0., 1., 0., 0., 0.],\n", " [ 0., 0., 1., 0., 0.],\n", " [ 0., 0., 0., 1., 0.],\n", " [ 0., 0., 0., 0., 1.]])" ] }, "execution_count": 2, "metadata": {}, "output_type": "execute_result" } ], "source": [ "import numpy as np\n", "I = np.identity(5)\n", "I" ] }, { "cell_type": "code", "execution_count": 3, "metadata": { "collapsed": false }, "outputs": [ { "data": { "text/plain": [ "matrix([[1, 2],\n", " [3, 4]])" ] }, "execution_count": 3, "metadata": {}, "output_type": "execute_result" } ], "source": [ "m = np.matrix('[1, 2; 3, 4]')\n", "m" ] }, { "cell_type": "code", "execution_count": 4, "metadata": { "collapsed": false }, "outputs": [ { "data": { "text/plain": [ "matrix([[-2. , 1. ],\n", " [ 1.5, -0.5]])" ] }, "execution_count": 4, "metadata": {}, "output_type": "execute_result" } ], "source": [ "m_inv = m.getI()\n", "m_inv" ] }, { "cell_type": "code", "execution_count": 5, "metadata": { "collapsed": false }, "outputs": [ { "data": { "text/plain": [ "matrix([[ 1.00000000e+00, 1.11022302e-16],\n", " [ 0.00000000e+00, 1.00000000e+00]])" ] }, "execution_count": 5, "metadata": {}, "output_type": "execute_result" } ], "source": [ "m * m_inv # within floating point error, the identity matrix" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Even without knowing what a matrix is, or how to multiply them, the idea the a pair of objects might be inverses of one another, such that their product is 1 (one), is familiar, from ordinary number types. 2 and 1/2 are inverses in this sense, because 2 * (1/2) equals 1.\n", "\n", "In this chapter, we'll create a template for a type of object called a Permutation, or P for short. P-objects will have inverses and p * ~p (p times inverse p) will give the Identity permutation.\n", "\n", "What would be the identity permutation? A permutation that maps every element to itself, such as 'a' to 'a', 'b' to 'b' and so on. Encrypting a message with the identity cipher would not change the message one bit.\n", "\n", "We're going to want our permutations to multiply apparently, and take the unary operator ~ for \"invert\", such that p * ~p turns out to be legal Python. Just as we might multiply matrices together, as above, we'd like to multiply permutations. How might this be arranged?\n", "\n", "Below is a lot more code in a single cell than we've yet seen. We're defining the blueprint for a P-type object, a Permutation. The objects made from this template are the \"selves\" i.e. each has a unique \"self\" that it brings to the table, when a method is run.\n", "\n", "p.shuffle( ) actually triggers P.shuffle(p) behind the scenes, meaning p gets to be its own first argument to P.shuffle, which is why we write *self* as the first parameter. The interpreter itself supplies that argument, knowing p has one, or is one (a self). \n", "\n", "Instances all share the same methods, but as selves each has room for it's own data, distinct from that of the other selves. We plan to make this clear.\n", "\n", "Think of how many permuations one might build from the 27 elements we've been using. The letter 'a' could map to 27 other elements, including itself. Every possible ordering of a-z plus space is fair game. That's 27 factorial, a huge number." ] }, { "cell_type": "code", "execution_count": 6, "metadata": { "collapsed": false }, "outputs": [ { "data": { "text/plain": [ "10888869450418352160768000000" ] }, "execution_count": 6, "metadata": {}, "output_type": "execute_result" } ], "source": [ "import math\n", "math.factorial(27)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Even so, that's a finite number, once the number of elements (27) is fixed. Think of each of those possible permuations as a \"self\" (an instance) all of which have methods in common, as implemented in the source code below:" ] }, { "cell_type": "code", "execution_count": 7, "metadata": { "collapsed": true }, "outputs": [], "source": [ "from random import shuffle \n", "from string import ascii_lowercase # all lowercase letters\n", "\n", "class P:\n", " \"\"\"\n", " A Permutation\n", " \n", " self._code: a dict, is a mapping of iterable elements \n", " to themselves in any order.\n", " \"\"\" \n", "\n", " def __init__(self, iterable = ascii_lowercase + ' '): # default domain\n", " \"\"\"\n", " start out with Identity\n", " \"\"\" \n", " try: # just making sure the caller passed in some iterable object\n", " seq1 = iter(iterable)\n", " seq2 = iter(iterable)\n", " except:\n", " raise TypeError # don't pass in a single integer for example\n", "\n", " self._code = dict(zip(seq1, seq2)) # identity, elements self-paired.\n", " \n", " def shuffle(self):\n", " \"\"\"\n", " return a random permutation of this permutation\n", " \n", " __init__ narrowed it down to the raw material, e.g. what\n", " letters or numbers make up the core _code\n", " \"\"\"\n", " # use shuffle\n", " # something like\n", " the_keys = list(self._code.keys()) # grab keys\n", " shuffle(the_keys) # shuffles 'em\n", " newP = P()\n", " # inject dict directly...\n", " newP._code = dict(zip(self._code.keys(), the_keys))\n", " return newP\n", " \n", " def encrypt(self, plain):\n", " \"\"\"\n", " turn plaintext into cyphertext using self._code\n", " \n", " Pass through any symbol the _code may not mention\n", " \"\"\"\n", " output = \"\" # empty string\n", " for c in plain:\n", " output = output + self._code.get(c, c) \n", " return output\n", " \n", " def decrypt(self, cypher):\n", " \"\"\"\n", " Turn cyphertext into plaintext using ~self\n", " \n", " Unmentioned elements stay as is.\n", " \"\"\"\n", " reverse_P = ~self # invert me!\n", " output = \"\"\n", " for c in cypher:\n", " output = output + reverse_P._code.get(c, c)\n", " return output\n", " \n", " def __getitem__(self, key):\n", " \"\"\"\n", " square bracket notation, used against a P-object,\n", " effectively passes through to _code dict.\n", " \n", " Don't raise a KeyError if there's no value, \n", " return None instead\n", " \"\"\"\n", " return self._code.get(key, None)\n", " \n", " def __repr__(self):\n", " \"\"\"\n", " Note showing the whole of _code for brevity\n", " \"\"\"\n", " return \"P class: \" + str(tuple(self._code.items())[:3]) + \"...\"\n", "\n", " def cyclic(self):\n", " \"\"\"\n", " cyclic notation, a compact view of a group, see \n", " Chapter 2.\n", " \"\"\"\n", " output = []\n", " the_dict = self._code.copy()\n", " while the_dict:\n", " start = tuple(the_dict.keys())[0]\n", " the_cycle = [start]\n", " the_next = the_dict.pop(start)\n", " while the_next != start:\n", " the_cycle.append(the_next)\n", " the_next = the_dict.pop(the_next)\n", " output.append(tuple(the_cycle))\n", " return tuple(output)\n", "\n", " def __mul__(self, other): \n", " \"\"\"\n", " look up my keys to get values that serve\n", " as keys to get other's \"target\" values\n", " \"\"\"\n", " new_code = {}\n", " for c in self._code: # going through my keys\n", " target = other._code[ self._code[c] ]\n", " new_code[c] = target\n", " new_P = P(' ') # start a new permutation\n", " new_P._code = new_code # inject new_code\n", " return new_P\n", " \n", " def __invert__(self):\n", " \"\"\"\n", " create new P with reversed dict\n", " \"\"\"\n", " newP = P(' ')\n", " newP._code = dict(zip(self._code.values(), self._code.keys()))\n", " return newP\n", " \n", " def __eq__(self, other):\n", " \"\"\"\n", " are these permutation the same? \n", " Yes if self._cod==e other._code\n", " \"\"\"\n", " return self._code == other._code" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Just going by the names of the methods, which look like functions, indented under the keyword *class*, we see ideas developed in chapter 2, around encrypting and decrypting. The main difference is the bare dictionary form of our permutation will be named \\_code and used internally. We're then adding capabilities that bare dictionaries do not have, such as the ability to multiply, or invert thanks to a single operator." ] }, { "cell_type": "code", "execution_count": 8, "metadata": { "collapsed": false }, "outputs": [ { "data": { "text/plain": [ "P class: (('a', 'a'), ('b', 'b'), ('c', 'c'))..." ] }, "execution_count": 8, "metadata": {}, "output_type": "execute_result" } ], "source": [ "p = P()\n", "p" ] }, { "cell_type": "code", "execution_count": 9, "metadata": { "collapsed": false }, "outputs": [ { "data": { "text/plain": [ "P class: (('a', 'b'), ('b', 'c'), ('c', 'f'))..." ] }, "execution_count": 9, "metadata": {}, "output_type": "execute_result" } ], "source": [ "p = p.shuffle()\n", "p" ] }, { "cell_type": "code", "execution_count": 10, "metadata": { "collapsed": false }, "outputs": [ { "data": { "text/plain": [ "'bcuxp blpnpxrxpnplbypxucb'" ] }, "execution_count": 10, "metadata": {}, "output_type": "execute_result" } ], "source": [ "p.encrypt('able was i ere i say elba')" ] }, { "cell_type": "code", "execution_count": 11, "metadata": { "collapsed": false }, "outputs": [ { "data": { "text/plain": [ "'able was i ere i say elba'" ] }, "execution_count": 11, "metadata": {}, "output_type": "execute_result" } ], "source": [ "p.decrypt(_) # reuse the most recent result" ] }, { "cell_type": "code", "execution_count": 12, "metadata": { "collapsed": true }, "outputs": [], "source": [ "q = ~p # get the inverse\n", "I = q * p" ] }, { "cell_type": "code", "execution_count": 13, "metadata": { "collapsed": false }, "outputs": [ { "data": { "text/plain": [ "P class: (('b', 'b'), ('c', 'c'), ('f', 'f'))..." ] }, "execution_count": 13, "metadata": {}, "output_type": "execute_result" } ], "source": [ "I # is it true? Is I the identity permutation?" ] }, { "cell_type": "code", "execution_count": 14, "metadata": { "collapsed": false }, "outputs": [ { "data": { "text/plain": [ "'this will do nothing hooray for identity permutation'" ] }, "execution_count": 14, "metadata": {}, "output_type": "execute_result" } ], "source": [ "I.encrypt(\"this will do nothing hooray for identity permutation\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Properties of a Group\n", "\n", "So you see the matrix type (numpy.matrix) and the permuation type (P) have some similarities. In Group Theory, a branch of mathematics, we talk about the properties of a group, a set of objects and an operation, usually called multiplication.\n", "\n", "These properties are:\n", "\n", "* Closure (two elements multiplied always give another element in the set)\n", "* Associativity (p * q) * r == p * (q * r)\n", "* Inverses (such that p * ~p is the neutral element)\n", "* Neutral Element (Identity)\n", "\n", "Some groups are also Abelian, meaning their operation is also Commutative, not just Associative.\n", "\n", "These concepts might seem foreign to learning a computer language such as Python, but Closure should remind you of types, and how two objects of the same type may or may not result in a new object of that type. \n", "\n", "For example two integers always add to give an integer, but the multiplicative inverse of an integer is not an integer except in the case of 1 itself. 2 * (1/2) equals 1. 1/2 is the inverse of 2. The set of integers is not a group with respect to multiplication, as one property of a Group is every object has its inverse.\n", "\n", "So what does it mean to \"multiply\" two permutations anyway? If the first one maps 'a' to 'q' and the second maps 'q' to 'k', then their product permutation takes us right from 'a' to 'k'. You could think of multiplying as a kind of \"chaining\" or \"pipelining\" in this case." ] }, { "cell_type": "code", "execution_count": 15, "metadata": { "collapsed": false }, "outputs": [ { "data": { "text/plain": [ "'q'" ] }, "execution_count": 15, "metadata": {}, "output_type": "execute_result" } ], "source": [ "p = P().shuffle() # quick way to start out randomized\n", "q = P().shuffle() # another one\n", "interim = p['a'] # a goes to something\n", "interim" ] }, { "cell_type": "code", "execution_count": 16, "metadata": { "collapsed": false }, "outputs": [ { "data": { "text/plain": [ "'i'" ] }, "execution_count": 16, "metadata": {}, "output_type": "execute_result" } ], "source": [ "terminus = q[interim] # something goes to final stop\n", "terminus" ] }, { "cell_type": "code", "execution_count": 17, "metadata": { "collapsed": false }, "outputs": [ { "data": { "text/plain": [ "True" ] }, "execution_count": 17, "metadata": {}, "output_type": "execute_result" } ], "source": [ "pq = p * q\n", "terminus == pq['a'] # should be true" ] }, { "cell_type": "code", "execution_count": 18, "metadata": { "collapsed": false }, "outputs": [ { "data": { "text/plain": [ "False" ] }, "execution_count": 18, "metadata": {}, "output_type": "execute_result" } ], "source": [ "p * q == q * p" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Multiplication is not Commutative in this case.\n", "\n", "Lets take a look at pq as a tuple of cyclic subgroups. We call them subgroups because each tuple describes a permutation with the Group properties above." ] }, { "cell_type": "code", "execution_count": 19, "metadata": { "collapsed": false }, "outputs": [ { "data": { "text/plain": [ "(('a',\n", " 'i',\n", " 'j',\n", " 's',\n", " 'f',\n", " 'c',\n", " 'n',\n", " 'u',\n", " 'd',\n", " 'v',\n", " 'o',\n", " 'h',\n", " 'y',\n", " 'b',\n", " 'w',\n", " 'm',\n", " 'r',\n", " 'p',\n", " 'l',\n", " 'q',\n", " 'e',\n", " 't',\n", " 'z'),\n", " ('g',),\n", " ('k',),\n", " ('x', ' '))" ] }, "execution_count": 19, "metadata": {}, "output_type": "execute_result" } ], "source": [ "cyc = pq.cyclic()\n", "cyc" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Lets start though, by completing our function from the last chapter. We'll morph it into the method *from_cyclic* and have it it build us a new P-object. Lets do that by refactoring, having the P object inherit from another class:" ] }, { "cell_type": "code", "execution_count": 20, "metadata": { "collapsed": true }, "outputs": [], "source": [ "class P_base:\n", " \"\"\"\n", " A Permutation\n", " \n", " self._code: a dict, is a mapping of iterable elements \n", " to themselves in any order.\n", " \"\"\" \n", "\n", " def __init__(self, iterable = ascii_lowercase + ' '): # default domain\n", " \"\"\"\n", " start out with Identity\n", " \"\"\" \n", " try:\n", " seq1 = iter(iterable)\n", " seq2 = iter(iterable)\n", " except:\n", " raise TypeError\n", "\n", " self._code = dict(zip(seq1, seq2))\n", " \n", " def __getitem__(self, key):\n", " return self._code.get(key, None)\n", " \n", " def __repr__(self):\n", " return \"P class: \" + str(tuple(self._code.items())[:3]) + \"...\"\n", "\n", " def __mul__(self, other): \n", " \"\"\"\n", " look up my keys to get values that serve\n", " as keys to get others \"target\" values\n", " \"\"\"\n", " new_code = {}\n", " for c in self._code: # going through my keys\n", " target = other._code[ self._code[c] ]\n", " new_code[c] = target\n", " new_P = P() \n", " new_P._code = new_code\n", " return new_P\n", " \n", " def __eq__(self, other):\n", " \"\"\"\n", " are these permutation the same? \n", " Yes if self._cod==e other._code\n", " \"\"\"\n", " return self._code == other._code\n", " \n", "class P(P_base): # first use of inheritance\n", " \n", " def __invert__(self):\n", " \"\"\"\n", " create new P with reversed dict\n", " \"\"\"\n", " newP = P()\n", " newP._code = dict(zip(self._code.values(), self._code.keys()))\n", " return newP\n", " \n", " def shuffle(self):\n", " \"\"\"\n", " return a random permutation of this permutation\n", " \"\"\"\n", " # use shuffle\n", " # something like\n", " the_keys = list(self._code.keys()) # grab keys\n", " shuffle(the_keys) # shuffles 'em\n", " newP = P()\n", " # inject dict directly...\n", " newP._code = dict(zip(self._code.keys(), the_keys))\n", " return newP\n", " \n", " def encrypt(self, plain):\n", " \"\"\"\n", " turn plaintext into cyphertext using self._code\n", " \"\"\"\n", " output = \"\" # empty string\n", " for c in plain:\n", " output = output + self._code.get(c, c) \n", " return output\n", " \n", " def decrypt(self, cypher):\n", " \"\"\"\n", " Turn cyphertext into plaintext using ~self\n", " \"\"\"\n", " reverse_P = ~self # invert me!\n", " output = \"\"\n", " for c in cypher:\n", " output = output + reverse_P._code.get(c, c)\n", " return output\n", "\n", " def cyclic(self):\n", " \"\"\"\n", " cyclic notation, a compact view of a group\n", " \"\"\"\n", " output = []\n", " the_dict = self._code.copy()\n", " while the_dict:\n", " start = tuple(the_dict.keys())[0]\n", " the_cycle = [start]\n", " the_next = the_dict.pop(start)\n", " while the_next != start:\n", " the_cycle.append(the_next)\n", " the_next = the_dict.pop(the_next)\n", " output.append(tuple(the_cycle))\n", " return tuple(output)\n", " \n", " def from_cyclic(self, incoming):\n", " \"\"\"\n", " create a P-type object from incoming cyclic view\n", " \n", " Think of zipping ('a', 'c', 'q', 'k') with \n", " ('c', 'q', 'k', 'a') -- the pairs ('a', 'c'),\n", " ('c', 'q'), ('q', 'k') and ('k', 'a') are what\n", " dict() will then consume. We go through each \n", " subgroup, updating the final dict with the results\n", " of each loop. When done, dump the dict into _code.\n", " \"\"\"\n", " output = {} \n", " for subgroup in incoming:\n", " output.update(dict(zip(subgroup, subgroup[1:] + tuple(subgroup[0]))))\n", " newP = P()\n", " newP._code = output\n", " return newP" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "class P will do everything class P_base does, and then some. This may not be the best illustration of how inheritance works, and what it means, but it will do for now. We might imagine an alternative class, inheriting from P_base, that implements a different assortment of methods. We'll get to an example soon." ] }, { "cell_type": "code", "execution_count": 21, "metadata": { "collapsed": false }, "outputs": [ { "data": { "text/plain": [ "P class: (('a', 'g'), ('b', 'k'), ('c', 'z'))..." ] }, "execution_count": 21, "metadata": {}, "output_type": "execute_result" } ], "source": [ "p = P().shuffle()\n", "p" ] }, { "cell_type": "code", "execution_count": 22, "metadata": { "collapsed": false }, "outputs": [ { "data": { "text/plain": [ "'ftq pq pgpfl fpftq pq panhjpgpfl f'" ] }, "execution_count": 22, "metadata": {}, "output_type": "execute_result" } ], "source": [ "c = p.encrypt(\"this is a test this is only a test\")\n", "c" ] }, { "cell_type": "code", "execution_count": 23, "metadata": { "collapsed": true }, "outputs": [], "source": [ "cyclic_view = p.cyclic() # get the tuple of tuples" ] }, { "cell_type": "code", "execution_count": 24, "metadata": { "collapsed": false }, "outputs": [], "source": [ "test_p = P().from_cyclic(cyclic_view) # test our new method" ] }, { "cell_type": "code", "execution_count": 25, "metadata": { "collapsed": false }, "outputs": [ { "data": { "text/plain": [ "'this is a test this is only a test'" ] }, "execution_count": 25, "metadata": {}, "output_type": "execute_result" } ], "source": [ "test_p.decrypt(c) # does the new permutation decrypt with the old one encrypted?" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The Standard Library includes a unittest module that would allow us to formalize the above testing. \n", "\n", "In Test Driven Development (TDD) we might write the tests first, giving expected outcomes, even before we flesh out the stub function or method we hope to build. We'll get to that in another chapter.\n", "\n", "In the meantime, look at what you've learned:\n", "\n", "* that Python comes with data structures, and has even more in 3rd party world\n", "* we have function syntax to build chunks of functionality\n", "* functions become internalized, indented under the class keyword, to define new types\n", "* we can use class syntax to define what operators will do, and even syntax like square brackets, as when we go p['a'] to see what 'a' maps to.\n", "\n", "\n", "On to Chapter 4: [Clock Arithmetic](Clock%20Arithmetic.ipynb)
\n", "Back to Chapter 2: [Functions At Work](Functions%20At%20Work.ipynb)
\n", "[Introduction](Introduction.ipynb)" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.6.0" } }, "nbformat": 4, "nbformat_minor": 2 }