Пользователь
0,0
рейтинг
26 мая 2012 в 22:59

Разработка → Вещи, о которых следует помнить, программируя на Python

Дзэн Питона



Изучение культуры, которая окружает язык, приближает вас на шаг к лучшим программистам. Если вы всё еще не прочли «Zen of Python», то откройте интерпретатор Python и введите import this. Для каждого элемента в списке вы найдете пример здесь

Однажды моё внимание привлекло:

Красивое лучше уродливого

UPDATE: На Hacker News было много обсуждений этой статьй. Несколько поправок от них.

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

  #-----------------------------------------------------------------------

  halve_evens_only = lambda nums: map(lambda i: i/2, filter(lambda i: not i%2, nums))

  #-----------------------------------------------------------------------

  def halve_evens_only(nums):
      return [i/2 for i in nums if not i % 2]


Помните очень простые вещи в Python



Обмен значений двух переменных:

    a, b = b, a


Шаговый аргумент для срезов. Например:

      a = [1,2,3,4,5]
      >>> a[::2]  # iterate over the whole list in 2-increments
      [1,3,5]


Частный случай x[::-1] является средством выражения x.reverse().

      >>> a[::-1]
      [5,4,3,2,1]


UPDATE: Имейте в виду x.reverse() переворачивает список, а срезы дают вам возможность делать это:

      >>> x[::-1]
      [5, 4, 3, 2, 1]

      >>> x[::-2]
      [5, 3, 1]


  • Не используйте изменяемые типы переменных для значений по умолчанию


  def function(x, l=[]):          # Don't do this

  def function(x, l=None):        # Way better
      if l is None:
          l = []


UPDATE: Я понимаю что не объяснил почему. Я рекомендую прочитать статью Fredrik Lundh. Вкратце этот дизайн иногда встречается. «Значения по умолчанию всегда вычисляются тогда, и только тогда, когда def заявлена на исполнение.»

  • Используйте iteritems а не items


iteritems использует generators и следовательно это лучше при работе с очень большими списками.

  d = {1: "1", 2: "2", 3: "3"}

  for key, val in d.items()       # builds complete list when called.

  for key, val in d.iteritems()   # calls values only when requested.


Это похоже на range и xrange когда xrange вызывает значения только когда попросят.

UPDATE: Заметьте что iteritems, iterkeys, itervalues удалены из Python 3.x. dict.keys(), dict.items() и dict.values() вернут views вместо списка. docs.python.org/release/3.1.5/whatsnew/3.0.html#views-and-iterators-instead-of-lists

  • Используйте isinstance а не type


Не делайте:

  if type(s) == type(""): ...
  if type(seq) == list or \
     type(seq) == tuple: ...


лучше:

  if isinstance(s, basestring): ...
  if isinstance(seq, (list, tuple)): ...


Почему не стоит делать так: stackoverflow.com/a/1549854/504262

Заметьте что я использую basestring а не str, поскольку вы можете пытаться проверить соответствие unicode к str. Например:

  >>> a=u'aaaa'
  >>> print isinstance(a, basestring)
  True
  >>> print isinstance(a, str)
  False


Это происходит потому что в Python версиях < 3.0 существует два строковых типа: str и unicode:

        object
           |
           |
       basestring
          / \
         /   \
       str  unicode




Python имеет различные типы контейнеров данных являющихся лучшей альтернативой базовым list и dict в различных случаях.

В большинстве случаев используются эти:

UPDATE: Я знаю большинство не использовало этого. Невнимательность с моей стороны. Некоторые могут написать так:

  freqs = {}
  for c in "abracadabra":
      try:
          freqs[c] += 1
      except:
          freqs[c] = 1


Некоторые могут сказать, лучше было бы:

  freqs = {}
  for c in "abracadabra":
      freqs[c] = freqs.get(c, 0) + 1


Скорее используйте коллекцию типа defaultdict:

  from collections import defaultdict
  freqs = defaultdict(int)
  for c in "abracadabra":
      freqs[c] += 1


