Pull to refresh

Pythonic

Reading time 7 min
Views 37K
Итак, что же это значит, когда кто-либо говорит, что foo выглядит как pythonic? Что значит, когда кто-либо смотрит в наш код и говорит, что он unpythonic? Давайте попробуем разобраться.

В Python-сообществе существует неологизм pythonic, который можно трактовать по разному, но в общем случае он характеризует стиль кода. Поэтому утверждение, что какой-либо код является pythonic, равносильно утверждению, что он написан в соответствии с идиома Python’a. Аналогично, такое утверждение в отношении интерфейса, или какой-либо функциональности, означает, что он (она) согласуется с идиомами Python’a и хорошо вписывается в экосистему.

Напротив, метка unpythonic означает, что код представляет собой грубую попытку записать код какого-либо другого языка программирования в синтаксисе Python, а не идиоматическую трансформацию.

Понятие Pythonicity плотно связано с минималистической концепцией Python’a и уходом от принципа «существует много способов сделать это». Нечитабельный код, или непонятные идиомы – все это unpythonic.

При переходе от одного языка к другому, некоторые вещи должны быть «разучены». Что мы знаем из других языков программирования, что не будет к месту в Python’e?


Используйте стандартную библиотеку


Стандартная библиотека – наш друг. Давайте использовать ее.


>>> foo = "/home/sa"
>>> baz = "somefile"
>>> foo + "/" + baz                    # unpythonic
'/home/sa/somefile'
>>> import os.path
>>> os.path.join(foo, baz)             # pythonic
'/home/sa/somefile'
>>>

Другие полезные функции в os.path: basename(), dirname() и splitext().


>>> somefoo = list(range(9))
>>> somefoo
[0, 1, 2, 3, 4, 5, 6, 7, 8]
>>> import random
>>> random.shuffle(somefoo)                     # pythonic
>>> somefoo
[8, 4, 5, 0, 7, 2, 6, 3, 1]
>>> max(somefoo)                                # pythonic
8
>>> min(somefoo)                                # pythonic
0
>>>

Существует много полезных встроенных функций, о которых многие люди по каким-либо причинам не знают. Например, min() и max(). Стандартная библиотека включает много полезных модулей. Например, random, который содержит кучу функционала, который люди по незнанию реализую самостоятельно.

Создание пустых списков, кортежей, словарей и т.д.




>>> bar = list()                        # unpythonic
>>> type(bar)
<class 'list'>
>>> del bar
>>> bar = []                            # pythonic
>>> type(bar)
<class 'list'>
>>> foo = {}
>>> type(foo)
<class 'dict'>
>>> baz= set()                          # {} is a dictionary so we need to use set()
>>> type(baz)
<class 'set'>
>>>


Использование обратной косой черты


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


if foo.bar()['first'][0] == baz.ham(1, 2)[5:9] and \            # unpythonic
   verify(34, 20) != skip(500, 360):
      pass


Использование «\» не является хорошей идеей. Такой подход может вызвать неприятный баг: случайный пробел после косой черты сделает строку неправильной. В лучшем случае мы получим syntax error, но если код предствляет что-то вроде этого:


value = foo.bar()['first'][0]*baz.ham(1, 2)[5:9] \              # unpythonic
        + verify(34, 20)*skip(500, 360)


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


value = (foo.bar()['first'][0]*baz.ham(1, 2)[5:9]               # pythonic
        + verify(34, 20)*skip(500, 360))


Import


Не используйте «from foo import *». Здесь и здесь можно найти более подробную информацию.

Общие исключения


Python имеет выражение «except», которое отлавливает все исключения. Т.к. любая ошибка генерирует исключение, такой код может сделать многие ошибки программирования похожими на ошибки времени исполнения, и затруднят отладку программы. Следующий пример является исчерпывающим:


try:
    foo = opne("somefile")                              # misspelled "open"
except:
    sys.exit("could not open file!")

Вторая строчка генерирует «NameError», который будет отловлен, что семантически неверно, поскольку «except» написан для отлавливания «IOError». Лучше написать такой код:


try:
    foo = opne("somefile")
except IOError:
    sys.exit("could not open file")

Когда Вы запустите этот код, Python сгенерирует «NameError», и Вы моментально увидите и исправите ошибку.

Поскольку «except» отлавливает все исключения, включая «SystemExit», «KeyboardInterrupt», и «GeneratorExit» (которые по сути не являются ошибками и не должны отлавливаться пользовательским кодом), использование голого «except» в любом случае плохая идея. В ситуациях, когда нам нужно все-таки покрыть все возможные исключительные ситуации, мы можем использовать базовый класс для всех исключений – «Exception».

Нам редко нужны счетчики




>>> counter = 0                         # unpythonic
>>> while counter < 10:
...     # do some stuff
...     counter += 1
...
...
>>> counter
10
>>> for counter in range(10):           # pythonic
...     # do some stuff
...     pass
...
...
>>>

Другой пример:


>>> food = ['donkey', 'orange', 'fish']
>>> for i in range(len(food)):          # unpythonic
...     print(food[i])
...
...
donkey
orange
fish
>>> for item in food:                   # pythonic
...     print(item)
...
...
donkey
orange
fish
>>>


Явные итераторы


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


>>> counter = 0                                                     # unpythonic
>>> while counter < len(somecontainer):
...     callable_consuming_container_elements(somecontainer[counter])
...     counter += 1
...
...
>>> for item in somecontainer:                                      # pythonic
...     callable_consuming_container_elements(item)
...
...
>>>

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


