Некоторые возможности Python о которых вы возможно не знали

Предисловие


Я очень полюбил Python после того, как прочитал книгу Марка Лутца «Изучаем Python». Язык очень красив, на нем приятно писать и выражать собственные идеи. Большое количество интерпретаторов и компиляторов, расширений, модулей и фреймворков говорит о том, что сообщество очень активно и язык развивается. В процессе изучения языка у меня появилось много вопросов, которые я тщательно гуглил и старался понять каждую непонятую мной конструкцию. Об этом мы и поговорим с вами в этой статье, статья ориентирована на начинающего Python разработчика.


Немного о терминах


Начну пожалуй с терминов, которые часто путают начинающих Python программистов.

List comprehensions или генераторы списков возвращают список. Я всегда путал генераторы списков и выражения — генераторы (но не генераторы выражений!). Согласитесь, по русский звучит очень похоже. Выражения — генераторы это generator expressions, специальные выражения, которые возвращают итератор, а не список. Давайте сравним:

f = (x for x in xrange(100)) # выражение - генератор
c = [x for x in xrange(100)] # генератор списков


Это две совершенно разные конструкции. Первый возвращает генератор (то есть итератор), второй обычный список.

Generators или генераторы это специальные функции, которые возвращают итератор. Что бы получить генератор нужно возвратить функции значение через yield:

def prime(lst):
    for i in lst:
        if i % 2 == 0:
            yield i

>>> f = prime([1,2,3,4,5,6,7])
>>> list(f)
[2, 4, 6]
>>> next(f)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration
>>>


Кстати, в Python 3.3 появилась новая конструкция yield from. Совместное использование yield и for используется настолько часто, что эти две конструкции решили объединить.

def generator_range(first, last):
    for i in xrange(first, last):
        yield i

def generator_range(first, last):
    yield from range(first, last)


Что такое контекстные менеджеры и для чего они нужны?


Контекстные менеджеры это специальные конструкции, которые представляют из себя блоки кода, заключенные в инструкцию with. Инструкция with создает блок используя протокол контекстного менеджера, о котором мы поговорим далее в этой статье. Простейшей функцией, использующей данный протокол является функция open(). Каждый раз, как мы открываем файл нам необходимо его закрыть, что бы вытолкнуть выходные данные на диск (на самом деле Python вызывает метод close() автоматически, но явное его использование является хорошим тоном). Например:

fp = open("./file.txt", "w")
fp.write("Hello, World")
fp.close()


Что бы каждый раз не вызывать метод close() мы можем воспользоваться контекстным менеджером функции open(), который автоматически закроет файл после выхода из блока:

with open("./file.txt", "w") as fp:
    fp.write("Hello, World")


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

In [4]: class Hello:
   ...:     def __del__(self):
   ...:         print 'destructor'
   ...:

In [5]: f = Hello()

In [6]: c = Hello()

In [7]: e = Hello()

In [8]: del e
destructor

In [9]: del c
destructor

In [10]: c = f

In [11]: e = f

In [12]: del f # <- деструктор не вызывается


Решим эту задачу через контекстные менеджеры:

In [1]: class Hello:
   ...:     def __del__(self):
   ...:         print u'деструктор'
   ...:     def __enter__(self):
   ...:         print u'вход в блок'
   ...:     def __exit__(self, exp_type, exp_value, traceback):
   ...:         print u'выход из блока'
   ...:

In [2]: f = Hello()

In [3]: c = f

In [4]: e = f

In [5]: d = f

In [6]: del d

In [7]: del e

In [8]: del c

In [9]: del f # <- деструктор вызвался тогда когда все ссылки на объект были удалены
деструктор


Теперь попробуем вызвать менеджер контекста:

In [10]: with Hello():
   ....:     print u'мой код'
   ....:
вход в блок
мой код
выход из блока
деструктор


Мы увидели, что произошел гарантированный выход из блока после выполнения нашего кода.

Протокол контекстного менеджера


Мы уже кратко рассмотрели протокол контекстного менеджера написав небольшой класс Hello. Давайте теперь разберемся в протоколе более подробно. Что бы объект стал контекстным менеджером в его класс обязательно нужно включить два метода: __enter__ и __exit__. Первый метод выполняется до входа в блок. Методу можно возвратить текущий экземпляр класса, что бы к нему можно было обращаться через инструкцию as.

Метод __exit__ выполняется после выхода из блока with, и он содержит три параметра — exp_type, exp_value и exp_tr. Контекстный менеджер может вылавливать исключения, которые были возбуждены в блоке with. Мы можем вылавливать только нужные нам исключения или подавлять ненужные.

