WTF Python? Задачки с собеседований

Часто ругают JavaScript за непонятную магию, не менее чаще ругают PHP, но вот Python сейчас переживает пик популярности и его все любят несмотря ни на что. Расскажу свой небольшой опыт, что у меня вызвало удивление в виде вопросов с собеседования.

Округления

x = (round(8.5) — round(7.5))
print(a) # = ?

Вопрос, чему будет равно x? Ноль или 1 ?

Задумались? Вот и я задумался. И в догоночку еще 2 варианта:

x = (round(8.5) - 7)
print(x) # = 1
x = (round(7.5) - 7)
print(x) # = 1

Вот тут вроде бы точно все логично? Но оказывается нет. У Python есть особенность в округлении и заключается она в том, что функция округления в языке работает не всегда так, как ожидается, а алгоритм может различаться в разных версиях. Оказывается в Python 3 используется банковское(бухгалтерское) округление — по правилам данного округления если за последней значащей цифрой стоит 5, то последнюю цифру увеличивают, если она нечетная и уменьшают, если четная.

round(8.5) # = 8
round(7.5) # = 8

В то время как у PHP:

<?php 
$x = (round(8.5) - 7);
print($x); # = 2
$x = (round(7.5) - 7);
print($x); # = 1

А у JS:

let x= Math.round(8.5) - 7
console.log(x) // = 2
let x = Math.round(7.5) - 7
console.log(x) // = 1

Потому что в других языках используется арифметическое округление. У меня возникла только одна мысль в голове: WTF, почему в питоне взяли такой способ округления? Неужто ради датасаентистов-финансистов? Искренне удивлен.

Объекты и переменные в Python

Да тех кто не из мира Python, напомню, что в этом языке все есть объекты. Поэтому когда вы вызываете функцию id(от числа), то вы получаете адрес этого числа в памяти, хотя оно никуда не присвоено. А так же если вы выполните:

instance(1, object) # True

т.е. даже примитивы — это объекты, каждый из которых содержит как минимум счётчик ссылок, тип и значение.

Но помимо того что все есть объекты, в Python нет переменных в привычном понимании как они есть в PHP, JS, C/C++, Go, Rust, etc…

В пайтоне есть имена! Это тоже частенько спрашивают на собеседованиях. Чаще всего имена в Python можно воспринимать в качестве переменных, но необходимо понимать разницу.

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

Т.е. если вы создаете переменную (в C, PHP, JS) и присваиваете ей примитив, то происходит следующее:

  1. Выделение достаточного количества памяти для значения.
  2. Присвоение этому месту в памяти значения.
  3. Привязывание переменной, которая указывает на это значение.

Если вы захотите изменить значение, присвоив другое, то произойдет перезаписывание предыдущего. Это означает, что переменная по своей сути изменяема (мутабельна). Адрес переменной останется прежней, но значение по адресу будет изменено. Для того, чтобы ссылаться на одну и ту же область памяти в зависимости от языка существуют ссылки и указатели. Например в PHP есть возможно указывать явно ссылки, а вот в JS есть ссылки, но они неявные и нужно знать правила, что объекты и массивы передаются по ссылкам (могу раскрыть эти тезисы в отдельной статье если интересно).

Если мы создаем новую переменную и присваиваем ей значение первой, то создается новая область памяти, в которую копируется значение первой. В виде кода поясняю:

<?php

$x = 8; # ячейка памяти со значением 8 на которое указывает наша переменная

$y = $x; # новая ячейка памяти, в которую скопировалось значение

$x = 4; # адрес тот же, но значение другое

$z = &$x; # $z ссылается на область, на которую указывает $x

Если очень простыми словами утрировано то как-то так.

В Python нет переменных, вместо этого есть имена. Вы можете использовать термин «переменная», однако на собеседовании к вам могут придраться. Когда в Python создается имя, происходит следующее:

  1. Создаётся PyObject.
  2. Значению для PyObject’а присваивается typecode и присваивается значение.
  3. Создаётся имя.
  4. Имя указывает на новый PyObject, а счётчик ссылок увеличивается на +1

PyObject владеет памятью, в которой живёт значение, а python-имя не владеет напрямую адресом в памяти.

Возвращаясь к началу абзаца мы и видим такое поведение:

id(100) # PyObject 100 с адресом в памяти 0001
a = 100 # а - это имя для PyObject с адресом 0001
a = 101 # а - теперь является иеменем для PyObject 101, а у PyObject 100 счетчик ссылок уменьшился на -1

Т.е. вроде бы похоже на переменные, но имена под капотом работают по другому и связано это с общей архитектурой CPython и его особенностью что все есть объект.

Проверка тождественности is

Оператор тождественности is сравнивает размещение двух объектов в памяти. Пример:

c = 100
a = c
b = c

x = a is b
print(x) # True

x = a+1 is b+1
print(x) # True

В обоих случаях будет True, так как адреса полученных объектов:

print(id(c)) # 0001
print(id(a)) # 0001
print(id(b)) # 0001
print(id(a+1)) # 0010
print(id(b+1)) # 0010

Немного модифицируем пример и вместо 100 просто пишем 300.

— триста!

— Что триста?

Зарплата программиста!

c = 300
a = c
b = c

x = a is b # True

x = a+1 is b+1 #False

И вот тут нежданчик, как так?

Кеширование небольших чисел в CPython

Оказывается в Python целые числа от -5 до 256 имеют заранее выделенные участки в памяти. Когда выполняется какая-либо операция и ее результатом является число из этого промежутка, т.е. вы получаете PyObject со значением из этого выделенного участка.

Эти объекты ссылаются на адреса, выделенные и закешированные под них. Это справедливо для любых значений, которые приводятся к целым числам этого диапазона. Можно завести число в двоичном или hex формате— кеширование будет работать.

Но если вы используете целые числа не из этого промежутка, вы получите новый объект со своим адресом, но есть нюанс: числа вне кешированного диапазона интерпретатор тоже будет пытаться оптимизировать, если они находятся в одной строке или одном файле:

> cat a.py
c = 300
a = c
b = c

print(id(c)) # 0001
print(id(a)) # 0001
print(id(b)) # 0001

А если:

> cat a.py
from x import a

print(a is 300) # False
print(id(300)) # 0001
print(id(a)) # 0010

Если вам нужно кеширование в интерактивном интерпретаторе, то вы можете писать все в одну строку:

Python 3.8.1 [Clang 6.0 (clang-600.0.57)] on darwin
>>>
>>> a = 300
>>> b = 300
>>> a is b
False
Но:
>>> a=300; b=300
>>> a is b
True

Итого, запоминаем: небольшие числа в Python хранятся не так, как большие и они заранее закешированы в памяти. Числа — это объекты со всеми вытекающими и они имеют свои адреса. Есть нюансы где и как инициализирован объект (в 1м файле, на 1й строке и т.д.) — от этого будет зависеть адрес.

Задачка с собеседования

Недавно, на собеседовании встретил такую задачку:

def foo(x):
if x + 1 is 1 + x:
print(0)
elif x + 2 is not 2 + x:
print(0)
else:
print(1)
foo(???) # чтобы напечатал 1 ?

Простой ответ: -7. Зная выше перечисленный материал вы сможете легко понять почему так. Эту задачу можно решить еще несколькими способами, например через перегрузку операторов, но собеседующий специалист ждал именно этого ответа, как оказалось, с объяснением вот этих самых подробностей.

P.S.: Продолжайте смеяться над JS и PHP 😎