Деякі заплутані фрагменти Python, які можуть вас вкусити!

33 хв. читання

Зауважте: Ви можете переглянути представлені в статті приклади у командному рядку. Для цього потрібно буде встановити npm-пакет wtfpython та запустити його у командному рядку, після чого колекція відкриється в обраному $PAGER.

$ npm install -g wtfpython

👀 Приклади

Пропуск рядків?

Вивід:

>>> value = 11
>>> value = 32
>>> value
11

Що??

💡 Пояснення:
Деякі символи Unicode виглядають ідентично до ASCII символів, але для інтерпретатора вони вважаються різними.

>>> value = 42 #ascii e
>>> value = 23 #cyrillic e, Python 2.x інтерпретатор видасть помилку `SyntaxError`
>>> value
42

Ну, якось підозріло...

def square(x):
    """
   Проста функція для підрахунку квадрату числа за допомогою додавання.
    """
    sum_so_far = 0
    for counter in range(x):
        sum_so_far = sum_so_far + x
  return sum_so_far

Вивід (Python 2.x):

>>> square(10)
10

Хіба не повинно вийти 100?

Примітка: якщо вам не вдається відтворити цей фрагмент, спробуйте запустити mixed_tabs_and_spaces.py через оболонку.

💡 Пояснення:

  • Не змішуйте табуляцію і пробіли! Символ перед return є табуляцією, а в інших місцях код має відступи кратні чотирьом пробілам.
  • Ось як Python обробляє табуляції:

По-перше, табуляції замінюються (зліва направо) пробілами, від одного до восьми, так що загальна кількість символів, до і включаючи заміну, може бути у вісім разів більша <...>

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

Вивід (Python 3.x):

TabError: inconsistent use of tabs and spaces in indentation

Час для хеш-брауні!

some_dict = {}
some_dict[5.5] = "Ruby"
some_dict[5.0] = "JavaScript"
some_dict[5] = "Python"

Вивід:

>>> some_dict[5.5]
"Ruby"
>>> some_dict[5.0]
"Python"
>>> some_dict[5]
"Python"

«Python» знищив «JavaScript»?

💡 Пояснення:

  • 5 (тип int) неявно перетворюється на 5.0 (тип float) перед обчисленням хешу в Python.
>>> hash(5) == hash(5.0)
True
  • Дана відповідь на StackOverflow прекрасно пояснює причини цього.

Невідповідність часу обробки

array = [1, 8, 15]
g = (x for x in array if array.count(x) > 0)
array = [2, 8, 22]

Вивід:

>>> print(list(g))
[8]

💡 Пояснення:

  • У виразі-генераторі (generator expression) конструкція in оброблюється під час оголошення, але умовна конструкція оброблюється під час виконання.
  • Таким чином, array перепризначається в список [2, 8, 22] перед часом виконання, і оскільки з 1, 8 і 15, тільки число 8 перевищує 0, генератор видає лише 8.

Модифікація словника під час ітерації по ньому

x = {0: None}

for i in x:
    del x[i]
    x[i+1] = None
    print(i)

Вивід:

0
1
2
3
4
5
6
7

Так, виконується рівно вісім разів і потім зупиняється.

💡 Пояснення:

  • Ітерація по словнику, який ви редагуєте в той самий час, не підтримується в Python.
  • Код виконується вісім разів через те, що це та точка, в якій словник змінює розмір, щоб утримувати більше ключей (у нас вісім записів видалення, тому потрібно змінити розмір). Це фактично деталь реалізації.
  • Зверніться до цього треду StackOverflow, де пояснюється подібний приклад.

Видалення елемента списку під час ітерації по ньому

list_1 = [1, 2, 3, 4]
list_2 = [1, 2, 3, 4]
list_3 = [1, 2, 3, 4]
list_4 = [1, 2, 3, 4]

for idx, item in enumerate(list_1):
    del item

for idx, item in enumerate(list_2):
    list_2.remove(item)

for idx, item in enumerate(list_3[:]):
    list_3.remove(item)

for idx, item in enumerate(list_4):
    list_4.pop(idx)

Вивід:

>>> list_1
[1, 2, 3, 4]
>>> list_2
[2, 4]
>>> list_3
[]
>>> list_4
[2, 4]

Зможете здогадатися, чому виводиться [2, 4]?

💡 Пояснення:

  • Ніколи не рекомендується змінювати об'єкт під час ітерації по ньому. Правильний спосіб це зробити — перезаписати копію об'єкта, а list_3[:] саме це і робить.
