Click here to Skip to main content
15,881,967 members
Articles / Programming Languages / Python2.7

Adding C#-like Property Events to Python

Rate me:
Please Sign up or sign in to vote.
4.50/5 (2 votes)
21 Apr 2016CPOL8 min read 20.2K   73   3   6
For C# devs, a look at Python. For Python devs, maybe something useful

Preamble

I've written this article mainly for C# programmers that are interested in Python, so for Python developers, you'll encounter some things that I'll say that will probably elicit a "well, duh!" moment.  Just ignore those.  Also, I'm not really into the pythonic way of naming functions like do_something_here.  I'm still a C# programmer, and I much prefer, and find equally readable, naming my functions like this: doSomethingHere.  Deal with it.  I've seen both conventions used in Python libraries -- sometimes rather inconsistently -- and yes, I understand that the underscore format is the more pythonic way of doing things.

Also, apologies for putting this in the C# section -- Code Project doesn't have a Python language section!

Introduction

There are times when you really do want a side-effect when setting a property in a class.  User interfaces are a good example, where, for example, in the C# Form class, you do this (given a Form instance form):

form.Location = new Point(100, 100);

Or even something simpler, like setting the width:

form.Width = 500;

The form is immediately updated, meaning that the change is visible immediately on the screen.

To implement property getter/setter side-effects in Python, we have to do something like this:

class TheUsualWay(object):
  def setWidth(self, w):
    self._width = w
    print("Call side-effect")

  width = property(lambda self: self._width,
                   lambda self, value: self.setWidth(value))

And a simple illustration of how to use the width property:

q = TheUsualWay()
q.width = 1
print("Width = " + str(q.width))

