[Python-ideas] real numbers with SI scale factors: next steps

Chris Angelico rosuav at gmail.com
Wed Aug 31 03:07:11 EDT 2016


On Wed, Aug 31, 2016 at 2:08 PM, Ken Kundert
<python-ideas at shalmirane.com> wrote:
> > What's the mnemonic here? Why "r" for scale factor?
>
> My thinking was that r stands for real like f stands for float.
> With the base 2 scale factors, b stands for binary.

"Real" has historically often been a synonym for "float", and it
doesn't really say that it'll be shown in engineering notation. But
then, we currently have format codes 'e', 'f', and 'g', and I don't
think there's much logic there beyond "exponential", "floating-point",
and... "general format"? I think that's a back-formation, frankly, and
'g' was used simply because it comes nicely after 'e' and 'f'. (C's
decision, not Python's, fwiw.) I'll stick with 'r' for now, but it
could just as easily become 'h' to avoid confusion with %r for repr.

>> (2) Support for full prefix names, so we can format (say) "kilograms" as well
>> as "kg"?
>
> This assumes that somehow this code can access the units so that it can switch
> between long form 'grams' and short form 'g'. That is a huge expansion in the
> complexity for what seems like a small benefit.
>

AIUI, it's just giving the full word.

class ScaledNumber(float):
    invert = {"μ": 1e6, "m": 1e3, "": 1, "k": 1e-3, "M": 1e-6}
    words = {"μ": "micro", "m": "milli", "": "", "k": "kilo", "M": "mega"}
    aliases = {"u": "μ"}
    def autoscale(self):
        if self < 1e-6: return None
        if self < 1e-3: return "μ"
        if self < 1: return "m"
        if self < 1e3: return ""
        if self < 1e6: return "k"
        if self < 1e9: return "M"
        return None
    def __format__(self, fmt):
        if fmt == "r" or fmt == "R":
            scale = self.autoscale()
            fmt = fmt + scale if scale else "f"
        if fmt.startswith("r"):
            scale = self.aliases.get(fmt[1], fmt[1])
            return "%g%s" % (self * self.invert[scale], scale)
        if fmt.startswith("R"):
            scale = self.aliases.get(fmt[1], fmt[1])
            return "%g %s" % (self * self.invert[scale], self.words[scale])
        return super().__format__(self, fmt)

>>> range = ScaledNumber(50e3)
>>> print('Attenuation = {:.1f} dB at {:r}m.'.format(-13.7, range))
Attenuation = -13.7 dB at 50km.
>>> print('Attenuation = {:.1f} dB at {:R}meters.'.format(-13.7, range))
Attenuation = -13.7 dB at 50 kilometers.
>>> print('Attenuation = {:.1f} dB at {:rM}m.'.format(-13.7, range))
Attenuation = -13.7 dB at 0.05Mm.
>>> print('Attenuation = {:.1f} dB at {:RM}meters.'.format(-13.7, range))
Attenuation = -13.7 dB at 0.05 megameters.

It's a minor flexibility, but could be very useful. As you see, it's
still not at all unit-aware; but grammatically, these formats only
make sense if followed by an actual unit name. (And not an SI base
unit, necessarily - you have to use "gram", not "kilogram", lest you
get silly constructs like "microkilogram" for milligram.)

Note that this *already works*. You do have to use an explicit class
for your scaled numbers, since Python doesn't want you monkey-patching
the built-in float type, but if you were to request that
float.__format__ grow support for this, it'd be a relatively
non-intrusive change. This class could live on PyPI until one day
becoming subsumed into core, or just be a permanent third-party float
formatting feature.

ChrisA


More information about the Python-ideas mailing list