LINUX.ORG.RU

list comprehensions

 , ,


1

1

В очередной раз при правке кода на питоне у меня пригорело от list comprehensions.

А вот что пишут настоящие живые люди, которых никто не заставляет под дулом пистолета:

I find the list comprehension much clearer than filter+lambda

Или:

Personally I find list comprehensions easier to read. It is more explicit what is happening from the expression [i for i in list if i.attribute == value] as all the behaviour is on the surface not inside the filter function.

Ну давайте посмотрим, как этот much clearer way выглядит in the wild. Как-то так:

    def getSupportedTrackers(self):
        trackers = self.getTrackers()

        if not self.site.connection_server.tor_manager.enabled:
            trackers = [tracker for tracker in trackers if ".onion" not in tracker]

        trackers = [tracker for tracker in trackers if self.getAddressParts(tracker)]  # Remove trackers with unknown address

        if "ipv6" not in self.site.connection_server.supported_ip_types:
            trackers = [tracker for tracker in trackers if helper.getIpType(self.getAddressParts(tracker)["ip"]) != "ipv6"]

        return trackers

Просто сплошной [blabla for blabla in blablas if ...blabla...].

Просто в начале каждой такой строки ты должен мысленно стирать кусок [tracker for tracker in trackers if и читать, что же там дальше. И как писал Роберт Мартин в «Чистом коде», любые конструкции, которые принуждают читателя тренироваться пропускать себя мимо глаз, являются источником скрытых ошибок. Пропустив 500 раз мимо глаз типовой фрагмент кода, на 501-й раз вы пропускаете ПОЧТИ такой же фрагмент, в котором содержится ошибка. И в силу одинаковой натренированности рефлексов у всех разработчиков продукта, эта ошибка может оставаться незамеченной годами.

Давайте посмотрим, как этот же код можно преписать на лямбдах на руби:

    def getSupportedTrackers():
        trackers = @getTrackers()

        if not @site.connection_server.tor_manager.enabled
            trackers = trackers.filter {|tracker| not tracker.include? ".onion"}
        end

        trackers = trackers.filter {|tracker| @getAddressParts(tracker)}

        if not @site.connection_server.supported_ip_types.include? "ipv6"
            trackers = trackers.filter {|tracker| helper.getIpType(@getAddressParts(tracker)["ip"]) != "ipv6"}
        end

        return trackers
    end

Уже стало лучше за счёт уменьшения количества бойлерплейта, который приходится пропускать мимо. Но 3 вызова trackers.filter подряд и два идентичных вызова getAddressParts говорят нам, что этот код надо переписать.

Заметьте, что необходимость рефакторинга для устранения дублирования не была очевидна в коде с list comprehensions, потому что они за своей многословностью и нечитабельным синтаксисом скрывают суть происходящего.

Убираем дублирование:

    def getSupportedTrackers()
        tor_enabled = @site.connection_server.tor_manager.enabled
        ipv6_enabled = @site.connection_server.supported_ip_types.include? "ipv6"

        trackers = @getTrackers()

        trackers = trackers.filter {|tracker|
            if (not tor_enabled) and (tracker.include? ".onion")
                next false
            end

            address_parts = @getAddressParts(tracker)
            if not address_parts
                next false
            end

            if (not ipv6_enabled) and (helper.getIpType(address_parts["ip"]) == "ipv6")
                next false
            end

            next true
        }

        return trackers
    end

Этот код хотя и выглядит не так компактно при взгляде на экран издалека, на самом деле проще в чтении и в поддержке. Здесь нет дублирования, которое заставляет читателя многократно сверять строки, чтобы убедиться, что разработчик имел в виду именно то, что увидел читатель. А каждая строка выражает свою мысль без лишних бессмысленных слов, которые нужно отфильтровывать глазами.

P.S. Или для любителей длинных однострочников:

    def getSupportedTrackers()
        tor_enabled = @site.connection_server.tor_manager.enabled
        ipv6_enabled = @site.connection_server.supported_ip_types.include? "ipv6"

        trackers = @getTrackers()

        trackers = trackers.filter {|tracker|
            next false if (not tor_enabled) and (tracker.include? ".onion")

            address_parts = @getAddressParts(tracker)
            next false if not address_parts

            next false if (not ipv6_enabled) and (helper.getIpType(address_parts["ip"]) == "ipv6")

            next true
        }

        return trackers
    end

Но этот вариант по моему мнению хуже.

Перемещено leave из talks

★★

Последнее исправление: wandrien (всего исправлений: 1)

Я не рубист, но почему бы не сделать что-то вроде такого:

def getSupportedTrackers()
  tor_enabled = @site.connection_server.tor_manager.enabled
  ipv6_enabled = @site.connection_server.supported_ip_types.include? "ipv6"

  return @getTrackers()
    .filter {|tracker| (tor_enabled) or (tracker.include? ".onion")}
    .filter {|tracker| not @getAddressParts(tracker)}
    .filter {|tracker| (ipv6_enabled) or (helper.getIpType(@getAddressParts(tracker)["ip"]) == "ipv6")}
end

или так:

def getSupportedTrackers()
  tor_enabled = @site.connection_server.tor_manager.enabled
  ipv6_enabled = @site.connection_server.supported_ip_types.include? "ipv6"

  return @getTrackers()
    .filter {|tracker| (tor_enabled) or (tracker.include? ".onion")}
    .map {|tracker| @getAddressParts(tracker)}
    .filter {|addressParts| not addressParts}
    .filter {|addressPrts| (ipv6_enabled) or (helper.getIpType(addressParts["ip"]) == "ipv6")}
end
Int64 ★★★
()
Последнее исправление: Int64 (всего исправлений: 3)
Ответ на: комментарий от Int64

По моему опыту, это write-only код. Впрочем, в этом случае имеет право на существование.

Но в особо запущенных случаях превращается в цепочки «ехал map через filter…» на весь экран.

wandrien ★★
() автор топика
Ответ на: комментарий от Int64

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

Я писал такие цепочки для коллбеков промисов в JS, от бедности языка. Но добровольно такое писать вряд ли стоит.

wandrien ★★
() автор топика

У тебя пример «как не надо» написан на Python, а все примеры «как надо» написаны на Ruby. Весь пост можно выразить в одной фразе. Тебе Ruby нравится больше, чем Python.

Ну так в чём проблема? Пиши на Ruby.

i-rinat ★★★★★
()

Не совсем понятно, зачем ты принёс Ruby. Ведь в питоне отродясь есть и map, и filter, и functools с itertools

Crocodoom ★★★★★
()
Ответ на: комментарий от Int64

Не знаю руби и знать не хочу, но гораздо лучше читается, чем у автора.

fsb4000 ★★★★★
()
Ответ на: комментарий от i-rinat

Ну так в чём проблема? Пиши на Ruby.

Я пишу на том, на чем написан проект. Прикинь, да? Языкофобов просим освободить помещение. :)

У тебя пример «как не надо» написан на Python, а все примеры «как надо» написаны на Ruby.

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

wandrien ★★
() автор топика
Ответ на: комментарий от Crocodoom

Ведь в питоне отродясь есть и map, и filter, и functools с itertools

Их чуть не выпилили ;)

wandrien ★★
() автор топика

Лямбды вообще не так давно в прошлом считались в среде питонистов какой-то непонятной и ненужной магией. Не то что бы «всех питонистов», но определённая прослойка была.

wandrien ★★
() автор топика
Ответ на: комментарий от wandrien

А по моему опыту это не write-only код. Люди на clojure пишут и у них там только так и можно и ничего, живут как-то.

Если не нравится функциональный подход, в питоне есть генераторы. Вроде как-то так это работает:

def get_supported_trackers(self):
    trackers = self.get_trackers()

    for tracker in trackers:
        if not self.site.connection_server.tor_manager.enabled and ".onion" not in tracker:
            yield tracker

        if self.get_address_parts(tracker):
            yield tracker

        address_parts = self.get_address_parts(tracker)
        is_supported_ip = "ipv6" not in self.site.connection_server.supported_ip_types;

        if is_supported_ip and helper.get_ip_type(address_parts["ip"]) != "ipv6":
            yield tracker

Int64 ★★★
()
Последнее исправление: Int64 (всего исправлений: 2)
Ответ на: комментарий от wandrien

Потому что Python последовательно продвигает «как не надо».

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

ты вот задумался и решил переделать все сразу. но причем тут педон? точно так же на чем угодно можно сделать.

n_play
()
Ответ на: комментарий от Int64

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

у тебя по отдеьлности один и тот же несоклько раз выехает

n_play
()
Ответ на: комментарий от wandrien

Я пишу на том, на чем написан проект. Прикинь, да? Языкофобов просим освободить помещение. :)

Я тебе говорю, что ты волен писать на том языке, на котором хочешь. Каким местом это языкофобия? У тебя с головой всё в порядке? Или ты себя языкофобом называешь?

Не нравится Python? Не работай над проектом на Python. Нужно работать над проектом на Python? Переставай ныть и продолжай работать.

Потому что Python последовательно продвигает «как не надо».

И ещё раз. Не нравится Python — не пользуйся. Тут тебе явно нужны блоки, а их в питоне и не было никогда. Так что нет никакого продвижения не туда. Просто тебе не нравится питон, а сил в этом публично признаться нет.

i-rinat ★★★★★
()
Ответ на: комментарий от Int64

Если не нравится функциональный подход

Не путай функциональный подход и бездумное навешивание километровых цепочек из map(), filter() и inject().

Такие цепочки характерны скорее для неофита в пылу решившего, что нашел серебряную пулю.

Я сам так писал 10 лет назад, когда только познакомился с Ruby.

Люди на clojure пишут и у них там только так и можно и ничего, живут как-то.

Я на clojure написал бы точно такую же единственную лямбду с ветвлением внутри, а не 4 разных лямбды, завернутых в 4 вызова библиотечной функции.

wandrien ★★
() автор топика
Ответ на: комментарий от i-rinat

Не нравится Python? Не работай над проектом на Python. Нужно работать над проектом на Python? Переставай ныть и продолжай работать.

Не нравится читать треды про Python? Не читай. Перестань ныть уже наконец, да.

Не работай над проектом на Python.

Тебя забыл спросить, над чем мне работать.

wandrien ★★
() автор топика
Последнее исправление: wandrien (всего исправлений: 1)
Ответ на: комментарий от n_play

Тогда так

def get_supported_trackers(self):
    trackers = self.get_trackers()

    while tracker in trackers:
        if self.site.connection_server.tor_manager.enabled or ".onion" in tracker:
            continue

        if not self.get_address_parts(tracker):
            continue

        address_parts = self.get_address_parts(tracker)
        is_supported_ip = "ipv6" not in self.site.connection_server.supported_ip_types;

        if not is_supported_ip and helper.get_ip_type(address_parts["ip"]) == "ipv6":
            continue

        yield tracker

Int64 ★★★
()
Ответ на: комментарий от wandrien

Я на clojure написал бы точно такую же единственную лямбду с ветвлением внутри, а не 4 разных лямбды, завернутых в 4 вызова библиотечной функции.

так и на педоне также можно. только не лябду, а вложенную локальную функцию с нужными ветвлениями например. и пусть она фильтрует последовательность.

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

n_play
()
Ответ на: комментарий от wandrien

Не читай.

Тебя забыл спросить

Пришёл на форум и огрызается на чужое мнение.

Тебе с таким подходом только в инстаграм постить. Там сможешь забанить всех несогласных и оставить только припевал.

i-rinat ★★★★★
()
Ответ на: комментарий от n_play

и в чем по сути разница-то?

А нет разницы, на чем писать. Я и говорил исходно о том, КАК писать, а не на чем.

Можно использовать вложенную функцию, генератор или вообще старый-добрый for.

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

wandrien ★★
() автор топика
Ответ на: комментарий от i-rinat

Пришёл на форум и огрызается на чужое мнение.

Это ты о себе? Рад, что мы друг друга поняли.

wandrien ★★
() автор топика
Ответ на: комментарий от wandrien

Не путай функциональный подход и бездумное навешивание километровых цепочек из map(), filter() и inject().

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

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

И для твоей задачи это скорее выглядело бы так:

def getSupportedTrackers()
  return @getTrackers()
    .filter(f1)
    .map(f2)
    .filter(f3)
    .filter(f4);

где f1, f2, f3 и f4 произвольные функции, я к сожалению не сильно знаком с предметной области, поэтому такие вот названия.

Int64 ★★★
()
    def getSupportedTrackers(self):
         return list(filter(self._is_supported_tracker, self.getTrackers()))
 
    def _is_supported_tracker(self, tracker):
        if (
            not self.site.connection_server.tor_manager.enabled
            and ".onion" in tracker
        ):
            return False
        
        if not self.getAddressParts(tracker):
            return False

        if (
            "ipv6" not in self.site.connection_server.supported_ip_types
            and helper.getIpType(self.getAddressParts(tracker)["ip"]) == "ipv6"
        ):
            return False

        return True

qaqa ★★
()

Зачем был приведён пример на python,если дальше он никак не используется?

grem ★★★★★
()

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

ZERG ★★★★★
()
Ответ на: комментарий от ZERG

Код не сопровождается никакой документацией/комментариями, вот в этом и проблема.

Сопровождать говнокод комментариями?

wandrien ★★
() автор топика

Ты ещё вложенные не видел.

Некрасиво очень, но гораздо читабельней чем фильтр. И быстрее, что тоже важно.

WitcherGeralt ★★
()

Или для любителей длинных однострочников

Ну да, а еще для ублажения рубокопа. А за предыдущий вариант он тебя ссаными тряпками будет гонять, и еще заставит богомерзкий unless юзать. Как я ненавижу эти говнолинтеры!

anonymous
()
Ответ на: комментарий от n_play

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

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

anonymous
()
Ответ на: комментарий от anonymous

и еще заставит богомерзкий unless юзать

буэ!

wandrien ★★
() автор топика

