What happens behind the scene of floor division in Python?

641 Views Asked by At

The Documentation about the floor division in Python says,

The / (division) and // (floor division) operators yield the quotient of their arguments. The numeric arguments are first converted to a common type. Division of integers yields a float, while floor division of integers results in an integer; the result is that of mathematical division with the ‘floor’ function applied to the result.

I'm focusing on the last line, that says,

the result is that of mathematical division with the ‘floor’ function applied to the result

I get a float in:

>>> 10//2.0
5.0

But an integer in:

>>> import math
>>> math.floor(10/2.0)
5

So it's clearly not the math.floor function that Python floor division is using. I want to know what exact procedure is Python following to compute the floor division of two arguments.

Because when taking the floor division of complex numbers, Python says,

>>> (2+3j)//(4+5j)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: can't take floor of complex number.

That means Python takes the division and then applies some sort of floor function. What really is it?

1

There are 1 best solutions below

3
On

Compare (both from that page):

the ‘floor’ function

The function math.fmod()

If they meant math.floor, they would've styled it like they styled fmod, i.e., as code and with math. prefix and with parentheses and with link. What they do mean there is just the mathematical floor function. Not a Python function.

Digging into how CPython implements it, let's first disassemble:

>>> import dis
>>> dis.dis('x // y')
  1           0 LOAD_NAME                0 (x)
              2 LOAD_NAME                1 (y)
              4 BINARY_FLOOR_DIVIDE
              6 RETURN_VALUE

Looking up BINARY_FLOOR_DIVIDE:

        case TARGET(BINARY_FLOOR_DIVIDE): {
            PyObject *divisor = POP();
            PyObject *dividend = TOP();
            PyObject *quotient = PyNumber_FloorDivide(dividend, divisor);
            Py_DECREF(dividend);
            Py_DECREF(divisor);
            SET_TOP(quotient);
            if (quotient == NULL)
                goto error;
            DISPATCH();
        }

Looking up PyNumber_FloorDivide

PyObject *
PyNumber_FloorDivide(PyObject *v, PyObject *w)
{
    return binary_op(v, w, NB_SLOT(nb_floor_divide), "//");
}

Looking up nb_floor_divide:

+-----------------+------------+-----------------+
| Slot            | Type       | special methods |
+-----------------+------------+-----------------+
| ...             | ...        | ...             |
| nb_floor_divide | binaryfunc | __floordiv__    |
| ...             | ...        | ...             |
+-----------------+------------+-----------------+

Now int and floor both have their own:

>>> int.__floordiv__
<slot wrapper '__floordiv__' of 'int' objects>
>>> float.__floordiv__
<slot wrapper '__floordiv__' of 'float' objects>

Looking up the function for the float one:

static PyNumberMethods float_as_number = {
    ...
    float_floor_div,    /* nb_floor_divide */
    ...
};

Looking up float_floor_div:

static PyObject *
float_floor_div(PyObject *v, PyObject *w)
{
    double vx, wx;
    double mod, floordiv;
    CONVERT_TO_DOUBLE(v, vx);
    CONVERT_TO_DOUBLE(w, wx);
    if (wx == 0.0) {
        PyErr_SetString(PyExc_ZeroDivisionError, "float floor division by zero");
        return NULL;
    }
    _float_div_mod(vx, wx, &floordiv, &mod);
    return PyFloat_FromDouble(floordiv);
}

Looking up _float_div_mod:

static void
_float_div_mod(double vx, double wx, double *floordiv, double *mod)
{
    ...
    /* snap quotient to nearest integral value */
    if (div) {
        *floordiv = floor(div);
        if (div - *floordiv > 0.5) {
            *floordiv += 1.0;
        }
    }
    ...
}

And I think floor is from C, so depends on whatever C compiler was used to compile your CPython.