Дивні "фішки" Python, які можуть вас здивувати

Дивні "фішки" Python, які можуть вас здивувати
Переклад 8 хв. читання
01 вересня 2023

Час від часу під час написання коду ми всі стикаємося з дивною поведінкою мови програмування. Іноді це "особливість", про яку ми не знали, іноді це просто химерна поведінка мови, а іноді це вже баг на межі. Python, як і будь-яка інша мова, має свої дивацтва, що викликають здивування, тож ось список дивних "фішок" Python, які можуть заскочити вас зненацька.

"Особливості"

Почнемо з деяких дивних поведінок, які дехто може вважати "фішками". А саме:

class A:
    def func(self):
        print("A")

class B:
    def func(self):
        print("B")

a = A()
a.func()  # A

a.__class__ = B
a.func()  # B

У наведеному вище прикладі ми присвоїли клас B атрибуту a.__class__, який змінюється на функції класу B.

Це працює, тому що __class__ - це лише атрибут на екземплярі. Ви можете перепризначити його як завгодно. Таким чином, ви можете змінити тип об'єкта, просто присвоївши інший клас його атрибуту __class__.

Далі йдуть цикли. Здавалося б, цикли настільки прості та базові, що в них може бути дивного?

values = "abc"
some_dict = {"key": ""}
for some_dict["key"] in values:
    print(some_dict)

# {'key': 'a'}
# {'key': 'b'}
# {'key': 'c'}

Інтерпретатору Python байдуже, яку змінну ви помістили в першу половину оператора циклу for, допоки він може присвоювати їй значення. У цьому випадку він просто присвоює окремі символи з послідовності (значення) ключу у словнику.

Хоча вищеописане здивувало мене, коли я вперше це побачив, але це не так вже й дивно, і це реальна функція, і для неї безумовно є виправдані випадки використання.

Кортежі

some_tuple = ([1], [2])
some_tuple[0].append(2)  # Worked!
print(some_tuple)
# ([1, 2], [2])

Ну, не зовсім. Хоча ви не можете змінювати сам кортеж, ви можете змінювати його елементи, якщо вони є змінюваними, а списки такими і є.

Важливо розуміти, що кортежі містять лише посилання на об'єкти, у цьому випадку списки. Тому ви не можете змінити посилання, наприклад, замінити/видалити список, але ви можете змінити його значення.

Також, щоб додати плутанини щодо незмінності кортежів, ви також можете успішно виконати наступний код:

some_tuple = ([1], [2])
print(id(some_tuple))
# 139997458815936
some_tuple += ([3],)
print(id(some_tuple))  # identity changed, therefore it's a new object
# 139997458018880

tuple реалізує обидва оператори + і +=, але вони не змінюють кортеж на місці. Вони створюють новий об'єкт кортежу, який замінює оригінальний. Ми можемо переконатися в цьому, перевіривши ідентичність змінної за допомогою функції id.

І наостанок, якщо попередні два приклади вас не здивували, то цей, безсумнівно, викличе у вас здивування. Цей фрагмент працює, але насправді ні, чи не так?

x = ([1, 2],)

try:
    x[0] += [3, 4]
except Exception as e:
    print(e)  # 'tuple' object does not support item assignment

# Traceback (most recent call last):
#   File "/home/martin/Projects/learning-notes/posts/Python Weirdness/examples.py", line 4, in <module>
#     x[0] += [3, 4]
# TypeError: 'tuple' object does not support item assignment

print(x)
# ([1, 2, 3, 4],)

У цьому фрагменті ми додали [3, 4] до першого елементу кортежу (x[0]) за допомогою оператора in-place (+=) і отримали TypeError. Але коли ми подивимося на змінну x після цього, то побачимо, що 2 нових елементи ([3, 4]) все одно було додано.

Ми вже з'ясували, що в незмінному кортежі можна змінювати змінювані елементи. Що ж тут відбувається?

# Pseudo-code
class List:
    def __iadd__(self, other):
        self.extend(other)
        return self

x = ([1, 2],)
x[0].extend([3, 4]); x[0] = x[0]

Проблема полягає в операторі +=, який у фоновому режимі викликає магічний метод __iadd__ списку. Цей метод спочатку використовує метод extend для додавання елементів до існуючого списку, а потім повертає сам список - фактично виконується x[0].extend([3, 4]); x[0] = x[0]. Розширення відбувається успішно, оскільки список є змінюваним, але присвоювання не вдається, оскільки кортеж не є змінюваним. Нам потрібно було виконати лише extend, але += не так реалізовано у класі list. Іноді деталі реалізації мають значення.