class Open(object):
    def __init__(self, file, flag):
        self.file = file
        self.flag = flag

    def __enter__(self):
        try:
            self.fp = open(self.file, self.flag)
        except IOError:
            self.fp = open(self.file, "w")
        return self.fp

    def __exit__(self, exp_type, exp_value, exp_tr):
        """ подавляем все исключения IOError """
        if exp_type is IOError:
            self.fp.close() # закрываем файл
            return True
        self.fp.close() # закрываем файл

with Open("asd.txt", "w") as fp:
    fp.write("Hello, World\n")


Переменная exp_type содержит в себе класс исключения, которое было возбуждено, exp_value — сообщение исключения. В примере мы закрываем файл и подавляем исключение IOError посредством возврата True методу __exit__. Все остальные исключения в блоке мы разрешаем. Как только наш код подходит к концу и блок заканчивается вызывается метод self.fp.close(), не зависимо от того, какое исключение было возбуждено. Кстати, внутри блока with можно подавлять и такие исключения как NameError, SyntaxError, но этого делать не стоит.

Протоколы контекстных менеджеров очень просты в использовании, но для обычных задач есть еще более простой способ, который поставляется вместе со стандартной библиотекой питона. Далее мы рассмотрим пакет contextlib.

Пакет contextlib


Создание контекстных менеджеров традиционным способом, то есть написанием классов с методами __enter__ и __exit__ не одна из сложных задач. Но для тривиального кода написание подобных классов требует больше возьни. Для этих целей был придуман декоратор contextmanager(), входящий в состав пакета contextlib. Используя декоратор contextmanager() мы можем из обычной функции сделать контекстный менеджер:

import contextlib
@contextlib.contextmanager
def context():
    print u'вход в блок'
    try:
        yield {}
    except RuntimeError, err:
        print 'error: ', err
    finally:
        print u'выход из блока'


Проверим работоспособность кода:

In [8]: with context() as fp:
   ...:     print u'блок'
   ...:
вход в блок
блок
выход из блока


Попробуем возбудить исключение внутри блока.

In [14]: with context() as value:
   ....:     raise RuntimeError, 'Error'
   ....:
вход в блок
error:  Error
выход из блока

In [15]:


Как видно из примера, реализация с использованием классов практически ничем не отличается по функциональности от реализации с использованием декоратора contextmanager(), но использование декоратора намного упрощает наш код.

Еще один интересный пример использования декоратора contextmanager():

import contextlib
@contextlib.contextmanager
def bold_text():
    print '<b>'
    yield # код из блока with выполнится тут
    print '</b>'

with bold_text():
    print "Hello, World"


Результат:
<b>Hello, World</b>


Похоже на блоки в руби не так ли?

И напоследок поговорим о вложенных контекстах. Вложенные контексты позволяют управлять несколькими контекстами одновременно. Например:

import contextlib
@contextlib.contextmanager
def context(name):
    print u'вход в контекст %s' % (name)
    yield name # наш блок
    print u'выход из контекста %s' % (name)

with contextlib.nested(context('first'), context('second')) as (first, second):
    print u'внутри блока %s %s' % (first, second)


Результат:

вход в контекст first
вход в контекст second
внутри блока first second
выход из контекста second
выход из контекста first

Аналогичный код без использования функции nested:

first, second = context('first'), context('second')
with first as first:
    with second as second:
        print u'внутри блока %s %s' % (first, second)


Этот код хоть и похож на предыдущий, в некоторых ситуациях он будет работать не так как нам хотелось бы. Объекты context('first') и context('second') вызываются до входа в блок, поэтому мы не сможем перехватывать исключения, которые были возбуждены в этих объектах. Согласитесь, первый вариант намного компактнее и выглядит красивее. А вот в Python 2.7 и 3.1 функция nested устарела и была добавлена новая синтаксическая конструкция для вложенных контекстов:

with context('first') as first, context('second') as second:
    print u'внутри блока %s %s' % (first, second)


range и xrange в Python 2.7 и Python 3


Известно, что Python 2.7 range возвращает список. Думаю все согласятся, что хранить большие объемы данных в памяти нецелесообразно, поэтому мы используем функцию xrange, возвращающий объект xrange который ведет себя почти так же как и список, но не хранит в памяти все выдаваемые элементы. Но меня немного удивило поведение xrange в Python 2.x, когда функции передаются большие значения. Давайте посмотрим на пример:

>>> f = xrange(1000000000000000000000)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
OverflowError: Python int too large to convert to C long
>>>


Python нам говорит о том, что int слишком длинный и он не может быть переконвертирован в C long. Оказывается у Python 2.x есть ограничения на целое число, в этом мы можем убедиться просмотрев константу sys.maxsize:

>>> import sys
>>> sys.maxsize
9223372036854775807
>>>


Вот оно максимальное значение целого числа:

>>> import sys
>>> sys.maxsize+1
9223372036854775808L
>>>


