Привет, Хабр. В преддверии старта курса «Python Developer. Professional» подготовили традиционный перевод полезного материала.

Также приглашаем всех желающих посетить открытый вебинар на тему
«Визуализация данных с помощью matplotlib».


В этой статье из серии про синтаксический сахар в Python я займусь на первый взгляд очень простым синтаксисом, но на самом деле, чтобы разобраться в механике его работы, нужно погрузиться вглубь на несколько слоев. Мы будем говорить о not.

Определение звучит на первый взгляд очень просто:

Оператор not выдает True, если его аргумент False, и False в противоположном случае.

Достаточно просто, не так ли? Но когда вы начинаете разбираться в том, что считать «истинным» или «ложным», вы быстро понимаете, что есть приличное количество вещей, подходящих под эти определения.

(Как и в других статьях этой серии, код на С предназначен для тех, кто хочет пройти путь «по хлебным крошкам», но вы можете пропустить его, если хотите)

Реализация not

Если вы посмотрите на байткод, то заметите единственный опкод, относящийся к not – это UNARY_NOT. 

Байткод для not a:

>>> import dis
>>> def spam(): not a
... 
>>> dis.dis(spam)
  1           0 LOAD_GLOBAL              0 (a)
              2 UNARY_NOT
              4 POP_TOP
              6 LOAD_CONST               0 (None)
              8 RETURN_VALUE

Реализация UNARY_NOT по сути вызывает функцию С, которая называется PyObject_IsTrue() и возвращает обратное переданному значение: True для False, False для True.

Реализация опкода UNARY_NOT из Python/ceval.c:

case TARGET(UNARY_NOT): {
            PyObject *value = TOP();
            int err = PyObject_IsTrue(value);
            Py_DECREF(value);
            if (err == 0) {
                Py_INCREF(Py_True);
                SET_TOP(Py_True);
                DISPATCH();
            }
            else if (err > 0) {
                Py_INCREF(Py_False);
                SET_TOP(Py_False);
                DISPATCH();
            }
            STACK_SHRINK(1);
            goto error;
        }

Определение того, что такое True

Вся хитрость нашего разбора not начинается с определения того, что такое True. Если посмотреть на реализацию PyObject_IsTrue() на языке Си, станет видно, что существует несколько возможных способов выяснить истинность объекта.

Реализация PyObject_IsTrue():

/* Test a value used as condition, e.g., in a for or if statement.
   Return -1 if an error occurred */


int
PyObject_IsTrue(PyObject *v)
{
    Py_ssize_t res;
    if (v == Py_True)
        return 1;
    if (v == Py_False)
        return 0;
    if (v == Py_None)
        return 0;
    else if (v->ob_type->tp_as_number != NULL &&
             v->ob_type->tp_as_number->nb_bool != NULL)
        res = (*v->ob_type->tp_as_number->nb_bool)(v);
    else if (v->ob_type->tp_as_mapping != NULL &&
             v->ob_type->tp_as_mapping->mp_length != NULL)
        res = (*v->ob_type->tp_as_mapping->mp_length)(v);
    else if (v->ob_type->tp_as_sequence != NULL &&
             v->ob_type->tp_as_sequence->sq_length != NULL)
        res = (*v->ob_type->tp_as_sequence->sq_length)(v);
    else
        return 1;
    /* if it is negative, it should be either -1 or -2 */
    return (res > 0) ? 1 : Py_SAFE_DOWNCAST(res, Py_ssize_t, int);
}

Если разбираться в реализации на С, то правило выглядит так:

  1. Если True, то True

  2. Если False, то False

  3. Если None, то False

  4. То, что возвращает bool, до тех пор, пока возвращаемый объект является подклассом bool (то, что показывает вызов nb_bool)

  5. Вызов len() на объекте (то, за что отвечают вызовы mp_length и sq_length):

    1. Если больше 0, то True

    2. В противном случае False

  6. Если ничего из вышеперечисленного не подходит, то True

Все правила 1-3 и 6 достаточно понятны, а вот правила 4 и 5 требуют углубления в детали.

bool

Определение специального/волшебного метода bool говорит нам, что метод используется для «реализации проверки истинности значений» и должен возвращать либо True, либо False.

len()

Встроенная функция len() возвращает целое число, представляющее количество элементов в контейнере. Реализация вычисления длины объекта представлена слотами sq_length (длина последовательностей) и mp_length (длина словарей/мэпов).

Легко подумать, что к объекту можно просто обратиться и запросить его длину, но тут есть два слоя.

len 

Первый слой – это специальный/волшебный метод len. Как и следовало ожидать, он «должен возвращать длину объекта, целое число >= 0». Но дело в том, что «целочисленный» не означает int, а означает объект, который вы можете «преобразовать без потерь»… к «целочисленному объекту». И как же выполнить это преобразование?

index

«Чтобы без потерь преобразовать численный объект в целочисленный», используется специальный/волшебный метод index. В частности, для обработки преобразования используется функция PyNumber_Index(). Эта функция слишком длинная, чтобы ее сюда вставлять, но делает она следующее:

  1. Если аргумент является экземпляром класса int, вернуть его

  2. В противном случае вызвать index на объекте

  3. Если index возвращает конкретный экземпляр класса int, вернуть его (технически возвращать подкласс не рекомендуется, но давайте оставим это в прошлом).

  4. В противном случае вернуть TypeError.