>>> some_list = [1, 2, 3, 4]
>>> id(some_list)
139798789457608
>>> id(some_list[:]) # Notice that python creates new object for sliced list.
139798779601192

Різниця між del, remove і pop:

  • remove видаляє перше відповідне значення, а не конкретний індекс, викликаючи ValueError при відсутності значення.
  • del видаляє певний індекс (тому list_1 не зазнав ніякого впливу), викликаючи IndexError, якщо вказано недійсний індекс.
  • pop вилучає елемент у певному індексі і повертає його, викликаючи IndexError, якщо вказано неправильний індекс.

Чому вийшло [2, 4]?

  • Ітерація списку виконується індекс за індексом, і коли ми видаляємо 1 з list_2 або list_4, вміст списків стає [2, 3, 4]. Інші елементи зсуваються вниз, тобто 2 знаходиться в індексі 0, а 3 — в індексі 1. Оскільки наступна ітерація буде стосуватися індексу 1 (де записано 3), 2 повністю пропуститься. Подібна річ відбудеться з кожним альтернативним елементом у послідовності списку.
  • Продивіться цей чудовий тред StackOverflow, пов'язаний зі словниками в Python, де розглядається подібний приклад.

Зворотні слеші в кінці рядка

Вивід:

>>> print("\\\\ some string \\\\")
>>> print(r"\\ some string")
>>> print(r"\\ some string \\")

    File "<stdin>", line 1
      print(r"\\ some string \\")
                             ^
SyntaxError: EOL while scanning string literal

💡 Пояснення:

  • У необробленому рядковому літералі, на що вказує префікс r, зворотний слеш не має спеціального значення.
  • Насправді інтерпретатор просто змінює поведінку зворотних слешів, тому вони й наступний символ після них просто пропускаються. Ось чому зворотні слеші не працюють в кінці рядка.

Зробімо гігантський рядок!

Це взагалі не WTF, а лише деякі цікаві речі, про які слід бути в курсі 😃

def add_string_with_plus(iters):
    s = ""
    for i in range(iters):
        s += "xyz"
    assert len(s) == 3*iters

def add_string_with_format(iters):
    fs = "{}"*iters
    s = fs.format(*(["xyz"]*iters))
    assert len(s) == 3*iters

def add_string_with_join(iters):
    l = []
    for i in range(iters):
        l.append("xyz")
    s = "".join(l)
    assert len(s) == 3*iters

def convert_list_to_string(l, iters):
    s = "".join(l)
    assert len(s) == 3*iters

Вивід:

>>> timeit(add_string_with_plus(10000))
100 loops, best of 3: 9.73 ms per loop
>>> timeit(add_string_with_format(10000))
100 loops, best of 3: 5.47 ms per loop
>>> timeit(add_string_with_join(10000))
100 loops, best of 3: 10.1 ms per loop
>>> l = ["xyz"]*10000
>>> timeit(convert_list_to_string(l, 10000))
10000 loops, best of 3: 75.3 µs per loop

💡 Пояснення:

  • Ви можете докладніше прочитати про timeit тут.
  • Не використовуйте + для генерації довгих рядків. В Python str незмінний, тому для кожної пари конкатенацій лівий й правий рядок повинні бути скопійовані в новий рядок. Якщо ви конкатенуєте чотири рядки, довжиною в 10 символів, то копіюйте (10+10) + ((10+10)+10) + (((10+10)+10)+10) = 90 символів, замість 40.
  • Тому рекомендується використовувати синтаксис .format. або % (однак, вони трохи повільніші для коротких рядків, ніж + ).
  • Або краще, якщо у вас вже є контент у формі ітеративного об'єкта, використайте ''.join(iterable_object), який набагато швидше.

Оптимізації інтерпретатора конкатенації рядків

>>> a = "some_string"
>>> id(a)
140420665652016
>>> id("some" + "_" + "string") # Зверніть увагу, що обидва ідентифікатори однакові.
140420665652016
# using "+", three strings:
>>> timeit.timeit("s1 = s1 + s2 + s3", setup="s1 = ' ' * 100000; s2 = ' ' * 100000; s3 = ' ' * 100000", number=100)
0.25748300552368164
# using "+=", three strings:
>>> timeit.timeit("s1 += s2 + s3", setup="s1 = ' ' * 100000; s2 = ' ' * 100000; s3 = ' ' * 100000", number=100)
0.012188911437988281