Другие коллекции

  namedtuple()    # factory function for creating tuple subclasses with named fields  
  deque           # list-like container with fast appends and pops on either end  
  Counter         # dict subclass for counting hashable objects 
  OrderedDict     # dict subclass that remembers the order entries were added 
  defaultdict     # dict subclass that calls a factory function to supply missing values


UPDATE: Как отметили в нескольких комментария на Hacker News я мог бы использовать Counter вместо defaultdict.

  >>> from collections import Counter
  >>> c = Counter("abracadabra")
  >>> c['a']
  5


  • Создавая классы в Python задействуйте magic methods


  __eq__(self, other)      # Defines behavior for the equality operator, ==.
  __ne__(self, other)      # Defines behavior for the inequality operator, !=.
  __lt__(self, other)      # Defines behavior for the less-than operator, <.
  __gt__(self, other)      # Defines behavior for the greater-than operator, >.
  __le__(self, other)      # Defines behavior for the less-than-or-equal-to operator, <=.
  __ge__(self, other)      # Defines behavior for the greater-than-or-equal-to operator, >=.


Существует ряд других магических методов.

  • Условные назначения


  x = 3 if (y == 1) else 2


Этот код делает именно то, как и звучит: «назначить 3 для x если y=1, иначе назначить 2 для x». Вы также можете применить это если у вас есть нечто более сложное:

  x = 3 if (y == 1) else 2 if (y == -1) else 1


Хотя в какой то момент оно идет слишком далеко.

Обратите внимание что вы можете использовать выражение if...else в любом выражение. Например:

  (func1 if y == 1 else func2)(arg1, arg2)


Здесь будет вызываться func1 если y=1, и func2 в противном случае. В обоих случаях соответствующая функция будет вызываться с аргументами arg1 and arg2.

Аналогично, также справедливо следующее:

  x = (class1 if y == 1 else class2)(arg1, arg2)


где class1 и class2 являются классами.

  • Используйте Ellipsis когда это необходимо.


UPDATE: Один из комментаторов с Hacker News упоминал: «Использование многоточие для получения всех элементов, является нарушением принципа „Только Один Путь Достижения Цели“. Стандартное обозначение это [:]». Я с ним согласен. ЛУчший пример использования в NumPy на stackoverflow:

Многоточие используется что бы нарезать многомерные структуры данных.

В данной ситуации это означает, что нужно расширить многомерный срез во всех измерениях.

Пример:

  >>> from numpy import arange
  >>> a = arange(16).reshape(2,2,2,2)


Теперь у вас есть 4-х мерная матрица порядка 2x2x2x2. Для того что бы выбрать все первые элементы 4-го измерения, вы можете воспользоваться многоточием:

  >>> a[..., 0].flatten()
  array([ 0,  2,  4,  6,  8, 10, 12, 14])


что эквивалентно записи:

  >>> a[:,:,:,0].flatten()
  array([ 0,  2,  4,  6,  8, 10, 12, 14])


Предыдущие предложения.

При создание класса вы можете использовать __getitem__ для того что бы ваш объект класса вел себя как словарь. Возьмите этот класс в качестве примера:

  class MyClass(object):
      def __init__(self, a, b, c, d):
          self.a, self.b, self.c, self.d = a, b, c, d

      def __getitem__(self, item):
          return getattr(self, item)

  x = MyClass(10, 12, 22, 14)


Из-за __getitem__ вы сможете получить значение a объекта x как x['a']. Вероятно это известный факт.

Этот объект используется для расширения срезов Python docs.python.org/library/stdtypes.html#bltin-ellipsis-object Таким образом если мы добавим:

  def __getitem__(self, item):
      if item is Ellipsis:
          return [self.a, self.b, self.c, self.d]
      else:
          return getattr(self, item)


Мы сможем использовать x[...] что бы получить список всех элементов.

  >>> x = MyClass(11, 34, 23, 12)
  >>> x[...]
  [11, 34, 23, 12]





P.S



