Странности языка Python, которые могут вас укусить

У языка Python, как и у любого другого, есть совершенно непонятные моменты, которые неплохо бы разобрать «по кирпичикам». Этим и займемся.

языка Python

Хорошим способом получить максимальную отдачу от приведенных здесь примеров будет их прочтение в хронологическом порядке:

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

  1. Проверьте, совпадает ли он с тем, что вы ожидали.
  2. Попробуйте подумать, что именно стало причиной такого результата?
  • Если не смогли, прочтите объяснение.
  • Если смогли, скажите себе, что вы молодец, и переходите следующему примеру.

P.S. Вы также можете воспользоваться командной строкой, установив wtfpython:

$ npm install -g wtfpython

Теперь просто запустите wtfpython в командной строке, которая откроет эту коллекцию в выбранном $PAGER.

Примеры странностей языка Python

Ну, что-то сомнительное...

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

Для языка Python нет невозможного, но... Разве здесь не должно быть 100?

Примечание. Если вы не можете воспроизвести это, попробуйте запустить файл mixed_tabs_and_spaces.py через оболочку.

Объяснение:

  • Не смешивайте табы и отступы! Символ, предшествующий return является «табом», а в другом месте код имеет отступ в «4 пробела».
  • Так Python обрабатывает табы: табы заменяются на один-восемь пробелов.
  • Таким образом, «таб» в последней строке заменяется на восемь пробелов, что создает цикл.
  • 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? «Python» уничтожил «JavaScript»?

Объяснение:

5 (тип int) неявно преобразован в 5.0 (тип float) перед вычислением хеша Python.

>>> hash(5) == hash(5.0)
True

Этот ответ из StackOverflow прекрасно объясняет причины.

Изменение словаря во время итерации по нему

x = {0: None}

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

Выход:

0
1
2
3
4
5
6
7

Объяснение:

  • Итерация по словарю, который вы редактируете, не поддерживается.
  • Эта штука срабатывает только восемь раз, ведь именно это та точка, в которой словарь должен бы изменить размер, чтобы удержать больше ключей (стандартно есть только восемь записей, поэтому меняйте размер, если нужно). Это фактически деталь реализации.
  • См. ответ 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 для языка Python:

  • 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

Объяснение:

Интерпретатор просто меняет поведение обратных косых черт, поэтому они проходят через себя и следующий символ. Вот почему обратная косая черта не работает в конце строки.

Да, он существует!

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 после цикла выполняется только тогда, когда нет явного прерывания после всех итераций.
  • else после блока try также называется «предложением завершения», поскольку достижение else в try означает, что блок try действительно успешно завершен.

is is not what it 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 и ==:

  • is используется, если оба операнда относятся к одному и тому же объекту (то есть он проверяет соответствие совпадений операндов).
  • == сравнивает значения двух операндов.

То есть is для ссылочного равенства и == для равенства значений. Пример, чтобы прояснить ситуацию:

>>> [] == []
True
>>> [] is [] # Это два пустых списка в двух разных ячейках памяти.
False

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]

Даже когда значения x были разными на каждой итерации до добавления 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]

Остерегайтесь измененных аргументов по умолчанию!

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__ #This will show the default argument values for the function
([],)
>>> 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 = 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, но ранее не была инициализирована в той же области, которая вызывает ошибку.
  • Дополнительно прочтите это краткое руководство.
  • Чтобы изменить внешнюю переменную области a в another_func, используйте глобальное ключевое слово:
def another_func()
    global a
    a += 1
    return a

Выход:

>>> another_func()
2

Return return everywhere!

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

Выход:

>>> some_func()
'from_finally'

Объяснение:

  • Когда в try-блоке «try ... finally» выполняется оператор return, break или continue, finally также выполняется «на выходе».
  • Возвращаемое значение функции определяется последним выполненным оператором return. Поскольку finally выполняется всегда, оператор return, выполняемый в finally, всегда будет последним.

МЕРОПРИЯТИЯ

Комментарии

ВАКАНСИИ

Добавить вакансию
Java Программист
по итогам собеседования
Middle Golang developer
Москва, от 150000 RUB до 200000 RUB
PHP программист
Москва, от 120000 RUB до 220000 RUB

ЛУЧШИЕ СТАТЬИ ПО ТЕМЕ