💡 Пояснення:

  • += швидше, ніж + для конкатенації більш, ніж двох рядків, тому що перший рядок (наприклад, s1 для s1 += s2 + s3) не знищується, поки рядок не буде обчислений повністю.
  • Обидва рядки посилаються на один і той самий об'єкт, через те, що оптимізація CPython намагається використовувати незмінні об'єкти, що існують (особливість реалізації), ніж кожен раз створювати новий об'єкт. Ви можете більше дізнатися про це тут.

Так, воно існує!

Конструкція else для циклів. Один типовий приклад:

 def does_exists_num(l, to_find):
      for num in l:
          if num == to_find:
              print("Exists!")
              break
      else:
          print("Does not exist")

Вивід:

>>> some_list = [1, 2, 3, 4, 5]
>>> does_exists_num(some_list, 4)
Exists!
>>> does_exists_num(some_list, -1)
Does not exist

Конструкція else в обробці виключень. Приклад:

try:
    pass
except:
    print("Exception occurred!!!")
else:
    print("Try block executed successfully...")

Вивід:

Try block executed successfully...

💡 Пояснення:

  • Конструкція else після циклу виконується тільки тоді, коли після всіх ітерацій немає явного break.
  • Конструкція else після блоку try також називається конструкцією завершення (completion clause), оскільки досягнення else у виразі try означає, що блок try успішно завершений.

is не те, чим воно є!

Наступний приклад дуже відомий і присутній по всьому інтернету:

>>> a = 256
>>> b = 256
>>> a is b
True

>>> a = 257
>>> b = 257
>>> a is b
False

>>> a = 257; b = 257
>>> a is b
True

💡 Пояснення:
Відмінність між is та ==

  • Оператор іs перевіряє, чи посилаються обидва операнди на один і той же об'єкт (тобто перевіряє, чи ідентичні операнди один одному).
  • Оператор == порівнює значення обох операндів і перевіряє, чи вони збігаються.
  • Отже, іs використовується для перевірки рівності посилань, а == — для перевірки рівності значень. Приклад для прояснення ситуації:
>>> [] == []
True
>>> [] is [] # Це два порожні списки в двох різних місцях пам'яті.
False

256 об'єкт, що існує, а 257 - ні

При запуску Python резервуються числа від -5 до 256. Ці числа використовуються дуже часто, тому є сенс тримати їх наготові.

Цитата із https://docs.python.org/3/c-api/long.html:

Поточна реалізація зберігає масив цілочислових об'єктів для всіх цілих чисел між -5 та 256, коли ви створюєте int в цьому діапазоні, ви просто повертаєте посилання на об'єкт, що існує. Тому повинна бути можливість змінити значення 1. Я підозрюю, що поведінка Python в даному випадку є непередбачуваною.

>>> id(256)
10922528
>>> a = 256
>>> b = 256
>>> id(a)
10922528
>>> id(b)
10922528
>>> id(257)
140084850247312
>>> x = 257
>>> y = 257
>>> id(x)
140084850247440
>>> id(y)
140084850247344

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

І a і b посилаються на один і той самий об'єкт, коли вони ініціалізуються з однаковим значенням в одному рядку.

>>> a, b = 257, 257
>>> id(a)
140640774013296
>>> id(b)
140640774013296
>>> a = 257
>>> b = 257
>>> id(a)
140640774013392
>>> id(b)
140640774013488
  • Коли a і b присвоюється значення 257 в одному рядку, інтерпретатор Python створює новий об'єкт, який в той самий час посилається на другу змінну. Якщо зробити це на окремих рядках, він не буде «знати», що вже існує 257 як об'єкт.
  • Це оптимізація компілятора і конкретно стосується інтерактивного середовища. Коли ви вводите два рядки у компілятор, що працює, то вони компілюються окремо, а значить і оптимізуються окремо. Якби ви спробували скористатись цим прикладом у файлі .py, то не побачили би таку саму поведінку, оскільки файл компілюється одразу.

is not ... відрізняється від is (not ...)

>>> 'something' is not None
True
>>> 'something' is (not None)
False

💡 Пояснення:

  • is not — це одиничний бінарний оператор, поведінка якого відмінна від окремого використання is та not.
  • is not видає False, якщо змінні з кожної сторони оператора вказують на один і той самий об'єкт, і True в іншому випадку.

Функція всередині циклу видає один і той самий результат

funcs = []
results = []
for x in range(7):
    def some_func():
        return x
    funcs.append(some_func)
    results.append(some_func())

