?

Log in

No account? Create an account

Previous Entry | Next Entry

Jun. 21st, 2012

Ничего, если я тут поспамлю про Питон? А то он заба-авный. И особенно мне в нём забавны некоторые ловушки для начинающих, особенно тех, кто использовал до того языки типа С и С++. Классическая, например -- в том, как надо и не надо задавать значения параметров по умолчанию.

Сидишь ты, скажем, и пишешь код для добавления элемента в список. (Этот пример я когда-то в Code Like a Pythonista прочитала.) Хочешь, чтобы если список в функцию не передан, он инициализировался бы пустым, элемент бы добавлялся и список бы возвращался. Если список передан, добавляешь элемент в него и возвращаешь. Например:

def append_item_wrong(my_item, my_list=[]):
    my_list.append(my_item)
    return my_list

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

new_list = append_item_wrong(1)
another_list = append_item_wrong(2)
print another_list

И что получаешь при распечатке вместо ожидаемого [2]? А вот что:

[1, 2]

Таки ой. Потому что в результате это не разные списки вовсе. Можно проверить:

print (new_list is another_list)

вернёт True.


Дело тут в том, что значение по умолчанию параметру функции присваевается во время определения функции, а не во время её исполнения. Cама функция -- тоже объект, создаваемый в момент её определения, и её дефолтные параметры инициализируются тогда же. Потом, если они mutable (а list -- mutable), они могут изменяться. (Это на stackoverflow тут хорошо описано.) Кстати, можно написать

print append_item_wrong.func_defaults

и посмотреть на дефолтные параметры после выполнения кода выше:

([1, 2],)

Чтобы таких сюрпризов не получалось, значения по умолчанию параметрам функции, являющимся изменяемыми объектами, присваивают прямо в теле функции. Например:

def append_item(my_item, my_list=None):
    if my_list is None:
        my_list = []
    my_list.append(my_item)
    return my_list

new_list_1 = append_item(1)
another_list_1 = append_item(2)
print another_list_1
print (new_list_1 is another_list_1)

Этот код выдаст ожидаемые результаты:

[2]
False

Шикарная ловушка.

Comments

( 26 comments — Leave a comment )
alexakarpov
Jun. 22nd, 2012 04:35 am (UTC)
приятно почитать )
be_unafraid
Jun. 22nd, 2012 04:39 am (UTC)
йа рада, что ты получаешь удовольствие!
alexakarpov
Jun. 22nd, 2012 05:00 am (UTC)
=)
layshu
Jun. 22nd, 2012 05:47 am (UTC)
Да, хорошая штука :) Спасибо, настроение на ночь подняло :)
morfizm
Jun. 22nd, 2012 06:41 am (UTC)
Офигенно, welcome to the Python world.

Присваивать None и потом конвертировать в [] попахивает костылями, главным образом, потому, что ты сохраняешь концепт мутабельного параметра (рассмотри случай передачи какого-то списка, а не использования умолчательного значения). Для питонщиков это противоестественно.

Они либо напишут:
def append_item(my_item, my_list):
   l = list(my_list)
   l.append(item)
   return l


либо (лучше):
def append_item(my_item, my_list):
    return my_list + [my_item]


либо (ещё лучше) - засабклассят список или агрегируют его, что бы перекрыть функциональность append:

class my_list(list):
    def append_item(self, my_item):
        super(my_list, self).append(my_item)

Вариант:

class my_list():
    def __init__(self, my_list):
        self.inner_list = my_list
    def append_item(self, my_item):
        self.inner_list.append(my_item)


В любом случае, естественное ожидание от параметров функции - это то, что они read-only, иначе это надо жёстко оговаривать в комментах, и потом отстаивать это извращение на code review,
be_unafraid
Jun. 22nd, 2012 10:30 pm (UTC)
Вот кстати, меня концепт мутабельного параметра как таковой не беспокоит. Кроме того, что это counterintuitive и error-prone, если привык к другим языкам.

Но слушай, как могут приведённые тобой варианты с копированием каждый раз всего списка быть естественными для питонщиков или вообще хоть кого?
morfizm
Jun. 25th, 2012 09:36 am (UTC)
А вариант со склеиванием строк в Паскале тебе кажется естественным хоть для кого-то?

Подумай: a = a + c.
В питоне это аллокация (!) новой строки, копирование с в её хвост, и последующий garbage collection прежнего a.

А если подумать, то аллокация - это операция-монстр. Это просмотр какого-то списка свободных блоков, или ещё чего похлеще, потом изменение нескольких элементов этого списка, и т.п. Про garbage collection я вежливо промолчу :)

Конечно, питонщики постараются писать a += c (см. коммент ниже), но в целом, концепция a = a + c имеет право на существование. Для начинающих это очень легко и интуитивно, они не понимают, зачем надо a += c. Кроме того, в большинстве случаев performance requirements твоего приложения на Питоне такие, что как ты будешь склеивать строки - совершенно без разницы. Поэтому рулит лишь читаемость.


Edited at 2012-06-25 09:47 am (UTC)
be_unafraid
Jun. 25th, 2012 10:04 pm (UTC)
А почему ты вдруг говоришь о склеивании строк? :) Как эффективно склеивать строки (или чем их заменить в зависимости от задачи) -- это отдельный вопрос. И не повод лишний раз копировать списки :)
morfizm
Jun. 25th, 2012 10:49 pm (UTC)
Я не вижу принципиальной разницы между копированием строк и копированием списков. Считай, что списки - это мутабельные строки, у которых символы это 4-байтные адреса :)
be_unafraid
Jun. 25th, 2012 10:51 pm (UTC)
Ну так "мутабельные" в данном случае важная разница!
morfizm
Jun. 25th, 2012 09:42 am (UTC)
Про список свободных блоков - ещё следует добавить, что многие имплементации такого списка хранят элементы в самих свободных блоках, что делает просмотр этого списка немеренно дорогущей операцией - ты прыгаешь по 4-8 байт через огромные куски памяти, каждый раз мимо всех процессорных кэшей :) Каждый такой прыжок - это как копирование всей твоей строки по времени :)
morfizm
Jun. 25th, 2012 09:46 am (UTC)
Только что проверил: конечно же, a += b это просто syntactial sugar over a = a + b, потому что строки не мутабельны.

>>> a = 'be_'
>>> b = 'unafraid'
>>> id(a)
33035408L
>>> id(b)
33026528L
>>> a += b
>>> a
'be_unafraid'
>>> id(a)
33026624L


Edited at 2012-06-25 09:48 am (UTC)
be_unafraid
Jun. 25th, 2012 10:00 pm (UTC)
Угу :)
morfizm
Jun. 25th, 2012 09:49 am (UTC)
Кстати, у меня есть подозрение, что паттерн вроде a = "%s%s%s%s" % (b, c, d, e) будет работать быстрее, чем a = b + c + d + e, при достаточном количестве этих переменных, потому что первый вариант позволит избежать кучи промежуточных аллокаций.
morfizm
Jun. 25th, 2012 09:58 am (UTC)
Да, быстрее. Но не при маленьком количестве :)

import timeit

t = timeit.Timer(stmt="""
    b = 'test1'
    c = 'test2'
    d = 'test3'
    e = 'test4'
    a = (b+c+d+e) + (b+c+d+e) + (b+c+d+e) + (b+c+d+e) + (b+c+d+e)
""")
print "%.2f usec/pass" % (1000000 * t.timeit(number=100000)/100000)

t = timeit.Timer(stmt="""
    b = 'test1'
    c = 'test2'
    d = 'test3'
    e = 'test4'
    a = "%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s" % (b,c,d,e, b,c,d,e, b,c,d,e, b,c,d,e, b,c,d,e)
""")
print "%.2f usec/pass" % (1000000 * t.timeit(number=100000)/100000)


-----
prints:
1.85 usec/pass
1.69 usec/pass
thorion
Jun. 22nd, 2012 12:50 pm (UTC)
Дааа. Красивая ловушка. В общем, параметры по умолчанию в Питоне как Ford T - они могут быть самыми разными, при условии, что они None.
elmitt
Jun. 22nd, 2012 11:52 pm (UTC)
Или при условии, что ты их не изменяешь :-) Очень удобно так строчки хранить.
dennyrolling
Jun. 23rd, 2012 06:15 pm (UTC)
я не понимаю почему люди накинулись на статическую переменную с возможностью временной замены из места вызова. по моему это прекрасно!
be_unafraid
Jun. 23rd, 2012 06:42 pm (UTC)
а опиши применение :)
dennyrolling
Jun. 23rd, 2012 07:13 pm (UTC)
как у любой статической переменной - когда лень заводить синглтон для контекста, только если ты ее можешь подменять то синглтоном просто так не обойдешься.

например:
def add_and_avg(n, list=[]):
list.append(n)
return float(sum(list)) / len(list)

теперь мы имеем без дупликации кода и без дополнительного класса функцию add_and_avg(n) для центрального добавления\вычисления и add_and_avg(n, l) которая работает над l. поди еще напиши по-другому ;)
be_unafraid
Jun. 24th, 2012 04:36 am (UTC)
о, вполне! :)
thorion
Jun. 25th, 2012 03:37 am (UTC)
Очень просто. Потому что ожидается отнюдь не статическая переменная. И мне вовсе неочевидно преимущество выбранной в Питоне конструкции перед классической.
dennyrolling
Jun. 25th, 2012 07:09 am (UTC)
ну еще скажите что в
h = {}
fill(h)
for i in h.keys():
# do stuff
не должен создаваться временный список ключей (true story!).
thorion
Jun. 25th, 2012 12:02 pm (UTC)
А при чем здесь это?
dennyrolling
Jun. 25th, 2012 04:52 pm (UTC)
Потому что ожидается отнюдь не копия списка ключей. И мне вовсе неочевидно преимущество выбранной в Питоне конструкции перед классической.
thorion
Jun. 25th, 2012 06:01 pm (UTC)
Стоп-стоп-стоп
Я так понимаю, что если список ключей внутри #do stuff не изменяется, то создается копия списка или нет - влияет только на производительность, но никак не на результат. Если же изменения происходят, то результат согласно спецификации не определен, и соответственно garbage in - garbage out. Или я что то не учел?

В случае же выбора статической переменной вместо параметра по умолчанию мне непонятна причина такого выбора потому что:
1. Необходимость использовать параметр по умолчанию как я считаю исходя из своего опыта возникает чаще, чем необходимость в статической переменной. Следовательно, было бы логично минимизировать количество кода для более часто встречающегося случая.
2. Большинство людей, программирующих на Питоне, до этого использовали какой либо другой язык программирования, в котором данная синтаксическая конструкция несет иную смысловую нагрузку. Что, соответственно, создает потенциал для появления ошибок.


( 26 comments — Leave a comment )