О, тред дурачка из лолксов перенесли.

        trackers = [tracker for tracker in trackers if self.getAddressParts(tracker)]  # Remove trackers with unknown address

        if "ipv6" not in self.site.connection_server.supported_ip_types:
            trackers = [tracker for tracker in trackers if helper.getIpType(self.getAddressParts(tracker)["ip"]) != "ipv6"]

А тебе не приходило в голову, что дело здесь вовсе не в кампрехеншонах, а в изначально убогой архитектуре и это в принципе ничем не поправить? Добавь сверху ещё временные ограничения, в которых никому нет дела до красоты кода, лишь бы работало и ты получишь парашу хоть на питоне, хоть на руби. Хорошая архитектура и красивый код требуют очень много затрат, которые, очевидно, твоя нищенская контора не может себе позволить. Это просто другой культурный уровень. А твой удел — ковыряться в говне и жаловаться.

anonymous
()

Код на Ruby написано так, будто бы и не на Ruby. На Ruby обычно получается короче и читабельнее.

Alve ★★★★★
()

О да, логика 80лвл - посмотрим что пишут про генераторы на питоне, напишем генераторы, посмотрим что пишут другие о стиле кодирования, ужаснемся, перепишем все на руби.

На самом деле это делается так. Вместо

trackers = [tracker for tracker in trackers if ".onion" not in tracker]

конечно же

trackers = filter(lambda x: '.onion' in x, trackers)

Вместо

trackers = [tracker for tracker in trackers if self.getAddressParts(tracker)]  # Remove trackers with unknown address
trackers = filter(self.getAddressParts, trackers)

Третья конструкция сама по себе настолько толстая что пофик через что ее делать.

AntonI ★★★★
()

Чем рубист отличается от перловика?

Хипстерской бородой.

anonymous
()
Ответ на: комментарий от ZERG

Сопровождать любой код комментариями.

Положи обратно книгу вредных советов.

wandrien ★★
() автор топика
Ответ на: комментарий от AntonI

Не люблю лямбды

In [1]: def contains(s):
   ...:     def f(v):
   ...:         return s in v
   ...:     return f
   ...: 

In [2]: list(filter(contains('onion'), ['a', 'aonionb']))
Out[2]: ['aonionb']
ei-grad ★★★★★
()
Последнее исправление: ei-grad (всего исправлений: 1)

Тебя обманули, list comprehension норм когда нужен list comprehension.

Если не нужен - filter или генератор с yield как выше подсказали.

Про «нужно обработать весь лист каждым условием целиком!» - ну возьми pandas тогда, чо, или другую либку которая умеет в векторизацию. Зачем тебе медленные list comprehension?

ei-grad ★★★★★
()
Ответ на: комментарий от Alve

Код на Ruby написано так, будто бы и не на Ruby.

Да нормально, это такой питон здорового человека. Я конечно написал бы в 10 раз короче (и на перле), только потом никто бы не распарсил.

bread
()
Ответ на: комментарий от AntonI

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

bread
()
Ответ на: комментарий от WitcherGeralt

Блин, у одного крутого python core developer был классный пост про то как правильно юзать генераторы vs comprehensions… Сам я не осилю это донести. А пост не гуглится. Придется тебе так и остаться на уровне замены квадратных скобочек на круглые.

ei-grad ★★★★★
()
Ответ на: комментарий от ei-grad

Тебя обманули, list comprehension норм когда нужен list comprehension.

То есть никогда.

Зачем тебе медленные list comprehension?

Давай спросим Гвидо, зачем он обмазался этим.

wandrien ★★
() автор топика
Ответ на: комментарий от wandrien

Ну в первую очередь он нужен вместо map, чтоб не писать некрасивую lambda. Но ньюанс в том, что map в builtin оставили, а filter утащили. Надо погуглить, наверное Гвидо уже спрашивали про это.

ei-grad ★★★★★
()

как этот much clearer way выглядит in the wild. Как-то так

Ещё один всё понял. Я уже который год тут это твержу, но мамкиным питонистам хоть ссы в глаза.

no-such-file ★★★★★
()
Ответ на: комментарий от i-rinat

пример «как не надо» написан на Python

Проблема в том, что там есть возможность писать «как не надо». По закону Мерфи, что возможно, то и происходит. Более того, это считается «канонично».

no-such-file ★★★★★
()
Последнее исправление: no-such-file (всего исправлений: 1)
Ответ на: комментарий от wandrien

Больной?

Я нет, а про тебя не знаю. Ты сначала переписал все на руби а потом принялся задавать странные вопросы.

AntonI ★★★★
()
Ответ на: комментарий от ei-grad

Но это же ты простейшую мысль донести не в состоянии, а я-то прекрасно знаю как и когда использовать генераторы. Твоё противопоставление довольно тупое. Обмазывание всего и вся генераторами — бессмысленная, глупая и даже вредная идея.

WitcherGeralt ★★
()
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.