Эффективные и не эффективные методы кодинга на Python

Привет, Хабр! Предлагаю Вашему вниманию перевод статьи Good and Bad Practices of Coding in Python автора Duomly.

Python – высокоуровневый язык программирования, акцентирующий внимание на удобочитаемости. Он разрабатывается, поддерживается и часто используется в соответствии с The Zen of Python или PEP 20.

В этой статье показано несколько примеров хороших и плохих методов кодинга в Python, с которыми вы, вероятно, столкнетесь.

Использование распаковки (Unpacking) для написания компактного кода


Упаковка и распаковка — мощные инструменты Python. Вы можете использовать распаковку для присваивания значений переменным:

>>> a, b = 2, 'my-string'
>>> a
2
>>> b
'my-string'

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

>>> a, b = b, a
>>> a
'my-string'
>>> b
2

Это потрясающе!

Распаковка также может быть использована для присвоения значения нескольким переменным в более сложных случаях. Например, Вы можете написать так:

>>> x = (1, 2, 4, 8, 16)
>>> a = x[0]
>>> b = x[1]
>>> c = x[2]
>>> d = x[3]
>>> e = x[4]
>>> a, b, c, d, e
(1, 2, 4, 8, 16)

Но вместо этого Вы также можете воспользоваться более лаконичным и, возможно, более читабельным способом:

>>> a, b, c, d, e = x
>>> a, b, c, d, e
(1, 2, 4, 8, 16)

Это круто, правда? Но можно написать еще круче:

>>> a, *y, e = x
>>> a, e, y
(1, 16, [2, 4, 8])

Фишка в том, что переменная с * собирает значения, не назначенные другим переменным.

Использование цепочки (Chaining) для написания компактного кода


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

>>> x = 4
>>> x >= 2 and x <= 8
True

Вместо этого Вы можете использовать более компактную форму написания:

>>> 2 <= x <= 8
True
>>> 2 <= x <= 3
False

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

>>> x = 2
>>> y = 2
>>> z = 2

Более компактный способ — использовать распаковку:

>>> x, y, z = 2, 2, 2

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

>>> x = y = z = 2
>>> x, y, z
(2, 2, 2)

Будьте осторожны, когда значения разные! Все переменные ссылаются на одно и то же значение.

Проверка на None


None не является уникальным объектом в Python. Он имеет аналоги, например, null в C-подобных языках.

Можно проверить, ссылается ли переменная на None с помощью операторов сравнения == и !=:

>>> x, y = 2, None
>>> x == None
False
>>> y == None
True
>>> x != None
True
>>> y != None
False

Однако, предпочтительнее использование is и is not:

>>> x is None
False
>>> y is None
True
>>> x is not None
True
>>> y is not None
False

Кроме того, лучше использовать конструкцию x is not None, а не менее читабельную альтернативу (x is None).

Перебор последовательностей (Sequences) и отображений (Mappings)


Вы можете реализовать циклы в Python несколькими способами. Python предлагает несколько встроенных классов для упрощения их реализации.

Почти всегда Вы можете использовать диапазон, чтобы получить цикл, который выдает целые числа:

>>> x = [1, 2, 4, 8, 16]
>>> for i in range(len(x)):
...     print(x[i])
... 
1
2
4
8
16

Однако для этого есть лучший способ:

>>> for item in x:
...     print(item)
... 
1
2
4
8
16

Но что, если Вы хотите запустить цикл в обратном порядке? Конечно, снова можно использовать диапазон:

>>> for i in range(len(x)-1, -1, -1):
...     print(x[i])
... 
16
8
4
2
1

Но «перевернуть» последовательность – более компактный способ:

>>> for item in x[::-1]:
...     print(item)
... 
16
8
4
2
1

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

>>> for item in reversed(x):
...     print(item)
... 
16
8
4
2
1

Иногда необходимы как элементы последовательности, так и соответствующие им индексы:

>>> for i in range(len(x)):
...     print(i, x[i])
... 
0 1
1 2
2 4
3 8
4 16

Лучше использовать перечисление, чтобы получить другой цикл, который возвращает значения индексов и элементов:

>>> for i, item in enumerate(x):
...     print(i, item)
... 
0 1
1 2
2 4
3 8
4 16

