Skip to content

Instantly share code, notes, and snippets.

@holdenweb
Created September 29, 2016 14:48
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save holdenweb/0f7f66ab7d31e84158f8e682e14c7611 to your computer and use it in GitHub Desktop.
Save holdenweb/0f7f66ab7d31e84158f8e682e14c7611 to your computer and use it in GitHub Desktop.
Display the source blob
Display the rendered blob
Raw
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### A Quick Introduction to Traits\n",
"\n",
"Traits aren't very well-documented.\n",
"Since they are ptentially extremely useful for \"software plumbing\"\n",
"this short notebook in intended to familiarise you with their behaviour.\n",
"\n",
"First let me show you how up-to-date I am:"
]
},
{
"cell_type": "code",
"execution_count": 1,
"metadata": {
"collapsed": false
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"3.7.0a0 (default, Sep 26 2016, 13:30:21) \n",
"[GCC 5.2.1 20151010]\n"
]
}
],
"source": [
"import sys; print(sys.version)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Since the Jupyter Notebook (and the IPython system generally) use the `traitlets` module, a lightweight implementation, we will too.\n",
"\n",
"We begin by making the whole module available, then we import a few specific names."
]
},
{
"cell_type": "code",
"execution_count": 2,
"metadata": {
"collapsed": false
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"traitlets 4.3.0 /home/bmll-dev/.virtualenvs/python370a/lib/python3.7/site-packages/traitlets/__init__.py\n"
]
}
],
"source": [
"import traitlets\n",
"print(\"traitlets\", traitlets.__version__, traitlets.__file__)\n",
"from traitlets import HasTraits, Integer, Float, Unicode, observe, Any, All\n",
"import time"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"To declare a class with traits, it must inherit from `traitlets.HasTraits`.\n",
"Adding a trait to a class is simply a matter of binding a new Trait\n",
"object to a class variable·"
]
},
{
"cell_type": "code",
"execution_count": 3,
"metadata": {
"collapsed": false
},
"outputs": [],
"source": [
"class MyClass(HasTraits):\n",
" i = Integer()\n",
" f = Float()\n",
" u = Unicode()\n",
"\n",
"mc = MyClass()\n",
"mc.i = 42"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"As you can see, the instance has an `i` attribute.\n",
"Creating a trait on the class autmatically defines a constrained instance variable.\n",
"The primary constraint is on the type of assigned values."
]
},
{
"cell_type": "code",
"execution_count": 4,
"metadata": {
"collapsed": false
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"<class 'traitlets.traitlets.TraitError'>\n",
"The 'i' trait of a MyClass instance must be an int, but a value of '33' <class 'str'> was specified.\n"
]
}
],
"source": [
"try:\n",
" mc.i = \"33\"\n",
"except Exception as e:\n",
" print(type(e))\n",
" print(e)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"You can ask a class with traits what traits it has."
]
},
{
"cell_type": "code",
"execution_count": 5,
"metadata": {
"collapsed": false
},
"outputs": [
{
"data": {
"text/plain": [
"{'f': <traitlets.traitlets.Float at 0x7f5c7306bc18>,\n",
" 'i': <traitlets.traitlets.Int at 0x7f5c7306bba8>,\n",
" 'u': <traitlets.traitlets.Unicode at 0x7f5c7306bc50>}"
]
},
"execution_count": 5,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"MyClass.class_traits()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"You can also ask instances what traits they have."
]
},
{
"cell_type": "code",
"execution_count": 6,
"metadata": {
"collapsed": false
},
"outputs": [
{
"data": {
"text/plain": [
"{'f': <traitlets.traitlets.Float at 0x7f5c7306bc18>,\n",
" 'i': <traitlets.traitlets.Int at 0x7f5c7306bba8>,\n",
" 'u': <traitlets.traitlets.Unicode at 0x7f5c7306bc50>}"
]
},
"execution_count": 6,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"mc.traits()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Most traits allow you to provide a default value as a positional argument to the instantiation call."
]
},
{
"cell_type": "code",
"execution_count": 7,
"metadata": {
"collapsed": false
},
"outputs": [
{
"data": {
"text/plain": [
"5"
]
},
"execution_count": 7,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"class MyClass(HasTraits):\n",
" i = Integer(5)\n",
"mc = MyClass()\n",
"mc.i"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"If an object has traits, you can set their initial values using their names as keyword arguments."
]
},
{
"cell_type": "code",
"execution_count": 8,
"metadata": {
"collapsed": false
},
"outputs": [
{
"data": {
"text/plain": [
"120"
]
},
"execution_count": 8,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"mc = MyClass(i=120)\n",
"mc.i"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"In order to track what's going on more effectively I'm going to give the instances of my next class names.\n",
"I'll use the names of the variable to which I bind them."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Traits Implement the Observer Pattern"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"When you create a class with traits you can monitor changes to the trait values.\n",
"Decorating a method with the `traitlets.observe` decorator makes this connection,\n",
"and also allows you to specify which traits you want the method to monitor."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"collapsed": false
},
"outputs": [],
"source": [
"class MyClass(HasTraits):\n",
" i = Integer()\n",
" f = Float()\n",
" u = Unicode()\n",
" def __init__(self, name):\n",
" self.name = name\n",
" @observe('i') # Single trait\n",
" def _class_observed_changed(self, c):\n",
" print(\"Class observed obj {}.{} was {!r} is now {!r}\".format(self.name, c.name, c.old, c.new), sep='\\n')\n",
" c[\"timestamp\"] = time.asctime(time.localtime())\n",
"\n",
"mc1 = MyClass('mc1')\n",
"mc1.i = 42\n",
"mc1.f = 3.14159\n",
"mc1.u = \"Python Programming\""
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"collapsed": false
},
"outputs": [],
"source": [
"class MyClass(HasTraits):\n",
" i = Integer()\n",
" f = Float()\n",
" u = Unicode()\n",
" def __init__(self, name):\n",
" self.name = name\n",
" @observe('u', 'f') # Selected traits\n",
" def _class_observed_changed(self, c):\n",
" print(\"Class observed obj {}.{} was {!r} is now {!r}\".format(self.name, c.name, c.old, c.new), sep='\\n')\n",
" c[\"timestamp\"] = time.asctime(time.localtime())\n",
"\n",
"mc1 = MyClass('mc1')\n",
"mc1.i = 42\n",
"mc1.f = 3.14159\n",
"mc1.u = \"Python Programming\""
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"collapsed": false
},
"outputs": [],
"source": [
"class MyClass(HasTraits):\n",
" i = Integer()\n",
" f = Float()\n",
" u = Unicode()\n",
" def __init__(self, name):\n",
" self.name = name\n",
" @observe(All) # Guess\n",
" def _class_observed_changed(self, c):\n",
" print(\"Class observed obj {}.{} was {!r} is now {!r}\".format(self.name, c.name, c.old, c.new), sep='\\n')\n",
" c[\"timestamp\"] = time.asctime(time.localtime())\n",
"\n",
"mc1 = MyClass('mc1')\n",
"mc1.i = 42\n",
"mc1.f = 3.14159\n",
"mc1.u = \"Python Programming\""
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Such static observation may not be sufficient for your needs.\n",
"In that case you can dynamically add and remove observers to\n",
"and from individual instances."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"collapsed": false
},
"outputs": [],
"source": [
"def report_change(c):\n",
" print(\"R_C: {}.{} was {!r} is now {!r}\".format(c.owner.name, c.name, c.old, c.new))"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"collapsed": false
},
"outputs": [],
"source": [
"mc1.observe(report_change) # reports all trait changes on instance"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"collapsed": false
},
"outputs": [],
"source": [
"mc1.i = 102 # Now reported by both class and instance"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"collapsed": false
},
"outputs": [],
"source": [
"mc1.f = 3.2"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"collapsed": false
},
"outputs": [],
"source": [
"mc1.u = \"the quick brown fox\""
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"collapsed": false
},
"outputs": [],
"source": [
"def _mc_changed(c):\n",
" print(\"{}.{} changed\".format(c.owner.name, c.name))\n",
"mc2 = MyClass('mc2')\n",
"#mc2.observe(report_change)\n",
"mc2.observe(_mc_changed, [\"i\", \"f\"]) # # reports selected trait changes on instance\n",
"\n",
"print(\"-- i changes --\")\n",
"mc2.i = 101\n",
"print(\"-- f changes --\")\n",
"mc2.f = 1.234\n",
"print(\"-- u changes --\")\n",
"mc2.u = \"Steve Holden\" # still gets class notification and report change"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"collapsed": false
},
"outputs": [],
"source": [
"mc2.unobserve(_mc_changed, \"i\")\n",
"mc2.f = 2.345\n",
"mc2.i = 202"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"collapsed": false
},
"outputs": [],
"source": [
"def instance_changed(c):\n",
" print(\"instance changed\", c.name, \"to\", c.new)\n",
"mc1.observe(instance_changed, [\"i\", \"f\"]) # reports changes only to mc2.i and mc2.f\n",
"\n",
"print(\"-- mc1.i changes --\")\n",
"mc1.i = 1001\n",
"print(\"-- mc1.f changes --\")\n",
"mc1.f = 1.543219\n",
"print(\"-- mc1.u changes --\")\n",
"mc1.u = \"Sydney Poitier!\" # still gets class notification and report change\n",
"print(\"-- mc2.i changes --\")\n",
"mc2.i = 999\n",
"print(\"-- mc2.f changes --\")\n",
"mc2.f = 3.14159\n",
"print(\"-- mc2.u changes --\")\n",
"mc2.u = \"Somebody\""
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"So, here's a question.\n",
"If you repeat the execution of the cell above, there are no notifications.\n",
"Why this different output?\n",
"\n",
"Here's another one.\n",
"If you change the values being bound to the traits and then execute the cell a _third_ time,\n",
"why do you see the repeated notifications?"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"If you have some processing that must be finished before an object's trait changes are actioned you can hold off notifications using the `hold_trait_notifications` method."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"collapsed": false
},
"outputs": [],
"source": [
"mc2 = MyClass('mc2')\n",
"mc2.observe(lambda c: (c.old, c.new))\n",
"with mc1.hold_trait_notifications():\n",
" mc1.i = 1\n",
" mc2.i = 101\n",
" print(\"** Releasing notification hold\")"
]
},
{
"cell_type": "markdown",
"metadata": {
"collapsed": true
},
"source": [
"Note that _class_ notifications cannot be held in the same way: they will always fire."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### A little funs\n",
"\n",
"Let's analyse this code before we run it."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"collapsed": true
},
"outputs": [],
"source": [
"class MyCounter(HasTraits):\n",
" i = Integer()\n",
"\n",
"from threading import Thread\n",
"from time import sleep\n",
"\n",
"class MyThread(Thread):\n",
" def __init__(self, counter):\n",
" Thread.__init__(self)\n",
" self.counter = counter\n",
" self.stopper = True\n",
" def run(self):\n",
" while self.stopper: # Allows easy termination\n",
" sleep(1)\n",
" self.counter.i += 1\n",
" def stop(self):\n",
" self.stopper = False\n",
"\n",
"thread = MyThread(MyCounter())\n",
"thread.start()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"We've just started a thread whose counter has an `Integer` trait.\n",
"As long as the thread runs, it will increment the counter (approximately) once a second.\n",
"We can observe changes in the counter as the thread runs in the background.\n",
"To give us an easy way to stop the threads we use a thread variable `stopper`,\n",
"and provide a `stop` mathod to set it (requirements gold-plating?)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Now we will monitor changes to the thread's counter."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"collapsed": false,
"scrolled": true
},
"outputs": [],
"source": [
"def report_counter(c):\n",
" print(\"Counter is\", c.new)\n",
"\n",
"thread.counter.observe(report_counter, 'i')"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"collapsed": false
},
"outputs": [],
"source": [
"thread.counter.unobserve(report_counter, 'i') # or skip this and just stop the thread ..."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"collapsed": false
},
"outputs": [],
"source": [
"thread.stop()"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"collapsed": false
},
"outputs": [],
"source": [
"thread.counter.i = 0"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Discussion\n",
"\n",
"1. How might traits be useful in building modular architectures?\n",
"\n",
"2. What kind of changes might usefully be observed?"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3.7.0a0",
"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.7.0a0"
}
},
"nbformat": 4,
"nbformat_minor": 0
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment