Поиск…


Вступление

Python - это язык, который должен быть понятным и читаемым без каких-либо двусмысленностей и неожиданного поведения. К сожалению, эти цели не достижимы во всех случаях, поэтому Python имеет несколько угловых случаев, где он может делать что-то другое, чем ожидалось.

В этом разделе будут показаны некоторые проблемы, которые могут возникнуть при написании кода Python.

Изменение последовательности, которую вы повторяете

Цикл for выполняет итерацию по последовательности, поэтому изменение этой последовательности внутри цикла может привести к неожиданным результатам (особенно при добавлении или удалении элементов):

alist = [0, 1, 2]
for index, value in enumerate(alist):
    alist.pop(index)
print(alist)
# Out: [1]

Примечание: list.pop() используется для удаления элементов из списка.

Второй элемент не был удален, так как итерация идет по индексам по порядку. Вышеупомянутый цикл повторяется дважды, со следующими результатами:

# Iteration #1
index = 0
alist = [0, 1, 2]
alist.pop(0) # removes '0'

# Iteration #2
index = 1
alist = [1, 2]
alist.pop(1) # removes '2'

# loop terminates, but alist is not empty:
alist = [1]

Эта проблема возникает из-за того, что индексы изменяются, итерации в направлении возрастания индекса. Чтобы избежать этой проблемы, вы можете перебирать петлю в обратном направлении :

alist = [1,2,3,4,5,6,7]
for index, item in reversed(list(enumerate(alist))):
    # delete all even items
    if item % 2 == 0:
        alist.pop(index)
print(alist)
# Out: [1, 3, 5, 7]

Итерацией по циклу, начинающимся с конца, поскольку элементы удаляются (или добавляются), это не влияет на индексы элементов ранее в списке. Таким образом, этот пример будет корректно удалять все элементы, которые даже с alist .


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

alist = [0, 1, 2]
for index, value in enumerate(alist):
    # break to avoid infinite loop:
    if index == 20:     
        break           
    alist.insert(index, 'a')
print(alist)
# Out (abbreviated): ['a', 'a', ..., 'a', 'a',  0,   1,   2]

Без условия break цикл будет вставлять 'a' пока компьютер не исчерпывает память, и программе разрешено продолжить. В такой ситуации обычно предпочтительнее создавать новый список и добавлять элементы в новый список по мере того, как вы просматриваете исходный список.


При использовании цикла for вы не можете изменять элементы списка с помощью переменной-заполнителя :

alist = [1,2,3,4]
for item in alist:
    if item % 2 == 0:
        item = 'even'
print(alist)
# Out: [1,2,3,4]

В приведенном выше примере изменение item ничего не меняет в исходном списке . Вам нужно использовать индекс списка ( alist[2] ), и enumerate() работает хорошо для этого:

alist = [1,2,3,4]
for index, item in enumerate(alist):
    if item % 2 == 0:
        alist[index] = 'even'
print(alist)
# Out: [1, 'even', 3, 'even']

В while цикл может быть лучшим выбором в некоторых случаях:

Если вы собираетесь удалить все элементы в списке:

zlist = [0, 1, 2]
while zlist:
    print(zlist[0])
    zlist.pop(0)
print('After: zlist =', zlist)

# Out: 0
#      1
#      2
# After: zlist = []

Хотя простой сброс zlist приведет к zlist же результату;

zlist = []

Вышеприведенный пример также можно объединить с len() для остановки после определенной точки или для удаления всех элементов, кроме x , в списке:

zlist = [0, 1, 2]
x = 1
while len(zlist) > x:
    print(zlist[0])
    zlist.pop(0)
print('After: zlist =', zlist)

# Out: 0
#      1
# After: zlist = [2]

Или прокручивать список при удалении элементов, удовлетворяющих определенному условию (в этом случае удаление всех четных элементов):

zlist = [1,2,3,4,5]
i = 0
while i < len(zlist):
    if zlist[i] % 2 == 0:
        zlist.pop(i)
    else:
        i += 1
print(zlist)
# Out: [1, 3, 5]

Обратите внимание, что вы не увеличиваете i после удаления элемента. zlist[i] элемент в zlist[i] , индекс следующего элемента уменьшился на единицу, поэтому, проверив zlist[i] с тем же значением для i на следующей итерации, вы будете правильно проверять следующий элемент в списке ,


