{ "cells": [ { "cell_type": "markdown", "metadata": { "collapsed": true, "pycharm": { "name": "#%% md\n" } }, "source": [ "# Be a responsible programmer when doing Object-Oriented Programming 🇬🇧\n", "\n", "## What you will learn\n", "\n", "After a first tutorial on Object-Oriented Programming in Python, I feel like it's necessary to write more on OOP because when you read source code of many popular frameworks (like `requests`), inevitably you will come across some more advanced concepts relevant to collaborative programming.\n", "\n", "No, I'm not talking about stuff like `inheritance` and `polymorphism`. These convoluted terms are actually the easiest ones to understand and I'm sure that you'll find tons of tutorials on these subjects.\n", "\n", "What you'll learn in this tutorial are:\n", "\n", "* `decorator`\n", "* `name mangling`\n", "* `__str__ and __repr__ method`\n", "* `class and instance method`\n", "* `class and instance variable`\n", "* `getter and setter`\n", "\n", "There are the minimal vocabulary that you should master before participating in collaborative projects.\n", "\n", "I will implement these concepts by simplifying the web scraping framework example built in the previous tutorial. Check [here](04_oop_web_scraping_en.ipynb) if interested.\n", "\n", "## Class and instance variable\n", "\n", "The scraper class that I built last time is a scraper capable of adjusting its behaviour according the newspaper.\n", "\n", "Let's say that this scraper is created by a company which offers all sorts of service related to web. The CEO wants every object instantiated by this class to be endowed with a property called `company` so that the clients know where the scraper comes from.\n", "\n", "That's when the class variable comes into play." ] }, { "cell_type": "code", "execution_count": 15, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Web Crawling®, nlpinfrench.fr\n" ] } ], "source": [ "class Crawler:\n", "\n", " company = \"Web Crawling®, nlpinfrench.fr\"\n", "\n", "print(Crawler.company)" ], "metadata": { "collapsed": false, "pycharm": { "name": "#%%\n" } } }, { "cell_type": "markdown", "source": [ "As you can see when people access this `class variable` there is no need to instantiate an object, contrary to a `instance variable` which can only be accessed inside an object.\n", "\n", "Note that it's also possible to access it in the `instance variable` way because a class variable is always a part of any instance, although it's admittedly less chic." ], "metadata": { "collapsed": false } }, { "cell_type": "code", "execution_count": 14, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Web Crawling®, nlpinfrench.fr\n" ] } ], "source": [ "crawler_class_var = Crawler()\n", "print(crawler_class_var.company)" ], "metadata": { "collapsed": false, "pycharm": { "name": "#%%\n" } } }, { "cell_type": "markdown", "source": [ "## Be visible\n", "\n", "Now imagine that the CEO is not satisfied with this branding strategy and he wishes something even more visible so that one doesn't even need to know the `company` property. How can you achieve it?\n", "\n", "Let's observe the following code.\n", "\n", "\n" ], "metadata": { "collapsed": false, "pycharm": { "name": "#%% md\n" } } }, { "cell_type": "code", "source": [ "print(\"I'm a string\")" ], "metadata": { "collapsed": false, "pycharm": { "name": "#%%\n" } }, "execution_count": 8, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "I'm a string\n" ] } ] }, { "cell_type": "markdown", "source": [ "The output of `print` is what is called a `string representation`. It is implemented by the `__str__` method (which is one of the magic methods/dunder methods in Python). Here is a minimal working example." ], "metadata": { "collapsed": false, "pycharm": { "name": "#%% md\n" } } }, { "cell_type": "code", "execution_count": 13, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Web Crawling®, nlpinfrench.fr\n" ] }, { "data": { "text/plain": "<__main__.Crawler at 0x7f81181b6a50>" }, "execution_count": 13, "metadata": {}, "output_type": "execute_result" } ], "source": [ "class Crawler:\n", "\n", " company = \"Web Crawling®, nlpinfrench.fr\"\n", "\n", " def __str__(self):\n", " return Crawler.company\n", "\n", "crawler_str = Crawler()\n", "print(crawler_str)\n", "crawler_str" ], "metadata": { "collapsed": false, "pycharm": { "name": "#%%\n" } } }, { "cell_type": "markdown", "source": [ "However it stills needs a call to print, is there a way to show the company name each time the user inspects an object? For the moment as you see from the previous code, the interpreter returns only a mysterious <__main__.Crawler at 0x7f81181d0790>`.\n", "\n", "The `__repr__` method creates a so-called printable representation visible each time one inspects an object." ], "metadata": { "collapsed": false, "pycharm": { "name": "#%% md\n" } } }, { "cell_type": "code", "execution_count": 12, "outputs": [ { "data": { "text/plain": "Web Crawling®, nlpinfrench.fr" }, "execution_count": 12, "metadata": {}, "output_type": "execute_result" } ], "source": [ "class Crawler:\n", "\n", " company = \"Web Crawling®, nlpinfrench.fr\"\n", "\n", " def __repr__(self):\n", " return Crawler.company\n", "\n", "crawler_repr = Crawler()\n", "\n", "crawler_repr" ], "metadata": { "collapsed": false, "pycharm": { "name": "#%%\n" } } }, { "cell_type": "markdown", "source": [ "## Be control freak\n", "\n", "Now that your CEO is happy with his self-branding strategy, you show your code to the senior software engineer who objects, rightly so, that the company name can be changed by anyone!\n", "\n", "Yes, one can easily changes the class variable to, I don't know, tell others that the spirit of prank is still alive." ], "metadata": { "collapsed": false, "pycharm": { "name": "#%% md\n" } } }, { "cell_type": "code", "execution_count": 18, "outputs": [ { "data": { "text/plain": "April Fool!" }, "execution_count": 18, "metadata": {}, "output_type": "execute_result" } ], "source": [ "class Crawler:\n", "\n", " company = \"Web Crawling®, nlpinfrench.fr\"\n", "\n", " def __repr__(self):\n", " return Crawler.company\n", "\n", "crawler_control = Crawler()\n", "Crawler.company = \"April Fool!\"\n", "crawler_repr" ], "metadata": { "collapsed": false, "pycharm": { "name": "#%%\n" } } }, { "cell_type": "markdown", "source": [ "This kind of jokes are certainly not appreciated by everyone. But the message that I'm conveying here is the danger of an easily modifiable class's property which can in turn break an entire program with a single line of code.\n", "\n", "In order to avoir this kind of accidents, the common practice is to put one `_` before a variable name to inform other programmers that this variable shouldn't be modified.\n", "\n", "And you are expected not to use `class variable` if you don't want it to be modified.\n", "\n", "However, as you see from below, a determined pranker could still access and modify it." ], "metadata": { "collapsed": false } }, { "cell_type": "code", "execution_count": 29, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "April fool again!\n" ] } ], "source": [ "class Crawler:\n", "\n", " def __init__ (self, company):\n", " self._company = company\n", "\n", "lemonde_crawler = Crawler(\"nlpinfrench.fr\")\n", "lemonde_crawler._company = \"April fool again!\"\n", "print(lemonde_crawler._company)" ], "metadata": { "collapsed": false, "pycharm": { "name": "#%%\n" } } }, { "cell_type": "markdown", "source": [ "Now this is troublesome. Can't we get rid of this determinant pranker for good?\n", "\n", "The answer lies somewhat between yes and no.\n", "\n", "In Python people often `name mangling` to change automatically the variable's name when an object is instantiated. Let's demonstrate it by an example." ], "metadata": { "collapsed": false } }, { "cell_type": "code", "execution_count": 39, "outputs": [ { "ename": "AttributeError", "evalue": "'Crawler' object has no attribute '__company'", "output_type": "error", "traceback": [ "\u001B[0;31m---------------------------------------------------------------------------\u001B[0m", "\u001B[0;31mAttributeError\u001B[0m Traceback (most recent call last)", "\u001B[0;32m\u001B[0m in \u001B[0;36m\u001B[0;34m\u001B[0m\n\u001B[1;32m 5\u001B[0m \u001B[0;34m\u001B[0m\u001B[0m\n\u001B[1;32m 6\u001B[0m \u001B[0mlemonde_crawler\u001B[0m \u001B[0;34m=\u001B[0m \u001B[0mCrawler\u001B[0m\u001B[0;34m(\u001B[0m\u001B[0;34m\"nlpinfrench.fr\"\u001B[0m\u001B[0;34m)\u001B[0m\u001B[0;34m\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n\u001B[0;32m----> 7\u001B[0;31m \u001B[0mprint\u001B[0m\u001B[0;34m(\u001B[0m\u001B[0mlemonde_crawler\u001B[0m\u001B[0;34m.\u001B[0m\u001B[0m__company\u001B[0m\u001B[0;34m)\u001B[0m\u001B[0;34m\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n\u001B[0m", "\u001B[0;31mAttributeError\u001B[0m: 'Crawler' object has no attribute '__company'" ] } ], "source": [ "class Crawler:\n", "\n", " def __init__ (self, company):\n", " self.__company = company\n", "\n", "lemonde_crawler = Crawler(\"nlpinfrench.fr\")\n", "print(lemonde_crawler.__company)" ], "metadata": { "collapsed": false, "pycharm": { "name": "#%%\n" } } }, { "cell_type": "markdown", "source": [ "As you see it's no longer possible to access the `__company` because of the `name mangling`. However this safety trick is easy to hack. It's always possible to access it by `_Class__property`, besides it's always possible to modify the value of the mangled variable." ], "metadata": { "collapsed": false } }, { "cell_type": "code", "execution_count": 41, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "nlpinfrench.fr\n", "Hello! April fool x3 :DDDD\n" ] } ], "source": [ "class Crawler:\n", "\n", " def __init__ (self, company):\n", " self.__company = company\n", "\n", "lemonde_crawler = Crawler(\"nlpinfrench.fr\")\n", "\n", "# I can still access it!\n", "print(lemonde_crawler._Crawler__company)\n", "\n", "# and modify it!\n", "lemonde_crawler.__company = \"Hello! April fool x3 :DDDD\"\n", "\n", "# and now access it again\n", "print(lemonde_crawler.__company)\n" ], "metadata": { "collapsed": false, "pycharm": { "name": "#%%\n" } } }, { "cell_type": "markdown", "source": [ "## We're all consenting adults here\n", "\n", "So what's the point of `name mangling`?\n", "\n", "The objective is really informative rather than restrictive. In Python programming, there is a commonly used phrase that says \"we're all consenting adults here\". We are all expected to behave like an adult.\n", "\n", "In practice you are not to expected to fully embrace these naming rules as novice. Just keep in mind that you should think twice before modifying values of a mangled variable if you are in an open-source project.\n", "\n", "## Time to become a senior engineer\n", "\n", "## Wrap-up\n", "\n", "As always, I recommend the book by Mark Lutz if you want to dive deep into OOP :\n", "\n", "*Programming Python: Powerful Object-Oriented Programming*\n", "\n", "\n" ], "metadata": { "collapsed": false } } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 2 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython2", "version": "2.7.6" } }, "nbformat": 4, "nbformat_minor": 0 }