Я делаю некоторую критическую по отношению к производительности работу Python и хочу создать функцию, которая удаляет несколько элементов из списка, если они соответствуют определенным критериям. Я не создал бы копий списка, потому что это заполнено большим количеством действительно больших объектов.
Функциональность я хочу реализовать:
def listCleanup(listOfElements):
i = 0
for element in listOfElements:
if(element.meetsCriteria()):
del(listOfElements[i])
i += 1
return listOfElements
myList = range(10000)
myList = listCleanup(listOfElements)
Я не знаком с работами низкого уровня Python. myList передает значением или ссылкой?
Как я могу сделать это быстрее?
Действительно ли возможно так или иначе расширить класс списка и реализовать listCleanup () в этом?
myList = range(10000)
myList.listCleanup()
Спасибо-
Jonathan
Python передает все одинаково, но вызов его «по значению» или «по ссылке» не очистит все, поскольку Семантика Python отличается от языков, к которым обычно применяются эти термины. Если бы я описал это, я бы сказал, что вся передача осуществляется по значению, а значение является ссылкой на объект. (Вот почему я не хотел этого говорить!)
Если вы хотите отфильтровать некоторые элементы из списка, вы создаете новый список
foo = range(100000)
new_foo = []
for item in foo:
if item % 3 != 0: # Things divisble by 3 don't get through
new_foo.append(item)
или, используя синтаксис понимания списка
new_foo = [item for item in foo if item % 3 != 0]
, Python не будет скопируйте объекты в список, но оба foo
и new_foo
будут ссылаться на одни и те же объекты.(Python никогда не копирует объекты неявно.)
Вы предположили, что у вас есть проблемы с производительностью по поводу этой операции. Использование повторяющихся операторов del
из старого списка приведет не к тому, что код будет менее идиоматическим и более запутанным в работе, а приведет к квадратичной производительности, поскольку весь список должен каждый раз перетасовываться.
Для повышения производительности:
Запустите его. Вы не сможете понять, какова ваша производительность, если у вас нет работающего кода. Это также скажет вам, нужно ли вам оптимизировать скорость или пространство; вы упоминаете в своем коде опасения по поводу обоих, но часто оптимизация предполагает получение одного за счет другого.
Профиль. Вы можете использовать инструменты stdlib для повышения производительности во времени. Существуют различные сторонние профилировщики памяти, которые могут быть полезны, но работать с ними не так приятно.
Мера. Время или перепрофилирование памяти, когда вы вносите изменение, чтобы увидеть, дает ли изменение улучшение, и если да, то что это за улучшение.
Чтобы сделать ваш код более чувствительным к памяти, вам часто требуется смена парадигмы в том, как вы храните свои данные, а не микрооптимизации, такие как отказ от создания второго списка для фильтрации. (На самом деле то же самое верно и для времени: переход на лучший алгоритм почти всегда дает лучшее ускорение. Однако сложнее сделать обобщения об оптимизации скорости).
Некоторые общие изменения парадигмы для оптимизации потребления памяти в Python включают
Использование генераторов.Генераторы - это ленивые итерации: они не загружают в память сразу весь список, они на лету выясняют, что им делать дальше. Для использования генераторов приведенные выше фрагменты будут выглядеть так:
foo = xrange (100000) # Как и генераторы, xrange ленив
def filter_divisible_by_three (iterable):
для элемента в foo: {{1 }} если элемент% 3! = 0:
yield item
new_foo = filter_divisible_by_three (foo)
или, используя синтаксис выражения генератора,
new_foo = (item for item in foo if item% 3! = 0)
Использование numpy
для однородных последовательностей, особенно тех, которые являются числовой математикой. Это также может ускорить код, выполняющий множество векторных операций.
Хранение данных на диске, например в базе данных.
изменять структуру данных во время итерации по ней - все равно что выстрелить себе в ногу... итерация не работает. с таким же успехом можно последовать совету других и просто создать новый список:
myList = [element for element in listOfElements if not element.meetsCriteria()]
старый список - если на него нет других ссылок - будет деаллоцирован, а память восстановлена. еще лучше даже не создавать копию списка. измените вышеприведенное выражение на выражение-генератор для более удобной для памяти версии:
myList = (element for element in listOfElements if not element.meetsCriteria())
весь доступ к объектам в Python осуществляется по ссылке. Объекты создаются, а переменные являются просто ссылками на эти объекты. Однако, если кто-то захочет задать пуристский вопрос: "Какой тип семантики вызова использует Python, вызов по ссылке или вызов по значению?", то ответ будет: "Ни то, ни другое... и то, и другое". Причина в том, что соглашения о вызове менее важны для Python, чем тип объекта.
если объект является изменяемым, его можно изменить независимо от того, в какой области видимости вы находитесь... пока у вас есть действительная ссылка на объект, объект можно изменить. если объект является неизменяемым, то этот объект нельзя изменить независимо от того, где вы находитесь и какая у вас ссылка.
Удаление элементов списка на месте возможно, но не при переходе вперед по списку. Ваш код просто не работает - поскольку список уменьшается, вы можете пропустить исследуемые элементы. Вам нужно идти назад, чтобы сокращающаяся часть была позади вас, с довольно ужасным кодом. Прежде чем я покажу вам это, есть несколько предварительных соображений:
Во-первых, как этот мусор попал в список? Профилактика лучше, чем лечение.
Во-вторых, сколько элементов в списке, и какой процент из них, вероятно, нуждается в удалении? Чем выше процент, тем больше вероятность того, что лучше создать новый список.
Хорошо, если вы все еще хотите сделать это на месте, подумайте об этом:
def list_cleanup_fail(alist, is_bad):
i = 0
for element in alist:
print "i=%d alist=%r alist[i]=%d element=%d" % (i, alist, alist[i], element)
if is_bad(element):
del alist[i]
i += 1
def list_cleanup_ok(alist, is_bad):
for i in xrange(len(alist) - 1, -1, -1):
print "i=%d alist=%r alist[i]=%d" % (i, alist, alist[i])
if is_bad(alist[i]):
del alist[i]
def is_not_mult_of_3(x):
return x % 3 != 0
for func in (list_cleanup_fail, list_cleanup_ok):
print
print func.__name__
mylist = range(11)
func(mylist, is_not_mult_of_3)
print "result", mylist
и вот вывод:
list_cleanup_fail
i=0 alist=[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10] alist[i]=0 element=0
i=1 alist=[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10] alist[i]=1 element=1
i=2 alist=[0, 2, 3, 4, 5, 6, 7, 8, 9, 10] alist[i]=3 element=3
i=3 alist=[0, 2, 3, 4, 5, 6, 7, 8, 9, 10] alist[i]=4 element=4
i=4 alist=[0, 2, 3, 5, 6, 7, 8, 9, 10] alist[i]=6 element=6
i=5 alist=[0, 2, 3, 5, 6, 7, 8, 9, 10] alist[i]=7 element=7
i=6 alist=[0, 2, 3, 5, 6, 8, 9, 10] alist[i]=9 element=9
i=7 alist=[0, 2, 3, 5, 6, 8, 9, 10] alist[i]=10 element=10
result [0, 2, 3, 5, 6, 8, 9]
list_cleanup_ok
i=10 alist=[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10] alist[i]=10
i=9 alist=[0, 1, 2, 3, 4, 5, 6, 7, 8, 9] alist[i]=9
i=8 alist=[0, 1, 2, 3, 4, 5, 6, 7, 8, 9] alist[i]=8
i=7 alist=[0, 1, 2, 3, 4, 5, 6, 7, 9] alist[i]=7
i=6 alist=[0, 1, 2, 3, 4, 5, 6, 9] alist[i]=6
i=5 alist=[0, 1, 2, 3, 4, 5, 6, 9] alist[i]=5
i=4 alist=[0, 1, 2, 3, 4, 6, 9] alist[i]=4
i=3 alist=[0, 1, 2, 3, 6, 9] alist[i]=3
i=2 alist=[0, 1, 2, 3, 6, 9] alist[i]=2
i=1 alist=[0, 1, 3, 6, 9] alist[i]=1
i=0 alist=[0, 3, 6, 9] alist[i]=0
result [0, 3, 6, 9]
Для ясности:
def listCleanup(listOfElements):
i = 0
for element in listOfElements:
if(element.meetsCriteria()):
del(listOfElements[i])
i += 1
return listOfElements
myList = range(10000)
myList = listCleanup(listOfElements)
совпадает с
def listCleanup(listOfElements):
i = 0
for element in listOfElements:
if(element.meetsCriteria()):
del(listOfElements[i])
i += 1
myList = range(10000)
listCleanup(listOfElements)
?
В Python списки всегда передаются по ссылке.
Размер объектов в списке не влияет на производительность списков, потому что в списках хранятся только ссылки на объекты. Однако количество элементов в списке действительно влияет на производительность некоторых операций, таких как удаление элемента, который равен O (n).
Как написано, listCleanup - это наихудший вариант O (n ** 2), поскольку у вас есть операция O (n) del внутри цикла, которая потенциально сама является O (n).
Если порядок элементов не имеет значения, вы можете использовать встроенный набор типов
вместо списка. Набор
имеет O (1) удалений и вставок. Однако вам нужно будет убедиться, что ваши объекты неизменяемы и хешируемы.
В противном случае вам лучше воссоздать список. Это O (n), и ваш алгоритм должен быть как минимум O (n), поскольку вам нужно проверить каждый элемент. Вы можете отфильтровать список в одну строку следующим образом:
listOfElements[:] = [el for el in listOfElements if el.MeetsCriteria()]
Похоже на преждевременную оптимизацию. Вам следует попытаться лучше понять, как работает python, прежде чем пытаться оптимизировать.
В данном конкретном случае вам не нужно беспокоиться о размере объекта. Копирование списка с использованием list comprehension или slice будет выполнять только поверхностное копирование (копирование ссылок на объекты, даже если этот термин не очень хорошо применим к python). Но количество элементов в списке может иметь значение, потому что del - это O(n). Могут быть и другие решения, например, заменить элемент на None или обычный объект Null, или использовать другую структуру данных, например, набор или словарь, где стоимость удаления элемента намного ниже.
Я не думаю, что кто-то упоминал о реальном использовании фильтра. Поскольку многие ответы исходили от уважаемых людей, я уверен, что я единственный, кто что-то упускает. Не мог бы кто-нибудь объяснить, что не так с этим:
new_list = filter(lambda o: o.meetsCriteria(), myList)