>>> somecontainer = list(range(7))
>>> type(somecontainer)
<class 'list'>
>>> somecontainer
[0, 1, 2, 3, 4, 5, 6]
>>> somecontaineriterator = iter(somecontainer)
>>> type(somecontaineriterator)
<class 'list_iterator'>

Теперь мы можем начать использовать наш итератор:


>>> for item in somecontaineriterator:          # start consuming the iterable somecontainer
...     if item < 4:
...         print(item)
...
...     else:
...         break                               # breaks out of the nearest enclosing for/while loop
...
...
...
0
1
2
3

Не дайте себя обмануть, итератор остановился на «somecontaineriterator[5]», который равен 4, а не 3. Давайте посмотрим, что будет дальше:


>>> print("Something unrelated to somecontaineriterator.")
Something unrelated to somecontaineriterator.
>>> next(somecontaineriterator)                    # continues where previous for/while loop left off
5
>>> next(somecontaineriterator)
6
>>> next(somecontaineriterator)
Traceback (most recent call last):                 # we have exhausted the iterator
  File "<input>", line 1, in <module>
StopIteration
>>>

Некоторым может показаться, что данный пример неоднозначен, на самом деле это не так. Итератор в цикле проходит массив, выходит из цикла по break на индексе 5 (значение внутри равно 4). Затем мы совершаем некие действия (выводим текст в консоль), и после этого продолжаем перебор итератора. Вот и все.

Присваивание


Здесь можно почитать подробно.

Циклы только когда это действительно необходимо


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

Python предоставляет много высокоуровневого функционала для оперирования любыми объектами. Для последовательностей это могут быть функции zip(), min(), max(). Затем, это такие вещи, как «list comprehensions», генераторы, «set comprehensions» и т.д.

Дело в том, что если мы сохраняем наши данные в базовых структурах Python, таких как списки, кортежи, словари, множества и д.р., мы получаем кучу функционала для работы с ними «из коробки». Даже если мы нуждаемся в специфичной структуре, скорее всего не составит труда создать ее, используя базовый структуры данных. Итак, в чем же преимущества. Как мы можем получить список имен некоторых людей, хранящийся в дисковом файле.


sa@wks:/tmp$ cat people.txt
   Dora
John
 Dora
Mike
Dora
     Alex
Alex
sa@wks:/tmp$ python
>>> with open('people.txt', encoding='utf-8') as a_file:     # context manager
...     { line.strip() for line in a_file }                  # set comprehension
...
...
{'Alex', 'Mike', 'John', 'Dora'}
>>>

Никаких циклов, пользовательских структур данных, убраны лишние пробелы и дубликаты, все pythonic ;-]

Кортежи – это не просто read-only списки


Это распространенное заблуждение. Очень часто списки и кортежи применяются для одних и тех же целей. Списки предназначены для хранения однотипных данных. В то время как кортежи – для объединения данных разного типа в набор. Другими словами

Целое — больше чем сумма его частей.
— Аристотель (384 д.н.э — 322 д.н.э)



>>> person = ("Steve", 23, "male", "London")
>>> print("{} is {}, {} and lives in {}.".format(person[0], person[1], person[2], person[3]))
Steve is 23, male and lives in London.
>>> person = ("male", "Steve", 23, "London")              #different tuple, same code
>>> print("{} is {}, {} and lives in {}.".format(person[0], person[1], person[2], person[3]))
male is Steve, 23 and lives in London.
>>>

Индекс в кортеже несет смысловую нагрузку. Давайте сравним эти структуры:


>>> foo = 2011, 11, 3, 15, 23, 59
>>> foo
(2011, 11, 3, 15, 23, 59)                               # tuple
>>> list(range(9))
[0, 1, 2, 3, 4, 5, 6, 7, 8]                             # list
>>>

  • Первая из них, кортеж, представляет собой структуру, где позиция элемента несет некий смысл (Первый элемент — год).
  • Вторая же, список, представляет собой последовательность, в которой значения функционально равнозначны, индекс не несет никакого смысла.

Отличный пример использования обоих структур – метод fetchmany() из Python DB API, который возвращает результат как список кортежей.

Классы не предназначены для группировки функциональности


C# и Java содержат код только внутри классов. В итоге возникают утилитарные классы, содержащие одни статические методы. Например, математическая функция sin(). В Python мы просто используем модуль верхнего уровня:


sa@wks:/tmp$ echo -e 'def sin():\n    pass' > foo.py; cat foo.py
def sin():
    pass
sa@wks:/tmp$ python
>>> import foo
>>> foo.sin()
>>>


Скажите нет геттерам и сеттерам


Способ достичь инкапсуляции в Python – использование свойств, а не геттеров и сеттеров. Используя свойства, мы можем изменить атрибуты объекта и исправить реализацию, не затрагивая вызываемый код (читайте, stable API).

Функции являются объектами


В Python – все является объектами. Функции тоже объекты. Функции – это объекты, которые можно вызывать.


>>> somefoo = [{'price': 9.99}, {'price': 4.99}, {'price': 10}]
>>> somefoo
[{'price': 9.99}, {'price': 4.99}, {'price': 10}]
>>> def lookup_price(someobject):
...     return someobject['price']
...
...
>>> somefoo.sort(key=lookup_price)                        # pass function object lookup_price
>>> somefoo
[{'price': 4.99}, {'price': 9.99}, {'price': 10}]         # in-place sort of somefoo took place
>>> type(somefoo)
<class 'list'>
>>> type(somefoo[0])
<class 'dict'>
>>>

Между lookup_price и lookup_price() существует разница — последнее вызывает функцию, а первое смотрим биндинг по имени lookup_price. Это дает нам возможность использовать функции в роли обычных объектов.
Tags:
Hubs:
+92
Comments 63
Comments Comments 63

Articles