funcs_results = [func() for func in funcs]

Вивід:

>>> results
[0, 1, 2, 3, 4, 5, 6]
>>> funcs_results
[6, 6, 6, 6, 6, 6, 6]

Навіть якщо до додавання some_func до funcs значення х у кожній ітерації були різними, всі функції повертатимуть 6.

>>> powers_of_x = [lambda x: x**i for i in range(10)]
>>> [f(2) for f in powers_of_x]
[512, 512, 512, 512, 512, 512, 512, 512, 512, 512]

💡 Пояснення:

  • При визначенні функції всередині циклу, в тілі якого використовується його змінна, закриття функції циклу прив'язане до змінної, а не до її значення. Тому, всі функції для обчислення використовують останнє значення, присвоєне змінній.
  • Щоб отримати бажану поведінку, ви можете передати в функцію змінну циклу в якості іменованої змінної. Чому це працює? Тому що це знову визначить змінну в області видимості функції.
funcs = []
for x in range(7):
    def some_func(x=x):
        return x
    funcs.append(some_func)

Вивід:

>>> funcs_results = [func() for func in funcs]
>>> funcs_results
[0, 1, 2, 3, 4, 5, 6]

Змінні циклу просочуються із локальної області видимості!

1

for x in range(7):
    if x == 6:
        print(x, ': for x inside loop')
print(x, ': x in global')

Вивід:

6 : for x inside loop
6 : x in global

Але х не була визначена поза областю видимості циклу for...

2

# Цього разу давайте спершу ініціалізуємо x
x = -1
for x in range(7):
    if x == 6:
        print(x, ': for x inside loop')
print(x, ': x in global')

Вивід:

6 : for x inside loop
6 : x in global

3

x = 1
print([x for x in range(5)])
print(x, ': x in global')

Вивід (на Python 2.x):

[0, 1, 2, 3, 4]
(4, ': x in global')

Вивід (на Python 3.x):

[0, 1, 2, 3, 4]
1 : x in global

💡 Пояснення:

  • У Python for-цикли використовують область видимості, в якій вони існують, залишаючи позаду свою визначену змінну циклу. Це також стосується випадку, якщо ми раніше явно визначили змінну циклу for у глобальному просторі імен.
  • Відмінності виводу в інтерпретаторах Python 2.x і Python 3.x стосовно прикладу генерування списків (list comprehension), може бути пояснена з допомогою наступної зміни, описаній в документації What's New In Python 3.0:

Генераторні списки більше не підтримують синтаксичну форму [... for var in item1, item2, ...]. Замість неї використовуйте [... for var in (item1, item2, ...)]. Також, зверніть увагу на те, що генераторні списки мають різні семантики: для генеруючого виразу всередині list() вони близькі до синтаксичного цукру і, зокрема, змінні керування циклу більше не просочуються в навколишню область видимості.


Хрестики-нулики, в яких Х виграє з першої спроби!

# ініціалізуємо row
row = [""]*3 #row i['', '', '']
# Let's make a board
board = [row]*3

Вивід:

>>> board
[['', '', ''], ['', '', ''], ['', '', '']]
>>> board[0]
['', '', '']
>>> board[0][0]
''
>>> board[0][0] = "X"
>>> board
[['X', '', ''], ['X', '', ''], ['X', '', '']]

Але ми не призначали 3 Х, чи все-таки призначили?

💡 Пояснення:

  • Ця візуалізація пояснює, що відбувається у пам'яті, коли ми ініціалізуємо змінну row

Деякі заплутані фрагменти Python, які можуть вас вкусити!

  • А коли, за допомогою множення row ініціалізується board, це те, що відбувається у пам'яті (кожен з елементів board[0], board[1] та board[2] є посиланням на один і той самий список, який вказаний у row).

Деякі заплутані фрагменти Python, які можуть вас вкусити!


Остерігайтеся змінних аргументів за замовчуванням

def some_func(default_arg=[]):
    default_arg.append("some_string")
    return default_arg

Вивід:

>>> some_func()
['some_string']
>>> some_func()
['some_string', 'some_string']
>>> some_func([])
['some_string']
>>> some_func()
['some_string', 'some_string', 'some_string']

💡 Пояснення:

  • У функціях Python змінні аргументи по замовчуванню насправді не ініціалізуються при кожному виклику функції. Замість цього, як значення по замовчуванню використовується нещодавно присвоєне значення. Коли ми явно передали як аргумент [ ] до some_func, значення по замовчуванню змінної default_arg не використалося, тому функція повернула те, що очікувалось.
