{ "cells": [ { "cell_type": "markdown", "metadata": { "deletable": true, "editable": true, "slideshow": { "slide_type": "slide" } }, "source": [ "# Descriptors: The magic behind attribute access in Python\n", "\n", "" ] }, { "cell_type": "markdown", "metadata": { "deletable": true, "editable": true, "slideshow": { "slide_type": "slide" } }, "source": [ "# What is encapsulation about? (IMNSHO)\n", "\n", "1. *Encapsulation* **is not** about hiding data.\n", "2. *Access control* **is** about hiding data.\n", "3. Encapsulation and access control are two different independent things.\n", " * You **don't need** access control to have encapsulation.\n", " * You can encapsulate behavior **without** having to restrict access.\n", "4. Encapsulation separates the concept of **what something does** from **how it is implemented**.\n", "5. Encapsulation decouples a programming construct's **public interface/API** from its **implemenation**.\n", "6. When calling code wants to retrieve a value, it should not depend on from where the value comes. Internally, the class can store the value in a field or retrieve it from some external resource (such as a file or a database). Perhaps the value is not stored at all, but calculated on-the-fly. This should not matter to the calling code." ] }, { "cell_type": "markdown", "metadata": { "deletable": true, "editable": true, "slideshow": { "slide_type": "slide" } }, "source": [ "# Before we begin: Underscores in attributes in Python\n", "\n", "1. Single underscore before a name (e.g. `_foo`)\n", " * Used as a convention, these attributes should be treated as a non-public part of the API (whether it is a function, a method or a data member) and considered an implementation detail and subject to change without notice (source: [Python documentation](https://docs.python.org/3/tutorial/classes.html#tut-private)).\n", " * It's more than a convention and actually does mean something to the interpreter; if you `from import *`, none of the names that start with an `_` will be imported unless the module's/package's `__all__` list explicitly contains them.\n", "2. Double underscore before a name (e.g. `__foo`)\n", " * This is not a convention, any identifier of the form `__foo` (at least two leading underscores, at most one trailing underscore) is textually replaced with `_classname__foo`, where classname is the current class name with leading underscore(s) stripped. This is called *name mangling*. (source: [Python documentation](https://docs.python.org/3/tutorial/classes.html)).\n", " * Name mangling is helpful for letting subclasses override methods without breaking intraclass method calls.\n", "3. Double underscore before and after a name (e.g. `__foo__`)\n", " * Methods that use this naming format are called *special* or *magic* methods and are automatically invoked when certain syntax is used. We typically override these methods to implement the desired behaviour in classes (e.g. constructors, operator overloading, indexing etc).\n", " * *Special* attributes that provide access to the implementation and are not intended for general use. Examples from class special attributes: `__name__` is the class name, `__module__` is the module name in which the class was defined, `__dict__` is the dictionary containing the class’s namespace, `__bases__` is a tuple containing the base classes." ] }, { "cell_type": "code", "execution_count": 1, "metadata": { "collapsed": false, "deletable": true, "editable": true, "slideshow": { "slide_type": "slide" } }, "outputs": [], "source": [ "# name mangling mechanism\n", "\n", "class Mapping:\n", " def __init__(self, iterable):\n", " self.items_list = []\n", " # self.__update inside the class is equivalent to self._Mapping__update\n", " # the same function will be called even if __update is overridden in inheriting classes\n", " self.__update(iterable)\n", "\n", " def __update(self, iterable):\n", " for item in iterable:\n", " self.items_list.append(item)\n", "\n", "class MappingSub(Mapping):\n", " def __update(self, keys, values):\n", " # provides new signature for __update() but does not break __init__()\n", " for item in zip(keys, values):\n", " self.items_list.append(item)" ] }, { "cell_type": "code", "execution_count": 2, "metadata": { "collapsed": false, "deletable": true, "editable": true, "slideshow": { "slide_type": "-" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "False False\n", "[1, 2, 3, 4]\n", "[1, 2, 3, 4, (5, 'five'), (6, 'six')]\n" ] } ], "source": [ "m = Mapping([1, 2])\n", "ms = MappingSub([1,2])\n", "print('__update' in dir(m), '__update' in dir(ms))\n", "\n", "m._Mapping__update([3, 4])\n", "print(m.items_list)\n", "\n", "ms._Mapping__update([3, 4]) # call update function of Mapping class\n", "ms._MappingSub__update([5, 6], ['five', 'six']) # call update of MappingSub class\n", "print(ms.items_list)\n" ] }, { "cell_type": "markdown", "metadata": { "collapsed": true, "deletable": true, "editable": true, "slideshow": { "slide_type": "slide" } }, "source": [ "# What is an attribute?\n", "\n", "* Quite simply, an attribute is a way to get from one object to another.\n", "* Apply the power of the almighty dot `objectname.attributename` and voila! you now have the handle to another object.\n", "* You also have the power to create attributes, by assignment: `objectname.attributename = anotherobject`.\n", "* Which object does an attribute access return, though? And where does the object set as an attribute end up?\n", "\n", "Depending on the programming language:\n", "* You don't have any control to attribute access (Java plebs).\n", "* You control attribute access through properties (C# cool kids).\n", "* You can completely customize attribute access in addition to properties (Python master race)." ] }, { "cell_type": "markdown", "metadata": { "collapsed": true, "deletable": true, "editable": true, "slideshow": { "slide_type": "slide" } }, "source": [ "" ] }, { "cell_type": "markdown", "metadata": { "collapsed": false, "deletable": true, "editable": true, "slideshow": { "slide_type": "slide" } }, "source": [ "# Instance attribute access\n", "* When we access an instance we actually call its `__getattribute__` method, i.e. `a.x -> a.__getattribute__(x)`.\n", "* `__getattribute__` has an order of priority that describes where to look for attributes and how to react to them.\n", "* Classes and instances have a `__dict__` where user provided attributes are stored and looked up.\n", " * Python provides extra attributes, most of which are not stored in `__dict__` (e.g. special methods).\n", " * `__dict__` is looked up first and this is how we override special methods.\n", " * We can also override this behavior to save memory for classes with a few fields using `__slots__`. However we cannot add new attributes to `__slots__`." ] }, { "cell_type": "code", "execution_count": 3, "metadata": { "collapsed": false, "deletable": true, "editable": true, "slideshow": { "slide_type": "-" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "a.x: 1, a.y: 2\n", "a.x: 1, a.y: 3, A.y: 2\n", "a.z: z\n", "{'__module__': '__main__', 'y': 2, '__getattr__': , '__doc__': None}\n", "{'y': 3}\n" ] } ], "source": [ "class B:\n", " x = 1\n", "\n", "class A(B):\n", " y = 2\n", " def __getattr__(self, value):\n", " return str(value)\n", " \n", "a = A()\n", "print(\"a.x: {}, a.y: {}\".format(a.x, a.y)) # x from B, y from A\n", "a.y = 3\n", "print(\"a.x: {}, a.y: {}, A.y: {}\".format(a.x, a.y, A.y)) # x from B, y from a (overrides y in A)\n", "print(\"a.z: {}\".format(a.z)) # call __getattr__\n", "\n", "print(A.__dict__)\n", "print(a.__dict__)" ] }, { "cell_type": "markdown", "metadata": { "deletable": true, "editable": true, "slideshow": { "slide_type": "slide" } }, "source": [ "# Descriptor protocol\n", "Raymond Hettinger ([Python Documentation](https://docs.python.org/3/howto/descriptor.html)):\n", "> In general, a descriptor is an object attribute with \"binding behavior\", one whose attribute access has been overridden by methods in the descriptor protocol.\n", "\n", "* Those methods are `__get__`, `__set__` and `__delete__`. If any of those methods are defined for an object, it is said to be a **descriptor**.\n", "* Only one of the methods *needs* to be implemented in order to be considered a descriptor, but any number of them *can* be implemented. \n", "* There are two types of descriptors based on which sets of these methods are implemented: **data** and **non-data** descriptors.\n", " 1. A **data** descriptor implements at least `__set__` or `__delete__`, but can include both. They also often include `__get__`, since it's rare to want to set something without also being able to get it too.\n", " 2. A **non-data** descriptor only implements `__get__`. If it adds `__set__` or `__delete__`to its method list, it becomes a data descriptor." ] }, { "cell_type": "markdown", "metadata": { "collapsed": true, "deletable": true, "editable": true, "slideshow": { "slide_type": "slide" } }, "source": [ "# `__get__(self, instance, owner)`\n", "\n", "1. `self` is the descriptor instance.\n", "2. `owner` is the class the descriptor is accessed *from*.\n", " * When you call `A.x`, where `x` is a descriptor object with `__get__`, it's called with `A` as owner and `instance` as `None`.\n", " * This lets the descriptor know that `__get__` is being called from a *class*, not an *instance*.\n", " * `A.x` is translated to `A.__dict__['x'].__get__(None, A)`.\n", "3. `instance` is the instance that the descriptor is accessed *from*.\n", " * If the discriptor is accessed from an *instance* it receives it as `instance` and the class of the instance as `owner`.\n", " * `a.x` is translated to `type(a).__dict__['x'].__get__(a, type(a))`\n", " * Note that the call starts with `type(a)`, not just `a`, because descriptors are stored on *classes* not *instances*.\n", " \n", "Two important points:\n", "1. In order to be able to apply per-instance as well as per-class functionality, descriptors are given `instance` and `owner` (the class of the instance).\n", "2. It is not the *instance* that the descriptor is being called from, but instead, the `instance` *parameter* is the instance the descriptor is being called from. It is actually being called from the instance class." ] }, { "cell_type": "markdown", "metadata": { "collapsed": true, "deletable": true, "editable": true, "slideshow": { "slide_type": "slide" } }, "source": [ "# `__set__(self, instance, value)`\n", "\n", "1. `__set__` does not have an owner parameter that accepts a class and does not need it, since data descriptors are generally designed for storing per-instance data.\n", "2. `A.x = value` does not get translated to anything; `value` replaces the descriptor object stored in `x` (however, see note below). \n", "3. `a.x = value` is translated to `type(a).__dict__['x'].__set__(a, value)`" ] }, { "cell_type": "markdown", "metadata": { "deletable": true, "editable": true, "slideshow": { "slide_type": "-" } }, "source": [ "# `__delete__(self, instance)`\n", "\n", "1. invoked when `del a.x` is called.\n", "2. `del a.x` is translated to `type(a).__dict__['x'].__delete__(a)`\n", "\n", "**Note**: If we want a descriptor's `__set__` or `__delete__` methods to work from the *class* level, the descriptor must be created on the class's *metaclass*. When doing so, everything that refers to `owner` is referring to the *metaclass*, while a reference to `instance` refers to the *class*. After all, classes are just instances of metaclasses." ] }, { "cell_type": "markdown", "metadata": { "deletable": true, "editable": true, "slideshow": { "slide_type": "slide" } }, "source": [ "# Instance & class attribute access\n", "* Descriptors are invoked by the `__getattribute__` method.\n", "* Overriding `__getattribute__` prevents automatic descriptor calls.\n", "* Class attribute access still uses `__getattribute__`, but it's the one defined on its *metaclass*.\n", "* Priorities when an *instance* attribute is looked up:\n", " 1. Data descriptors in its class (up the MRO).\n", " 2. Instance attributes.\n", " 3. Non-data descriptors in its class / class attributes (up the MRO).\n", " 4. The `__getattr__` method.\n", "* Priorities when an *class* attribute is looked up:\n", " 1. Data descriptors in its metaclass (up the MRO).\n", " 2. Class attributes (up the MRO).\n", " 3. Non-data descriptors in its metaclass / metaclass attributes (up the MRO).\n", " 4. The `__getattr__` method." ] }, { "cell_type": "markdown", "metadata": { "collapsed": true, "deletable": true, "editable": true, "slideshow": { "slide_type": "slide" } }, "source": [ "# Instance attribute access priority (`a.x`)\n", "1. Look in the class `__dict__`, working up the MRO.\n", " * If found, check if it's a data descriptor.\n", " * If it has a `__get__` method, call it and return the result.\n", "2. Look in the instance `__dict__`.\n", " * If found, return the value in `__dict__`.\n", "3. Check class `__dict__` again, working up the MRO.\n", " * If found, check if it's a descriptor.\n", " * If it has a `__get__` method, call it and return the result.\n", " * If it doesn't have a `__get__` method, return the descriptor object itself.\n", " * If found and not a descriptor, return the value in `__dict__`.\n", "4. Call `__getattr__` if it exists and return the result.\n", "5. If everything up to this point has failed, raise `AttributeError`." ] }, { "cell_type": "markdown", "metadata": { "collapsed": true, "deletable": true, "editable": true, "slideshow": { "slide_type": "slide" } }, "source": [ "# Class attribute access priority (`A.x`)\n", "1. Look in the metaclass `__dict__`, working up the MRO.\n", " * If found, check if it's a data descriptor.\n", " * If it has a `__get__` method, call it and return the result.\n", "2. Look in the class `__dict__`, working up the MRO.\n", " * If found, check if it's a descriptor.\n", " * If it has a `__get__` method, call it and return the result.\n", " * If it doesn't have a `__get__` method, return the descriptor object itself.\n", " * If found and not a descriptor, return the value in `__dict__`.\n", "3. Check metaclass `__dict__` again, working up the MRO.\n", " * If found, check if it's a descriptor.\n", " * If it has a `__get__` method, call it and return the result.\n", " * If it doesn't have a `__get__` method, return the descriptor object itself.\n", " * If found and not a descriptor, return the value in `__dict__`.\n", "4. Call `__getattr__` if it exists and return the result.\n", "5. If everything up to this point has failed, raise `AttributeError`." ] }, { "cell_type": "markdown", "metadata": { "deletable": true, "editable": true, "slideshow": { "slide_type": "slide" } }, "source": [ "# Instance attribute access priority:\n", "## `__set__` & `__delete__` (`a.x = value` & `del a.x`)\n", "\n", "1. Look in the class `__dict__`, working up the MRO.\n", " * If found, check if it's a data descriptor.\n", " * If it has a `__set__` or `__delete__` method, call `__set__` or `__delete__`.\n", " * If it doesn't have the corresponding method, raise `AttributeError`.\n", "2. Look in the instance `__dict__`.\n", " * `a.x = value`\n", " * Set attribute to value.\n", " * `del a.x`\n", " * If found, delete attribute.\n", " * If not found, raise `AttributeError`." ] }, { "cell_type": "markdown", "metadata": { "deletable": true, "editable": true, "slideshow": { "slide_type": "slide" } }, "source": [ "# Class attribute access priority:\n", "## `__set__` & `__delete__` (`A.x = value` & `del A.x`)\n", "\n", "1. Look in the metaclass `__dict__`, working up the MRO.\n", " * If found, check if it's a data descriptor.\n", " * If it has a `__set__` or `__delete__` method, call `__set__` or `__delete__`.\n", " * If it doesn't have the corresponding method, raise `AttributeError`.\n", "2. Look in the class `__dict__`.\n", " * `A.x = value`\n", " * Set attribute to value.\n", " * `del A.x`\n", " * If found, delete attribute.\n", " * If not found, raise `AttributeError`." ] }, { "cell_type": "markdown", "metadata": { "deletable": true, "editable": true, "slideshow": { "slide_type": "slide" } }, "source": [ "" ] }, { "cell_type": "code", "execution_count": 4, "metadata": { "collapsed": false, "deletable": true, "editable": true, "slideshow": { "slide_type": "slide" } }, "outputs": [], "source": [ "# implementation of classmethod and staticmethod, equivalent to the standard library\n", "\n", "class MyClassmethod:\n", " def __init__(self, func):\n", " self.func = func\n", " \n", " # ignore the instance, provide the class as first argument (usually named cls) so the \n", " # returned function can be called with the arguments the user wants to explicitly provide\n", " def __get__(self, instance, owner):\n", " def cls_wrapper(*args, **kwargs):\n", " return self.func(owner, *args, **kwargs) # what if I put cls=owner?\n", " return cls_wrapper\n", " \n", "class MyStaticmethod:\n", " def __init__(self, func):\n", " self.func = func\n", "\n", " # essentially just accepts a function and then returns it when __get__ is called\n", " def __get__(self, instance, owner):\n", " return self.func" ] }, { "cell_type": "code", "execution_count": 5, "metadata": { "collapsed": false, "deletable": true, "editable": true, "slideshow": { "slide_type": "-" } }, "outputs": [], "source": [ "class A:\n", " def foo(self):\n", " print(self)\n", " \n", " @MyClassmethod # same as: bar = MyClassmethod(bar)\n", " def bar(cls):\n", " print(cls)\n", " \n", " @MyStaticmethod # same as: baz = MyStaticmethod(baz)\n", " def baz():\n", " print('static method')\n", " \n", " # both methods are accessed through their respective descriptors" ] }, { "cell_type": "code", "execution_count": 6, "metadata": { "collapsed": false, "deletable": true, "editable": true, "slideshow": { "slide_type": "slide" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "<__main__.A object at 0x0000022EE39CCF60>\n", "\n", ".cls_wrapper at 0x0000022EE39D2510>\n", "\n", "\n", "\n", "\n", "static method\n", "static method\n" ] } ], "source": [ "a = A()\n", "\n", "# instance method, business as always\n", "a.foo()\n", "print()\n", "\n", "# access the method object, descriptor is called and returns the cls_wrapper method object\n", "print(A.bar)\n", "\n", "# call the method, instance in __get__ is None (don't care), owner is A\n", "A.bar()\n", "\n", "# run it on the instance, instance in __get__ is a (don't care), owner is A\n", "a.bar()\n", "print()\n", "\n", "# access the method object, descriptor returns a function with no arguments\n", "print(A.baz)\n", "\n", "# call the method without any instance (self) or class (cls) object\n", "# of course same result if we call it in the instance\n", "A.baz()\n", "a.baz()" ] }, { "cell_type": "code", "execution_count": 7, "metadata": { "collapsed": true, "deletable": true, "editable": true, "slideshow": { "slide_type": "slide" } }, "outputs": [], "source": [ "# implementation of property, equivalent to the standard library\n", "\n", "class MyProperty:\n", " def __init__(self, fget=None, fset=None, fdel=None):\n", " self.fget = fget\n", " self.fset = fset\n", " self.fdel = fdel\n", " \n", " def __get__(self, instance, owner):\n", " if instance is None: # this was called from the class, not the instance\n", " return self\n", " elif self.fget is None:\n", " raise AttributeError(\"unreadable attribute\")\n", " else:\n", " return self.fget(instance)\n", " \n", " def __set__(self, instance, value):\n", " if self.fset is None:\n", " raise AttributeError(\"can't set attribute\")\n", " else:\n", " self.fset(instance, value)\n", " \n", " def __delete__(self, instance):\n", " if self.fdel is None:\n", " raise AttributeError(\"can't delete attribute\")\n", " else:\n", " self.fdel(instance)\n", " \n", " def getter(self, fget):\n", " return type(self)(fget, self.fset, self.fdel)\n", " \n", " def setter(self, fset):\n", " return type(self)(self.fget, fset, self.fdel)\n", " \n", " def deleter(self, fdel):\n", " return type(self)(self.fget, self.fset, fdel)" ] }, { "cell_type": "code", "execution_count": 8, "metadata": { "collapsed": false, "deletable": true, "editable": true, "slideshow": { "slide_type": "slide" } }, "outputs": [], "source": [ "class A:\n", " def __init__(self, x):\n", " self._x = x\n", " \n", " @MyProperty\n", " def x(self):\n", " print(\"returning _x: {}\".format(self._x))\n", " return self._x\n", " \n", " @x.setter\n", " def x(self, value):\n", " print(\"setting _x to {}\".format(value))\n", " self._x = value" ] }, { "cell_type": "code", "execution_count": 9, "metadata": { "collapsed": false, "deletable": true, "editable": true, "scrolled": true, "slideshow": { "slide_type": "-" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "<__main__.MyProperty object at 0x0000022EE39EBF28>\n", "returning _x: 1\n", "1\n", "\n", "setting _x to 2\n", "returning _x: 2\n", "2\n", "\n", "bye bye descriptor\n" ] } ], "source": [ "a = A(1)\n", "print(A.x) # call the descriptor from the class, returns the descriptor object\n", "print(a.x)\n", "print()\n", "a.x = 2\n", "print(a.x)\n", "print()\n", "A.x = 'bye bye descriptor'\n", "print(a.x) # descriptor is gone from class, instance gets attribute from class" ] }, { "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": [ "# Is this useful?\n", "* Why do I need to know this? Can't I just use `property`?\n", " * No problem. `property` is awesome! Use `property` for greater good!\n", "* However there are times where logic needs to be repeated in properties.\n", "* This can lead to code duplication.\n", "* We can try to fix this by writing helper methods.\n", "* But then in each property, code for these method calls will be duplicated.\n", "* Descriptors allow us to **capture the logic** for attribute access and **re-use** it for different attributes." ] }, { "cell_type": "code", "execution_count": 10, "metadata": { "collapsed": true, "deletable": true, "editable": true, "slideshow": { "slide_type": "slide" } }, "outputs": [], "source": [ "class BasketballGame:\n", " def __init__(self, points, rebounds, steals):\n", " self.points = points\n", " self.rebounds = rebounds\n", " self.steals = steals\n", "\n", " @property\n", " def points(self):\n", " return self._points\n", "\n", " @points.setter\n", " def points(self, value):\n", " if value < 0:\n", " raise ValueError('Positive values only!')\n", " self._points = value\n", "\n", " @property\n", " def rebounds(self):\n", " return self._rebounds\n", "\n", " @rebounds.setter\n", " def rebounds(self, value):\n", " if value < 0:\n", " raise ValueError('Positive values only!')\n", " self._rebounds = value\n", "\n", " @property\n", " def steals(self):\n", " return self._steals\n", "\n", " @steals.setter\n", " def steals(self, value):\n", " if value < 0:\n", " raise ValueError('Positive values only!')\n", " self._steals = value" ] }, { "cell_type": "code", "execution_count": 11, "metadata": { "collapsed": false, "deletable": true, "editable": true, "slideshow": { "slide_type": "slide" } }, "outputs": [], "source": [ "class NonNegativeField:\n", " def __init__(self, name=''):\n", " # need to store the field name on the descriptor object itself\n", " # as descriptors are defined on the class level\n", " self.name = name\n", " \n", " def __get__(self, instance, owner):\n", " return instance.__dict__[self.name]\n", "\n", " def __set__(self, instance, value):\n", " if value < 0:\n", " raise ValueError('Positive values only!')\n", " instance.__dict__[self.name] = value" ] }, { "cell_type": "code", "execution_count": 12, "metadata": { "collapsed": true, "deletable": true, "editable": true, "slideshow": { "slide_type": "-" } }, "outputs": [], "source": [ "class BasketballGame:\n", " # is there a better way, so that we don't have to repeat the field name?\n", " points = NonNegativeField('points')\n", " rebounds = NonNegativeField('rebounds')\n", " steals = NonNegativeField('steals')\n", "\n", " def __init__(self, points, rebounds, steals):\n", " self.points = points\n", " self.rebounds = rebounds\n", " self.steals = steals" ] }, { "cell_type": "code", "execution_count": 13, "metadata": { "collapsed": false, "deletable": true, "editable": true, "slideshow": { "slide_type": "-" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "points: 100, rebounds: 30\n", "Error! Positive values only!\n" ] } ], "source": [ "a = BasketballGame(points=100, rebounds=30, steals=10)\n", "print(\"points: {}, rebounds: {}\".format(a.points, a.rebounds))\n", "try:\n", " a.points = -5\n", "except ValueError as e:\n", " print(\"Error! {}\".format(e))" ] }, { "cell_type": "code", "execution_count": 14, "metadata": { "collapsed": false, "deletable": true, "editable": true, "slideshow": { "slide_type": "slide" } }, "outputs": [], "source": [ "def named_descriptors(cls):\n", " for name, attr in cls.__dict__.items():\n", " if isinstance(attr, NonNegativeField):\n", " attr.name = name\n", " return cls\n", "\n", "@named_descriptors\n", "class BasketballGame:\n", " points = NonNegativeField()\n", " rebounds = NonNegativeField()\n", " steals = NonNegativeField()\n", "\n", " def __init__(self, points, rebounds, steals):\n", " self.points = points\n", " self.rebounds = rebounds\n", " self.steals = steals" ] }, { "cell_type": "code", "execution_count": 15, "metadata": { "collapsed": false, "deletable": true, "editable": true, "slideshow": { "slide_type": "-" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "points: 100, rebounds: 30\n", "Error! ('Positive values only!',)\n" ] } ], "source": [ "a = BasketballGame(points=100, rebounds=30, steals=10)\n", "print(\"points: {}, rebounds: {}\".format(a.points, a.rebounds))\n", "try:\n", " a.points = -5\n", "except ValueError as e:\n", " print(\"Error! {}\".format(e.args))" ] }, { "cell_type": "code", "execution_count": 16, "metadata": { "collapsed": true, "deletable": true, "editable": true, "slideshow": { "slide_type": "slide" } }, "outputs": [], "source": [ "class NonNegativeField:\n", " def __get__(self, instance, owner):\n", " return instance.__dict__[self.name]\n", "\n", " def __set__(self, instance, value):\n", " if value < 0:\n", " raise ValueError('Positive values only!')\n", " instance.__dict__[self.name] = value\n", " \n", " # new in Python 3.6\n", " def __set_name__(self, owner, name):\n", " self.name = name\n", "\n", "class BasketballGame:\n", " points = NonNegativeField()\n", " rebounds = NonNegativeField()\n", " steals = NonNegativeField()\n", "\n", " def __init__(self, points, rebounds, steals):\n", " self.points = points\n", " self.rebounds = rebounds\n", " self.steals = steals" ] }, { "cell_type": "code", "execution_count": 17, "metadata": { "collapsed": false, "deletable": true, "editable": true, "slideshow": { "slide_type": "-" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "points: 100, rebounds: 30\n", "Error! Positive values only!\n" ] } ], "source": [ "a = BasketballGame(points=100, rebounds=30, steals=10)\n", "print(\"points: {}, rebounds: {}\".format(a.points, a.rebounds))\n", "try:\n", " a.points = -5\n", "except ValueError as e:\n", " print(\"Error! {}\".format(e))" ] }, { "cell_type": "markdown", "metadata": { "deletable": true, "editable": true, "slideshow": { "slide_type": "slide" } }, "source": [ "" ] }, { "cell_type": "markdown", "metadata": { "deletable": true, "editable": true, "slideshow": { "slide_type": "slide" } }, "source": [ "# References\n", "1. [StackOverflow - What is encapsulation? How does it actually hide data?](http://stackoverflow.com/questions/5673829/what-is-encapsulation-how-does-it-actually-hide-data)\n", "* [Shahriar Tajbakhsh - Underscores in Python](https://shahriar.svbtle.com/underscores-in-python)\n", "* [Shalabh Chaturvedi - Python Attributes and Methods](http://www.cafepy.com/article/python_attributes_and_methods/python_attributes_and_methods.html)\n", "* [Raymond Hettinger - Descriptor HowTo Guide](https://docs.python.org/3/howto/descriptor.html)\n", "* Simeon Franklin - Python Descriptors [video](https://www.youtube.com/watch?v=ZdvpNaWwx24) & [presentation](http://simeonfranklin.com/talk/descriptors.html)\n", "* [Laura Rupprecht - Describing Descriptors - PyCon 2015](https://www.youtube.com/watch?v=h2-WPwGnHqE)\n", "* Jacob Zimmerman - Python Descriptors, Apress Publishing (2006)\n", "* [Dan Sackett - An introduction to Python descriptors](http://programeveryday.com/post/an-introduction-to-python-descriptors/)" ] } ], "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 }