This article serves as an extension to Declarative Programming with Python. Reading the first part is mandatory, since we'll only go through a comparison of the declarative API from the first part, against a non-declarative version of it.

Desired API

>>> import myoarm
>>>
>>> myo = myoarm.Myo('xx:xx:xx:xx:xx')
>>> myo.batter_level
57
>>> myo.vibrate(myoarm.STRONG_VIBRATION)
myo vibrates strongly...
>>> myo.set_mode(myoarm.EMG_MODE)
myo is now in EMG mode
>>> unsubscribe = myo.emg_subscribe(print)
subscribed to notifications that get passed to print
(0, 0, 0, 0, 0, 0, 0, 0)
(1, 0, 0, 99, 0, 0, 0, 0)
(0, -1, 0, 0, 0, 0, 0, 0)
...
>>> unsubscribe()
unsubscribe from notifcations
>>>

Declarative Implementation

from bluew.rapid import RapidAPI, Read, Write, Notify


CONTROL_SERVICE = 'd5060401-a904-deb9-4748-2c7f4a124842'
BATTERY_CHRC = '00002a19-0000-1000-8000-00805f9b34fb'
EMG_CHAR1 = 'd5060405-a904-deb9-4748-2c7f4a124842'
EMG_CHAR2 = 'd5060305-a904-deb9-4748-2c7f4a124842'
EMG_CHAR3 = 'd5060205-a904-deb9-4748-2c7f4a124842'
EMG_CHAR4 = 'd5060105-a904-deb9-4748-2c7f4a124842'
IMU_CHAR = 'd5060402-a904-deb9-4748-2c7f4a124842'
ALL_EMG_CHRCS = (EMG_CHAR1, EMG_CHAR2, EMG_CHAR3, EMG_CHAR4)
VIB_STRONG = [0x3, 0x1, 0x1]
EMG_MODE = [0x1, 0x3, 0x3, 0x1, 0x2]


class Myo(RapidAPI):
    """
    Myo armband API
    """
    battery_level = Read(BATTERY_CHRC)
    vibrate = Write(CONTROL_SERVICE, accept=[VIB_STRONG])
    set_mode = Write(CONTROL_SERVICE, accept=[EMG_MODE])
    subscribe_to_emg = Notify(ALL_EMG_CHRCS)

Imperative Implementation

from typing import List, Callable
import bluew


CONTROL_SERVICE = 'd5060401-a904-deb9-4748-2c7f4a124842'
BATTERY_CHRC = '00002a19-0000-1000-8000-00805f9b34fb'
EMG_CHAR1 = 'd5060405-a904-deb9-4748-2c7f4a124842'
EMG_CHAR2 = 'd5060305-a904-deb9-4748-2c7f4a124842'
EMG_CHAR3 = 'd5060205-a904-deb9-4748-2c7f4a124842'
EMG_CHAR4 = 'd5060105-a904-deb9-4748-2c7f4a124842'
IMU_CHAR = 'd5060402-a904-deb9-4748-2c7f4a124842'
ALL_EMG_CHRCS = (EMG_CHAR1, EMG_CHAR2, EMG_CHAR3, EMG_CHAR4)
VIB_STRONG = [0x3, 0x1, 0x1]
VIB_LEVELS = [VIB_STRONG, ]
EMG_MODE = [0x1, 0x3, 0x3, 0x1, 0x2]
VALID_MODES = [EMG_MODE, ]


class Myo:
    """
    Myo armband API
    """
    def __init__(mac: str) -> None:
        self._con = bluew.Connection(mac)

    @property
    def battery_level():
        return self._con.read_attribute(BATTERY_CHRC)

    def vibrate(vib_level: List[int, int, int]) -> None:
        if vib_level not in VIB_LEVELS:
            raise ValueError('Invalid vibration level')
        self._con.write_attribute(CONTROL_SERVICE, vib_level)

    def set_mode(mode: List[int, int, int, int, int]) -> None:
        if mode not in VALID_MODES:
            raise ValueError('Invalid mode')
        self._con.write_attribute(CONTROL_SERVICE, mode)

    def emg_subscribe(callback: Callable) -> Callable:
        for chrc in ALL_EMG_CHRCS:
            self._con.notify(chrc, callback)

        def _unsub() -> None:
            for chrc in ALL_EMG_CHRCS:
                self._con.stop_notify(chrc)

        return _unsub

Comparison Poem

Not counting the constants, our Myo class in the imperative version has 20 LOCs whereas the declarative one only 4; imperative one uses 'return', 'raise', 'for', and 'if' whereas the declarative, no control statements at all.

Conclusion

For certain types of APIs like those of database models, or bluetooth low energy devices, using python descriptors can make them much easier to read and write. The focus shifts away from validating input or iterating through a range, and ends up on constants like the max_length of a Django CharField or the UUID of a GATT attribute instead.

By overriding behavior using __get__()/__set__()/__del__(), we can provide a way for users of our API, to simply declare the intent/logic of a certain component, without implementing it themselves.