Это круто. Но что, если необходимо перебрать две или более последовательности? Конечно, можно снова использовать диапазон:

>>> y = 'abcde'
>>> for i in range(len(x)):
...     print(x[i], y[i])
... 
1 a
2 b
4 c
8 d
16 e

В этом случае Python также предлагает лучший способ решения. Вы можете применить zip:

>>> for item in zip(x, y):
...     print(item)
... 
(1, 'a')
(2, 'b')
(4, 'c')
(8, 'd')
(16, 'e')

Можно также скомбинировать этот метод с распаковкой:

>>> for x_item, y_item in zip(x, y):
...     print(x_item, y_item)
... 
1 a
2 b
4 c
8 d
16 e

Пожалуйста, имейте в виду, что диапазон может быть весьма полезным. Тем не менее, есть случаи (как, например, выше), где оптимальнее использовать альтернативы.
Перебор словаря дает его ключи:

>>> z = {'a': 0, 'b': 1}
>>> for k in z:
... print(k, z[k])
... 
a 0
b 1

Однако вы можете применить метод .items () и получить ключи и соответствующие им значения:

>>> for k, v in z.items():
...     print(k, v)
... 
a 0
b 1

Можно также использовать методы .keys () и .values ​​() для перебора ключей и значений соответственно.

Сравнение с нулем


Когда у Вас есть числовые данные, и нужно проверить, равны ли числа нулю, можно (но не всегда нужно) использовать операторы сравнения == и !=:

>>> x = (1, 2, 0, 3, 0, 4)
>>> for item in x:
...     if item != 0:
...         print(item)
... 
1
2
3
4

Способ, предлагаемый Python, состоит в том, чтобы интерпретировать нуль как False, а все другие числа как True:

>>> bool(0)
False
>>> bool(-1), bool(1), bool(20), bool(28.4)
(True, True, True, True)

Имея это в виду, Вы можете использовать if item вместо if item! = 0:

>>> for item in x:
...     if item:
...         print(item)
... 
1
2
3
4

Вы можете следовать той же логике и использовать if not item вместо if item == 0.

Избегание изменяемых необязательных аргументов


У Python очень гибкая система предоставления аргументов функциям и методам. Необязательные аргументы являются частью этого. Но будьте осторожны: Вы обычно не хотите использовать изменяемые необязательные аргументы. Рассмотрим следующий пример:

>>> def f(value, seq=[]):
...     seq.append(value)
...     return seq

На первый взгляд это выглядит так, если вы не укажете seq, f () добавляет value в пустой список и возвращает что-то вроде [value]:

>>> f(value=2)
[2]

Выглядит здорово, правда? Нет! Рассмотрим следующие примеры:

>>> f(value=4)
[2, 4]
>>> f(value=8)
[2, 4, 8]
>>> f(value=16)
[2, 4, 8, 16]

Удивлены? Если да, то не Вы одни.

Кажется, что каждый раз, когда вызывается функция, предоставляется один и тот же экземпляр необязательного аргумента (в данном случае, списка). Может быть, иногда это и будет то, что Вам нужно. Тем не менее, гораздо более вероятно, что Вам необходимо будет избежать это. Один из способов заключается в следующем:

>>> def f(value, seq=None):
...     if seq is None:
...         seq = []
...     seq.append(value)
...     return seq

Версия покомпактней:

>>> def f(value, seq=None):
...     if not seq:
...         seq = []
...     seq.append(value)
...     return seq

Теперь мы получаем другой вывод:

>>> f(value=2)
[2]
>>> f(value=4)
[4]
>>> f(value=8)
[8]
>>> f(value=16)
[16]

В большинстве случаев это то, что нужно.

Избегание классических Getter и Setter


Python позволяет определять методы getter и setter так же, как C ++ и Java:

>>> class C:
...     def get_x(self):
...         return self.__x
...     def set_x(self, value):
...         self.__x = value

Вот как Вы можете использовать их:

>>> c = C()
>>> c.set_x(2)
>>> c.get_x()
2

В некоторых случаях это лучший способ. Однако часто лучше использовать свойства, особенно в простых случаях:

>>> class C:
...     @property
...     def x(self):
...         return self.__x
...     @x.setter
...     def x(self, value):
...         self.__x = value