Непосредственный способ подумать об удалении нежелательных элементов из списка - добавить нужные элементы в новый список . Следующий пример является альтернативой последнего в while , например петли:

zlist = [1,2,3,4,5]

z_temp = []
for item in zlist:
    if item % 2 != 0:
        z_temp.append(item)
zlist = z_temp
print(zlist)
# Out: [1, 3, 5]

Здесь мы собираем желаемые результаты в новый список. Затем мы можем переназначить временный список исходной переменной.

С этой тенденцией мышления вы можете вызывать одну из самых элегантных и мощных функций Python, список понятий , которые устраняют временные списки и расходятся с ранее обсуждавшейся идеологией мувирования списка / индекса.

zlist = [1,2,3,4,5]
[item for item in zlist if item % 2 != 0]
# Out: [1, 3, 5]

Mutable аргумент по умолчанию

def foo(li=[]):
    li.append(1)
    print(li)

foo([2])
# Out: [2, 1]
foo([3])
# Out: [3, 1]

Этот код ведет себя так, как ожидалось, но что, если мы не передадим аргумент?

foo()
# Out: [1] As expected...

foo()
# Out: [1, 1]  Not as expected...

Это объясняется тем, что аргументы по умолчанию и методы вычисляются во время определения, а не времени выполнения. Поэтому у нас только один экземпляр списка li .

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

def foo(li=None):
    if not li:
        li = []
    li.append(1)
    print(li)

foo()
# Out: [1]

foo()
# Out: [1]

В то время как улучшение, и хотя, if not li правильно оценивает значение False , многие другие объекты также работают, например, последовательности нулевой длины. Следующие примеры аргументов могут привести к непредвиденным результатам:

x = []
foo(li=x)
# Out: [1]

foo(li="")
# Out: [1]

foo(li=0) 
# Out: [1]

Идиоматический подход заключается в прямом проверке аргумента против объекта None :

def foo(li=None):
    if li is None:
        li = []
    li.append(1)
    print(li)

foo()
# Out: [1]

Перемножение списков и общие ссылки

Рассмотрим случай создания вложенной структуры списка путем умножения:

li = [[]] * 3
print(li)
# Out: [[], [], []]

На первый взгляд мы думаем, что у нас есть список из 3 разных вложенных списков. Попробуем добавить 1 к первому:

li[0].append(1)
print(li)
# Out: [[1], [1], [1]]

1 добавлен ко всем спискам в li .

Причина в том, что [[]] * 3 не создает list из 3 разных list s. Скорее, он создает list содержащий 3 ссылки на один и тот же объект list . Таким образом, когда мы добавляем к li[0] изменение видимо во всех подэлементах li . Это эквивалентно:

li = []
element = [[]]
li = element + element + element
print(li)
# Out: [[], [], []]
element.append(1)
print(li)
# Out: [[1], [1], [1]]

Это может быть дополнительно подтверждено, если мы печатаем адреса памяти содержащегося list с помощью id :

li = [[]] * 3
print([id(inner_list) for inner_list in li])
# Out: [6830760, 6830760, 6830760]

Решением является создание внутренних списков с помощью цикла:

li = [[] for _ in range(3)]

Вместо того, чтобы создавать один list и затем делать 3 ссылки на него, мы теперь создаем 3 разных отдельных списка. Это, опять же, можно проверить, используя функцию id :

print([id(inner_list) for inner_list in li])
# Out: [6331048, 6331528, 6331488]

Вы также можете это сделать. Это приводит к созданию нового пустого списка в каждом append .

>>> li = []
>>> li.append([])
>>> li.append([])
>>> li.append([])
>>> for k in li: print(id(k))
... 
4315469256
4315564552
4315564808

Не используйте индекс для перебора последовательности.

Не рекомендуется :

for i in range(len(tab)):
    print(tab[i])

Сделайте :

for elem in tab:
    print(elem)

for будет автоматизировать большинство операций итерации для вас.

Используйте перечисление, если вам действительно нужен как индекс, так и элемент .

for i, elem in enumerate(tab):
     print((i, elem))

Будьте осторожны при использовании «==» для проверки True или False

if (var == True):
    # this will execute if var is True or 1, 1.0, 1L

if (var != True):
    # this will execute if var is neither True nor 1

if (var == False):
    # this will execute if var is False or 0 (or 0.0, 0L, 0j)

if (var == None):
    # only execute if var is None

if var:
    # execute if var is a non-empty string/list/dictionary/tuple, non-0, etc

if not var:
    # execute if var is "", {}, [], (), 0, None, etc.

if var is True:
    # only execute if var is boolean True, not 1

if var is False:
    # only execute if var is boolean False, not 0

if var is None:
    # same as var == None

Не проверяйте, можете ли вы, просто сделайте это и обработайте ошибку

Питонисты обычно говорят: «Легче просить прощения, чем разрешения».

Не рекомендуется :

if os.path.isfile(file_path):
    file = open(file_path)
else:
    # do something

Делать:

try:
    file = open(file_path)
except OSError as e:
    # do something

Или даже лучше с Python 2.6+ :

with open(file_path) as file:

Это намного лучше, потому что это гораздо более общий. Вы можете применить try/except почти ничего. Вам не нужно заботиться о том, что делать, чтобы предотвратить это, просто заботьтесь об ошибке, которую вы рискуете.

Не проверяйте тип

Python динамически типизирован, поэтому проверка на тип позволяет вам потерять гибкость. Вместо этого используйте утиную печать , проверяя поведение. Если вы ожидаете строку в функции, используйте str() для преобразования любого объекта в строку. Если вы ожидаете список, используйте list() для преобразования любого итерабельного в список.

Не рекомендуется :

def foo(name):
    if isinstance(name, str):
        print(name.lower())

def bar(listing):
    if isinstance(listing, list):
        listing.extend((1, 2, 3))
        return ", ".join(listing)

Делать:

def foo(name) :
    print(str(name).lower())

def bar(listing) :
    l = list(listing)
    l.extend((1, 2, 3))
    return ", ".join(l)

Используя последний способ, foo примет любой объект. bar будет принимать строки, кортежи, наборы, списки и многое другое. Дешевые сухие.

Не смешивайте пространства и вкладки

Использовать объект в качестве первого родителя

Это сложно, но это укусит вас по мере роста вашей программы. В Python 2.x есть старые и новые классы. Старые, старые, старые. Им не хватает некоторых функций, и они могут иметь неудобное поведение с наследованием. Чтобы быть полезным, любой из ваших классов должен быть «нового стиля». Для этого сделайте его наследуемым от object .

Не рекомендуется :

class Father:
    pass

class Child(Father):
    pass

Делать:

class Father(object):
    pass


class Child(Father):
    pass

В Python 3.x все классы являются новым стилем, поэтому вам не нужно это делать.

Не инициализировать атрибуты класса вне метода init

Люди, приезжающие из других языков, находят это заманчивым, потому что это то, что вы делаете на Java или PHP. Вы пишете имя класса, затем указываете свои атрибуты и даете им значение по умолчанию. Кажется, что это работает на Python, однако это не работает так, как вы думаете. Это будет определять атрибуты класса (статические атрибуты), а затем, когда вы попытаетесь получить атрибут объекта, он даст вам свое значение, если оно не пусто. В этом случае он вернет атрибуты класса. Это подразумевает две большие опасности:

  • Если атрибут класса изменяется, то начальное значение изменяется.

  • Если вы установите изменяемый объект в качестве значения по умолчанию, вы получите тот же объект, общий для экземпляров.

Не нужно (если вы не хотите статического):

class Car(object):
    color = "red"
    wheels = [Wheel(), Wheel(), Wheel(), Wheel()]

Делать :

class Car(object):
    def __init__(self):
        self.color = "red"
        self.wheels = [Wheel(), Wheel(), Wheel(), Wheel()]

Целое и строковое тождество

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

По сути, это может привести к запутанному поведению при сравнении целых тождеств:

>>> -8 is (-7 - 1)
False
>>> -3 is (-2 - 1)
True

и, используя другой пример:

>>> (255 + 1) is (255 + 1)
True
>>> (256 + 1) is (256 + 1)
False

Чего ждать?

Мы можем видеть , что операция идентичности is доходностью True для некоторых целых чисел ( -3 , 256 ) , но не для других ( -8 , 257 ).

Чтобы быть более конкретным, целые числа в диапазоне [-5, 256] внутренне кэшируются во время запуска интерпретатора и создаются только один раз. Таким образом , они идентичны и сравнивая их идентичность с is дают True ; целые числа вне этого диапазона (обычно) создаются «на лету», а их идентификаторы сравниваются с False .

