Обновить

Комментарии 6

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

Мне кажется уже пора завязывать с этой мантрой про простоту и читаемость. Я понимаю в двухтысячных на фоне C++ и Java, Python казался действительно компактным, но сейчас с учетом развития новых языков и того насколько перегрузили сам Python - это звучит нелепо.

Вот это читабельно?

result = [x for x in data if x.get("enabled") and x["value"] > 10]

или вот это?

def process(items: list[tuple[str, int | float]]) -> dict[str, float]:

По мне, Python давно уже потерял и свою компактность и читаемость и стал ровно таким же мейнстрим языком как Kotlin, TS, Swift и т.д. и забавно говорить но уже Java кажется проще.

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

even_numbers = [x for x in range(10) if x % 2 == 0]

Это точно понятнее и выразительнее чем нормальный цикл?

logs_dir = Path('var') / 'logs' / 'app.log'

А если для logs нужно какую то другую операцию выполнить а не join()?

Вот это читабельно?

Если отформатировать, то вполне читабельно:

# такое форматирование выглядит намного лучше, когда имя не однобуквенное
result = [
    x
    for x in data
    if x.get("enabled") and x["value"] > 10
]

В примере с аннотациями стоит вынести анонимные типы в именованные. Для примитивных типов можно даже использовать NewType, но это совсем необязательно:

type Item = tuple[str, int | float]
type Response = dict[str, float]

def process(items: list[Item]) -> Response:

Это точно понятнее и выразительнее чем нормальный цикл?

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

А если для logs нужно какую то другую операцию выполнить а не join()?

Не совсем понял, в чём вопрос. Если нужно выполнить другую операцию, то... вы просто выполняете другую операцию.

Согласен с вами по поводу форматирования и выноса типов, хороший совет. В статье я не стал глубоко уходить в оформление, чтобы не перегружать начинающих, но для production-кода такое форматирование и именованные типы точно делают код понятнее

  • Если передаётся пустая коллекция:

    all([])  # → True
    any([])  # → False

    Это поведение может быть неожиданным для новичков.

Почему? Тут же всё очень просто: если нужны все элементы, то подойдёт и случай, когда нет ни одного элемента (тогда True), а если нужен некоторый, а нет и одного, то и выбирать нечего (тогда False).

Вообще, все эти вполне здравые и понятные рекомендации несколько однобоки. Например, функции map/filter/reduce. Да, если использовать lambda-инструкцию, то код может стать не читаемым. Но кто мешает поставлять обычные функции? Это, что называется, во-первых. А во-вторых, есть ещё и замыкания, когда можно какие-то элементы данных запомнить внутри вызываемых функций, что позволяет существенно упростить код. Рассмотрим, для примера, следующий код:

def TableSelect(TABLE, SelectedNames, Statement=None):
    TableName, ColumnNames, TableRows = TABLE
    if Statement:
        SelectedRows = list(filter(lambda Row: apply(Statement, Row), TableRows))
    else:
        SelectedRows = TableRows[:]
    SelectedRows = SelectColumns(SelectedRows, SelectedNames)        

Эта функция применяет к каждой строке некоторой таблицы заданное условие (Statement). Само условие формулируется следующим образом (например):

statemnet = OR(FieldValueEq("Код", '1009'), FieldValueEq("Код", '1010'))

Реализация используемых здесь функций такова:

def FieldValueEq(FieldName, FieldValue):
    def EqualTo(Row):
        return Row[FieldName] == FieldValue
    return EqualTo   

def FieldValuesEq(LeftFieldName, RightFieldName):
    def EqualTo(Row):
        return Row[LeftFieldName] == Row[RightFieldName]
    return EqualTo

def OR(*args):
    return (any, args)    

def AND(*args):
    return (all, args) 

def apply(fun, obj):
    # if type(fun) is function:
    if callable(fun):
        return fun(obj)
    else:
        fun, input_args = fun
        output_args = map(lambda arg: apply(arg, obj), input_args)
        return fun(output_args)

Другими словами, мы упаковываем сложные условия в создаваемую нами налету функцию в момент вызова

statemnet = OR(FieldValueEq("Код", '1009'), FieldValueEq("Код", '1010'))

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

apply(Statement, Row)

который разбирает заданное условие шаг за шагом, подставляя для каждой фигурирующей в этом условии функции данные текущей строки (Row).

Наверное, можно было бы проделать этот трюк и с самим вызовом

apply(Statement, Row)

написав что-то вроде

SelectedRows = list(filter(apply_statement(Statement), TableRows))

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

В целом, да, поведение all([]) и any([]) логично, но у новичков оно иногда вызывает вопросы, особенно когда они впервые сталкиваются с пустыми коллекциями.

Про map и filter вы правы: использование lambda может ухудшать читаемость, в статье я специально отметил, что нужно знать меру:

Если код становится трудночитаемым из-за использования lambda внутри map — в таком случае list comprehension часто понятнее.

О чём следовало бы говорить, так это о некоторой ограниченности функции map. Например, мы могли бы написать функцию

def func_apply(main_func, input_array, filter_func=None, reduce_func=None):
    def process_element(array, i):
        
        input_value = array[i]
        
        value = None
        if not filter_func or (filter_func and filter_func(input_value, array)):
            value = main_func(input_value)
        
        if value is None:
            return None

        output_value = None
        if not reduce_func or (reduce_func and reduce_func(value, array)):
            output_value = value      
        
        return output_value

    n = len(input_array)
    output_array = [ process_element(input_array, i) for i in range(n) ]
    return output_array

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

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации