{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "Python for Everyone!
[Oregon Curriculum Network](http://4dsolutions.net/ocn/)\n", "\n", "# Descriptors and Properties in Python\n", "\n", "\"Retired\n", "\n", "## Descriptors\n", "\n", "Lets take a look at the descriptor protocol. When and how binding happens, and later lookup, is intimately controlled by \\_\\_set\\_\\_ and \\_\\_get\\_\\_ methods respectively. When defined for a class of object, getting and setting become mediated operations, without changes to the outward API (user interface).\n", "\n", "For example, here is some code directly from the Python docs:" ] }, { "cell_type": "code", "execution_count": 1, "metadata": { "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Retrieving var \"x\"\n", "m1.x: 10\n", "Updating var \"x\"\n", "Updating var \"x\"\n", "Retrieving var \"x\"\n", "m1.x: 10\n", "Retrieving var \"x\"\n", "m2.x: 10\n", "m1.y: 5\n", "Retrieving var \"x\"\n", "Retrieving var \"x\"\n", "True\n" ] } ], "source": [ "class RevealAccess(object):\n", " \"\"\"A data descriptor that sets and returns values\n", " normally and prints a message logging their access.\n", "\n", " Descriptor Example: \n", " https://docs.python.org/3/howto/descriptor.html\n", " \"\"\"\n", "\n", " def __init__(self, initval=None, name='var'):\n", " self.val = initval\n", " self.name = name\n", "\n", " def __get__(self, obj, objtype):\n", " print('Retrieving', self.name)\n", " return self.val\n", "\n", " def __set__(self, obj, val):\n", " print('Updating', self.name)\n", " self.val = val\n", " \n", "class MyClass(object):\n", " x = RevealAccess(10, 'var \"x\"')\n", " y = 5\n", "\n", "# Let's test...\n", "m1 = MyClass()\n", "m2 = MyClass()\n", "print(\"m1.x: \", m1.x)\n", "m1.x = 20\n", "m2.x = 10\n", "print(\"m1.x: \", m1.x)\n", "print(\"m2.x: \", m2.x)\n", "print(\"m1.y: \", m1.y)\n", "print(m1.x is m2.x)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "*y*'s value is an ordinary int, equivalently the value of MyClass.\\_\\_dict\\_\\_['y'], whereas the x attribute, a descriptor, will police getting and setting through \\_\\_get\\_\\_ and \\_\\_set\\_\\_ methods, using the name 'x' as a proxy to x.val behind the scenes (think of x.val as \"more secret\" as in less directly accessible). \n", "\n", "Notice our distinct instances of MyClass nevertheless share both x and y as class level names. Changing the value for one changes it for all. Building on this behavior, a Pythonic way to define setters and getters that store data at the instance level becomes possible.\n", "\n", "## Properties\n", "\n", "The code below is likewise from the Python 3.5 version of the docs at Python.org, and shows how the built-in property() type may be modeled as a pure Python class." ] }, { "cell_type": "code", "execution_count": 2, "metadata": { "collapsed": true }, "outputs": [], "source": [ "class Property(object):\n", " \"Emulate PyProperty_Type() in Objects/descrobject.c\"\n", "\n", " def __init__(self, fget=None, fset=None, fdel=None, doc=None):\n", " self.fget = fget\n", " self.fset = fset\n", " self.fdel = fdel\n", " if doc is None and fget is not None:\n", " doc = fget.__doc__\n", " self.__doc__ = doc\n", "\n", " def __get__(self, obj, objtype=None):\n", " if obj is None:\n", " return self\n", " if self.fget is None:\n", " raise AttributeError(\"unreadable attribute\")\n", " return self.fget(obj)\n", "\n", " def __set__(self, obj, value):\n", " if self.fset is None:\n", " raise AttributeError(\"can't set attribute\")\n", " self.fset(obj, value)\n", "\n", " def __delete__(self, obj):\n", " if self.fdel is None:\n", " raise AttributeError(\"can't delete attribute\")\n", " self.fdel(obj)\n", "\n", " def getter(self, fget):\n", " return type(self)(fget, self.fset, self.fdel, self.__doc__)\n", "\n", " def setter(self, fset):\n", " return type(self)(self.fget, fset, self.fdel, self.__doc__)\n", "\n", " def deleter(self, fdel):\n", " return Property(self.fget, self.fset, fdel, self.__doc__)\n", " " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The getter, setter and deleter methods allow swapping out new versions of fset, fget and fdel by keeping whatever is unchanged and making a new instance with a call to type(self) -- remember that types are callables.\n", "\n", "The C class now uses the Property class to fully develop a pet class level attribute named 'x', which in turn fully implements the descriptor protocol, as an instance of the Property descriptor." ] }, { "cell_type": "code", "execution_count": 3, "metadata": { "collapsed": true }, "outputs": [], "source": [ "class C:\n", " def getx(self): \n", " print(\"getting...\") \n", " return self.__x\n", " def setx(self, value): \n", " print(\"setting...\") \n", " self.__x = value\n", " def delx(self): \n", " print(\"deleting...\")\n", " del self.__x\n", " x = Property(getx, setx, delx, \"I'm the 'x' property.\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Think of x as an object prepared to delegate to the three methods. Every time a new C instance is created, that instance is bound to a deeply internal \"secret\" \\_\\_x. The public or class level x is a proxy to a set of instance methods living inside C.\\_\\_dict\\_\\_. Each instance of C talks to its respective self.\\_\\_x i.e. don't confuse the shared nature of x with the private individuality of each self.\\_\\_x\n", "\n", "The code below goes a step further in using two instances of C to demonstrate how properties work. In this case, the same Property class is used as a decorator. Notice how .setter() becomes available once the getter is defined, because the decorator has \"[abucted](http://nbviewer.jupyter.org/github/4dsolutions/Python5/blob/master/Abducted%21.ipynb)\" the original method and turned it into an instance of something, of which .setter is a now an attribute." ] }, { "cell_type": "code", "execution_count": 4, "metadata": { "collapsed": true }, "outputs": [], "source": [ "class Generic:\n", " \n", " def __init__(self, a=None, b=None):\n", " self.__dict__['y'] = C()\n", " self.y = a\n", " self.__dict__['z'] = C()\n", " self.z = b\n", "\n", " @Property\n", " def y(self):\n", " return self.__dict__['y'].x\n", "\n", " @y.setter\n", " def y(self, val):\n", " print(\"Generic setter for y\")\n", " self.__dict__['y'].x = val\n", "\n", " @Property\n", " def z(self):\n", " return self.__dict__['z'].x\n", " \n", " @z.setter\n", " def z(self, val):\n", " print(\"Generic setter for z\")\n", " self.__dict__['z'].x = val " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The reason for all the \\_\\_getitem\\_\\_ syntax i.e. talking to self.\\_\\_dict\\_\\_ in \"longhand\", is to avoid a recursive situation where a setter or getter calls itself. \\_\\_getitem\\_\\_ syntax lets us set and get in a way that bypasses \\_\\_getattribute\\_\\_ and its internal mechanisms, which are responsible for triggering the descriptor protocol in the first place.\n", "\n", "Once we have our instance of C in the guts of a setter or getter, we talk directly to its proxy, an instance of Property, which responds accordingly to setting and getting." ] }, { "cell_type": "code", "execution_count": 5, "metadata": { "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Generic setter for y\n", "setting...\n", "Generic setter for z\n", "setting...\n", "getting...\n", "me.y: 3\n", "getting...\n", "me.z: Hello\n", "Generic setter for y\n", "setting...\n", "Generic setter for z\n", "setting...\n", "getting...\n", "little_me.y: 4\n", "getting...\n", "little_me.z: World\n", "Generic setter for y\n", "setting...\n", "Generic setter for z\n", "setting...\n", "Generic setter for y\n", "setting...\n", "Generic setter for z\n", "setting...\n", "getting...\n", "me.y: 5\n", "getting...\n", "me.z: Ciao\n", "getting...\n", "little_me.y: 6\n", "getting...\n", "little_me.z: Mondo\n" ] } ], "source": [ "me = Generic(3, \"Hello\")\n", "print(\"me.y:\", me.y)\n", "print(\"me.z:\", me.z)\n", "little_me = Generic(4, \"World\")\n", "print(\"little_me.y:\", little_me.y)\n", "print(\"little_me.z:\", little_me.z)\n", "me.y = 5\n", "me.z = \"Ciao\"\n", "little_me.y = 6\n", "little_me.z = \"Mondo\"\n", "print(\"me.y:\", me.y)\n", "print(\"me.z:\", me.z)\n", "print(\"little_me.y:\", little_me.y)\n", "print(\"little_me.z:\", little_me.z)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Fun though that was, there's more indirection going on than necessary. \n", "\n", "The methods of Generic are themselves suitable as setters and getters, without needing to delegate to some instance of C with its fancy 'x' property. \n", "\n", "What you see below is the more usual Python program, except instead of using the pure Python class above, the equivalent built-in property type (lowercase) is used as a decorator instead. \n", "\n", "Reading the pure Python version shows how it works." ] }, { "cell_type": "code", "execution_count": 6, "metadata": { "collapsed": true }, "outputs": [], "source": [ "class Generic:\n", " \n", " def __init__(self, a=None, b=None):\n", " self.y = a\n", " self.z = b\n", " \n", " @property\n", " def y(self):\n", " return self.__y\n", " \n", " @y.setter\n", " def y(self, val):\n", " print(\"Generic setter for y\")\n", " self.__y = val\n", "\n", " @property\n", " def z(self):\n", " return self.__z\n", " \n", " @z.setter\n", " def z(self, val):\n", " print(\"Generic setter for z\")\n", " self.__z = val" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "This time, we've cut out the middle man, C. \n", "\n", "The Property class is where the descriptor protocol gets implemented. \n", "\n", "We turn Generic.y and Generic.z into properties by decorating methods of the same names. \n", "\n", "Through decorating, two class level Property objects, similar to C.x, get created, with each one providing a set of instance methods happy to work with a specific self. \n", "\n", "These four instance methods, defined within Generic itself, consult self.\\_\\_y and self.\\_\\_z much as x worked with self.\\_\\_x behind the scenes." ] }, { "cell_type": "code", "execution_count": 7, "metadata": { "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Generic setter for y\n", "Generic setter for z\n", "me.y: 3\n", "me.z: Hello\n", "Generic setter for y\n", "Generic setter for z\n", "little_me.y: 4\n", "little_me.z: World\n", "Generic setter for y\n", "Generic setter for z\n", "Generic setter for y\n", "Generic setter for z\n", "me.y: 5\n", "me.z: Ciao\n", "little_me.y: 6\n", "little_me.z: Mondo\n" ] } ], "source": [ "me = Generic(3, \"Hello\")\n", "print(\"me.y:\", me.y)\n", "print(\"me.z:\", me.z)\n", "little_me = Generic(4, \"World\")\n", "print(\"little_me.y:\", little_me.y)\n", "print(\"little_me.z:\", little_me.z)\n", "me.y = 5\n", "me.z = \"Ciao\"\n", "little_me.y = 6\n", "little_me.z = \"Mondo\"\n", "print(\"me.y:\", me.y)\n", "print(\"me.z:\", me.z)\n", "print(\"little_me.y:\", little_me.y)\n", "print(\"little_me.z:\", little_me.z)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "By the way, notice that method( ) has a single argument 'this', showing that 'self' is by convention and, furthermore, the value of 'this' will depend on whether method( ) is called: on an instance or on a class. \n", "\n", "Calling me.method() sets 'this' to the instance object i.e. what 'self' is used for. \n", "\n", "However Generic.method(Generic) is likewise legal Python, and in this case you must pass the class explicitly if that's what's needed. \n", "\n", "The @classmethod decorator, applied to a method, will pass in the class automatically." ] }, { "cell_type": "code", "execution_count": 8, "metadata": { "collapsed": false, "scrolled": true }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Generic setter for y\n", "Generic setter for z\n", "On an instance: this is: Instance\n", "On the class: this is: Class\n", "Generic setter for y\n", "Generic setter for z\n", "With @classmethod decorator: this is: Class\n" ] } ], "source": [ "class Generic2(Generic):\n", " \n", " def method(this):\n", " return (\"this is: \" + \n", " (\"Instance\" if isinstance(this, Generic2) \n", " else \"Class\"))\n", "\n", "class Generic3(Generic):\n", " \n", " @classmethod\n", " def method(this):\n", " return (\"this is: \" + \n", " (\"Instance\" if isinstance(this, Generic2) \n", " else \"Class\"))\n", " \n", "me = Generic2(1,2)\n", "print(\"On an instance: \", me.method())\n", "print(\"On the class: \", Generic2.method(Generic2))\n", "\n", "me = Generic3(1,2)\n", "print(\"With @classmethod decorator: \", me.method())" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "So that's a lot of fancy theory, but what might be a practical application of the above. Suppose we want a circle to let us modify its radius at will, and to treat area as an ordinary attribute nonetheless..." ] }, { "cell_type": "code", "execution_count": 9, "metadata": { "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Circle(1)\n", "Area of the circle: 3.141593\n", "Area of the circle: 12.566371\n" ] } ], "source": [ "import math\n", "\n", "class Circle:\n", " \n", " def __init__(self, r):\n", " self.radius = r\n", " \n", " @property\n", " def area(self):\n", " return self.radius ** 2 * math.pi\n", " \n", " def __repr__(self):\n", " return \"Circle({})\".format(self.radius)\n", " \n", "the_circle = Circle(1)\n", "print(the_circle) # triggers __repr__ in the absence of __str__\n", "print(\"Area of the circle: {:f}\".format(the_circle.area))\n", "the_circle.radius = 2\n", "print(\"Area of the circle: {:f}\".format(the_circle.area))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "In decorating only the area method, we provide the area property with a getter, i.e. fget has been set to this method. No setter proxy (self.fset) has been defined, hence an assignment to the area property, which triggers its \\_\\_set\\_\\_ method, raises an AttributeError (see Property.\\_\\_set\\_\\_)." ] }, { "cell_type": "code", "execution_count": 10, "metadata": { "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Can't set the area directly\n" ] } ], "source": [ "try:\n", " the_circle.area = 90\n", "except AttributeError:\n", " print(\"Can't set the area directly\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Might we make both radius and area into properties, such that setting either recalculates the other? \n", "\n", "Let's try:" ] }, { "cell_type": "code", "execution_count": 11, "metadata": { "collapsed": false }, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "..\n", "----------------------------------------------------------------------\n", "Ran 2 tests in 0.002s\n", "\n", "OK\n" ] }, { "data": { "text/plain": [ "" ] }, "execution_count": 11, "metadata": {}, "output_type": "execute_result" } ], "source": [ "import unittest\n", "\n", "class Circle:\n", " \"\"\"setting either the radius or area attribute sets the other\n", " as a dependent value. Initialized with radius only, unit \n", " circle by default.\n", " \"\"\"\n", " \n", " def __init__(self, radius = 1):\n", " self.radius = radius\n", " \n", " @property\n", " def area(self):\n", " return self._area\n", "\n", " @property \n", " def radius(self):\n", " return self._radius\n", " \n", " @area.setter\n", " def area(self, value):\n", " self._area = value\n", " self._radius = math.sqrt(self._area / math.pi)\n", " \n", " @radius.setter\n", " def radius(self, value):\n", " self._radius = value\n", " self._area = math.pi * (self._radius ** 2)\n", " \n", " def __repr__(self):\n", " return \"Circle(radius = {})\".format(self.radius)\n", " \n", "class TestCircle(unittest.TestCase):\n", "\n", " def testArea(self):\n", " the_circle = Circle(1)\n", " self.assertEqual(the_circle.area, math.pi, \"Uh oh\")\n", " \n", " def testRadius(self):\n", " the_circle = Circle(1)\n", " the_circle.area = math.pi * 4 # power rule\n", " self.assertEqual(the_circle.radius, 2, \"Uh oh\")\n", "\n", "a = TestCircle() # the test suite\n", "suite = unittest.TestLoader().loadTestsFromModule(a) \n", "unittest.TextTestRunner().run(suite) # run the test suite" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Related reading:\n", "\n", "[Abducted by Aliens: Decorators in Python](https://github.com/4dsolutions/Python5/blob/master/Abducted!.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.5.1" }, "widgets": { "state": {}, "version": "1.1.2" } }, "nbformat": 4, "nbformat_minor": 0 }