Python аккуратно переконвертировал наше число в long int. Не удивляйтесь, если xrange в Python 2.x будет вести себя иначе при больших значениях.

В Python 3.3 целое число может быть бесконечно большим, давайте проверим:

>>> import sys
>>> sys.maxsize
9223372036854775807
>>> range(sys.maxsize+1)
range(0, 9223372036854775808)
>>>


Конвертирование в long int не произошло. Вот еще пример:

>>> import sys
>>> sys.maxsize + 1
9223372036854775808
>>> f = sys.maxsize + 1
>>> type(f)
<class 'int'>
>>>


В Python 2.7

>>> import sys
>>> type(sys.maxsize + 1)
<type 'long'>
>>>


Не очевидное поведение некоторых конструкций


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

>>> f = [[]] * 3
>>> f[0].append('a')
>>> f[1].append('b')
>>> f[2].append('c')
>>>


Каков будет результат выполнения данной конструкции? Неподготовленный разработчик сообщит о результате: [['a'], [b'], [c']]. Но на самом деле мы получаем:

>>> print f
[['a', 'b', 'c'], ['a', 'b', 'c'], ['a', 'b', 'c']]
>>>


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

>>> c = [[], [], []]
>>> hex(id(c[0])), hex(id(c[1])), hex(id(c[2]))
('0x104ede7e8', '0x104ede7a0', '0x104ede908')
>>>

>>> hex(id(f[0])), hex(id(f[1])), hex(id(f[2]))
('0x104ede710', '0x104ede710', '0x104ede710')
>>>


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

Второй пример уже рассматривался на хабре, но мне захотелось включить его в статью. Посмотрим на lambda — функцию, которую мы будет прогонять через цикл for, и помещать каждую функцию в словарь:

>>> tmp = {}
>>> for i in range(10):
...     tmp[i] = lambda: i
>>> tmp[0]()
9
>>> tmp[1]()
9
>>>


В пределах lambda функции переменная i замыкается и как бы создается экземпляр еще одной переменной i в блоке lambda — функции, которая является ссылкой на переменную i в цикле for. Каждый раз когда счетчик цикла for меняется, меняются и значения во всех lambda функциях, поэтому мы получаем значение i-1 во всех функциях. Исправить это легко, явно передав lambda функции в качестве первого параметра значение по умолчанию — переменную i:

>>> tmp = {}
>>> for i in range(10):
...     tmp[i] = lambda i = i: i
>>> tmp[0]()
0