Notice a couple things:

  • The underlying attribute (or in C# parlance, "field") has a leading underscore, which is necessary to prevent recursion when we set the value, because otherwise it's using the property setter again!
  • We have a lot of typing to do, creating the property with the two lambda expressions

Don't like lambdas?  You could write this instead:

class TheUsualWay(object):
  def getWidth(self):
      return self._width

  def setWidth(self, w):
    self._width = w
    print("Call side-effect")

  width = property(getWidth, setWidth)

You're still typing a bunch -- in this case, explicit getters and setters.

Now, the nice thing about Python is that, if you started with a simple attribute width but you decide later that you want to add some side-effect behaviors with a property of the same name, you can make the change to your class without having to fix all the references of width in other classes.  Hence why in C# it's really bad practice to access fields directly, and instead use properties, for the same reason.

For a small set of attributes that you decide needs some side-effect, the above two examples is a great way to go.  However, if you have a large number of fields where you'd like to have side-effects (UI programming is a good example), this becomes annoyingly tedious, so I'll show you a different (not claiming to be better) way of associating "events" with property get and set calls.

The Basic Implementation

To support property events, we'll require that the class requiring them derives from PropertyEvents.  This is not a problem in Python because Python supports multiple inheritance (that's for C# devs and is one of those "well, duh!" moments for Python devs.)

First, we'll sub-class the Python dictionary dict (why will be explained later):

class CallbackDictionary(dict):
  pass

For C# devs, the pass keyword is basically a "do nothing" statement, meaning that in this case, CallbackDictionary doesn't extend dict with any additional functionality -- yet.

Basic PropertyEvents Class

This class provides the functionality for wiring up callbacks ("events") for property get and set activities.  First, we initialize two dictionaries, one for the "get" events associated with one or more properties, the other for the "set" events:

class PropertyEvents(object):
  def __init__(self):
    self._getCallbacks = CallbackDictionary(self)
    self._setCallbacks = CallbackDictionary(self)

For C# devs, __init__ is like a class constructor, but not quite (more on this later.)  The variable name self is object instance -- every class method gets its instance, and you must use this instance to call other class methods or access class attributes and properties.  It's sort of like the C# this keyword.  The use of the name "self" is just a convention, but it's one that's universally adopted.

The dictionaries are intended to be key-value pairs where the key is the property name and the value is an array of callbacks to fire:

class PropertyEvents(object):
  def __init__(self):
    self._getCallbacks = CallbackDictionary()
    self._setCallbacks = CallbackDictionary()

Internal Binding and Unbinding Callback of Functions

Internally, we bind and unbind callbacks functions with "private" class methods -- methods with a leading underscore:

def _bind(self, name, callback, callbacks):
  # We use setdefault here because lambdas cannot have assigment expressions.
  callbacks[name].append(callback) if (callbacks.has_key(name)) else callbacks.setdefault(name, [callback])

def _unbind(self, name, callback, callbacks):
  if (callbacks.has_key(name) and callback in callbacks[name]):
    callbacks[name].remove(callback)

A Couple Helper Properties

These internal functions and the two properties that are defined are useful later on:

def _get_getters(self):
  return self._getCallbacks

def _get_setters(self):
  return self._setCallbacks

getters = property(_get_getters, lambda self, value: ())
setters = property(_get_setters, lambda self, value: ())

Note that the "set" function for the property is a do-nothing function (we can't use pass here.)

Exposed Methods for Binding and Unbinding Callback Functions

To the "user" of the class, we expose class methods (I'm using "function" and "method" interchangeably here) for binding and unbinding getter and setter methods to/from properties:

def bindGetter(self, name, callback):
  self._bind(name, callback, self.getters)

def unbindGetter(self, name, callback):
  self._unbind(name, callback, self.setters)

def bindSetter(self, name, callback):
  self._bind(name, callback, self.setters)

def unbindSetter(self, name, callback):
  self._unbind(name, callback, self.setters)

Here you can see the minor variance in these functions, in that they determine which "private" method to call and which collection to modify.

Calling the Property Get/Set Callbacks

Lastly, we have the class methods for actually handling the get property value and set property value behavior:

 def get(self, name):
  """ Calls any getter callbacks for the attribute [name] and then returns the value of the attribute [name]. """
  self._doCallbacks(name, self.getters)
  return getattr(self, self._privateName(name))

def set(self, name, value):
  """ Sets the value of attribute [name] and then calls any setter callbacks for the attribute [name]. """
  setattr(self, self._privateName(name), value)
  self._doCallbacks(name, self.setters)

def _privateName(self, name):
  """ Prepends the attribute [name] with '_', firstly to indicate that it is "private", secondly to avoid infinite recursion. """
  return '_' + name

def _doCallbacks(self, name, callbacks):
  if (callbacks.has_key(name)):
    for callback in callbacks[name]:
      callback(self)

Let's See How It Works So Far

Here's a test class defining the property "x":

class Test(PropertyEvents):
  def __init__(self):
    PropertyEvents.__init__(self)

  x = property(lambda self: self.get('x'), 
               lambda self, value: self.set('x', value))

For C# users, notice that the initializer has to explicitly call the base class initializer.  In Python, construction (instantiating the object) and initialization are separated out into two steps, which the Python programmer almost always doesn't need to think about.

Also notice how we're using lambda expressions to define the "get" and "set" functions of the property.

As a side note, it's (probably) impossible to get away from hard-coding a string literal for the property name (there might be some magic techniques for inspecting the code and figuring out the property name, but that is way beyond the scope of this article.)

We define a getter and setter callback:

def x_getter(obj):
print("Getter called")

def x_setter(obj):
print("Setter called")

And here is how we can test the code (without going into unit testing):

t = Test()
t.bindSetter('x', x_setter)
t.bindGetter('x', x_getter)
t.x = 5
print(t.x)

Which results in the output:

Image 1

Some So-Called Improvements

This section discusses various possible improvements (as in, less typing and other syntactical sugar).

Improving Defining Properties

I still think this is too much typing:

x = property(lambda self: self.get('x'), 
             lambda self, value: self.set('x', value))

But the options (actually, only one as far as I could figure out) are a bit bizarre.  What we can do is dynamically create the property when the class is instantiated, like this:

def __new__(cls):
  PropertyEvents.defineProperty(cls, 'y')
  return PropertyEvents.create(Test, cls)

Here we are defining what should happen when a class is created -- the other half of a C# constructor.  Because we have no instance yet, only the type of the object being constructed, we cannot do any initialization of member attributes, nor call member functions, etc.  To illustrate the difference, this is what the debugger says "cls" is:

Image 2

It is of type "type"!  The create function (see below) is just a thin wrapper so we don't have to type as much.

Contrast that with the type of self in the __init__(self) function:

Image 3

Here, "self" is of the type "Test", which is our test class -- here we have the actual instance.

We also need a couple static helper methods that we add to the PropertyEvents class:

@staticmethod
  def defineProperty(cls, name):
    setattr(cls, name, property(fget = lambda self: self.get(name), 
    fset = lambda self, value: self.set(name, value)))

@staticmethod
  def create(sub, base):
    obj = super(sub, base).__new__(base)
    return obj

You may ask, if defineProperty is called without the class instance existing yet, how can the lambda expression use self?  That's because (this should be a "well, duh!" for both C# and Python programmers) the lambda function is a closure and is not evaluated until it is called.

Binding setters and getters is still exactly the same:

t.bindSetter('y', y_setter)
t.bindGetter('y', y_getter)

Is this approach better?  Well, it's less typing if we are creating many properties, and it avoid the possibility of a typo, like this:

y = property(lambda self: self.get('x'),
             lambda self, value: self.set('x', value))

Oops, we're assigning a property to y but the attribute name is x!

Most Events Are Wired to Property Setters

Most of the time, we're wanting to wire up a callback to just the property setter.  We can do that in a more C# way by implementing an override to the += operator.  This requires defining the operator functions for += and -= (the latter so we can unbind a property):

def __add__(self, callback):
  PropertyEvents._assertIsInstanceOfDict(callback)
  self.bindSetter(callback.keys()[0], callback.values()[0])
  return self

def __sub__(self, callback):
  PropertyEvents._assertIsInstanceOfDict(callback)
  self.unbindSetter(callback.keys()[0], callback.values()[0])
  return self

@staticmethod
def _assertIsInstanceOfDict(src):
  if (not isinstance(src, dict)):
    raise ParameterException("Expected dictionary, got " + str(type(src)))

And the exception class:

class ParameterException(Exception):
  pass

We can now add and remove setters with the += and -= "syntactical sugar":

t += {'x': x_setter}

Notice the assertion that the type being passed in is a dictionary.  That's because, if you wire up the property setter like this:

t += {'x', x_setter}

You are creating an unordered set, not a dictionary!  Do you see the difference?  A comma vs. a colon.  I make this mistake all too often (ok, I guess that reveals I'm still new at Python coding!)

Operator Overloading for Property Getters

To use the += and -= syntax for property getters, we have to explicitly state whether we're adding a callback to a setter or getter.  Remember the CallbackDictionary class at the beginning of the article?  This is where it comes in handy.  We'll define what it does now:

class CallbackDictionary(dict):
  def __init__(self, events):
    self.events = events

def __add__(self, callback):
  PropertyEvents._assertIsInstanceOfDict(callback)
  self.events._bind(callback.keys()[0], callback.values()[0], self)
  return self

def __sub__(self, callback):
  PropertyEvents._assertIsInstanceOfDict(callback)
  self.events._unbind(callback.keys()[0], callback.values()[0], self)
  return self

and modify the PropertyEvents initialization slightly, passing in itself:

def __init__(self):
  self._getCallbacks = CallbackDictionary(self)
  self._setCallbacks = CallbackDictionary(self)

Now we can write this:

t.getters += {'x': x_getter}

So, effectively, we have three ways of creating setter callbacks:

t += {'x': x_setter}
t.setters += {'x': x_setter}
t.bindSetter('x', x_setter)

and two ways of creating getters callbacks:

t.getters += {'x': x_getter}
t.bindGetter('x', x_getter)

Similarly for unbinding getters and setters.

Read-Only Properties

Lastly, we can create a read-only property.  Using the dynamic property creation technique, we can define a read-only property like this:

PropertyEvents.defineReadOnlyProperty(cls, 'z')

utilizing a new static method:

@staticmethod
def defineReadOnlyProperty(cls, name):
  setattr(cls, name, property(fget = lambda self: self.get(name), 
                              fset = lambda self, value: self._readOnlyException(name)))

and the "private" exception function (since we can't do a raise in a lambda expression):

def _readOnlyException(self, name):
  raise ReadOnlyPropertyException(name + " is read only")

and the exception class itself:

class ReadOnlyPropertyException(Exception):
  pass

Conclusion

Well, that was a lot of work to avoid a little bit of getter and setter typing.  On the other hand, we have a re-usable module for wiring up property get/set callbacks, and the syntax is about as good as it's going to get.

If you're a C# programmer, you will by now hopefully appreciate the event capability of the language.  If you're a Python programmer, hopefully I haven't made too many un-pythionic mistakes.  If you're a C# programmer looking at Python, well, I hope this all made sense and you learned some interesting things!

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Architect Interacx
United States United States
Blog: https://marcclifton.wordpress.com/
Home Page: http://www.marcclifton.com
Research: http://www.higherorderprogramming.com/
GitHub: https://github.com/cliftonm

All my life I have been passionate about architecture / software design, as this is the cornerstone to a maintainable and extensible application. As such, I have enjoyed exploring some crazy ideas and discovering that they are not so crazy after all. I also love writing about my ideas and seeing the community response. As a consultant, I've enjoyed working in a wide range of industries such as aerospace, boatyard management, remote sensing, emergency services / data management, and casino operations. I've done a variety of pro-bono work non-profit organizations related to nature conservancy, drug recovery and women's health.

Comments and Discussions

 
QuestionIndentation Pin
frankazoid22-Apr-16 7:47
frankazoid22-Apr-16 7:47 
AnswerRe: Indentation Pin
Marc Clifton22-Apr-16 9:06
mvaMarc Clifton22-Apr-16 9:06 
BugImg1.png is missing Pin
webmaster44221-Apr-16 18:34
webmaster44221-Apr-16 18:34 
GeneralRe: Img1.png is missing Pin
Marc Clifton22-Apr-16 1:13
mvaMarc Clifton22-Apr-16 1:13 
QuestionA quick note about property() Pin
Brisingr Aerowing21-Apr-16 16:50
professionalBrisingr Aerowing21-Apr-16 16:50 
AnswerRe: A quick note about property() Pin
Marc Clifton22-Apr-16 1:19
mvaMarc Clifton22-Apr-16 1:19 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.