def some_func(default_arg=[]):
    default_arg.append("some_string")
    return default_arg

Вивід:

>>> some_func.__defaults__ #Це покаже значення аргументів по замовчуванню для функції
([],)
>>> some_func()
>>> some_func.__defaults__
(['some_string'],)
>>> some_func()
>>> some_func.__defaults__
(['some_string', 'some_string'],)
>>> some_func([])
>>> some_func.__defaults__
(['some_string', 'some_string'],)
  • Звичайна практика уникнення багів через змінні аргументи — присвоїти None в якості аргументу по замовчуванню з подальшою перевіркою, чи передається якесь значення функції, відповідної цьому аргументу. Приклад:
def some_func(default_arg=None):
    if not default_arg:
        default_arg = []
    default_arg.append("some_string")
    return default_arg

Ті самі операнди, інша історія!

1

a = [1, 2, 3, 4]
b = a
a = a + [5, 6, 7, 8]

Вивід:

>>> a
[1, 2, 3, 4, 5, 6, 7, 8]
>>> b
[1, 2, 3, 4]

2

a = [1, 2, 3, 4]
b = a
a += [5, 6, 7, 8]

Вивід:

>>> a
[1, 2, 3, 4, 5, 6, 7, 8]
>>> b
[1, 2, 3, 4, 5, 6, 7, 8]

💡 Пояснення:

  • a += b поводиться не так, як a = a + b
  • Вираз a = a + [5,6,7,8] генерує новий об'єкт і присвоює посилання a на цей новий об'єкт, залишаючи b незмінним
  • Вираз a +=[5,6,7,8] насправді відображається в функції «extend», яка діє на об'єкт таким чином, що a і b все ще вказують на той самий об'єкт, який був модифікований на місці.

Змінення незмінного

some_tuple = ("A", "tuple", "with", "values")
another_tuple = ([1, 2], [3, 4], [5, 6])

Вивід:

>>> some_tuple[2] = "change this"
TypeError: 'tuple' object does not support item assignment
>>> another_tuple[2].append(1000) #Ніяких помилок
>>> another_tuple
([1, 2], [3, 4], [5, 6, 1000])
>>> another_tuple[2] += [99, 999]
TypeError: 'tuple' object does not support item assignment
>>> another_tuple
([1, 2], [3, 4], [5, 6, 1000, 99, 999])

Але я думав, що кортежі незмінні...

💡 Пояснення:

Незмінні послідовності. Об'єкт послідовності незмінного типу не можна змінити після його створення. Якщо об'єкт містить посилання на інші об'єкти, то ці об'єкти можуть бути змінними й можуть бути модифіковані; однак, колекцію об'єктів, на яку прямо посилається незмінний об'єкт, не можна змінити.)

  • Оператор += змінює список на місці. Присвоєння об'єкта не працює, але коли виникає виключення, об'єкт вже змінений.

Використання змінної, яка не визначена в області видимості

a = 1
def some_func():
    return a

def another_func():
    a += 1
    return a

Вивід:

>>> some_func()
1
>>> another_func()
UnboundLocalError: local variable 'a' referenced before assignment

💡 Пояснення:

  • Коли ви присвоюєте змінну в області видимості, вона стає локальною для цієї області. Таким чином, a стає локальною для області видимості another_func, але вона не була ініціалізована раніше в тій самій області, в якій виникає помилка.
  • Прочитайте цей короткий, але чудовий гайд, щоб дізнатися більше про те, як в Python працюють простори імен та розширення області видимості.
  • Для модифікації змінної зовнішньої області a в another_func використовуйте ключове слово global
def another_func()
    global a
    a += 1
    return a

Вивід:

>>> another_func()
2

Зникнення змінної із зовнішньої області видимості

e = 7
try:
    raise Exception()
except Exception as e:
    pass

Вивід (Python 2.x):

>>> print(e)
# prints nothing

Вивід (Python 3.x):

>>> print(e)
NameError: name 'e' is not defined

💡 Пояснення:

Це якби:

except E as N:
    foo

було перетворене в

except E as N:
    try:
        foo
    finally:
        del N

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

  • У Python конструкції не входять в область видимості. У прикладі все представлене в одній області видимості, і змінна e видаляється через те, що виконується конструкція except. Але це не стосується функцій, які мають окремі області видимості. Приклад нижче це ілюструє:
