Привет, Хабр. В преддверии старта курса 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()
на языке Си, станет видно, что
существует несколько возможных способов выяснить истинность
объекта.
/* Test a value used as condition, e.g., in a for or if statement. Return -1 if an error occurred */intPyObject_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);}
Если разбираться в реализации на С, то правило выглядит так:
-
Если True, то True
-
Если False, то False
-
Если None, то False
-
То, что возвращает
bool
, до тех пор, пока возвращаемый объект является подклассомbool
(то, что показывает вызовnb_bool
) -
Вызов
len()
на объекте (то, за что отвечают вызовыmp_length
иsq_length
):-
Если больше 0, то True
-
В противном случае False
-
-
Если ничего из вышеперечисленного не подходит, то True
Все правила 1-3 и 6 достаточно понятны, а вот правила 4 и 5 требуют углубления в детали.
bool
Определение специального/волшебного метода bool говорит нам, что метод используется для реализации проверки истинности значений и должен возвращать либо True, либо False.
len()
Встроенная функция len()
возвращает целое число,
представляющее количество элементов в контейнере. Реализация
вычисления длины объекта представлена слотами
sq_length
(длина последовательностей) и
mp_length
(длина словарей/мэпов).
Легко подумать, что к объекту можно просто обратиться и запросить его длину, но тут есть два слоя.
len
Первый слой это
специальный/волшебный метод len
. Как и следовало
ожидать, он должен возвращать длину объекта, целое число >= 0.
Но дело в том, что целочисленный не означает int
, а
означает объект, который вы можете преобразовать без потерь к
целочисленному объекту. И как же выполнить это преобразование?
index
Чтобы без потерь преобразовать численный объект в целочисленный,
используется специальный/волшебный метод index
. В
частности, для обработки преобразования
используется функция PyNumber_Index()
. Эта функция
слишком длинная, чтобы ее сюда вставлять, но делает она
следующее:
-
Если аргумент является экземпляром класса
int
, вернуть его -
В противном случае вызвать
index
на объекте -
Если
index
возвращает конкретный экземпляр классаint
, вернуть его (технически возвращать подкласс не рекомендуется, но давайте оставим это в прошлом). -
В противном случае вернуть
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.