Свойства считаются более Pythonic, чем классические геттеры и сеттеры. Вы можете использовать их так же, как в C#, то есть так же, как обычные атрибуты данных:

>>> c = C()
>>> c.x = 2
>>> c.x
2

Так что, в общем, лучше использовать свойства, когда это возможно.

Избегание доступа к защищенным членам класса


В Python отсутствуют как таковые private члены класса. Однако, если в начале названия элемента написать (_), то доступ к его изменению за пределами класса будет запрещен.

Например, рассмотрим код:

>>> class C:
...     def __init__(self, *args):
...         self.x, self._y, self.__z = args
... 
>>> c = C(1, 2, 4)

Экземпляры класса C имеют три элемента данных: .x, .y и ._Cz. Если имя участника начинается с двойного подчеркивания (dunder), оно становится измененным. Вот почему вместо ._z элемент ._Cz.

Теперь можно доступ или изменить напрямую .x:

>>> c.x  # OK
1

Вы также можете получить доступ или изменить ._y вне его класса, но это считается моветоном:

>>> c._y  # Possible, but a bad practice!
2

Вы не можете получить доступ к .z, потому что переменная изменена, но вы можете получить доступ или изменить ._Cz:

>>> c.__z # Error!
Traceback (most recent call last):
File "", line 1, in 
AttributeError: 'C' object has no attribute '__z'
>>> c._C__z # Possible, but even worse!
4
>>>

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

Использование контекстных менеджеров (Context Managers) для освобождения ресурсов


Иногда требуется написать код для правильного управления ресурсами. Это часто бывает при работе с файлами, подключениями к базам данных или другими объектами с неуправляемыми ресурсами. Например, вы можете открыть файл и обработать его:

>>> my_file = open('filename.csv', 'w')
>>> # do something with `my_file`

Чтобы правильно управлять памятью, Вам необходимо закрыть этот файл после завершения работы:

>>> my_file = open('filename.csv', 'w')
>>> # do something with `my_file and`
>>> my_file.close()

Делать это таким способом лучше, чем не делать вовсе. Но что, если во время обработки Вашего файла возникает исключение? Тогда my_file.close () никогда не выполняется. Можно справиться с этим с помощью обработки исключений или с помощью контекстных менеджеров. Второй способ означает, что вы помещаете свой код в блок with:

>>> with open('filename.csv', 'w') as my_file:
...     # do something with `my_file`

Использование блока with означает, что специальные методы .enter () и .exit () вызываются даже в случаях исключений. Эти методы должны заботиться о ресурсах.
Вы можете добиться особенно надежных конструкций, комбинируя контекстные менеджеры и обработку исключений.

Стилистические советы


Код Python должен быть элегантным, лаконичным и читабельным.

Основной ресурс на тему того, как написать красивый код Python, — это Style Guide for Python Code или PEP 8. Вам стоит прочитать его, если хотите писать код на Python.

Выводы


Эта статья дает несколько советов о том, как написать более компактный, читабельный код. Короче говоря, она показывает, как сделать код более Pythonic. Кроме того, PEP 8 предоставляет Style Guide for Python Code, а PEP 20 представляет принципы языка Python.

Наслаждайтесь написанием полезного и красивого кода!