Рекурсія

(lambda x : x(x))(lambda x : x(x))
# Traceback (most recent call last):
#   File "<stdin>", line 1, in <module>
#   File "<stdin>", line 1, in <lambda>
#   File "<stdin>", line 1, in <lambda>
#   File "<stdin>", line 1, in <lambda>
#   [Previous line repeated 996 more times]
# RecursionError: maximum recursion depth exceeded

Я не думаю, що той факт, що наведений вище рядок коду викликає переповнення стеку, є чимось дивним, скоріше, він є коректним фрагментом коду в Python з якоїсь причини. Думаю, немає сенсу намагатися розшифрувати його, тому що жодна притомна людина не повинна так писати.

Трохи менш езотеричний і, можливо, корисний факт про рекурсію в Python полягає в тому, що ви можете створювати циклічні посилання:

a = [1, 2, 3]
a.append(a)
print(a)
# [1, 2, 3, [...]]

print(a[3])
# [1, 2, 3, [...]]

a = a[1:]
print(a)
# [2, 3, [1, 2, 3, [...]]]

Тут ми додали список до самого себе, і Python навіть має гарний спосіб його представлення за допомогою [...]. Якщо ви спробуєте отримати доступ до елемента, що посилається на себе, ви - як не дивно - отримаєте те ж саме. Очевидно, що ви можете виконати будь-яку іншу операцію з цим списком, наприклад, нарізку, яка дає цікаві результати, як ви можете бачити вище.

F-Strings

f-рядки чудові, вони надзвичайно потужні і з роками отримали багато корисних функцій. До такої міри, що ви можете робити з ними деякі дивні речі. Наприклад, вставляти в них лямбда-вирази:

Наступна повна сюрпризів область - це рекурсія, і коли ви поєднуєте її з лямбда-виразом - природно - відбуватимуться дивні речі:

Ми всі (напевно) знаємо, що кортежі є незмінними - ви визначаєте їх один раз і потім не можете змінити їх вміст, чи не так?

print(f"{(lambda x: x**2)(3)}")
# 9

А якщо ви вирішите поєднати f-рядки з нещодавно введеним walrus оператором (:=), то ви також можете визначати змінні всередині f-рядка:

from datetime import datetime

print(f"Today is: {(today:=datetime.today()):%Y-%m-%d}, which is {today:%A}")
# Today is: 2023-05-01, which is Monday

print(today)
# 2023-05-01 13:43:30.827182

А оскільки f-рядок не має власної області видимості, змінна - у наведеному вище прикладі - може бути використана за межами/поза межами самого f-рядка! Напевно, це має сенс, але мені це не здається правильним або інтуїтивно зрозумілим...

Граничні помилки

Останньою "примхою" Python, яку я особисто вважаю багом, є поведінка сирих рядкових (raw strings) літералів. Якщо ви не знайомі з сирими рядками, то це рядки, позначені/префіксом r, і вони сприймають зворотний слеш як буквальний символ, а не як ескейп/спеціальний символ.

Добре, але в чому проблема з цими необробленими рядками?

literal = r"some string\"
# SyntaxError: unterminated string literal (detected at line 1)

literal = r"some string\\"  # 2 backslashes
print(literal)  # some string\\

Якщо ви спробуєте створити raw string, що закінчується зворотною косою рискою - r"...\" - ви отримаєте синтаксичну помилку, яка стверджує, що рядок не завершується. Очевидно, що інтерпретатор Python сприймає зворотну косу риску як символ екранування для закриття лапок, але це не має сенсу, тому що це сирий рядок, і зворотна коса риска повинна сприйматися як буквений символ.

Насправді це досить поширена проблема, про яку є стаття у FAQ по дизайну Python, а також повідомлення про помилку у баг-трекері.

Заключні думки

Хоча ви, ймовірно, не зіткнетеся з більшістю з цих особливостей у своєму повсякденному програмуванні, я вважаю, що корисно знати про них, і ми можемо багато чому навчитися з цих дивних мовних примх. Вони змушують нас зазирнути трохи глибше і зрозуміти, що відбувається під капотом, що, своєю чергою, робить нас кращими Python розробниками.

Джерело: Weird Python "Features" That Might Catch You By Surprise
Помітили помилку? Повідомте автору, для цього достатньо виділити текст з помилкою та натиснути Ctrl+Enter
Коментарі (0)

    Ще немає коментарів

Щоб залишити коментар необхідно авторизуватися.

Вхід / Реєстрація