На уровне Python это делается с помощью operator.index(). К сожалению, здесь не реализуется семантика PyNumber_Index(), поэтому на самом деле с точки зрения not и len, функция работает немного неточно. Если бы она все же была реализована, то выглядела бы так:

Реализация PyNumber_Index() на Python:

def index(obj: Object, /) -> int:
    """Losslessly convert an object to an integer object.

    If obj is an instance of int, return it directly. Otherwise call __index__()
    and require it be a direct instance of int (raising TypeError if it isn't).
    """
    # https://github.com/python/cpython/blob/v3.8.3/Objects/abstract.c#L1260-L1302
    if isinstance(obj, int):
        return obj

    length_type = builtins.type(obj)
    try:
        __index__ = _mro_getattr(length_type, "__index__")
    except AttributeError:
        msg = (
            f"{length_type!r} cannot be interpreted as an integer "
            "(must be either a subclass of 'int' or have an __index__() method)"
        )
        raise TypeError(msg)
    index = __index__(obj)
    # Returning a subclass of int is deprecated in CPython.
    if index.__class__ is int:
        return index
    else:
        raise TypeError(
            f"the __index__() method of {length_type!r} returned an object of "
            f"type {builtins.type(index).__name__!r}, not 'int'"
        )

Реализация len()

Еще один интересный факт о реализации len(): она всегда возвращает конкретный int. Так, несмотря на то, что index() или len() могут возвращать подкласс, ее реализация на уровне С через PyLong_FromSsize_t() гарантирует, что всегда будет возвращаться конкретный экземпляр int.

В противном случае len() будет проверять, что возвращают len() и index (), например, является ли возвращаемый объект подклассом int, больше ли значение или равно 0 и т.д. Таким образом, вы можете реализовать len() так:

def len(obj: Object, /) -> int:
    """Return the number of items in a container."""
    # https://github.com/python/cpython/blob/v3.8.3/Python/bltinmodule.c#L1536-L1557
    # https://github.com/python/cpython/blob/v3.8.3/Objects/abstract.c#L45-L63
    # https://github.com/python/cpython/blob/v3.8.3/Objects/typeobject.c#L6184-L6209
    type_ = builtins.type(obj)
    try:
        __len__ = _mro_getattr(type_, "__len__")
    except AttributeError:
        raise TypeError(f"type {type!r} does not have a __len__() method")
    length = __len__(obj)
    # Due to len() using PyObject_Size() (which returns Py_ssize_t),
    # the returned value is always a direct instance of int via
    # PyLong_FromSsize_t().
    index = int(_index(length))
    if index < 0:
        raise ValueError("__len__() should return >= 0")
    else:
        return index

Реализация operator.truth()

Во многих языках программирования при определении операции not, распространенной идиомой является превращение объекта в его сравнительное логическое значение с помощью передачи его в not дважды – через not not. Первый раз, чтобы получить инвертированное логическое значение, и второй раз, чтобы инвертировать инверсию, и получить логическое значение, которое вы хотели изначально.

В Python нам не нужна эта идиома. Спасибо bool() (а конкретно bool.new()), за то, что у нас есть вызов функции, который мы можем использовать для получения конкретного логического значения, а именно operator.truth(). Если вы посмотрите на этот метод, то увидите, что он использует PyObject_IsTrue() для определения логического значения объекта. Посмотрев slot_nb_bool (), вы увидите, что в конечном итоге он делает то же, что и PyObject_IsTrue(). То есть, если мы можем реализовать аналог PyObject_IsTrue(), то можем определить, какое логическое значение имеет объект. 

По старой схеме и с тем, что мы узнали только что, мы можем реализовать operator.truth() для этой логики (я предпочитаю не реализовывать bool, поскольку не хочу реализовывать все его численные функции, и не придумал хорошего способа сделать True и False с нуля, которые наследовались бы от 1 и 0 на чистом Python).

Реализация operator.truth():

def truth(obj: Any, /) -> bool:
    """Return True if the object is true, False otherwise.

    Analogous to calling bool().

    """
    if obj is True:
        return True
    elif obj is False:
        return False
    elif obj is None:
        return False
    obj_type = type(obj)
    try:
        __bool__ = debuiltins._mro_getattr(obj_type, "__bool__")
    except AttributeError:
        # Only try calling len() if it makes sense.
        try:
            __len__ = debuiltins._mro_getattr(obj_type, "__len__")
        except AttributeError:
            # If all else fails...
            return True
        else:
            return True if debuiltins.len(obj) > 0 else False
    else:
        boolean = __bool__(obj)
        if isinstance(boolean, bool):
            # Coerce into True or False.
            return truth(boolean)
        else:
            raise TypeError(
                f"expected a 'bool' from {obj_type.__name__}.__bool__(), "
                f"not {type(boolean).__name__!r}"
            )

Реализация not

С реализованным оператором operator.truth(), реализовать operator.not_() – дело всего одной лямбды:

lambda a, /: False if truth(a) else True

Итоговый результат прост, но, чтобы добраться до него, нужно было проделать немало работы.

Как обычно, код из этой статьи можно найти в моем проекте desugar


Узнать подробнее о курсе «Python Developer. Professional».

Смотреть открытый вебинар на тему «Визуализация данных с помощью matplotlib».