def f(x):
    del(x)
    print(x)

x = 5
y = [5, 4, 3]

Вивід:

>>>f(x)
UnboundLocalError: local variable 'x' referenced before assignment
>>>f(y)
UnboundLocalError: local variable 'x' referenced before assignment
>>> x
5
>>> y
[5, 4, 3]
  • У Python 2.x ім'я змінної e, яке присвоєне екземпляру Exception(), тому, коли ви спробуєте зробити вивід на екран, нічого не виведеться.

Вивід (Python 2.x):

>>> e
Exception()
>>> print e
# Нічого не вивелося!

Return, return повсюди!

def some_func():
    try:
        return 'from_try'
    finally:
        return 'from_finally'

Вивід:

>>> some_func()
'from_finally'

💡 Пояснення:

  • Коли return, break або continue виконується у блоці try, виразу try…finally, на виході також виконується конструкція finally.
  • Повернення значення функції визначається останнім виконаним виразом return. Бо конструкція finally завжди виконується, вираз return виконаний всередині конструкції finally, буде завжди виконуватися останнім.

Коли True насправді False

True = False
if True == False:
    print("I've lost faith in truth!")

Вивід:

I've lost faith in truth!

💡 Пояснення:

  • Раніше в Python не було типу bool (люди використовували 0 для false і ненульове значення, як наприклад 1, — для true). Потім було додано True, False, і тип bool, але для зворотньої сумісності не можна було зробити True і False константами — вони були всього лише вбудованими змінними.
  • Python 3 став зворотньо-несумісним, тому, нарешті, з'явилася можливість це виправити, тому цей приклад не буде працювати на Python 3.x!

Будьте обережні з ланцюговими операціями

>>> True is False == False
False
>>> False is False is False
True
>>> 1 > 0 < 1
True
>>> (1 > 0) < 1
False
>>> 1 > (0 < 1)
False

💡 Пояснення:
Відповідно до https://docs.python.org/2/reference/expressions.html#not-in

Формально, якщо a, b, c, ..., y, z — вирази, а op1, op2, ..., opN — оператори порівняння, тоді a op1 b op2 c ... y opN z еквівалентне op1 b and b op2 c and ... y opN z, за винятком того, що кожен вираз обчислюється максимум один раз.

Тоді як у вищевказаних прикладах така поведінка могла здаватися вам нерозумною, вона дуже зручна в ситуаціях як a == b == c і 0 <= x <= 100.

  • False is False is False еквівалентна до (False is False) and (False is False)
  • True is False == False еквівалентна до True is False and False == False, і оскільки перша частина виразу (True is False) обчислюється як False, увесь вираз обчислюється як False.
  • 1 > 0 < 1 еквівалентна до 1 > 0 and 0 < 1, який обчислюється як True.
  • Вираз (1 > 0) < 1 еквівалентний до True < 1 і
>>> int(True)
1
>>> True + 1 #не релевантно для цього прикладу, але просто для забави
2

Отже, 1 < 1 обчислюється як False.


Роздільність імен ігнорує область видимості класу

1

x = 5
class SomeClass:
    x = 17
    y = (x for i in range(10))

Вивід:

>>> list(SomeClass.y)[0]
5

2

x = 5
class SomeClass:
    x = 17
    y = [x for i in range(10)]

Вивід (Python 2.x):

>>> SomeClass.y[0]
17

Вивід (Python 3.x):

>>> SomeClass.y[0]
5

💡 Пояснення:

  • Області видимості, вкладені у визначення класу, ігнорують імена, прив'язані до рівня класу.
  • Вираз-генератор має свою власну область видимості.
  • Починаючи з Python 3.X генераторні списки також мають свою область видимості.

Від заповненості до None в одній інструкції...

some_list = [1, 2, 3]
some_dict = {
  "key_1": 1,
  "key_2": 2,
  "key_3": 3
}

some_list = some_list.append(4)
some_dict = some_dict.update({"key_4": 4})

Вивід:

>>> print(some_list)
None
>>> print(some_dict)
None

💡 Пояснення:
Більшість методів, які модифікують елементи об'єктів послідовності/відображення , таких як list.append, dict.update, list.sort і т.д. змінюють об'єкти на місці й повертають None. Обгрунтування цього полягає в підвищенні продуктивності, уникаючи створення копії об'єкта, якщо операцію можна виконати на місці (наведено тут).


Явне приведення типів рядків

