有时只读 属性

Sometimes-readonly property

我有一个 class、CameraInterface,有两个属性:

用户可以将硬件按钮(这是一个 Raspberry Pi 项目)推到 select recording_quality,但如果相机已经 recording,硬件将blink/buzz/otherwise 表示录制过程中不能更改recording_quality

我希望 CameraInterface 对象强制执行此要求。我不希望未来的我能够忘记这个要求,并且在没有首先确保 recording is False 的情况下不小心尝试设置 recording_quality。最 Pythonic 的方法是什么?

我最熟悉 Objective-C,我可能会尝试在外部制作 属性 readonly,然后做这样的事情:

- (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();
        }
    }
}

更新

澄清一下我在找什么:

  1. 如果可能,我想避免使用控制流异常。
  2. 我想将 recording_quality 的有时可设置的特性构建到 CameraInterface 的编程接口中,这样您就不必等到 运行-时间来查找证明你做错了什么。
  3. 我已经知道 Python 属性。我只是在寻找使用它们的最佳方式(或替代方式)。

更新 2

说服我使用 运行 时间异常,并在尝试设置 recording_quality 时使用 try/catch。说服我的部分原因是尝试在 Python 中实现我的 Objective-C 示例。在这样做的过程中,我意识到它确实更适合静态类型的语言。部分让我信服的是 Martijn 的答案中链接的答案,该答案更详细地解释了异常是 Python 控制流的一个完全正常的部分。

这是上面 Objective-C 示例的 Python 版本。我把它留在这里作为对其他人的警告:

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)

这会打印:

recording quality starting at 1
successfully set
it was recording, so we couldn't set the quality

您可以使用 @property,其 setter 引发异常:

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

其中CurrentlyRecordingError是自定义异常;它可能是 ValueError.

的子类

在Python中,使用异常处理在特定情况下无法设置的属性的最自然方式。另见 Is it a good practice to use try-except-else in Python?

另一种方法是使用 Look-before-you-leap 方法,每次要设置质量值时,您都必须明确测试摄像机的录制状态。这会使 API 变得复杂,并且很容易导致竞争条件,即相机在您测试状态之后但在更改质量设置之前开始录制。

例外的是你 Ask for Forgiveness;这简化了对象的使用,因为您假设可以进行质量设置并且可以处理异常处理程序中的只读情况。

从 Python 的角度(除了不使用 属性 之外)对您的 Objective-C 启发版本的一些进一步说明:

  • 可以使用 isinstance() 时不要使用 type(..) is [not] typeobj
  • 您可以使用 callable() function 来测试是否可以调用某些东西,而不是测试 types.FunctionType;函数不是唯一的可调用对象。
  • 在Python中,您信任API的用户;如果他们传入无法调用的东西,那是他们自己的错,你的对象不需要在这里进行显式类型检查。 Python 是成人同意的语言。
  • if 声明已经验证了真实性;使用 is True 不仅是多余的,而且在与其他比较运算符一起使用时会导致难以调试错误,因为此类运算符是 链式 value == someothervalue is True并不代表你想的那样。

你可以使用 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

您需要创建与属性同名的 Python 方法,然后使用 @property 装饰器实现 getter 和 setter 功能:

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

这样您就可以执行以下操作:

camera = CameraInterface()
camera.recording_quality = HD
camera.start_recording()
camera.recording_quality = SD # Exception!