{ "cells": [ { "cell_type": "markdown", "metadata": { "deletable": true, "editable": true, "slideshow": { "slide_type": "slide" } }, "source": [ "# It's metaclasses all the way down:\n", "## Understanding and using Python's metaprogramming facilities\n", "" ] }, { "cell_type": "markdown", "metadata": { "deletable": true, "editable": true, "slideshow": { "slide_type": "slide" } }, "source": [ "# What is metaprogramming?\n", "\n", ">Metaprogramming is a technique of writing computer programs that can treat themselves as data, so you can introspect, generate, and/or modify them while running\n", "\n", "- Python has the ability to introspect its basic elements such as functions, classes, or types and to create or modify them on the fly.\n", "- Higher order functions allow us to add/modify functionality of existing functions, methods, or classes.\n", "- Special methods of classes that allow us to interfere with the creation of class objects. These are called **Metaclasses** and allow us to even completely redesign the Python's object-oriented paradigm. \n", "- A selection of different tools that allow programmers to work directly with code either in its raw plain text format or in the more programmatically accessible **Abstract Syntax Tree (AST)** form. This approach is allows for really extraordinary things, such as extending Python's language syntax or even creating your own Domain Specific Language (DSL), see e.g. [Hy](https://github.com/hylang/hy)." ] }, { "cell_type": "markdown", "metadata": { "collapsed": true, "deletable": true, "editable": true, "slideshow": { "slide_type": "slide" } }, "source": [ "# Why would you use metaclasses?\n", "\n", "Well, usually you don't.\n", "\n", ">Metaclasses are deeper magic than 99% of users should ever worry about. If you wonder whether you need them, you don’t (the people who actually need them know with certainty that they need them, and don’t need an explanation about why). **Tim Peters**\n", "\n", "However:\n", "\n", ">The potential uses for metaclasses are boundless. Some ideas that have been explored include logging, interface checking, automatic delegation, automatic property creation, proxies, frameworks, and automatic resource locking/synchronization. **Python Documentation**" ] }, { "cell_type": "markdown", "metadata": { "collapsed": true, "deletable": true, "editable": true, "slideshow": { "slide_type": "slide" } }, "source": [ "# Quick example 1: Django ORM\n", "\n", "A typical use for a metaclass is creating an API (e.g. the Django ORM). It allows us to define something like this:\n", "\n", "```python\n", "class Person(models.Model):\n", " name = models.CharField(max_length=30)\n", " age = models.IntegerField()\n", "```\n", "But if we do this:\n", "```python\n", "guy = Person(name='bob', age=35)\n", "print(guy.age)\n", "```\n", "\n", "- It won't return an `IntegerField` object. It will return an `int`, and can even take it directly from the database.\n", "- This is possible because `models.Model` uses the `ModelBase` metaclass and it uses some magic that will turn the `Person` you just defined with simple statements into a complex hook to a database field.\n", "- Without specifying an `__init__` method, I am able to instantiate a `Person` object. This is done through the metaclass.\n", "- Django makes something complex look simple by exposing a simple API and using metaclasses, recreating code from this API to do the real job behind the scenes." ] }, { "cell_type": "markdown", "metadata": { "collapsed": true, "deletable": true, "editable": true, "slideshow": { "slide_type": "slide" } }, "source": [ "# Quick example 2: Abstract Base Classes\n", "\n", "- Python does not have special syntax for abstract classes.\n", "- We are able to implement this behavior using metaclasses.\n", "- Classes that use the meta-class can use `@abstractmethod` and `@abstractproperty` to define abstract methods and properties.\n", "- The metaclass will ensure that derived classes override the abstract methods and properties.\n", "- Also, classes that implement the ABC without actually inheriting from it can register as implementing the interface, so that `issubclass` and `isinstance` will work.\n", "- When we want to enforce contracts between classes in python just as interfaces do in Java, abstract base classes is the way to go." ] }, { "cell_type": "code", "execution_count": 1, "metadata": { "collapsed": false, "deletable": true, "editable": true, "slideshow": { "slide_type": "slide" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Can't instantiate abstract class Car with abstract methods change_gear, start_engine\n" ] } ], "source": [ "from abc import ABCMeta, abstractmethod\n", "\n", "class Vehicle(metaclass=ABCMeta):\n", " @abstractmethod\n", " def change_gear(self):\n", " pass\n", "\n", " @abstractmethod\n", " def start_engine(self):\n", " pass\n", "\n", "class Car(Vehicle): # subclass the ABC, abstract methods MUST be overridden\n", " def __init__(self, make, model, color):\n", " self.make = make\n", " self.model = model\n", " self.color = color\n", "\n", "try:\n", " car = Car('Toyota', 'Avensis', 'silver')\n", "except TypeError as e:\n", " print(e)" ] }, { "cell_type": "markdown", "metadata": { "deletable": true, "editable": true, "slideshow": { "slide_type": "slide" } }, "source": [ "# You are using them already\n", "\n", "- You are using metaclasses even if you are not aware of using them.\n", "- They make things possible in Python that wouldn't be otherwise or would require substantial changes to the language.\n", "- You are a programmer willing to go out of his comfort zone, learn new things and challenge common practices.\n", "\n", "but but but... I want to make my own metaclasses!" ] }, { "cell_type": "markdown", "metadata": { "deletable": true, "editable": true, "slideshow": { "slide_type": "slide" } }, "source": [ "# OK, I got a simple problem for you\n", "\n", "- You are friends with some guys, they are quite good programmers.\n", "- The learned Python but all have a Javascript background (you know, from that **other** meetup).\n", "- They wrote a Python library that you really want to use.\n", "- They used `camelCase` for all their methods instead of `underscore_method_names`.\n", "- You really want to use their library but love `get_first_name` and hate `getFirstName`" ] }, { "cell_type": "markdown", "metadata": { "collapsed": true, "deletable": true, "editable": true, "slideshow": { "slide_type": "slide" } }, "source": [ "# Possible solutions\n", "\n", "1. Override `__getattr__`:\n", " - Renaming must happen everytime you access the method.\n", " - When trying to get attribute/method of an object, the object's superclasses are looked up **before** `__getattr__`. Problems with inherited methods.\n", "2. Introspection. Use `inspect.getmembers(MyClass, inspect.isfunction)` and rename functions. This works but:\n", " - You need to run this everytime you load each class.\n", " - Is too specific. What if you want to add extra functionality in how this classes operate (e.g. make a class Singleton or make each class register itself somewhere when it's being used)?\n", "3. Class decorator:\n", " - Need to redefine a new nested class for every class you want to alter.\n", " - You will run into problems with inheritance.\n", "4. Use metaclasses." ] }, { "cell_type": "markdown", "metadata": { "deletable": true, "editable": true, "slideshow": { "slide_type": "slide" } }, "source": [ "# Warning! Metaclasses ahead!\n", "" ] }, { "cell_type": "markdown", "metadata": { "collapsed": true, "deletable": true, "editable": true, "slideshow": { "slide_type": "slide" } }, "source": [ "# Classes as objects\n", "- In most languages, classes are just pieces of code that describe how to produce an object.\n", "- But classes are more than that in Python. Classes are objects too.\n", "- This object (the class) is itself capable of creating objects (the instances), and this is why it's a class. But still, it's an object, and therefore:\n", " - You can assign it to a variable.\n", " - You can copy it.\n", " - You can add attributes to it.\n", " - You can pass it as a function parameter." ] }, { "cell_type": "code", "execution_count": 2, "metadata": { "collapsed": false, "deletable": true, "editable": true, "slideshow": { "slide_type": "-" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\n" ] } ], "source": [ "class ObjectCreator:\n", " pass\n", "print(ObjectCreator) # you can pass a class as a parameter because it's an object" ] }, { "cell_type": "code", "execution_count": 3, "metadata": { "collapsed": false, "deletable": true, "editable": true, "slideshow": { "slide_type": "-" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "False\n", "True\n", "foo\n" ] } ], "source": [ "print(hasattr(ObjectCreator, 'new_attribute'))\n", "ObjectCreator.new_attribute = 'foo' # you can add attributes to a class\n", "print(hasattr(ObjectCreator, 'new_attribute'))\n", "print(ObjectCreator.new_attribute)" ] }, { "cell_type": "code", "execution_count": 4, "metadata": { "collapsed": false, "deletable": true, "editable": true, "slideshow": { "slide_type": "-" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "foo\n", "<__main__.ObjectCreator object at 0x00000169EECB63C8>\n" ] } ], "source": [ "ObjectCreatorMirror = ObjectCreator # you can assign a class to a variable\n", "print(ObjectCreatorMirror.new_attribute)\n", "print(ObjectCreatorMirror())" ] }, { "cell_type": "markdown", "metadata": { "collapsed": true, "deletable": true, "editable": true, "slideshow": { "slide_type": "slide" } }, "source": [ "# Creating classes dynamically\n", "1. Remember the function `type`? The good old function that lets you know what type an object is.\n", "2. `type` can also create classes on the fly. `type` can take the description of a class as parameters, and return a class as `type(name, bases, attrs)`.\n", " - `name` is a string giving the name of the class to be constructed.\n", " - `bases` is a tuple giving the parent classes of the class to be constructed.\n", " - `attrs` is a dictionary of the attributes and methods of the class to be constructed.\n", "\n", "For example:\n", "```python\n", "class Foo:\n", " bar = True\n", "```\n", "is equivalent with:\n", "```python\n", "Foo = type('Foo', (), {'bar':True})\n", "```" ] }, { "cell_type": "markdown", "metadata": { "collapsed": true, "deletable": true, "editable": true, "slideshow": { "slide_type": "slide" } }, "source": [ "# What are metaclasses\n", "\n", "1. Metaclasses are the \"stuff\" that creates classes. You define classes in order to create objects, right?\n", "2. We learned that Python classes are objects.\n", "3. Well, metaclasses are what creates these objects. They are the classes' classes, you can picture them this way:\n", "\n", "`MyClass = MetaClass()`, `MyObject = MyClass()`\n", "\n", "" ] }, { "cell_type": "markdown", "metadata": { "collapsed": true, "deletable": true, "editable": true, "slideshow": { "slide_type": "slide" } }, "source": [ "# What can act as a metaclass?\n", "\n", "- Something that can create a class, i.e. anything that subclasses or uses `type`.\n", "- Any python **callable**. A callable is an object that can be invoked with the function operator `()`.\n", " - Any Python class. `MyClass()` calls the `__init__()` function of the class\n", " - Any Python function or instance whose class implements `__call__()`. `foo()` is equivalent to `foo.__call__()`. This is true in both cases (i.e. all functions objects implement `__call__`).\n", "- i.e. a metaclass is something that we call with some parameters and returns a class object\n", "- When you write:\n", "```python\n", "class A:\n", " ...\n", "```\n", "you are actually *not creating* the class, you *describe the class* and `type` creates this class object for you.\n", "\n", "- Each class can have **one** metaclass, however this metaclass can subclass **multiple** metaclasses." ] }, { "cell_type": "markdown", "metadata": { "deletable": true, "editable": true, "slideshow": { "slide_type": "slide" } }, "source": [ "# Our example with a function as metaclass" ] }, { "cell_type": "code", "execution_count": 5, "metadata": { "collapsed": false, "deletable": true, "editable": true, "slideshow": { "slide_type": "-" } }, "outputs": [], "source": [ "import re\n", "\n", "def convert(name):\n", " s1 = re.sub('(.)([A-Z][a-z]+)', r'\\1_\\2', name)\n", " return re.sub('([a-z0-9])([A-Z])', r'\\1_\\2', s1).lower()\n", "\n", "# the metaclass will automatically get passed the same arguments that we pass to `type`\n", "def camel_to_snake_case(name, bases, attrs):\n", " \"\"\"Return a class object, with its attributes from camelCase to snake_case.\"\"\"\n", " print(\"Calling the metaclass camel_to_snake_case to construct class: {}\".format(name))\n", " \n", " # pick up any attribute that doesn't start with '__' and snakecase it\n", " snake_attrs = {}\n", " for attr_name, attr_val in attrs.items():\n", " if not attr_name.startswith('__'):\n", " snake_attrs[convert(attr_name)] = attr_val\n", " else:\n", " snake_attrs[attr_name] = attr_val\n", " return type(name, bases, snake_attrs) # let `type` do the class creation" ] }, { "cell_type": "code", "execution_count": 6, "metadata": { "collapsed": false, "deletable": true, "editable": true, "slideshow": { "slide_type": "-" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Calling the metaclass camel_to_snake_case to construct class: MyVector\n", "['add_to_vector', 'calculate_cross_product', 'calculate_dot_product', 'calculate_triple_product', 'subtract_from_vector']\n" ] } ], "source": [ "class MyVector(metaclass=camel_to_snake_case):\n", " def addToVector(self, other): pass\n", " def subtractFromVector(self, other): pass\n", " def calculateDotProduct(self, other): pass\n", " def calculateCrossProduct(self, other): pass\n", " def calculateTripleProduct(self, other): pass\n", "\n", "print([a for a in dir(MyVector) if not a.startswith('__')])" ] }, { "cell_type": "markdown", "metadata": { "collapsed": true, "deletable": true, "editable": true, "slideshow": { "slide_type": "slide" } }, "source": [ "# Instance creation: `__new__` and `__init__`" ] }, { "cell_type": "code", "execution_count": 7, "metadata": { "collapsed": false, "deletable": true, "editable": true, "slideshow": { "slide_type": "-" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Calling meta_function\n" ] } ], "source": [ "def meta_function(name, bases, attrs):\n", " print('Calling meta_function')\n", " return type(name, bases, attrs)\n", "\n", "\n", "class MyClass1(metaclass=meta_function):\n", " def __new__(cls, *args, **kwargs):\n", " \"\"\"\n", " Called to create a new instance of class `cls`. __new__ takes the class\n", " of which an instance was requested as its first argument. The remaining \n", " arguments are those passed to the object constructor expression \n", " (the call to the class). The return value of __new__ should be the \n", " new object instance (usually an instance of cls).\n", " \"\"\"\n", " print('MyClass1.__new__({}, *{}, **{})'.format(cls, args, kwargs))\n", " return super().__new__(cls)\n", "\n", " def __init__(self, *args, **kwargs):\n", " \"\"\"\n", " Called after the instance has been created (by __new__), but before it \n", " is returned to the caller. The arguments are those passed to the object\n", " constructor. Note: both __new__ and __init__ receive the same arguments.\n", " \"\"\"\n", " print('MyClass1.__init__({}, *{}, **{})'.format(self, args, kwargs))" ] }, { "cell_type": "code", "execution_count": 8, "metadata": { "collapsed": false, "deletable": true, "editable": true, "slideshow": { "slide_type": "-" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "MyClass1.__new__(, *(1, 2, 3), **{'x': 'ex', 'y': 'why'})\n", "MyClass1.__init__(<__main__.MyClass1 object at 0x00000169EED619E8>, *(1, 2, 3), **{'x': 'ex', 'y': 'why'})\n" ] } ], "source": [ "a = MyClass1(1, 2, 3, x='ex', y='why')" ] }, { "cell_type": "markdown", "metadata": { "deletable": true, "editable": true, "slideshow": { "slide_type": "slide" } }, "source": [ "" ] }, { "cell_type": "markdown", "metadata": { "collapsed": true, "deletable": true, "editable": true, "slideshow": { "slide_type": "slide" } }, "source": [ "# Class creation: `__prepare__`, `__new__`, `__init__`, `__call__`" ] }, { "cell_type": "code", "execution_count": 9, "metadata": { "collapsed": false, "deletable": true, "editable": true, "slideshow": { "slide_type": "-" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Metaclass MyMeta created\n" ] } ], "source": [ "class MyMeta(type):\n", " @classmethod\n", " def __prepare__(mcs, name, bases, **kwargs):\n", " \"\"\"\n", " Called before the class body is executed and it must return a dictionary-like object\n", " that's used as the local namespace for all the code from the class body.\n", " \"\"\"\n", " print(\"Meta.__prepare__(mcs={}, name={}, bases={}, **{}\".format(\n", " mcs, name, bases, kwargs))\n", " return {}\n", "\n", " def __new__(mcs, name, bases, attrs, **kwargs):\n", " \"\"\"\n", " Like __new__ in regular classes, which returns an instance object of the class\n", " __new__ in metaclasses returns a class object, i.e. an instance of the metaclass\n", " \"\"\"\n", " print(\"MyMeta.__new__(mcs={}, name={}, bases={}, attrs={}, **{}\".format(\n", " mcs, name, bases, list(attrs.keys()), kwargs))\n", " return super().__new__(mcs, name, bases, attrs)\n", "\n", " def __init__(cls, name, bases, attrs, **kwargs):\n", " \"\"\"\n", " Like __init__ in regular classes, which initializes the instance object of the class\n", " __init__ in metaclasses initializes the class object, i.e. the instance of the metaclass\n", " \"\"\"\n", " print(\"MyMeta.__init__(cls={}, name={}, bases={}, attrs={}, **{}\".format(\n", " cls, name, bases, list(attrs.keys()), kwargs))\n", " super().__init__(name, bases, attrs)\n", "\n", " # Note: all three above methods receive as arguments:\n", " # 1. The name, bases and attrs of the future class that will be created\n", " # 2. Keyword arguments passed in the class inheritance list\n", "\n", " def __call__(cls, *args, **kwargs):\n", " \"\"\"\n", " This is called when we make an instance of the class constructed with the metaclass\n", " \"\"\"\n", " print(\"MyMeta.__call__(cls={}, args={}, kwargs={}\".format(cls, args, kwargs))\n", " self = super().__call__(*args, **kwargs)\n", " print(\"MyMeta.__call__ return: \", self)\n", " return (self)\n", "\n", "print(\"Metaclass MyMeta created\")" ] }, { "cell_type": "code", "execution_count": 10, "metadata": { "collapsed": false, "deletable": true, "editable": true, "slideshow": { "slide_type": "-" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Meta.__prepare__(mcs=, name=MyClass2, bases=(), **{'extra': 1}\n", "MyMeta.__new__(mcs=, name=MyClass2, bases=(), attrs=['__module__', '__qualname__', '__new__', '__init__', '__classcell__'], **{'extra': 1}\n", "MyMeta.__init__(cls=, name=MyClass2, bases=(), attrs=['__module__', '__qualname__', '__new__', '__init__', '__classcell__'], **{'extra': 1}\n", "Class MyClass created\n" ] } ], "source": [ "class MyClass2(metaclass=MyMeta, extra=1):\n", " def __new__(cls, s, a=0, b=0):\n", " print(\"MyClass2.__new__(cls={}, s={}, a={}, b={})\".format(cls, s, a, b))\n", " return super().__new__(cls)\n", "\n", " def __init__(self, s, a=0, b=0):\n", " print(\"MyClass2.__init__(self={}, s={}, a={}, b={})\".format(self, s, a, b))\n", " self.a, self.b = a, b\n", "\n", "print(\"Class MyClass created\")" ] }, { "cell_type": "code", "execution_count": 11, "metadata": { "collapsed": false, "deletable": true, "editable": true, "slideshow": { "slide_type": "-" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "MyMeta.__call__(cls=, args=('hello',), kwargs={'a': 1, 'b': 2}\n", "MyClass2.__new__(cls=, s=hello, a=1, b=2)\n", "MyClass2.__init__(self=<__main__.MyClass2 object at 0x00000169EED7D860>, s=hello, a=1, b=2)\n", "MyMeta.__call__ return: <__main__.MyClass2 object at 0x00000169EED7D860>\n", "MyClass instance created: <__main__.MyClass2 object at 0x00000169EED7D860>\n" ] } ], "source": [ "a = MyClass2('hello', a=1, b=2)\n", "print(\"MyClass instance created: \", a)" ] }, { "cell_type": "markdown", "metadata": { "deletable": true, "editable": true, "slideshow": { "slide_type": "slide" } }, "source": [ "" ] }, { "cell_type": "markdown", "metadata": { "collapsed": true, "deletable": true, "editable": true, "slideshow": { "slide_type": "slide" } }, "source": [ "# Our example with a class as metaclass" ] }, { "cell_type": "code", "execution_count": 12, "metadata": { "collapsed": false, "deletable": true, "editable": true, "slideshow": { "slide_type": "-" } }, "outputs": [], "source": [ "class CamelToSnake(type): \n", " def __new__(mcs, name, bases, attrs):\n", " # pick up any attribute that doesn't start with '__' and snakecase it\n", " snake_attrs = {}\n", " for attr_name, attr_val in attrs.items():\n", " if not name.startswith('__'):\n", " snake_attrs[convert(attr_name)] = attr_val\n", " else:\n", " snake_attrs[attr_name] = attr_val\n", " return super().__new__(mcs, name, bases, snake_attrs)" ] }, { "cell_type": "code", "execution_count": 13, "metadata": { "collapsed": false, "deletable": true, "editable": true, "slideshow": { "slide_type": "-" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "['add_to_vector', 'calculate_cross_product', 'calculate_dot_product', 'calculate_triple_product', 'subtract_from_vector']\n" ] } ], "source": [ "class MyVector(metaclass=CamelToSnake):\n", " def addToVector(self, other): pass\n", " def subtractFromVector(self, other): pass\n", " def calculateDotProduct(self, other): pass\n", " def calculateCrossProduct(self, other): pass\n", " def calculateTripleProduct(self, other): pass\n", "\n", "print([a for a in dir(MyVector) if not a.startswith('__')])" ] }, { "cell_type": "markdown", "metadata": { "deletable": true, "editable": true, "slideshow": { "slide_type": "slide" } }, "source": [ "# Make our class a Singleton" ] }, { "cell_type": "code", "execution_count": 14, "metadata": { "collapsed": false, "deletable": true, "editable": true, "slideshow": { "slide_type": "-" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "['add_to_vector', 'calculate_cross_product', 'calculate_dot_product', 'calculate_triple_product', 'subtract_from_vector']\n", "True\n" ] } ], "source": [ "class Singleton(type):\n", " _instances = {}\n", " def __call__(cls, *args, **kwargs):\n", " if cls not in cls._instances:\n", " cls._instances[cls] = super().__call__(*args, **kwargs)\n", " return cls._instances[cls]\n", "\n", "class SnakeSingleton(CamelToSnake, Singleton):\n", " pass\n", "\n", "class MyVector(metaclass=SnakeSingleton):\n", " def addToVector(self, other): pass\n", " def subtractFromVector(self, other): pass\n", " def calculateDotProduct(self, other): pass\n", " def calculateCrossProduct(self, other): pass\n", " def calculateTripleProduct(self, other): pass\n", "\n", "print([a for a in dir(MyVector) if not a.startswith('__')])\n", "v1 = MyVector(); v2 = MyVector()\n", "print(v1 is v2)" ] }, { "cell_type": "markdown", "metadata": { "deletable": true, "editable": true, "slideshow": { "slide_type": "slide" } }, "source": [ "# References\n", "\n", "\n", "1. [StackOverflow - What is a metaclass in Python?](http://stackoverflow.com/questions/100003/what-is-a-metaclass-in-python)\n", "2. [StackOverflow - Good real-world uses of metaclasses (e.g. in Python)](http://stackoverflow.com/questions/2907498/good-real-world-uses-of-metaclasses-e-g-in-python)\n", "3. [StackOverflow - Creating a singleton in Python](http://stackoverflow.com/questions/6760685/creating-a-singleton-in-python)\n", "4. [ionel's codelog - Understanding Python Metaclasses](https://blog.ionelmc.ro/2015/02/09/understanding-python-metaclasses/)\n", "5. [Eli Bendersky - Python metaclasses by example](http://eli.thegreenplace.net/2011/08/14/python-metaclasses-by-example/)\n", "6. M. Jaworski, T. Ziade - Expert Python Programming, 2nd edition (2006), Packt Publishing\n", "7. David Beazley - Python Essential Reference, 4th edition (2009), Addison-Wesley" ] }, { "cell_type": "markdown", "metadata": { "collapsed": true, "deletable": true, "editable": true, "slideshow": { "slide_type": "slide" } }, "source": [ "# Thank you!\n", "\n", "" ] } ], "metadata": { "celltoolbar": "Slideshow", "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 }