Support multiple divisors in divmod()

What do you think about idea of supporting multiple divisors in divmod()?

So instead of

minutes, seconds = divmod(t, 60)
hours, minutes = divmod(minutes, 60)
days, hours = divmod(hours, 24)
weeks, days = divmod(days, 7)

you could write:

weeks, days, hours, minutes, seconds = divmod(t, 7, 24, 60, 60)

Sample implementation:

def new_divmod(dividend, *divisors):
    if not divisors:
        raise TypeError('required at least one divisor')
    remainders = []
    for divisor in reversed(divisors):
        dividend, remainder = old_divmod(dividend, divisor)
        remainders.append(remainder)
    return (dividend, *remainders[::-1])

I am not sure about this idea. There are not so much use cases for it. On other hand, in these rare cases when you need to write similar code, the possibility to express it in one line would be nice.

It is also not clear which order of divisors is most natural: divmod(t, 7, 24, 60, 60) or divmod(t, 60, 60, 24, 7)? Big-endian or little-endian?

1 Like

IMO, the one-liner is harder to interpret than the “verbose” four-liner. With the current code, it is immediately easy to see what each line does.

I would not oppose the feature, though, so count me -0.

3 Likes

I could have sworn this already existed. I can’t work out what gave me that impression, though.

It’s basically mixed-base decomposition of a number into “digits”, which is something I’ve needed occasionally, but not particularly frequently (decomposing times is the obvious example). I agree that the one-line version is a bit tricky to read, mainly because of the question of which order the divisors go in - I’m somewhat in favour of 7,24,60,60 over 60,60,24,7, but it’s not a strong preference.

I’m assuming there’s a (small) performance benefit to doing the whole thing in one call, although I doubt that would matter often enough to care.

I think I’m probably +0. It’s not a big deal, but it feels like a minor quality of life improvement.

1 Like

I’d agree with 7,24,60,60. Would it make sense for that to be a tuple rather than simply additional arguments?

weeks, days, hours, minutes, seconds = divmod(t, (7, 24, 60, 60))
3 Likes

Just a thought, I’ve certainly used that pattern before. Not sure though it’s worth modifing divmod for. Maybe a helper in the datetime module instead?

4 Likes

Why the artificial discrimination against zero, with additional code? If you remove the

    if not divisors:
        raise TypeError('required at least one divisor')

then a call like new_divmod(5) works perfectly fine and gives the expected result (5,).

1 Like

Naively, I would have assumed that divmod(x, a, b, c) means repeated application over the remainder, i.e.

t = 12345
divmod(t, 3600, 60) == (3, 25, 45)

So the divmod(t, 7, 24, 60, 60) endianness looks more natural to me, and I would find it convenient that the function not only does the repeat application, but performs the “inner multiplication” of the argument for me.

1 Like

I wonder how well this idea generalises to other cases where divmod is used. Most commonly divmod is used with integers but it can be defined for any type via __divmod__. In SymPy (and other systems) this is used for various generalisations of the idea of Euclidean division for integers for example Euclidean division of polynomials:

>>> from sympy import ring, QQ
>>> R, x = ring('x', QQ)
>>> R
Polynomial ring in x over QQ with lex order
>>> divmod(x**2 - 1, x + 2)
(x - 2, 3)

For nonnegative integers the definition of Euclidean division is that if q, r = divmod(a, b) then a = q*b + r and then the condition 0 <= r < b is sufficient to uniquely define q and r. For univariate polynomials over a field the same definition applies except that the condition is changed to deg(r) < deg(b).

The further generalisation of this concept to multivariate polynomials allows the notion of dividing by a set of polynomials:

>>> R, x, y, z = ring('x, y, z', QQ)
>>> a = x**2*y + z**3 - 1
>>> b1 = y - 3
>>> b2 = z**2 - y
>>> a.div([b1, b2])
([x**2, 0], 3*x**2 + z**3 - 1)