Это обычная ошибка, так как это общий диапазон для тестирования, но достаточно часто код не работает в более позднем этапе (или хуже - производство) без видимых причин после полной работы в разработке.

Решение состоит в том, чтобы всегда сравнивать значения, используя оператор равенства ( == ), а не тождественный ( is ) оператор.


Python также сохраняет ссылки на часто используемые строки , и может привести к аналогичным странному поведению при сравнении идентичности (т.е. используя is ) строк.

>>> 'python' is 'py' + 'thon'
True

Обычно используется строка 'python' , поэтому у Python есть один объект, который использует все ссылки на строку 'python' .

Для необычных строк сравнение идентификатора не выполняется, даже когда строки равны.

>>> 'this is not a common string' is 'this is not' + ' a common string'
False
>>> 'this is not a common string' == 'this is not' + ' a common string'
True

Итак, как и правило для целых чисел, всегда сравнивайте строковые значения с помощью оператора равенства ( == ), а не идентификатора ( is ).

Доступ к атрибутам int литералов

Возможно, вы слышали, что все в Python - это объект, даже литералы. Это означает, например, что 7 - объект, а значит, он имеет атрибуты. Например, одним из этих атрибутов является bit_length . Он возвращает количество бит, необходимое для представления значения, на которое оно вызвано.

x = 7
x.bit_length()
# Out: 3

Увидев вышеприведенный код, вы можете интуитивно подумать, что будет работать 7.bit_length() , но только для того, чтобы узнать, что он вызывает SyntaxError . Зачем? потому что интерпретатор должен различать доступ к атрибуту и ​​плавающее число (например, 7.2 или 7.bit_length() ). Этого не может быть, и поэтому возникает исключение.

Существует несколько способов доступа к атрибутам int литералов:

# parenthesis
(7).bit_length()
# a space
7 .bit_length()

Использование двух точек (например, 7..bit_length() ) не работает в этом случае, потому что это создает литерал с float а float не имеют bit_length() .

Эта проблема не существует при доступе к атрибутам float литералов, поскольку интерпертер «умный» достаточно, чтобы знать, что литерал с float не может содержать два . , например:

7.2.as_integer_ratio()
# Out: (8106479329266893, 1125899906842624)

Цепочка или оператор

При тестировании любого из нескольких сравнений сравнений:

if a == 3 or b == 3 or c == 3:

заманчиво сокращать это до

if a or b or c == 3: # Wrong

Это не верно; оператор or имеет более низкий приоритет, чем == , поэтому выражение будет оцениваться как if (a) or (b) or (c == 3): Правильный способ явно проверяет все условия:

if a == 3 or b == 3 or c == 3:  # Right Way

Альтернативно, встроенная функция any() может использоваться вместо цепочки or операторов:

if any([a == 3, b == 3, c == 3]): # Right

Или, чтобы сделать его более эффективным:

if any(x == 3 for x in (a, b, c)): # Right

Или, чтобы сделать его короче:

if 3 in (a, b, c): # Right

Здесь мы используем оператор in для проверки, присутствует ли значение в кортеже, содержащем значения, которые мы хотим сравнить.

Аналогично, неправильно писать

if a == 1 or 2 or 3:

который должен быть записан как

if a in (1, 2, 3):

sys.argv [0] - это имя исполняемого файла

Первый элемент sys.argv[0] - это имя исполняемого файла python. Остальные элементы являются аргументами сценария.

# script.py
import sys

print(sys.argv[0])
print(sys.argv)

$ python script.py
=> script.py
=> ['script.py']

$ python script.py fizz
=> script.py
=> ['script.py', 'fizz']

$ python script.py fizz buzz
=> script.py
=> ['script.py', 'fizz', 'buzz']

Словари неупорядочены

Вы можете ожидать, что словарь Python будет отсортирован по таким ключам, как, например, C ++ std::map , но это не так:

myDict = {'first': 1, 'second': 2, 'third': 3}
print(myDict)
# Out: {'first': 1, 'second': 2, 'third': 3}

print([k for k in myDict])
# Out: ['second', 'third', 'first']

Python не имеет встроенного класса, который автоматически сортирует свои элементы по ключу.

Однако, если сортировка не является обязательной, и вы просто хотите, чтобы ваш словарь запоминал порядок вставки его пар ключ / значение, вы можете использовать collections.OrderedDict :