Це взагалі не WTF, але в мене пішло багато часу на те, щоб усвідомити, що таки речі існуть в Python. Тому ділюсь цим з новачками.

a = float('inf')
b = float('nan')
c = float('-iNf')  # Ці рядки нечутливі до регістру
d = float('nan')

Вивід:

>>> a
inf
>>> b
nan
>>> c
-inf
>>> float('some_other_string')
ValueError: could not convert string to float: some_other_string
>>> a == -c #inf==inf
True
>>> None == None # None==None
True
>>> b == d #але nan!=nan
False
>>> 50/a
0.0
>>> a/a
nan
>>> 23 + b
nan

💡 Пояснення:
inf та nan— спеціальні рядки (нечутливі до регістру), які, якщо явно привести їх до типу float, використовуються для представлення математичної «нескінченності» й «не числа» відповідно.


Атрибути класів і екземплярів

1

class A:
    x = 1

class B(A):
    pass

class C(A):
    pass

Вивід:

>>> A.x, B.x, C.x
(1, 1, 1)
>>> B.x = 2
>>> A.x, B.x, C.x
(1, 2, 1)
>>> A.x = 3
>>> A.x, B.x, C.x
(3, 2, 3)
>>> a = A()
>>> a.x, A.x
(3, 3)
>>> a.x += 1
>>> a.x, A.x
(4, 3)

2

class SomeClass:
    some_var = 15
    some_list = [5]
    another_list = [5]
    def __init__(self, x):
        self.some_var = x + 1
        self.some_list = self.some_list + [x]
        self.another_list += [x]

Вивід:

>>> some_obj = SomeClass(420)
>>> some_obj.some_list
[5, 420]
>>> some_obj.another_list
[5, 420]
>>> another_obj = SomeClass(111)
>>> another_obj.some_list
[5, 111]
>>> another_obj.another_list
[5, 420, 111]
>>> another_obj.another_list is SomeClass.another_list
True
>>> another_obj.another_list is some_obj.another_list
True>>> some_obj = SomeClass(420)
>>> some_obj.some_list
[5, 420]
>>> some_obj.another_list
[5, 420]
>>> another_obj = SomeClass(111)
>>> another_obj.some_list
[5, 111]
>>> another_obj.another_list
[5, 420, 111]
>>> another_obj.another_list is SomeClass.another_list
True
>>> another_obj.another_list is some_obj.another_list
True

💡 Пояснення:

  • Змінні класу й змінні в екземплярах класу обробляються як словники об'єкта класу. Якщо назву змінної не знайдено в словнику поточного класу, для неї шукають батьківські класи.
  • Оператор += модифікує змінний об'єкт на місці, без створення нового об'єкту. Тому зміна атрибуту одного екземпляру діє так само на інші екземпляри атрибут класу.

Ловля виключень!

some_list = [1, 2, 3]
try:
   # Це повинно викликати ``IndexError``
   print(some_list[4])
except IndexError, ValueError:
   print("Caught!")

try:
   # Це повинно викликати ``ValueError``
   some_list.remove(4)
except IndexError, ValueError:
   print("Caught again!")

Вивід (Python 2.x):

Caught!

ValueError: list.remove(x): x not in list

Вивід (Python 3.x):

File "<input>", line 3
    except IndexError, ValueError:
                     ^
SyntaxError: invalid syntax

💡 Пояснення:

  • Для додавання кількох виключень в конструкцію except, вам потрібно передати їх у вигляді взятого у лапки кортежу в якості першого аргументу. Другий аргумент — опціональне ім'я, яке потім прив'язується до екземпляру виключення, яке виникло. Приклад:
some_list = [1, 2, 3]
try:
  # Це повинно викликати ``ValueError``
  some_list.remove(4)
except (IndexError, ValueError), e:
  print("Caught again!")
  print(e)

Вивід (Python 2.x):

Caught again!
list.remove(x): x not in list

Вивід (Python 3.x):

File "<input>", line 4
    except (IndexError, ValueError), e:
                                     ^
IndentationError: unindent does not match any outer indentation level
  • Відокремлення комою виключення від змінної не рекомендується, бо це не працює в Python 3; правильним способом є використання as. Наприклад:
some_list = [1, 2, 3]
try:
    some_list.remove(4)

except (IndexError, ValueError) as e:
    print("Caught again!")
    print(e)

Вивід:

Caught again!
list.remove(x): x not in list

Півночі не існує?

from datetime import datetime