Here division of a by b1, b2, … returns q1, q2, … and a remainder r such that a = b1*q1 + b2*q2 + ... + r. In general this is not necessarily uniquely defined but if the divisors b1, b2, … are a reduced Groebner basis with respect to some monomial ordering then requiring that the leading monomial of r be less than the leading monomials of all of the divisors (r is reduced wrt to the basis) makes the result unique.

I don’t think that the algorithm shown for new_divmod(a, b1, b2, ...) would compute this correctly though.

2 Likes

This approach can just be a change to int.__divmod__. What is unclear is how divmod would call __rdivmod__ in this scenario: does it just call tuple.__rdivmod__ (and if so what should that do)?

1 Like

Yes, there are two ways:

  1. Change int.__divmod__, float.__divmod__, Decimal.__divmod__, Fraction.__divmod__, and all third-party implementations of __divmod__. They all will contain similar boring code (but usually in C):
def int.__divmod__(self, other):
    if type(other) is tuple:
        return new_divmod(self, *other) # new_divmod() is defined above
    # old code
  1. Implement tuple.__rdivmod__:
def tuple.__rdivmod__(self, other):
    return new_divmod(other, *self) # new_divmod() is defined above

I do not see obvious advantages over changing divmod() directly. There is a disadvantage – it will not work with object which has __divmod__ that already supports tuple. For example – a Mock object.

The only reason is that it will allow some errors to slip through. divmod((x, y)) will successfully return a tuple (x, y). But if ignore this, yes, 1-argument divmod() makes sense.

Ideally anything here would be customisable via __(r)divmod__ which would not be the case if the divmod function itself hard-codes a particular interpretation of the operation. It is not clear that the proposed generalisation of divmod is always applicable.

I think it doesn’t belong as a complication of divmod’s currently clean behavior.

Mixed radix conversions have builtin operator syntax in the APL language (look in APL docs for “encode” and “decode”), and in Mathematica are supplied by a MixedRadix gimmick. I recommend the Mathematica docs because they give examples of various uses.

Note that mixed radix conversions can work in both directions, and the change to divmod would cover only the “to mixed radix” case. The “from mixed radix” direction would be left unaddressed. So APL and Mathematica use a vector of bases, which can be used as-is to compute conversions in either direction (it’s the operation name that changes).

5 Likes

I think this would be a small neat quality-of-life adjustment for situations where you have to work with time data, I don’t see any downside. +1

Just a thought, but you could support an infinite iterable of divisors:

def new_divmod(dividend: int,
               divisors: Iterable[int]
               ) -> tuple[int, tuple[int, ...]]:
    remainders = []
    for divisor in divisors:
        if dividend == 0:
            break
        dividend, remainder = old_divmod(dividend, divisor)
        remainders.append(remainder)
    return (dividend, tuple(remainders))

You would have to switch to little-endian, but I think that’s more logical to me (since it puts the b^i digit in position i of the output).

This could help when doing a base conversion:

x = 3492
base = 11
digits = new_divmod(x, itertools.repeat(base))  # (5, 9, 6, 2)
3 Likes

@NeilGirdhar With an infinite iterable, that’ll never return…

As long as it’s not an infinite sequence of 1, eventually the number will be all the way in the remainder. As a simple example, imagine dividing a large (but finite) number by 10 repeatedly - the remainders at each step are the digits in the decimal representation of that number. With any finite integer, this representation will itself be finite.

1 Like

Ah, embarrassing. I’m in bad shape and didn’t read/test it properly. Sorry.

But…

Not with -1, for example.

Ah true. I’m used to doing “convert to decimal” as an unsigned operation (so if you get handed a negative number, store a hyphen and negate the value).

Yes, I’d say it would be the fault of the caller if they did that.

given that timedelta already has a structured way of splitting its total seconds,
i strongly question this “overpowered utility” that turns a series of clearly related expressions into a parameter with a magical meaning one will always have to re-read in the docs

2 Likes