Pull to refresh

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

Reading time8 min
Views114K

Предисловие


Я очень полюбил 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
>>>
Tags:
Hubs:
Total votes 92: ↑75 and ↓17+58
Comments31

Articles