midnight = datetime(2018, 1, 1, 0, 0)
midnight_time = midnight.time()

noon = datetime(2018, 1, 1, 12, 0)
noon_time = noon.time()

if midnight_time:
   print("Time at midnight is", midnight_time)

if noon_time:
   print("Time at noon is", noon_time)

Вивід:

('Time at noon is', datetime.time(12, 0))

Північ не вивелася.

💡 Пояснення:
До Python 3.5 логічне значення для об'єкта datetime.time розглядалося як False, якщо воно відображало північ у форматі UTC. Це може призвести до помилок, при використанні синтаксису if obj: для перевірки, чи obj є null, чи якийсь інший еквівалент «порожнечі».


Підрахунок булевих значень

# Простий приклад для підрахунку кількості булевих значень та
# цілочислених значень в ітерабельному змішаному типі даних.
mixed_list = [False, 1.0, "some_string", 3, True, [], False]
integers_found_so_far = 0
booleans_found_so_far = 0

for item in mixed_list:
    if isinstance(item, int):
        integers_found_so_far += 1
    elif isinstance(item, bool):
        booleans_found_so_far += 1

Вивід:

>>> booleans_found_so_far
0
>>> integers_found_so_far
4

💡 Пояснення:

  • Булеві значення — це підклас int
>>> isinstance(True, int)
True
>>> isinstance(False, int)
True
  • Продивіться цю відповідь на StackOverflow для розуміння причин, які за цим стоять.

Голка в копиці сіна

Майже кожен Python-програміст стикався з цією ситуацією.

t = ('one', 'two')
for i in t:
    print(i)

t = ('one')
for i in t:
    print(i)

t = ()
print(t)

Вивід:

one
two
o
n
e
tuple()

💡 Пояснення:

  • Правильним виразом для очікуваної поведінки є t = ('one',), або t = 'one', (кома відсутня), інакше інтерпретатор вважатиме, що t — це str й ітерує його символ за символом.
  • () — це спеціальний токен і позначає пустий кортеж.

Дрібніші приклади

  • join() — рядкова операція, а не спискова операція (при першому використанні це здається контрінтуїтивним).

💡 Пояснення:
Якщо join() — це метод для рядка, то він може працювати з будь-якими ітерабельними (список, кортеж, ітератори). Якби це був списковий метод, його потрібно було б реалізовувати кожним типом окремо. Також не має сенсу вводити рядковий метод у загальний список. Крім того, це виглядає неправильним — вставляти метод, призначений для рядкових значень, у загальний список.

  • Декілька дивно виглядаючих, але семантично коректних, виразів:
    • [] = () семантично коректний вираз (розпаковує пустий tuple у пустий list).
    • 'a'[0][0][0][0][0] також семантично коректний, тому що рядки у Python ітеровані.
    • 3 --0-- 5 == 8 та --5 == 5 обидва семантично коректні вирази, й повертають True.
  • Python використовує 2 байти для зберігання локальної змінної в функціях. У теорії це означає, що в функції можна визначити лише 65536 змінних. Тим не менш, Python має вбудоване зручне рішення, яке може використовуватися для зберігання більш ніж 2 ^ 16 імен змінних. Наступний код демонструє, що відбувається в стеку, коли визначено понад 65536 локальних змінних (Попередження: цей код друкує близько 2 ^ 18 рядків тексту, тому будьте готові!):
import dis
exec("""
def f():*     """ + """
    """.join(["X"+str(x)+"=" + str(x) for x in range(65539)]))

f()

print(dis.dis(f))
  • Кілька потоків Python не працюють паралельно (так, ви правильно прочитали — це дійсно так!). Здається інтуїтивно зрозумілим: запусти декілька потоків і дозволь їм виконуватися паралельно, однак, через Global Interpreter Lock в Python, усі ваші потоки будуть виконуватися по черзі. Щоб досягти фактичного розпаралелювання в Python, можливо, вам знадобиться використовувати модуль мультипроцесора Python.
  • Створення зрізів списку з індексами, які виходять за межі, не призведе до помилок:
>>> some_list = [1, 2, 3, 4, 5]
>>> some_list[111:]
[]

Це все, що в мене є на даний час. Якщо ви знаєте ще якісь цікаві приклади, не соромтеся додавати їх у коментарях.

Помітили помилку? Повідомте автору, для цього достатньо виділити текст з помилкою та натиснути Ctrl+Enter
Codeguida 4K
Приєднався: 11 місяців тому
Коментарі (0)

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

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

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