03.07.2020       Выпуск 341 (29.06.2020 - 05.07.2020)       Статьи

А вы можете решить эти три (обманчиво) простые задачи на Python?

С самого начала своего пути как разработчика программного обеспечения я очень любил копаться во внутренностях языков программирования. Мне всегда было интересно, как устроена та или иная конструкция, как работает та или иная команда, что под капотом у синтаксического сахара и т.п. Недавно мне на глаза попалась интересная статья с примерами того, как не всегда очевидно работают mutable- и immutable-объекты в Python. На мой взгляд, ключевое — это то, как меняется поведение кода в зависимости от используемого типа данных, при сохранении идентичной семантики и используемых языковых конструкциях. Это отличный пример того, что думать надо не только при написании, но и при использовании. Предлагаю всем желающим ознакомиться с переводом.

Читать>>




Экспериментальная функция:

Ниже вы видите текст статьи по ссылке. По нему можно быстро понять ссылка достойна прочтения или нет

Просим обратить внимание, что текст по ссылке и здесь может не совпадать.

С самого начала своего пути как разработчика программного обеспечения я очень любил копаться во внутренностях языков программирования. Мне всегда было интересно, как устроена та или иная конструкция, как работает та или иная команда, что под капотом у синтаксического сахара и т.п. Недавно мне на глаза попалась интересная статья с примерами того, как не всегда очевидно работают mutable- и immutable-объекты в Python. На мой взгляд, ключевое — это то, как меняется поведение кода в зависимости от используемого типа данных, при сохранении идентичной семантики и используемых языковых конструкциях. Это отличный пример того, что думать надо не только при написании, но и при использовании. Предлагаю всем желающим ознакомиться с переводом.

Попробуйте решить эти три задачи, а потом сверьтесь с ответами в конце статьи.

Совет

: у задач есть кое-что общее, поэтому освежите в памяти решение первой задачи, когда перейдёте ко второй или третьей, так вам будет проще.

Первая задача

Есть несколько переменных:

x = 1
y = 2
l = [x, y]
x += 5

a = [1]
b = [2]
s = [a, b]
a.append(5)

Что будет выведено на экран при печати

l

и

s

?

Вторая задача

Определим простую функцию:

def f(x, s=set()):
    s.add(x)
    print(s)

Что будет, если вызвать:

>>f(7)
>>f(6, {4, 5})
>>f(2)

Третья задача

Определим две простые функции:

def f():
    l = [1]
    def inner(x):
        l.append(x)
        return l
    return inner

def g():
    y = 1
    def inner(x):
        y += x
        return y
    return inner

Что мы получим после выполнения этих команд?

>>f_inner = f()
>>print(f_inner(2))

>>g_inner = g()
>>print(g_inner(2))

Насколько вы уверены в своих ответах? Давайте проверим вашу правоту.

Решение первой задачи

>>print(l)
[1, 2]

>>print(s)
[[1, 5], [2]]

Почему второй список реагирует на изменение своего первого элемента

a.append(5)

, а первый список полностью игнорирует такое же изменение

x+=5

?

Решение второй задачи

Посмотрим, что произойдёт:

>>f(7)
{7}

>>f(6, {4, 5})
{4, 5, 6}

>>f(2)
{2, 7}

Погодите, разве последним результатом не должно быть

{2}

?

Решение третьей задачи

Результат будет таким:

>>f_inner = f()
>>print(f_inner(2))
[1, 2]

>>g_inner = g()
>>print(g_inner(2))
UnboundLocalError: local variable ‘y’ referenced before assignment

Почему

g_inner(2)

не выдала

3

? Почему внутренняя функция

f()

помнит о внешней области видимости, а внутренняя функция

g()

не помнит? Они же практически идентичны!

Объяснение

Что если я скажу вам, что все эти примеры странного поведения связаны с различием между изменяемыми и неизменяемыми объектами в Python?

Изменяемые объекты, такие как списки, множества или словари, могут быть изменены на месте. Неизменяемые объекты, такие как числовые и строковые значения, кортежи, не могут быть изменены; их «изменение» приведёт к созданию новых объектов.

Объяснение первой задачи

x = 1
y = 2
l = [x, y]
x += 5

a = [1]
b = [2]
s = [a, b]
a.append(5)

>>print(l)
[1, 2]

>>print(s)
[[1, 5], [2]]

Поскольку

x

неизменяема, операция

x+=5

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

Т.к. a изменяемый объект, то команда

a.append(5)

меняет исходный объект (а не создает новый), и список

s

«видит» изменения.

Объяснение второй задачи

def f(x, s=set()):
    s.add(x)
    print(s)

>>f(7)
{7}

>>f(6, {4, 5})
{4, 5, 6}

>>f(2)
{2, 7}

С первыми двумя результатами всё понятно: первое значение

7

добавляется к изначально пустому множеству и получается

{7}

; потом значение

6

добавляется к множеству

{4, 5}

и получается

{4, 5, 6}

.

А потом начинаются странности. Значение

2

добавляется не к пустому множеству, а к {7}. Почему? Исходное значение опционального параметра

s

вычисляется только один раз: при первом вызове s будет инициализировано как пустое множество. А поскольку оно изменяемое, после вызова

f(7)

оно будет будет изменено “на месте”. Второй вызов

f(6, {4, 5})

не повлияет на параметр по умолчанию: его заменяет множество

{4, 5}

, то есть

{4, 5}

является другой переменной. Третий вызов

f(2)

использует ту же переменную

s

, что использовалась при первом вызове, но она не переинициализируется как пустое множество, а вместо этого берётся её предыдущее значение

{7}

.

Поэтому не следует использовать изменяемые аргументы в качестве аргументов по умолчанию. В этом случае функцию нужно изменить:

def f(x, s=None):
    if s is None:
        s = set()
    s.add(x)
    print(s)

Объяснение третьей задачи

def f():
   l = [1]
   def inner(x):
       l.append(x)
       return l
   return inner

def g():
   y = 1
   def inner(x):
       y += x
       return y
   return inner

>>f_inner = f()
>>print(f_inner(2))
[1, 2]

>>g_inner = g()
>>print(g_inner(2))
UnboundLocalError: local variable ‘y’ referenced before assignment

Здесь мы имеем дело с замыканиями: внутренние функции помнят, как выглядели их внешние пространства имён на момент своего определения. Или хотя бы должны помнить, однако вторая функция делает покерфейс и ведёт себя так, словно не слышала о своём внешнем пространстве имён.

Почему так происходит? Когда мы исполняем

l.append(x)

, меняется изменяемый объект, созданный при определении функции. Но переменная

l

всё ещё ссылается на старый адрес в памяти. Однако попытка изменить неизменяемую переменную во второй функции

y += x

приводит к тому, что y начинает ссылаться на другой адрес в памяти: исходная y будет забыта, что приведёт к ошибке UnboundLocalError.

Заключение

Разница между изменяемыми и неизменяемыми объектами в Python очень важна. Избегайте странного поведения, описанного в этой статье. В особенности:

  • Не используйте по умолчанию изменяемые аргументы.
  • Не пытайтесь менять неизменяемые переменные-замыкания во внутренних функциях.





Разместим вашу рекламу

Пиши: mail@pythondigest.ru

Нашли опечатку?

Выделите фрагмент и отправьте нажатием Ctrl+Enter.

Система Orphus