1

I have a class, CameraInterface, with two properties:

  • recording (a boolean)
  • recording_quality (an enumeration with three possible values)

The user can push hardware buttons (this is a Raspberry Pi project) to select the recording_quality, but if the camera is already recording, the hardware will blink/buzz/otherwise indicate that you can’t change the recording_quality while you are recording.

I would like the CameraInterface object to enforce this requirement. I don’t want future me to be able to forget this requirement, and accidentally try setting the recording_quality without first making sure that recording is False. What is the most Pythonic way to do this?

I’m most familiar with Objective-C, where I might try making the property externally readonly, and then doing something like this:

- (void)setQuality:(Quality)quality
    successHandler:(void (^)(void))successHandler
    failureHandler:(void (^)(void))failureHandler
{
    if ( self.recording ) {
        if ( failureHandler ) {
            failureHandler();
        }
    }
    else {
        self.quality = quality; // internally writable
        if ( successHandler ) {
            successHandler();
        }
    }
}

Update

A clarification on what I am looking for:

  1. I would like to avoid using exceptions for control flow if possible.
  2. I would like to build the sometimes-settable nature of recording_quality into the programming interface to CameraInterface, so you don’t have to wait until run-time to find out that you did something wrong.
  3. I already know about Python properties. I’m just looking for the best way to use them (or an alternative).

Update 2

I have been convinced by Martijn Pieters’s answer to use run-time exceptions, and use try/catch when trying to set recording_quality. Part of what convinced me was trying to implement my Objective-C example in Python. In doing so, I realized that it is really better suited for a statically-typed language. Part of what convinced me was the answer that was linked in Martijn’s answer, which explains in more detail that exceptions are a perfectly normal part of Python control flow.

Here is my Python version of the Objective-C example, above. I leave it here as a warning to others:

import types

class CameraInterface(object):
    def __init__(self):
        self._recording_quality = 1
        self.recording = False

    @property
    def recording_quality(self):
        return self._recording_quality

    def set_recording_quality(self, quality, success_handler, failure_handler):
        if type(success_handler) is not types.FunctionType:
            raise ValueError("You must pass a valid success handler")
        if type(failure_handler) is not types.FunctionType:
            raise ValueError("You must pass a valid failure handler")

        if self.recording is True:
            # reject the setting, and call the failure handler
            failure_handler()
        else:
            self._recording_quality = quality
            success_handler()

def success():
    print "successfully set"

def failure():
    print "it was recording, so we couldn't set the quality"

camera = CameraInterface()
print "recording quality starting at {0}".format(camera.recording_quality)

camera.set_recording_quality(3, success, failure)
camera.recording = True
camera.set_recording_quality(2, success, failure)

This prints:

recording quality starting at 1
successfully set
it was recording, so we couldn't set the quality
Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
Zev Eisenberg
  • 8,080
  • 5
  • 38
  • 82

3 Answers3

3

You can use a @property whose setter raises an exception:

class CameraInterface(object):
    @property
    def recording_quality(self):
        return self._quality

    @recording_quality.setter
    def recording_quality(self, quality):
        if self.recording:
            raise CurrentlyRecordingError()
        self._quality = quality

Where CurrentlyRecordingError is a custom exception; it could be a subclass of ValueError.

In Python, using exceptions is the most natural way to handle an attribute that under specific circumstances cannot be set. Also see Is it a good practice to use try-except-else in Python?

The alternative would be to use a Look-before-you-leap approach where you have to explicitly test for the camera recording state each time you want to set the quality value. This complicates the API and can an easily lead to race conditions, where the camera started recording after you tested the state but before you changed the quality setting.

With an exception you instead Ask for Forgiveness; this simplifies the use of your object as you assume the quality setting can be made and can handle the read-only case in the exception handler.

Some further notes on your Objective-C inspired version from a Python perspective (apart from not using a property):

  • Don't use type(..) is [not] typeobj when you can use isinstance() instead.
  • Rather than test for types.FunctionType you can use the callable() function to test if something can be called; functions are not the only callables.
  • In Python, you trust the user of the API; if they pass in something that cannot be called, it's their own fault, and your object doesn't need to make explicit type checks here. Python is a language for consenting adults.
  • The if statement already tests for truth; using is True is not only redundant, but leads to hard to debug errors when used with other comparison operators, as such operators are chained. value == someothervalue is True does not mean what you think it means.
Community
  • 1
  • 1
Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
  • 1
    @ZevEisenberg: then simply ignore setting the attribute. But in Python, exceptions are the normal way to handle such situations. Unlike in C# and Java, exceptions are used for control flow like this *all the time*. – Martijn Pieters Mar 14 '15 at 23:42
  • Then I'll use exceptions to deal with invalid code paths, but I would like to make this behavior part of the API, so a user of the `CameraInterface` doesn’t even waste time trying to do the wrong thing. It shouldn't be *possible* to do the wrong thing. – Zev Eisenberg Mar 14 '15 at 23:44
  • 1
    In Python are frequently part of the API, which is why I proposed a custom exception. Exceptions are not just for invalid code paths. – Martijn Pieters Mar 14 '15 at 23:45
  • @Zev: the core Python language uses exceptions for flow control all the time; generators have `GeneratorExit`, iterators signal they are exhausted with `StopIteration`, etc. They are the ultimate out-of-band signal and do not carry the performance penalty they impose on other languages. I added a reference to a post by core Python developer Raymond Hettinger for you. – Martijn Pieters Mar 15 '15 at 00:23
  • You have me convinced. I guess this reveals a difference between Python and Objective-C. I tried to build a Python version of my Objective-C setter, and it quickly became unwieldily. It is definitely a solution intended for a more strongly typed language. In the Python version, I wasted so much code on run-time error checking that I lost all benefit of a clean interface. Thanks for showing me the light! – Zev Eisenberg Mar 15 '15 at 00:33
1

You could use properties:

class CameraInterface(object):
    def __init__(self):
        self.recording = False
        self._quality = None

    @property
    def recording_quality(self):
        return self._quality

    @recording_quality.setter
    def recording_quality(self, new):
        if self.recording:
            print("Can't change quality while recording")
        else:
            self.quality = new
jedwards
  • 29,432
  • 3
  • 65
  • 92
1

You'll want to make Python methods for the same name as your attributes and then use the @property decorator for getter and setter functionality:

class CameraInterface(object):
    def __init__(self):
        self.recording = False
        self._recording_quality = False

    def start_recording(self):
        # do stuff
        self.recording = True

    @property
    def recording_quality(self):
        return self._recording_quality

    @recording_quality.setter
    def recording_quality(self, value):
        if self.recording:
            raise Exception("Whoa! You're already recording!")
        self._recording_quality = value

This allows you to then do something like this:

camera = CameraInterface()
camera.recording_quality = HD
camera.start_recording()
camera.recording_quality = SD # Exception!
OozeMeister
  • 4,638
  • 1
  • 23
  • 31