>>> tmp[1]()
1
>>>
Метки:
Поделиться публикацией
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама
Комментарии 31
  • +2
    Кто этот человек, который не знал о таких возможностях? Это же все, практически курс туториала по питону
    • +8
      Опытные программисты не найдут в ней ничего нового. Статья для начинающих
      • +13
        Добавьте тогда тег «tutorial» и сделайте предвступление об этом.
        А так по заголовку ожидания были завышенными.
        • +4
          Программирую на python больше 2 лет. Не знаю, могу ли я назвать себя опытным, но из статьи узнал много полезного.
          • +3
            У меня плохие новости. За два года вы должны были продвинуться гораздо гораздо глубже. Это же первые месяцы, максимум.
            • +2
              Прелесть питона, то что для начала работы, хватает порой, выучить статью на вики по синтаксису. Многое в нём понятно интуитивно. Это если знаешь другой ЯП.
              • 0
                С вами не согласен. Метаклассы например достаточно сложная тема. На уровне синтаксиса можно выучить почти любой язык за короткий срок, но когда дело доходит до «подводных камней», статьи из вики уже бывает недостаточно.
                • 0
                  Метаклассы это очень редко, многие успешно клепающий к примеры сайты на Джанге, Пирамиде или Флаке знают про них только то что они существуют)
                  • 0
                    Я не спорю. Многие вещи и через годы работы можно открывать для себя. Но я имел ввиду именно «начать».
                    • 0
                      Просто у меня был уже опыт изучения языков подобным способом. В голове образуется какая то каша, а знания остаются поверхностными. Вики можно использовать как справочник, нежели как учебник. Вот в той же джанги ModelBase — метакласс.
                      • 0
                        На всякий случай про метаклассы на stack-overflow есть очень хорошее объяснение :) stackoverflow.com/questions/100003/what-is-a-metaclass-in-python
                        • 0
                          Кажется на хабре тоже перевод лежит. В книге Марка Лутца «Изучаем Python» метаклассам посвящена целая глава )
        • +4
          Я на питон только начинаю смотреть. И он прекрасен. Спасибо!!!
          • 0
            Я когда с php перешёл только на питон. При том сразу же в действующий старый проект. По началу ругался. Так как примерно 8 лет за плечами на пхп и две недели на питоне. Не давали возможности быстро решать поставленные задачи. В то время когда как решить их на пхп я знал. Но уже спустя 2 месяца, не существовала способа, заставить меня вернуться на пхп.
          • +3
            В свое время это было бы мне очень полезно. В питоне много тонкостей и автор рассмотрел те что вполне пользительны и неочевидны для годовалых питонистов, а возможно и опытнее.
            • +4
              Вот это в Python 2.x почти всегда неправильно (то, что используется Python 2.x, можно понять из синтаксиса print):

              print 'выход из блока'

              Если unicode_literals не импортированы сверху файла, то 'выход из блока' тут — это байтовая строка, которая закодирована в ту кодировку, в которой хранится исходный код.

              print байтовые строки выводит «как есть», никак не преобразуя — просто передает байты на выход. Поэтому если кодировка, которую использует консоль, не совпадает с кодировкой исходного кода, то консоль будет декодирует байты неправильно, и получим мусор на выходе. С ascii текстом это обычно не проблема, т.к. большинство кодировок кодирую ascii одинаково, но тут есть не-ascii символы. Для того, чтобы print использовал кодировку консоли, нужно печатать юникодные строки:

              print u'выход из блока'

              В этом случае Python перед тем, как передать строку на выход, закодирует ее именно в кодировку консоли. Там не все так просто (иногда кодировку консоли сложно определить), но все же печать юникодных строк гораздо надежнее.

              Тут еще может смутить то, что если код набрать в интерпретаторе (REPL), то обычно print 'выход из блока' работает, т.к. константа берется из консоли, а sys.stdin.encoding обычно совпадает с sys.stdout.encoding. А если потом код сохранить в py файл, то все может сломаться, т.к. кодировка файла вполне может не совпадать с кодировкой консоли.
              • 0
                Да вы правы, большое спасибо! Поставил юникодные литералы.
              • 0
                >>Оказывается у Python 2.x есть ограничения на целое число, в этом мы можем убедиться просмотрев константу sys.maxsize

                Не очень хорошо сформулировано. Ведь задавать и использовать большие числа можно без проблем.
                >>> import sys
                >>> print sys.version
                2.7.4 (default, Sep 26 2013, 03:20:26) 
                >>> print sys.maxsize*10**10+1
                92233720368547758070000000001
                

                • 0
                  Большие числа конвертируются в long
                  >>> import sys
                  >>> sys.version
                  '2.7.1 (r271:86832, Jul 31 2011, 19:30:53) \n[GCC 4.2.1 (Based on Apple Inc. build 5658) (LLVM build 2335.15.00)]'
                  >>> sys.maxsize * 10 ** 10 + 1
                  92233720368547758070000000001L
                  >>> type(sys.maxsize * 10 ** 10 + 1)
                  <type 'long'>
                  >>>
                  


                  В Python3

                  >>> import sys
                  >>> type(sys.maxsize * 10 ** 10 + 1)
                  <class 'int'>
                  >>> sys.maxsize * 10 ** 10 + 1
                  92233720368547758070000000001
                  >>> sys.version
                  '3.3.2 (v3.3.2:d047928ae3f6, May 13 2013, 13:52:24) \n[GCC 4.2.1 (Apple Inc. build 5666) (dot 3)]'
                  >>>
                  
                  • 0
                    Тем не менее, говорить о том, что в Python 3 конвертации не произошло, не следует. Если вы говорите, что её не произошло, то это намекает на то, что она могла произойти. Но на самом деле конвертации не произошло, потому что в Python 3 объединили два типа.
                  • 0
                    Ну у меня в 2.7.4 суффикса L нет, хотя тип, конечно, long.

                    Я писал про то, что не совсем корректно говорить, что «есть ограничения на целое число». long это тоже целое.
              • +5
                g <название языка программирования> hidden features stackoverflow
                • +4
                  Но статья познавательная, хорошо написано.
                • 0
                  Написано хорошо, но заголовок… Извини, но это чуть больше чем азы, а не некоторые возможности о которых мы не знали)
                  • +1
                    Самому стыдно за заголовок )
                  • 0
                    Пишу Python 3. Использую исключительно для научных вычислений (SciPy + NumPy etc.). Вчера писал контест на codeforces (я не олимпиадник и рейтинг у меня низкий, но занимаюсь иногда, чтобы мозг размять) и решал задачу, где нужен был dict. И меня дико удивило, что я на третьем питоне не мог сдать задачу — не проходила по времени. После контеста посмотрел на чем сдают люди, и перписал на Python 2.7. Программа стала работать быстрее чем в 2 раза и кушать памяти на 500 Кб меньше. Вот этого я точно не знал о Python.
                  • 0
                    В прошлом топике про неочевидные особенности Питона дали ссылку на alexbers.com/python_quiz/ — там, действительно, хорошая подборка таких «подводных камней».

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