Repository navigation
Functions vs attributes in object API #378
Description
Activity
my 2 cents - I like the attribute approach you outline for one reason - speed.
If I'm reading an ADC value - I want to sample as close to when I ask as possible. If I'm setting a servo value - same thing.I don't think the function call adds extra meaning in these cases and can be explained by documentation.
Clearly when a specific action has to occur, or more than one parameter has to be sent - then a function call is probably better.
Passing around the attribute in a lambda might be needed but I think its more likely the accel object will be passed - so no disadvantage there.
Speed/efficiency is important. But I think it's equally important to be consistent with the API. For example, for an LED object, you can imagine the following.
All functions:
LED.on() LED.off() LED.toggle() i = LED.intensity() # read intensity LED.intensity(128) # set intensityAll attributes:
LED.on = True LED.off = True # this is a bit crazy state = LED.on # returns True if on LED.toggle = True # toggles i = LED.intensity LED.intensity = 128A mixture:
LED.on() LED.off() LED.toggle() i = LED.intensity LED.intensity = 128x_attr is more efficient because it's only 1 call to the underlying C object. x_fun() is 2 calls: 1 to lookup the x_fun method, the other to call it.
That's interesting point of view. I always treated it the opposite: Because method call lookup is standardized, and it can be optimized under some circumstances (replacing dynamic dispatch with static - we discussed that once already; it's not what uPy compiler expected to ever do, but rather optimizer on big box, but the point is that it's possible). On the other hand, current API for querying attrs (which is pretty much based on Python
__getattribute__semantics, so no proposals to change that) is fully dynamic and mandates dynamic dispatch.So, I don't see that big a difference, it's pretty symmetric: method - lookup and call, attr - call and lookup. Method processing is optimizable (think static dispatch, think Viper, also think that lookup+call looks like a good candidate for single bytecode instr.), attr processing is as good as it is, and that includes unavoidable (so, granted, relatively small) lookup cost.
Maybe you could look at it like:
Led.on() is a shortcut for Led.intensity = 255
so the action is a verb and therefore a function. Setting a value is an attribute.
so:Led.on() # its a verb Led.off() Led.toggle() Led.intensity = 128 val = Led.intensity lamp = pyb.Led() lamp.intensity = 128This is your third choice.
for accel ?a = pyb.Accel(setup_args) # e.g. Accel(16, pyb.SAR) val = a.sample # i.e. read the latest sample a.sample_rate = 1000 # rather than a.set_rate(1000) a.trigger(timer, arg) # a verb againfor servo:
s = pyb.Servo(...) # forgptten how we're doing this now - pin mapping right ?if they are the same from a speed point of view... then its down to style... you choose
Objections against having x_attr is that it changes asynchronously
IMHO, this external interface trait alone is enough to choosing methods over attributes. Like, methods everywhere. Because for each access, there's actual action performed. Perhaps name methods well to allow recasting them as properties.
if they are the same from a speed point of view... then its down to style... you choose
Currently, attribute access perhaps slightly faster per @dpgeorge comment, but main point is that method calls are optimizable, while attributes - not (w/o providing for them similar standard infra like for methods, which sounds like bloat). If you want speed, you should aim for JITs like viper. And in machine code, it's all function call. So, you hope to optimize any lookups and be able to call needed function directly. Using methods enable this, using attrs - disable.
In your initial example you are describing a sample capture. The sample happens to have a complex set of ten values instead of the usual single value we normally associate with the concept of a "sample" like a temperature measurement. But it is still a sample capture.
Therefore, you should use a function that returns the sample structure.
data = accel.getReading() # this returns a new sample data structure upon each call x1 = data.x # this is invariant and read-only x2 = data.x # this is invariant and read-only y1 = accel.getReading().y # this will return a fresh data value each time y2 = accel.getReading().y # this will return a fresh data value each time
This would give you the desired behavior in all cases, wouldn't it? You can pass around the function itself or you can pass around the resolved data sample. Both are legitimate usage scenarios.
All attributes:
LED.on = True
LED.off = True # this is a bit crazy
state = LED.on # returns True if on
LED.toggle = True # toggles
i = LED.intensity
LED.intensity = 128I think that to use attributes then you need to use nouns. If you can't represent it as a noun, then I think it should stay as a function.
Personally, I feel that the function interface is richer. You can have multiple arguments, and optional arguments.
LED.state = True
LED.state = FalseI'm not sure that it makes sense for comms based APIs (the UART, or I2C) to use an attribute based API, although I suppose you could.
char = UART.data
UART.data = "string to be written"For the UART, ideally I'd like to see an API which follows the pyserial API.
Dealing with timeouts would be interesting, although they could just be additional attributes.
Having coded both styles, right now coding the attribute stuff in C as considerably more work, and requires the introduction of a meta class. For example, in order to implement pyb.Exti.MODE_IRQ_RISING required adding a pyb_Exti meta class.
I think that using attributes needs to be simplfied (on the C side) before we can consider using them eveywhere.
I also think that if you're looking for a consistent API across everything, then you should really come up with the API first.
Then write the code that implements the API. Right now, the APIs have been made up in a rather adhoc manner as each component has been implemented.
Thanks for the comments. As usual, design is a tricky thing requiring tradeoffs.
The "all attributes" approach is definitely not sensible. UART and similar should surely have .read and .write methods.
I think it's probably sensible to declare that attributes should be used for constants (eg Led.MAXIMUM_INTENSITY) and members of objects only (like a struct object), not for functionality.
Closing because discussion has been had and things have moved on. Mainly using functions, not attributes.
In designing the API for pyboard objects (like accelerometer, servo, LED), there is a choice for some parts of the interface whether it is implemented as a function or an attribute. For example, consider the accelerometer:
Which is better (if there is such a thing)? x_attr is more efficient because it's only 1 call to the underlying C object. x_fun() is 2 calls: 1 to lookup the x_fun method, the other to call it. You only ever need to read the value of the x axis, so there will never be any arguments to x_fun(), thus x_attr will suffice.
It's sensible to consider the axis' as variables. Behind the scenes, the accelerometer is just an I2C device with 10 or so registers. You read and write these registers, and that's it. The first 3 registers are the x, y and z axis values.
Objections against having x_attr is that it changes asynchronously (ie one read then another read will in general give different values, which is not what you expect for an attribute), and that it's read only (no sense in writing the x axis value).
A counter argument is that the mode register (and other settings) are read/write, and only change when you write to them, so they really are proper attributes (as opposed to methods).
Having x_fun() allows you to pass accel.x_fun as a function to be called later on. To do this with x_attr you will need a lambda: lambda:accel.x_attr.
Other objects like Servo could have angle as an attribute, which is sensible to read and write. LED has intensity which acts exactly like an attribute (LED.intensity=128).
Comments?