{
"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
}