Комментарии 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()?
Не совсем понял, в чём вопрос. Если нужно выполнить другую операцию, то... вы просто выполняете другую операцию.
Если передаётся пустая коллекция:
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Здесь, мы также применяем одну и туже функцию к каждому элементу заданного массива, но, предварительно проверяем фильтрующее условие, и, уже постфактум, проверяем выходное значение.

13 базовых конструкций Python для новичков (и тех, кто хочет освежить основы)