{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Effective Python - 59 Specific Ways to Write Better Python. \n", "# *Chapter 3 - Classes and Inheritance*\n", "Book by Brett Slatkin. \n", "Summary notes by Tyler Banks." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 22: Prefer Helper Classes Over Bookkeping with Dictionaries and Tuples" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "* Avoid making dictionaries with values that are other dictionaries or long tuples\n", "* Use `namedtuple` for lightweight, immutable data containers before classes\n", "* Move bookkeeping code to use multiple helper classes when internal state dictionaries get complicated" ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [], "source": [ "#Named Tuple example\n", "import collections\n", "Grade = collections.namedtuple('Grade', ('score', 'weight'))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Notes:\n", "* You can't specify default arguments for `namedtuple` classes.\n", "* The attribute values of `namedtuple` are accessible using indexes and iteration\n", "* If you're not in control of the usage of your namedtuple instances, use your own classes." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Item 23: Accept Functions for Simple Interfaces Instead of Classes" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "* You can pass function definitions as arguments to be called\n", "* Example: `list` type's `sort` method takes optional `key` argument" ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "['Archimedes', 'Aristotle', 'Socrates', 'Plato']\n" ] } ], "source": [ "names = ['Socrates', 'Archimedes', 'Plato', 'Aristotle']\n", "names.sort(key=lambda x: -len(x))\n", "print(names)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "* The `__call__` method allows for classes to be called just like functions\n", "* It also allows for the `callable` built-in function to return True for the instance\n", "* Ex: (default dict allows for a function to be called when an item is added)" ] }, { "cell_type": "code", "execution_count": 15, "metadata": {}, "outputs": [], "source": [ "from collections import defaultdict\n", "class BetterCountMissing(object):\n", " def __init__(self):\n", " self.added = 0\n", " def __call__(self):\n", " self.added += 1\n", " return 0\n", "\n", "counter = BetterCountMissing()\n", "counter()\n", "assert callable(counter)" ] }, { "cell_type": "code", "execution_count": 16, "metadata": {}, "outputs": [], "source": [ "counter = BetterCountMissing()\n", "current = {'greed': 12, 'blau': 5}\n", "increments = [\n", " ('red', 2),\n", " ('greed', 23)\n", "]\n", "result = defaultdict(counter, current) # Relies on __call__\n", "for key, amount in increments:\n", " result[key] += amount\n", "assert counter.added == 1" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Item 24: Use `@classmethod` Polymorphism to Construct Objects Generically" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "To see how we can bind classes together more cohesively, see the following example:" ] }, { "cell_type": "code", "execution_count": 19, "metadata": {}, "outputs": [], "source": [ "class GenericInputData(object):\n", " def read(self):\n", " raise NotImplementedError\n", " @classmethod\n", " def generate_inputs(cls, config):\n", " raise NotImplementedError\n", "\n", "class PathInputData(GenericInputData):\n", " def __init__(self, path):\n", " super().__init__()\n", " self.path = path\n", " def read(self):\n", " return open(self.path).read()\n", " @classmethod\n", " def generate_inputs(cls, config):\n", " data_dir = config['data_dir']\n", " for name in os.listdir(data_dir):\n", " yield cls(os.path.join(data_dir, name))\n", "\n", "class GenericWorker(object):\n", " # …\n", " def map(self):\n", " raise NotImplementedError\n", " def reduce(self, other):\n", " raise NotImplementedError\n", " @classmethod\n", " def create_workers(cls, input_class, config):\n", " workers = []\n", " for input_data in input_class.generate_inputs(config):\n", " workers.append(cls(input_data))\n", " return workers\n", " \n", "def mapreduce(worker_class, input_class, config):\n", " workers = worker_class.create_workers(input_class, config)\n", " return execute(workers)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Item 25: Initialize Parent Classes with `super`" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "* Old way of initializing a parent class from a child class was to call parent's `__init__`\n", "* Ex:" ] }, { "cell_type": "code", "execution_count": 20, "metadata": {}, "outputs": [], "source": [ "class MyBaseClass(object):\n", " def __init__(self, value):\n", " self.value = value\n", "class MyChildClass(MyBaseClass):\n", " def __init__(self):\n", " MyBaseClass.__init__(self, 5)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "* If you were to have classes with multiple inheritance you could end up with odd results" ] }, { "cell_type": "code", "execution_count": 21, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "First ordering is (5 * 2) + 5 = 15\n", "Second ordering still is 15\n" ] } ], "source": [ "class TimesTwo(object):\n", " def __init__(self):\n", " self.value *= 2\n", "class PlusFive(object):\n", " def __init__(self):\n", " self.value += 5\n", " \n", "class OneWay(MyBaseClass, TimesTwo, PlusFive):\n", " def __init__(self, value):\n", " MyBaseClass.__init__(self, value)\n", " TimesTwo.__init__(self)\n", " PlusFive.__init__(self)\n", "foo = OneWay(5)\n", "print('First ordering is (5 * 2) + 5 =', foo.value)\n", "\n", "class AnotherWay(MyBaseClass, PlusFive, TimesTwo):\n", " def __init__(self, value):\n", " MyBaseClass.__init__(self, value)\n", " TimesTwo.__init__(self)\n", " PlusFive.__init__(self)\n", "\n", "bar = AnotherWay(5)\n", "print('Second ordering still is', bar.value)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Diamond inherticance happens when two parent classes have the same parent" ] }, { "cell_type": "code", "execution_count": 22, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Should be (5 * 5) + 2 = 27 but is 7\n" ] } ], "source": [ "class TimesFive(MyBaseClass):\n", " def __init__(self, value):\n", " MyBaseClass.__init__(self, value)\n", " self.value *= 5\n", "class PlusTwo(MyBaseClass):\n", " def __init__(self, value):\n", " MyBaseClass.__init__(self, value)\n", " self.value += 2\n", "class ThisWay(TimesFive, PlusTwo):\n", " def __init__(self, value):\n", " TimesFive.__init__(self, value)\n", " PlusTwo.__init__(self, value)\n", " \n", "foo = ThisWay(5)\n", "print('Should be (5 * 5) + 2 = 27 but is', foo.value)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Because `PlusTwo.__init__` causes self.value to be reset back to 5 when `MyBaseClass.__init__` gets called a second time.\n", "* This is solved with the super built in keyword, which defines the method resolution order" ] }, { "cell_type": "code", "execution_count": 23, "metadata": {}, "outputs": [], "source": [ "class Explicit(MyBaseClass):\n", " def __init__(self, value):\n", " super(__class__, self).__init__(value * 2)\n", "class Implicit(MyBaseClass):\n", " def __init__(self, value):\n", " super().__init__(value * 2)\n", "assert Explicit(10).value == Implicit(10).value" ] }, { "cell_type": "code", "execution_count": 24, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Should be 5 * (5 + 2) = 35 and is 35\n" ] } ], "source": [ "#Use MRO method to see resolution order\n", "class TimesFiveCorrect(MyBaseClass):\n", " def __init__(self, value):\n", " super(TimesFiveCorrect, self).__init__(value)\n", " self.value *= 5\n", "class PlusTwoCorrect(MyBaseClass):\n", " def __init__(self, value):\n", " super(PlusTwoCorrect, self).__init__(value)\n", " self.value += 2\n", "class GoodWay(TimesFiveCorrect, PlusTwoCorrect):\n", " def __init__(self, value):\n", " super(GoodWay, self).__init__(value)\n", "foo = GoodWay(5)\n", "print('Should be 5 * (5 + 2) = 35 and is ', foo.value)" ] }, { "cell_type": "code", "execution_count": 25, "metadata": { "scrolled": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "[,\n", " ,\n", " ,\n", " ,\n", " ]\n" ] } ], "source": [ "from pprint import pprint\n", "pprint(GoodWay.mro())" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Item 26: Use Multiple Inheritance Only for Mix-in Utility Classes" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "* A mix-in is a small class that only defines a set of additional methods that a class should provide. \n", "* Mix-in classes don't define their own instance attributes or require `__init__` to be called.\n", "* Following example is a generic mix-in to convert a Python object from in-memory representation to a dictionary ready for serialization." ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [], "source": [ "class ToDictMixin(object):\n", " def to_dict(self):\n", " return self._traverse_dict(self.__dict__)\n", " def _traverse_dict(self, instance_dict):\n", " output = {}\n", " for key, value in instance_dict.items():\n", " output[key] = self._traverse(key, value)\n", " return output\n", " def _traverse(self, key, value):\n", " if isinstance(value, ToDictMixin):\n", " return value.to_dict()\n", " elif isinstance(value, dict):\n", " return self._traverse_dict(value)\n", " elif isinstance(value, list):\n", " return [self._traverse(key, i) for i in value]\n", " elif hasattr(value, '__dict__'):\n", " return self._traverse_dict(value.__dict__)\n", " else:\n", " return value" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Following uses dynamic attribute access using `hasattr`, dynamic type inspection with `isinstance` and accessing the instance dictionary `__dict__`." ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [], "source": [ "class BinaryTree(ToDictMixin):\n", " def __init__(self, value, left=None, right=None):\n", " self.value = value\n", " self.left = left\n", " self.right = right" ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "{'value': 10, 'left': {'value': 7, 'left': None, 'right': {'value': 9, 'left': None, 'right': None}}, 'right': {'value': 13, 'left': {'value': 11, 'left': None, 'right': None}, 'right': None}}\n" ] } ], "source": [ "tree = BinaryTree(10,\n", " left=BinaryTree(7, right=BinaryTree(9)),\n", " right=BinaryTree(13, left=BinaryTree(11)))\n", "print(tree.to_dict())" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "* Avoid multiple inheritance if mix-ins can achive same outcome\n", "* Use pluggable behaviors at instance level to provide per-class customization\n", "* Compose mix-ins to create complex function from simple behavior" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Item 27: Prefer Public Attributes Over Private Ones" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "* Python has *public* and *private* fields\n", "* *private* fields can be set by prefacing variable names with a double underscore (`__`)" ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [], "source": [ "class MyObject(object):\n", " def __init__(self):\n", " self.public_field = 5\n", " self.__private_field = 10\n", " def get_private_field(self):\n", " return self.__private_field" ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [ { "ename": "AttributeError", "evalue": "'MyObject' object has no attribute '__private_field'", "output_type": "error", "traceback": [ "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[1;31mAttributeError\u001b[0m Traceback (most recent call last)", "\u001b[1;32m\u001b[0m in \u001b[0;36m\u001b[1;34m()\u001b[0m\n\u001b[0;32m 2\u001b[0m \u001b[1;32massert\u001b[0m \u001b[0mfoo\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mpublic_field\u001b[0m \u001b[1;33m==\u001b[0m \u001b[1;36m5\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 3\u001b[0m \u001b[1;32massert\u001b[0m \u001b[0mfoo\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mget_private_field\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m)\u001b[0m \u001b[1;33m==\u001b[0m \u001b[1;36m10\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m----> 4\u001b[1;33m \u001b[0mfoo\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0m__private_field\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m", "\u001b[1;31mAttributeError\u001b[0m: 'MyObject' object has no attribute '__private_field'" ] } ], "source": [ "foo = MyObject()\n", "assert foo.public_field == 5\n", "assert foo.get_private_field() == 10\n", "foo.__private_field" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "* Class methods can access private fields" ] }, { "cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": [], "source": [ "class MyOtherObject(object):\n", " def __init__(self):\n", " self.__private_field = 71\n", " @classmethod\n", " def get_private_field_of_instance(cls, instance):\n", " return instance.__private_field\n", "bar = MyOtherObject()\n", "assert MyOtherObject.get_private_field_of_instance(bar) == 71" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "* Subclasses can't access its parent class's private fields" ] }, { "cell_type": "code", "execution_count": 10, "metadata": {}, "outputs": [ { "ename": "AttributeError", "evalue": "'MyChildObject' object has no attribute '_MyChildObject__private_field'", "output_type": "error", "traceback": [ "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[1;31mAttributeError\u001b[0m Traceback (most recent call last)", "\u001b[1;32m\u001b[0m in \u001b[0;36m\u001b[1;34m()\u001b[0m\n\u001b[0;32m 6\u001b[0m \u001b[1;32mreturn\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0m__private_field\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 7\u001b[0m \u001b[0mbaz\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mMyChildObject\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m----> 8\u001b[1;33m \u001b[0mbaz\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mget_private_field\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m", "\u001b[1;32m\u001b[0m in \u001b[0;36mget_private_field\u001b[1;34m(self)\u001b[0m\n\u001b[0;32m 4\u001b[0m \u001b[1;32mclass\u001b[0m \u001b[0mMyChildObject\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mMyParentObject\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 5\u001b[0m \u001b[1;32mdef\u001b[0m \u001b[0mget_private_field\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mself\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m----> 6\u001b[1;33m \u001b[1;32mreturn\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0m__private_field\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m 7\u001b[0m \u001b[0mbaz\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mMyChildObject\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 8\u001b[0m \u001b[0mbaz\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mget_private_field\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n", "\u001b[1;31mAttributeError\u001b[0m: 'MyChildObject' object has no attribute '_MyChildObject__private_field'" ] } ], "source": [ "class MyParentObject(object):\n", " def __init__(self):\n", " self.__private_field = 71\n", "class MyChildObject(MyParentObject):\n", " def get_private_field(self):\n", " return self.__private_field\n", "baz = MyChildObject()\n", "baz.get_private_field()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "* To minimize damage PEP 8 style indicates that any variable prefaced with a single underscore(`_`) should be accessed with caution.\n", "* Private attributes are NOT rigerously enforced by the Python compiler. Can access with `_classname__privatevar`\n", "* Use docs for protected fields to guide subclasses" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Item 28: Inherit from `collections.abc` for Custom Container Types" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "* Most python classes contain data and describe how objects held within relate to one another.\n", "* Extend built in collection types to retain functionality\n", "* Ex:" ] }, { "cell_type": "code", "execution_count": 11, "metadata": {}, "outputs": [], "source": [ "class FrequencyList(list):\n", " def __init__(self, members):\n", " super().__init__(members)\n", " def frequency(self):\n", " counts = {}\n", " for item in self:\n", " counts.setdefault(item, 0)\n", " counts[item] += 1\n", " return counts" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "* Implement methods like `__getitem__` to take advantage of `list`-like features" ] }, { "cell_type": "code", "execution_count": 19, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "LRR = 7\n" ] } ], "source": [ "class BinaryNode(object):\n", " def __init__(self, value, left=None, right=None):\n", " self.value = value\n", " self.left = left\n", " self.right = right\n", "\n", "class IndexableNode(BinaryNode):\n", " def _search(self, count, index):\n", " # …\n", " # Returns (found, count)\n", " return\n", " def __getitem__(self, index):\n", " found, _ = self._search(0, index)\n", " if not found:\n", " raise IndexError('Index out of range')\n", " return found.value\n", " \n", "tree = IndexableNode(\n", " 10,\n", " left=IndexableNode(\n", " 5,\n", " left=IndexableNode(2),\n", " right=IndexableNode(\n", " 6, right=IndexableNode(7))),\n", " right=IndexableNode(\n", " 15, left=IndexableNode(11)))\n", "\n", "print('LRR =', tree.left.right.right.value)\n", "#print('Index 0 =', tree[0])\n", "#print('Index 1 =', tree[1])\n", "#print('11 in the tree?', 11 in tree)\n", "#print('17 in the tree?', 17 in tree)\n", "#print('Tree is', list(tree))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "* Watch out for large number of methods you need to implement to create a custom container correctly" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [] } ], "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.5" } }, "nbformat": 4, "nbformat_minor": 2 }