Zev Eisenberg Zev Eisenberg - 29 days ago 7
Python Question

Sometimes-readonly property

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

Answer Source

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.