Telegram Group & Telegram Channel
🐍 Задача с подвохом: Декораторы и изменяемые объекты

Условие:

Что выведет следующий код и почему?

def memoize(fn):
cache = {}
def wrapper(arg):
if arg in cache:
print("Из кэша")
return cache[arg]
else:
result = fn(arg)
cache[arg] = result
return result
return wrapper

@memoize
def add_to_list(val, lst=[]):
lst.append(val)
return lst

res1 = add_to_list(1)
res2 = add_to_list(2)
res3 = add_to_list(1)

print(res1)
print(res2)
print(res3)

Вопрос:
Что именно выведется? В чём здесь двойная ловушка?

🔍 Анализ:

Сначала кажется, что:

1. add_to_list(1) вернёт [1].
2. add_to_list(2) вернёт [2].
3. add_to_list(1) либо вызовет функцию снова, либо вернёт результат из кэша.

Но есть два подвоха:

Подвох №1: изменяемый аргумент по умолчанию

Аргумент lst=[] создаётся один раз при определении функции. Все вызовы без передачи списка будут использовать один и тот же список.

Подвох №2: кэширование по ключу

Декоратор memoize сохраняет результат в кэше по ключу arg. Но функция возвращает список, который изменяется при каждом вызове. Даже если результат берётся из кэша, вы получите ссылку на тот же список, который менялся между вызовами!

🧮 Что реально произойдёт:

- `res1 = add_to_list(1)` → функция вызвана, список становится `[1]`.
- `res2 = add_to_list(2)` → функция вызвана снова с другим аргументом, список теперь `[1, 2]`.
- `res3 = add_to_list(1)` → аргумент `1` есть в кэше, сработает ветка `print("Из кэша")`, и вернётся ссылка на тот же изменённый список.

🔢 Итог:

```
[1, 2]
[1, 2]
Из кэша
[1, 2]
```

Все переменные указывают на один и тот же изменённый список.

💥 Почему это важно:

1️⃣ Изменяемые аргументы по умолчанию сохраняются между вызовами функции.
2️⃣ Кэширование изменяемых объектов может привести к неожиданным результатам: возвращается не неизменяемый результат, а ссылка на объект, который может изменяться позже.

🛡️ Как исправить:

1️⃣ Использовать `lst=None` и создавать новый список внутри функции:
```python
def add_to_list(val, lst=None):
if lst is None:
lst = []
lst.append(val)
return lst
```

2️⃣ Если кэшировать изменяемые объекты, лучше возвращать их копии:
```python
import copy
cache[arg] = copy.deepcopy(result)
```

Итог:

Декораторы вместе с изменяемыми аргументами — это ловушка даже для опытных программистов. Особенно, если изменяемые объекты кэшируются и потом меняются за кулисами.

@Python_Community_ru



tg-me.com/Python_Community_ru/2597
Create:
Last Update:

🐍 Задача с подвохом: Декораторы и изменяемые объекты

Условие:

Что выведет следующий код и почему?

def memoize(fn):
cache = {}
def wrapper(arg):
if arg in cache:
print("Из кэша")
return cache[arg]
else:
result = fn(arg)
cache[arg] = result
return result
return wrapper

@memoize
def add_to_list(val, lst=[]):
lst.append(val)
return lst

res1 = add_to_list(1)
res2 = add_to_list(2)
res3 = add_to_list(1)

print(res1)
print(res2)
print(res3)

Вопрос:
Что именно выведется? В чём здесь двойная ловушка?

🔍 Анализ:

Сначала кажется, что:

1. add_to_list(1) вернёт [1].
2. add_to_list(2) вернёт [2].
3. add_to_list(1) либо вызовет функцию снова, либо вернёт результат из кэша.

Но есть два подвоха:

Подвох №1: изменяемый аргумент по умолчанию

Аргумент lst=[] создаётся один раз при определении функции. Все вызовы без передачи списка будут использовать один и тот же список.

Подвох №2: кэширование по ключу

Декоратор memoize сохраняет результат в кэше по ключу arg. Но функция возвращает список, который изменяется при каждом вызове. Даже если результат берётся из кэша, вы получите ссылку на тот же список, который менялся между вызовами!

🧮 Что реально произойдёт:

- `res1 = add_to_list(1)` → функция вызвана, список становится `[1]`.
- `res2 = add_to_list(2)` → функция вызвана снова с другим аргументом, список теперь `[1, 2]`.
- `res3 = add_to_list(1)` → аргумент `1` есть в кэше, сработает ветка `print("Из кэша")`, и вернётся ссылка на тот же изменённый список.

🔢 Итог:

```
[1, 2]
[1, 2]
Из кэша
[1, 2]
```

Все переменные указывают на один и тот же изменённый список.

💥 Почему это важно:

1️⃣ Изменяемые аргументы по умолчанию сохраняются между вызовами функции.
2️⃣ Кэширование изменяемых объектов может привести к неожиданным результатам: возвращается не неизменяемый результат, а ссылка на объект, который может изменяться позже.

🛡️ Как исправить:

1️⃣ Использовать `lst=None` и создавать новый список внутри функции:
```python
def add_to_list(val, lst=None):
if lst is None:
lst = []
lst.append(val)
return lst
```

2️⃣ Если кэшировать изменяемые объекты, лучше возвращать их копии:
```python
import copy
cache[arg] = copy.deepcopy(result)
```

Итог:

Декораторы вместе с изменяемыми аргументами — это ловушка даже для опытных программистов. Особенно, если изменяемые объекты кэшируются и потом меняются за кулисами.

@Python_Community_ru

BY Python Community


Warning: Undefined variable $i in /var/www/tg-me/post.php on line 283

Share with your friend now:
tg-me.com/Python_Community_ru/2597

View MORE
Open in Telegram


Python Community Telegram | DID YOU KNOW?

Date: |

Newly uncovered hack campaign in Telegram

The campaign, which security firm Check Point has named Rampant Kitten, comprises two main components, one for Windows and the other for Android. Rampant Kitten’s objective is to steal Telegram messages, passwords, and two-factor authentication codes sent by SMS and then also take screenshots and record sounds within earshot of an infected phone, the researchers said in a post published on Friday.

Start with a fresh view of investing strategy. The combination of risks and fads this quarter looks to be topping. That means the future is ready to move in.Likely, there will not be a wholesale shift. Company actions will aim to benefit from economic growth, inflationary pressures and a return of market-determined interest rates. In turn, all of that should drive the stock market and investment returns higher.

Python Community from no


Telegram Python Community
FROM USA