Вещи, о которых следует помнить, программируя на 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 кармы, и сохранить черновик не позволяет, поэтому выкладываю как есть. Просьба все замечания по переводу и орфографии присылать в личку, и сильно не пинать =)
    Поделиться публикацией
    Похожие публикации
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама
    Комментарии 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
                          Таких статей уже было миллион. Перевод очень плохой.

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