# Coroutine-based two-in-one properties

Over the holidays I played around with coroutines and their use as two-in-one getter and setter methods:

#! /usr/bin/env python3
#  coroproperty.py - Coroutine-based two-in-one properties.
#
#  Copyright (C) 2020 Richard Neumann <mail at richard dash neumann period de>
#
#  This program is free software: you can redistribute it and/or modify
#  the Free Software Foundation, either version 3 of the License, or
#  (at your option) any later version.
#
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with this program.  If not, see <https://www.gnu.org/licenses/>.
"""Coroutine-based two-in-one decorator."""

from contextlib import suppress
from math import pi, sqrt
from typing import Any, Callable

def coroproperty(method: Callable) -> property:
"""Single decorator for getter and setter methods."""

def getter(self) -> Any:
"""Property getter function."""
coro = method(self)
value = next(coro)
coro.close()
return value

def setter(self, value: Any):
"""Property setter function."""
coro = method(self)
next(coro)
next(coro)

with suppress(StopIteration):
coro.send(value)

return property(getter, setter)

class Circle:

"""Initializes the circle with its radius."""

@coroproperty
def diameter(self):
"""Gets and sets the diameter."""

@coroproperty
def circumference(self):
"""Gets and sets the circumference."""
yield self.diameter * pi
self.diameter = (yield) / pi

@coroproperty
def area(self):
"""Gets and sets the area."""


The downsides I already discovered are, that each set access implies that an unused get is performed before. Furthermore, these decorators are significantly slower than a "classical" property with separate getter and setter methods:

Coro get: 0.2394869327545166
Classic get: 0.09408378601074219
Ratio coro / classic: 2.5454644515174247
Coro set: 0.7087152004241943
Classic set: 0.10921454429626465
Ratio coro / classic: 6.489201644257868


What other side-effects could arise when using these kind of properties?

Note that this code is not intended for use in productive environments, but is of academic interest only.

The proper way to prime a generator for accepting values is to send an initial None value.

Yield will simultaneously return a value to the caller, and accept a value sent from the caller.

Using those two facts, you could rewrite the code like:

...

def coroproperty(method: Callable) -> property:

...

def setter(self, value: Any) -> None:
coro = method(self)
coro.send(None)
with suppress(StopIteration):
coro.send(value)

...

class Circle:

...

@coroproperty
def diameter(self):

...


# Side-effects

Wasted time and computation. Setting the diameter, circumference and/or area first involves computing those quantities, which is unnecessary.

You could circumvent that by accepting a new value before returning the value.

    @coroproperty
def diameter(self):
diameter = yield None
if diameter is not None:
else:


The getter would need to call next(coro) twice, ignoring the first returned value: None.

If you desired a property which could store None, you'd need to create and use your own sentinel value.

Assignment to diameter, even if not changing the value, will change the radius. In this example, it is changed from an int to a float:

>>> c.radius = 100

Add in sqrt, and multiplication & division by π, and accuracy can be lost:
>>> c.radius = 1_000_000_000_000