Это перевод поста Satyajit Ranjeev – "A few things to remember while coding in python.". Но оформить в хаб переводов не хватило 0.5 кармы, и сохранить черновик не позволяет, поэтому выкладываю как есть. Просьба все замечания по переводу и орфографии присылать в личку, и сильно не пинать =)
Юртаев Егор @yurtaev
карма
37,0
рейтинг 0,0
Реклама помогает поддерживать и развивать наши сервисы

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

Самое читаемое Разработка

Комментарии (27)

  • +1
    Напрашивается, что использование коллекций противоречит «Простое лучше сложного». Нет?
  • 0
    Добавил в заметки. Лучше периодически подобные статьи просматривать, чтобы не написать того, о чем потом будет стыдно :)
    • НЛО прилетело и опубликовало эту надпись здесь
  • +1
    Все что полезно описано в любой книге, а что не описано вызывает смешанные чувства. Например это:
    >> Создавая классы в Python задействуйте magic methods
    ??, так ли это обязательно?

    или это
    >>Обратите внимание что вы можете использовать выражение if...else в любом выражение.
    Это тоже врядли добавит читабельности
    • +1
      >>Обратите внимание что вы можете использовать выражение if...else в любом выражение.
      Это тоже врядли добавит читабельности

      Это гораздо читабельнее, чем тернарный условный оператор в других языках. Я при чтении кода всегда спотыкаюсь о конструкции вида a?b:c, потому что там, конечно, не a, b и c стоят, а что-нибудь посложнее. А так гораздо симпатичнее.
      Плюс применение в форме (func1 if y == 1 else func2)(arg1, arg2) выглядит вполне прилично и, что гораздо важнее, естественно. Обычно в таких ситуациях надо писать вилку из условий или делегировать выбор вызываемой функции в другую функцию. Если первое вполне ничего (и в большинстве случаев, конечно, так и надо писать), то второе и нужно редко, и читать неудобно (и незачем плодить сущности, кроме того).
  • +3
    На счёт скорости словарей:
    # coding: utf-8
    
    from collections import defaultdict
    
    def trydict():
        freqs = {}
        for c in "abracadabra":
            try:
                freqs[c] += 1
            except:
                freqs[c] = 1
    
    def getdict():
        freqs = {}
        for c in "abracadabra":
            freqs[c] = freqs.get(c, 0) + 1
    
    def defdict():
        freqs = defaultdict(int)
        for c in "abracadabra":
            freqs[c] += 1
    
    def indict():
        freqs = {}
        for c in "abracadabra":
            freqs[c] = freqs[c] + 1 if c in freqs else 1
    
    if __name__ == '__main__':
        from timeit import Timer
        t = Timer("trydict()", "from __main__ import trydict")
        print t.timeit()
        t = Timer("getdict()", "from __main__ import getdict")
        print t.timeit()
        t = Timer("defdict()", "from __main__ import defdict")
        print t.timeit()
        t = Timer("indict()", "from __main__ import indict")
        print t.timeit()
    


    7.2616994618
    3.58429660753
    3.52491727301
    2.65262847652
    • +1
      А это:
      c = Counter("abracadabra")
      

      Хоть и красиво, ещё медленнее, чем с try (около 10, относительно этих замеров).
    • +7
      Интересно, спасибо. Решил сравнить с PyPy:
      ➜ ~ time python dict_speed.py
      6.94295406342
      4.35391998291
      4.43011784554
      3.0556640625
      python dict_speed.py 18,81s user 0,02s system 99% cpu 18,828 total
      ➜ ~ time pypy dict_speed.py
      0.70256114006
      0.649827003479
      0.807382106781
      0.672923088074
      pypy dict_speed.py 2,87s user 0,02s system 99% cpu 2,893 total
      • 0
        Нда с pypy разница в подходах уже особого значения не имеет.

        у меня
        0.593121051788
        0.550466060638
        0.690548181534
        0.56803393364
    • 0
      Забавно, но следующий вариант просто рвет остальных конкурсантов по скорости:
      def mydict():
        freqs = {}
        for c in "abracadabra":
         if freqs.has_key(c):
          freqs[c]+=1
         else:
          freqs[c]=1
      

      Результат: 0.0511064953786

      что я делаю не так?
      • 0
        Ай ли? Запускаете не так, читайте внимательно с чего все началось и как тестится производительность.

        4.08951997757
        2.25080990791
        2.26123785973
        1.59414696693
        1.96405720711
  • +2
    У меня есть небольшое замечание, я обычно для проверки чёртности/нечётности использую не num % 2, а num & 1. Решил протестировать на интерпретаторе Python 2.7.3. Скорость выполнения моего вариант с & оказался чуточку быстрее, чем %. Написал я два скрипта следующих:
    Первый:
    # odd1.py
    numbers = range(30000000)
    numbers = [num for num in numbers if not num & 1]
    

    Второй:
    # odd2.py
    numbers = range(30000000)
    numbers = [num for num in numbers if not num % 2]
    

    И далее выпонение:
    time python odd1.py 
    
    real	0m4.704s
    user	0m4.228s
    sys	0m0.388s
    

    и

    time python odd2.py 
    
    real	0m5.122s
    user	0m4.708s
    sys	0m0.376s
    

    • НЛО прилетело и опубликовало эту надпись здесь
      • 0
        Да, именно спортивный интерес, не болеее. Эти, так сказать, эмпирические данные могут быть полезны программистам-олимпиадникам, которые используют Python (хотя, как я слышал, по перфомансу очень многих он не устраивает). & 1 — соглашусь, не особо очевидно.
  • +1
    По поводу x.reverse() есть одна опасность, которая по невнимательности может насолить. reverse() является методом списка и изменяет сам список, а не только возвращает перевёрнутую копию.
    Кроме того, есть ещё built-in функция reversed, которая возвращает итератор, идущий в обратном направлении по тому же списку. Были ещё всякие reviter, ireverse, inreverse, но сейчас ими никто не пользуется вроде.
    • +1
      > и изменяет сам список, а не только возвращает перевёрнутую копию.

      Не возвращает он ничего. Возвращает функция reversed(), а не метод списка.
      • 0
        Ну вот, ещё того хуже.
        Частный случай x[::-1] является средством выражения x.reverse()

        Совсем разные вещи получаются.

        Хотя, с другой стороны, так даже стройнее: одно для изменения на месте, одно для изменённой копии, одно для итератора — никаких пересечений и дублирования.
        • +2
          reversed и срезы не то чтобы пересекаются — они работают с разными «интерфейсами» объекта.

          Срез работает через метод __getitem__() объекта, причем этот метод должен уметь принимать экземпляр slice в качестве параметра.
          reversed() работает только с объектами, которые имеют и __getitem__() и __len__(), но зато reversed() работает поэлементно и делает это лениво (итератор). При этом reversed() вызывает __getitem__() с целочисленным аргументом.

          Таким образом, reversed(), в общем случае, предполагает, что источником данных для него будет объект-контейнер с произвольным доступом к содержимому.
          А срез можно реализовать и для объектов, представляющих потоковые данные — через пропуски ненужных элементов. Правда без буферизации не будут работать отрицательные шаги и срез от конца. Так работает itertools.islice — итератор-срез от итерируемого источника

          Ну и reversed()/срезы, это, так сказать, функциональный подход — результат отвязан от источника.
          А reverse(), это ООП подход — метод объекта, изменяющий его состояние.
  • +3
    «Вкратце этот дизайн иногда встречается.»
    «Использование многоточие для получения всех элементов, является нарушением принципа „Только Один Путь Достижения Цели“.»
    «Хотя в какой то момент оно идет слишком далеко.»
    Ад.
    • +1
      Лучше прочитать оригинал на нормальном английском языке. Переводчик не старался.
      А ещё, для выделения подразделов предпочтительнее использовать теги <h2>, <h3>, <h4> вместо списка <ul> с одним элементом.
  • –1
    Существует ряд других магических методов.
    кто-нибудь объясните, почему

    >>> from datetime import date, timedelta
    
    >>> 1 + 1
    2
    
    >>> date(2012, 01, 01) + timedelta(days=1)
    datetime.date(2012, 1, 2)
    

    но
    >>> range(0, 3, 1)
    [0, 1, 2]
    
    >>> range(date(2012,01,01), date(2012,01,03), timedelta(days=1))
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    TypeError: range() integer end argument expected, got datetime.date.
    • +1
      А как связана range() с магическими методами?

      Касательно магии сложения/вычитания: обычно операции сложения/вычитания работают над объектами одного типа (если для их класса реализовано соответствующее поведение), а datetime/timedelta, это скорее исключение: нельзя сложить две даты — это лишено смысла, но можно изменить дату на некоторое кол-во фиксированных единиц (дней/часов/минут...) и нельзя увеличить дату на n месяцев/лет — кол-во дней плавает.

      range() работает только с числами и «магию» не использует (за пределами операций над числами).
      При этом её аргумент «шаг» — необязателен. А как тогда как ф-ция должна определять, чем инкрементить дату?
      Можно написать всеядную «магическую» версию range() (я напишу «ленивый» вариант):
      def range_(from_, to_, step):
          while from_ < to_:
              yield from_
              from_ += step
      

      Этот вариант работать будет для дат/времени (да и для списков, скажем), но потребует обязательного указания объекта-инкремента. Такое поведение неявно, ИМХО.
      • 0
        просто у меня сложилось впечатление, что build-in функции в python, в основном, обобщенные: len(), min(), max()… — а специальные живут в отдельных модулях. кстати:

        >>> from datetime import date, timedelta
        >>> min(date(2012,01,01), date(2012, 01, 02))
        datetime.date(2012, 1, 1)
        
        >>> min(timedelta(days=3), timedelta(days=2))
        datetime.timedelta(2)
        

        а range([start], stop, [step]) какой-то очень специфичный. не понятно, только — зачем? ведь, как вы указали, обобщенный вариант реализуется тривиально.
        • 0
          len/min/max — это не функции даже, это обёртки над вызовом __len__/__lt__/__gt__ (ну может min/max посложнее чуток — там на входе итерируемый источник)
          Т.е. len() возвращает длину объекта, который может её предоставить, min()/max() сравнивают сравнимые объекты, а их sort() сортирует, int()/str() приводит к целому/строке те объекты, которые сами могут представлять себя целым/строкой. Всё логично.

          А range() именно такой, потому что имеет [start] и [step] — необязательные параметры, и такая сигнатура в общем случае не позволяет определить начальный элемент, и уж тем более шаг! Поэтому поддерживаемый типы аргументов ограничены числами, и начало/шаг имеют значения по-умолчанию. Ну и range() чаще всего используется в качестве источника индексов для прохода по индексируемому контейнеру, а тут как раз целые числа используются в качестве индекса, и начальный элемент контейнера обычно имеет индекс «0».

          Если бы можно было ввести такие маг.методы:
          __min__ — минимальный элемент данного типа
          __max__ — максимальный элемент данного типа
          __succ__(__pred__) — следующий(предыдущий) за текущим объектом элемент данного типа
          можно было-бы реализовать range более всеядным — по-умолчанию отсчет шел бы от __min__, а инкремент происходил бы через __succ__. Всё это реализуемо, но встроенные типы данных — нерасширяемы, поэтому смысла особого нет. Проще под ситуацию написать свой my_range(), c датой и дельтами :)

          P.S. Ну и в конце концов, в документации к range() указано, что он работает с числами — всё прозрачно.
    • 0
      В целом, «магические методы» — это в первую очередь Методы, и уж во вторую — «магические». И как любые методы — должны быть определены для объекта где-то в его иерархии наследования: «a + b» это, всего навсего, «a.__add__(b)», только с некоторой долей синтаксического сахара, отсюда и вся «волшебность». И для datetime определен метод __add__, который может принимать объект timedelta в качестве аргумента.

      А range() — обычная функция, реализованная с поддержкой чисел и ничего кроме. При этом ф-ция имеет проверку на вызов для других типов данных — и возбуждает исключение, причем вполне конкретное, с объяснением причины!
  • +1
    Таких статей уже было миллион. Перевод очень плохой.

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