from collections import OrderedDict

oDict = OrderedDict([('first', 1), ('second', 2), ('third', 3)])

print([k for k in oDict])
# Out: ['first', 'second', 'third']

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

Реализация словарей была изменена в Python 3.6 для улучшения потребления памяти. Побочным эффектом этой новой реализации является то, что она также сохраняет порядок аргументов ключевого слова, переданных функции:

Python 3.x 3.6
def func(**kw): print(kw.keys())

func(a=1, b=2, c=3, d=4, e=5) 
dict_keys(['a', 'b', 'c', 'd', 'e']) # expected order 

Предостережение : остерегайтесь того, что « сохраняющий порядок аспект этой новой реализации рассматривается как деталь реализации, и на него нельзя положиться » , поскольку это может измениться в будущем.

Глобальный блокиратор перехвата (GIL) и блокирующие потоки

Много написано о GIL Python . Иногда это может вызвать путаницу при работе с многопоточными (не путать с многопроцессорными) приложениями.

Вот пример:

import math
from threading import Thread

def calc_fact(num):
    math.factorial(num)

num = 600000
t = Thread(target=calc_fact, daemon=True, args=[num])
print("About to calculate: {}!".format(num))
t.start()
print("Calculating...")
t.join()
print("Calculated")

Вы ожидали увидеть, что Calculating... распечатано сразу после начала потока, мы хотели, чтобы вычисления произошли в новом потоке! Но на самом деле, вы видите, что он печатается после завершения расчета. Это связано с тем, что новый поток использует функцию C ( math.factorial ), которая будет блокировать GIL во время ее выполнения.

Есть пара способов обойти это. Первый - реализовать вашу факториальную функцию в родном Python. Это позволит основному потоку захватить управление, находясь внутри вашего цикла. Недостатком является то, что это решение будет намного медленнее, поскольку мы больше не используем функцию C.

def calc_fact(num):
    """ A slow version of factorial in native Python """
    res = 1
    while num >= 1:
        res = res * num
        num -= 1
    return res

Вы также можете sleep течение определенного периода времени перед началом выполнения. Примечание: это фактически не позволит вашей программе прервать вычисление, происходящее внутри функции C, но это позволит вашему основному потоку продолжить работу после появления, что вы можете ожидать.

def calc_fact(num):
    sleep(0.001)
    math.factorial(num)

Переменная утечка в списках и для циклов

Рассмотрим следующее понимание списка

Python 2.x 2.7
i = 0
a = [i for i in range(3)]
print(i) # Outputs 2

Это происходит только в Python 2 из-за того, что понимание списка «утечки» переменной управления циклом в окружающую область ( источник ). Такое поведение может привести к труднодоступным ошибкам и было исправлено в Python 3 .

Python 3.x 3.0
i = 0
a = [i for i in range(3)]
print(i) # Outputs 0

Аналогично, для циклов нет частной области для их переменной итерации

i = 0
for i in range(3):
    pass
print(i) # Outputs 2

Этот тип поведения происходит как в Python 2, так и в Python 3.

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

Многократный возврат

Функция xyz возвращает два значения a и b:

def xyz():
  return a, b

Код, вызывающий xyz, сохраняет результат в одну переменную, предполагая, что xyz возвращает только одно значение:

t = xyz()

Значение t на самом деле является кортежем (a, b), поэтому любое действие по t предполагающее, что он не является кортежем, может сильно затухать в коде с неожиданной ошибкой в кортежах.

TypeError: тип кортежа не определяет ... метод

Исправить будет:

a, b = xyz()

Начинающим будет сложно найти причину этого сообщения, только прочитав сообщение об ошибке кортежа!

Питонические ключи JSON

my_var = 'bla';
api_key = 'key';
...lots of code here...
params = {"language": "en", my_var: api_key}

Если вы используете JavaScript, оценка переменных в словарях Python будет не такой, как вы ожидаете. Этот оператор в JavaScript приведет к объекту params следующим образом:

{
    "language": "en",
    "my_var": "key"
}

В Python, однако, это приведет к следующему словарю:

{
    "language": "en",
    "bla": "key"
}

my_var оценивается и его значение используется как ключ.



Modified text is an extract of the original Stack Overflow Documentation
Лицензировано согласно CC BY-SA 3.0
Не связан с Stack Overflow