Спасибо за чтение.
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама

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

    +9
    Только вот x != 0 и bool(x) — по-разному с None взаимодейтсвуют и надо об этом помнить. Поэтому, поскольку «явное лучше неявного», если работаем с числами, то и сравнивать лучше с числами.
      +2
      Эффективные и не эффективные методы кодинга на Python

      Перевод — уровень Бог. Простите, речь шла действительно про эффективные методики кодинга (т.е. как писать больше кода за единицу времени)? Или скорее про вкус? Оригинал не читал, но предполагаю, что имелись в виду "Практики написания хорошего или плохого кода на Python" или "Хорошие или неудачные шаблоны кода на Python" (т.е. речь именно про "хороший качественный" и "дурно пахнущий, не очевидный, тяжелый в поддерживании" код)

        0
        А мне больше понравилось:
        Если имя участника начинается с двойного подчеркивания (dunder), оно становится измененным. Вот почему вместо ._z элемент ._Cz.

        Теперь можно доступ или изменить напрямую .x:
        0
        А когда будут примеры хорошего кодинга?
          +2
          В профиле написано студент. Так что через 2-3 года практики не раньше. =)
          +1
          x is None сработает не так, если x не унаследован от NoneType. При этом x == None может отрабатывать нормально, через переопределенный __eq__. Сталкивался с таким в SQLAlchemy filter
            +3

            В примере с параметрами по умолчанию можно ещё короче написать:


            def f(value, seq=None):
                seq = seq or []
                ...
              +1

              Короче, но неверно:


              def f(value, seq=None):
                  seq = seq or []
                  seq.append(value)
                  return seq
              
              x = []
              f(1, x)
              print(x)  # []
                +1

                Мне казалось, что в примере в статье как раз хотели избавиться от лишнего побочного эффекта. Если использовать возвращаемое значение, то всё Ок.

                  +1

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

                    +1

                    Предложение от AAngstrom полностью эквивалентно "рекомендованной" авторской версии. В ней тоже будет создаваться новый список, если в seq передать пустой список.
                    Что интересно, не первый раз вижу такой баг в руководствах для начинающих.

                      +1

                      Ох ты ж… я не обратил внимание, что в статье not seq предлагается, я сравнивал с seq is None. Тогда да, просто бить по рукам автора статьи за такие "оптимизации".

                0
                Я конечно сам не питонист, но по-моему самое питонное решение:
                def f(value, seq=[]):
                    return [*seq, value]
                

                Ну или со старым питоном
                def f(value, seq=[]):
                    return seq+[value]
                
                  0
                  Изменяемый тип данных в значении аргумента по умолчанию запрещается большинством линтеров
                +2
                Но «перевернуть» последовательность – более компактный способ:
                for item in x[::-1]:

                Подскажите, pls, кто знает потроха питона. А что кроется внутри взятия инверсии массива?
                Порождается в памяти новый массив который заполняется из x? И ли это как то очень умно понимается интерпретатором и лишней (относительно классического прохода по индексу в массиве) работы не делается?


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

                  +2

                  Впрочем сам не поленился проверить…
                  Вредный совет.
                  Экономия на написании кода (сомнительная) и бессмысленный для данной задачи расход памяти и ресурсов.
                  Порождается еще один массив в памяти.


                  import tracemalloc
                  
                  def display(info, snapshot, key_type='lineno', limit=3):
                      snapshot = snapshot.filter_traces((
                          tracemalloc.Filter(False, "<frozen importlib._bootstrap>"),
                          tracemalloc.Filter(False, "<unknown>"),
                      ))
                      top_stats = snapshot.statistics(key_type)
                      total = sum(stat.size for stat in top_stats)
                      print(info+": size: %.1f KiB" % (total / 1024))
                  
                  def test():
                      display("1", tracemalloc.take_snapshot())
                      x = [1] * (10 ** 6)
                      sum = 0
                      display("2",tracemalloc.take_snapshot())
                      test = True
                      for item in x[::-1]:
                          if test:
                              display("3",tracemalloc.take_snapshot())
                              test = False
                          sum += item
                      display("4",tracemalloc.take_snapshot())
                      del x
                      display("5",tracemalloc.take_snapshot())
                  
                  if __name__ == '__main__':
                      tracemalloc.start()
                      test()

                  1: size: 0.0 KiB
                  2: size: 3910.8 KiB
                  3: size: 7830.2 KiB
                  4: size: 3926.5 KiB
                  5: size: 22.6 KiB
                  
                    +1

                    Да, конечно, x[::-1] возвращает новый массив, отсортированный в обратном порядке, применять его только для итерации не стоит, для этого есть reversed():


                    # x[::-1]:
                    1: size: 0.0 KiB
                    2: size: 7818.2 KiB
                    3: size: 15647.6 KiB
                    4: size: 7842.5 KiB
                    5: size: 34.3 KiB
                    
                    # reversed(x):
                    1: size: 0.0 KiB
                    2: size: 7818.2 KiB
                    3: size: 7835.1 KiB
                    4: size: 7842.5 KiB
                    5: size: 34.3 KiB

                Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                Самое читаемое