Пожаловаться

Трюки и советы по Python, которые облегчат вашу жизнь

30429
Пожаловаться

Python – это мощный язык общего назначения. Давайте погрузимся в советы по Python, а также изучение трюков, потоков и библиотек.

Введение

Python и его библиотеки используются для автоматизации систем, для написания веб-приложений, а также в отраслях Big Data, аналитики и софтверной безопасности. Эта статья призвана показать малоизвестные советы по Python, чтобы наставить вас на путь быстрой разработки, более легкой отладки и общего удовольствия.

Как и в случае с любым языком, который вы изучаете, получаемый вами реальный ресурс – это не языковая мощь, а возможность использовать идиомы, библиотеки и общие знания Python-комьюнити.

Изучение стандартных типов данных

1. collections.namedtuple

Когда вам не нужно добавлять методы к классу, но все же вы хотите удобства foo.prop, пробуйте использовать подобие кортежа namedtuple. Вы заранее определяете поля, а затем можете создавать легковесный класс, который занимает меньше памяти, чем полный объект.

LightObject = namedtuple('LightObject', ['shortname', 'otherprop'])
m = LightObject()
m.shortname = 'athing'
> Traceback (most recent call last):
> AttributeError: can't set attribute

Вы не можете устанавливать атрибуты namedtuple, так же как вы не можете изменять членов кортежа. Вам необходимо установить атрибуты при создании экземпляра namedtuple.

LightObject = namedtuple('LightObject', ['shortname', 'otherprop'])
n = LightObject(shortname='something', otherprop='something else')
n.shortname # something

2. collections.defaultdict

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

login_times = {}
for t in logins:
    if login_times.get(t.username, None):
        login_times[t.username].append(t.datetime)
    else:
        login_times[t.username] = [t.datetime]

Выйдем за рамки того, что предлагает defaultdict, и установим вложенные ключи в качестве атрибутов.

normal_dict = {
    'a': {
        'b': {
            'c': {
                'd': {
                    'e': 'really really nested dict'
                }
            }
        }
    }
}

from addict import Dict
addicted = Dict()
addicted.a.b.c.d.e = 'really really nested'
print(addicted)
# {'a': {'b': {'c': {'d': {'e': 'really really nested'}}}}}

С этим сниппетом будет проще писать, чем со стандартным dict. Еще легче только с defaultdict:

from collections import defaultdict
default = defaultdict(dict)
default['a']['b']['c']['d']['e'] = 'really really nested dict' # fails

Кажется, что выглядит нормально, но на самом деле будет генерироваться исключение KeyError, потому что default['a'] – это dict, а не defaultdict. Нужно сделать defaultdict, который по умолчанию использует дефолтные словари.

Если вам нужен только счетчик по умолчанию, вы можете использовать класс collection.counter, который предоставляет некоторые удобные функции, такие как most_common.

3. Перечисление

Итерация по любому содержимому в Python проста (как и в любом другом языке) – простой цикл for:

drinks = ["coffee", "tea", "milk", "water"]
for drink in drinks:
    print("thirsty for", drink)
#thirsty for coffee
#thirsty for tea
#thirsty for milk
#thirsty for water

Очень часто требуются одновременно индекс элемента, и сам элемент. Программисты используют len() и range() для перебора списка по индексу, но есть более простой способ.

drinks = ["coffee", "tea", "milk", "water"]
for index, drink in enumerate(drinks):
    print("Item {} is {}".format(index, drink))
#Item 0 is coffee
#Item 1 is tea
#Item 2 is milk
#Item 3 is water

Функция перечисления возвращает, как индекс, так и элемент.

Управление потоком

Управляющие конструкции – это for, while, if-elif-else и try-except. Советы по Python и правильное использование этих структур помогут в большинстве случаев.

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

1. Исключения

Исключения в качестве управления потоком – это общий шаблон при работе с базами данных, сокетами, файлами или любым ресурсом, который является потенциально опасным. При использовании стандартных try и catch такая простая задача, как работа с базой данных, может превратиться в проблему:

try:
    # get API data
    data = db.find(id='foo') # may raise exception
    # manipulate the data
    db.add(data)
    # save it again
    db.commit() # may raise exception
except Exception:
    # log the failure
    db.rollback()

db.close()

Это определенно не то, что мы хотим, потому что исключение сработает еще до начала транзакции. Откат также не является верным ответом на отказ соединения, поэтому давайте разберем эти случаи.

Сначала мы рассмотрим данные:

try:
    # get API data
    data = db.find(id='foo') # may raise exception
except Exception:
    # log the failure and bail out
    log.warn("Could not retrieve FOO")
    return

# manipulate the data
db.add(data)

Теперь давайте используем советы по Python и изящно обернем commit:

try:
    db.commit() # may raise exception
except Exception:
    log.warn("Failure committing transaction, rolling back")
    db.rollback()
else:
    log.info("Saved the new FOO")
finally:
    db.close()

Условие finally дает понять, что db.close() будет выполняться всегда. Оглядываясь назад, мы видим, что весь код, связанный с сохранением наших данных, оказался в хорошей логической группировке на том же уровне отступов. Редактируя этот код позже, нам будет легко увидеть, что все строки привязаны к commit.

2. Контекст и контроль

Ранее мы видели управление потоком с использованием исключений. В общем случае алгоритм выглядит следующим образом:

  • Попытка получить ресурс (файл, сетевое соединение, что угодно).
  • Если это не удается, очистить все, что осталось.
  • В противном случае выполнить действия на ресурсе.
  • Записать что произошло.
  • Программа завершена.

Имея это в виду, давайте рассмотрим еще некоторые советы по Python и второй пример с базами данных из последнего раздела. Мы используем связку try-except-finally, чтобы убедиться, что любая транзакция, которую мы начали, была либо закоммичена, либо отменена.

try:
    # attempt to acquire a resource
    db.commit()
except Exception:
    # If it fails, clean up anything left behind
    log.warn("Failure committing transaction, rolling back")
    db.rollback()
else:
    # If it works, perform actions
    # In this case, we just log success
    log.info("Saved the new FOO")
finally:
    # Clean up
    db.close()
# Program complete

Наш предыдущий пример повторяет по шагам описанный выше алгоритм. Но как часто эта логика меняется? Не очень часто.

db = db_library.connect("fakesql://")
# as a function
commit_or_rollback(db)

# context manager
with transaction("fakesql://") as db:
    # retrieve data here
    # modify data here

2.1 Менеджер контекста

Менеджер контекста упрощает защиту некоторого блока, настраивая ресурсы (контекст), необходимые блоку во время выполнения. В нашем примере нам нужна транзакция базы данных, которая будет:

  • Подключаться к БД.
  • Начинаться в начале блока.
  • Коммитить или откатывать в конце блока.
  • Очищать в конце блока.

Метод __enter __() очень простой, поэтому начнем с него.

class DatabaseTransaction(object):
    def __init__(self, connection_info):
        self.conn = db_library.connect(connection_info)

    def __enter__(self):
        return self.conn

Метод __enter __ практически ничего не делает, только возвращает подключение к базе, которое мы можем использовать внутри блока для извлечения или сохранения данных.

Метод __init__  – это то место, где выполняется соединение с БД, и, если он не работает, блок вообще запускаться не будет.

2.2 Работа методов

Теперь рассмотрим, как транзакция будет завершаться в методе __exit __. Этот метод имеет больше обязанностей, т. к. он должен обрабатывать любые исключения в блоке и закрывать транзакцию.

def __exit__(self, exc_type, exc_val, exc_tb):
        if exc_type is not None:
            self.conn.rollback()

        try:
            self.conn.commit()
        except Exception:
            self.conn.rollback()
        finally:
            self.conn.close()

Теперь мы можем использовать нашу DatabaseTransaction как диспетчер контекстов для блока с действиями. В этом блоке будут запускаться методы __enter __  и __exit __, обрабатывать подключение к БД и сбрасывать его, когда мы закончим.

# context manager
with DatabaseTransaction("fakesql://") as db:
    # retrieve data here
    # modify data here

Чтобы улучшить нашу примитивную транзакцию, мы можем добавить обработку различных типов исключений. Даже в текущем состоянии этот код скрывает тонну сложностей, о которых вы беспокоитесь каждый раз, когда что-то достаете из базы данных.

3. Генераторы

Представленные в Python 2 генераторы – это следующие советы по Python и простой способ реализовать итератор, который не хранит сразу все его значения. Обычно функция в Python начинает свое выполнение, выполняет некоторые операции и возвращает результат (или ничего).

Генераторы бывают разные.

def my_generator(v):
    yield 'first ' + v
    yield 'second ' + v
    yield 'third ' + v

print(my_generator('thing'))
# <generator object my_generator at 0x....>

3.1 Ключевые слова

Вместо возврата мы используем yield-слова, что делает генератор особенным. При вызове my_generator('thing') вместо получения результата функции мы получаем объект генератора, который можно использовать везде, где вы могли бы использовать список или другой итератор.

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

for value in my_generator('thing'):
    print value

# first thing
# second thing
# third thing

gen = my_generator('thing')
next(gen)
# 'first thing'
next(gen)
# 'second thing'
next(gen)
# 'third thing'
next(gen)
# raises StopIteration exception

После создания экземпляра генератор ничего не делает, пока у него не попросят параметр. Он выполняется до первого yield и передает это значение обратившемуся, а затем ждет, сохраняя свое состояние, пока не будет запрошено другое значение.

3.2 Генератор Фибоначчи

Теперь давайте создадим генератор, который немного полезнее, чем просто возврат трех жестко закодированных элементов. Пример классического генератора – бесконечный генератор Фибоначчи, который мы попробуем сделать. Он начнется с 1 и возвращает сумму двух предыдущих чисел столько раз, сколько вы попросите.

def fib_generator():
    a = 0
    b = 1
    while True:
        yield a
        a, b = b, a + b

Нужно быть осторожными с конечным условием, когда используем генератор т. к.  он “зациклится” и будет бесконечно добавлять значения.

Теперь давайте используем наш генератор, чтобы посчитать первое число Фибоначчи, которое превышает 10 000.

min = 10000
for number in fib_generator():
    if number > min:
        print(number, "is the first fibonacci number over", min)
        break

Это было довольно просто, и мы можем сделать это число настолько большим, насколько захотим, и код все равно найдет первое число, большее X в последовательности Фибоначчи.

3.3 Пример с API

Теперь попробуем более реальный пример. В API пагинации есть распространенная практика ограничения и предотвращения отправки более 50 мегабайт при помощи JSON на мобильное устройство. Сначала мы объявим API, с которой работаем, а потом напишем генератор.

API, которое мы используем, называется Scream – это сервис, где пользователи могут обсуждать и оставлять отзывы о ресторанах, в которых они были. Оно выглядит так:

GET http://scream-about-food.com/search?q=coffee
{
    "results": [
        {"name": "Coffee Spot",
         "screams": 99
        },
        {"name": "Corner Coffee",
         "screams": 403
        },
        {"name": "Coffee Moose",
         "screams": 31
        },
        {...}
    ]
    "more": true,
    "_next": "http://scream-about-food.com/search?q=coffee?p=2"
}

Разработчики ввели ссылку на следующую страницу в ответе API, поэтому будет очень легко получить следующую страницу. Мы также можем покинуть эту страницу и перейти на первую. Чтобы получить данные, мы будем использовать библиотеку запросов в нашем генераторе для отображения результатов поиска.

Генератор имеет ограниченную логику и обрабатывает пагинацию примерно так:

  • Получает поисковой запрос.
  • Запрашивает API-интерфейс scream-about-food.
  • Повторяет попытку, если API не работает.
  • Получает результаты со страницы.
  • Получает следующую страницу, если есть.
  • Выходит, когда результатов больше нет.

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

import requests

api_url = "http://scream-about-food.com/search?q={term}"

def infinite_search(term):
    url = api_url.format(term)
    while True:
        data = requests.get(url).json()

        for place in data['results']:
            yield place

        # end if we've gone through all the results
        if not data['more']: break

        url = data['_next']

После того, как генератор создан, нужно получить условия поиска, а генератор будет строить запрос и показывать результат. Код, конечно, грубый – исключения не обрабатываются вообще, а если API не отвечает или пришел неизвестный JSON-ответ, то генератор вызовет исключение.

Несмотря на эти огрехи, мы можем использовать генератор для поиска номера позиции в поисковой выдаче нашего ресторана по поисковому запросу “кофе”.

# pass a number to start at as the second argument if you don't want
# zero-indexing
for number, result in enumerate(infinite_search("coffee"), 1):
    if result['name'] == "The Coffee Stain":
        print("Our restaurant, The Coffee Stain is number ", number)
        return
print("Our restaurant, The Coffee Stain didnt't show up at all! :(")

Генератор обрабатывает итерации на каждой странице поискового результата. Все, что нам нужно, – использовать встроенное перечисление, описанное ранее в статье, чтобы отслеживать количество результатов и выводить их.

В качестве упражнения добавим счетчик к бесконечному поиску infinite_search:

for result in infinite_search("coffee"):
    if result['name'] == "The Coffee Stain":
        print("Our restaurant, The Coffee Stain is number ", result['number'])
        return
print("Our restaurant, The Coffee Stain didn't show up at all! :(")

Если вы пишете на Python 3, то уже применяли генераторы при использовании стандартной библиотеки. Вызовы вроде dict.items() теперь возвращают генераторы вместо списков. Чтобы получить это поведение в Python 2, был добавлен dict.iteritems(), но он используется не так часто.

Совместимость Python 2 и 3

Переход от Python 2 к Python 3 может быть осуществлен для любого кода, но можно писать код, который работает в обеих версиях. Поддержку Python 2.7 продлили до 2020 года, но маловероятно, что многие новые функции будут поддерживаться. На данный момент рекомендуется писать код с поддержкой Python 2.7 и 3+, если только вы не сможете полностью отказаться от поддержки Python 2.

Подробное руководство по поддержке обеих версий смотрите в гайде на python.org.

Продолжим разбирать  советы по Python и рассмотрим наиболее распространенные вещи, с которыми вы могли столкнуться при попытке написать совместимый код, и разберемся, как использовать __future__ для их работы.

1. print или print()

Почти каждый разработчик, который переключился с Python 2 на 3, использовал неверный оператор печати. К счастью, вы можете стандартизировать использование print как функции (в стиле Python 3): просто импортируем print_function.

print "hello"  # Python 2
print("hello") # Python 3

from __future__ import print_function
print("hello") # Python 2
print("hello") # Python 3

2. Деление

По умолчанию поведение деления также изменилось в версии 3. В Python 2 при делении целых чисел будет выполняться целочисленное деление с отбрасыванием десятичных чисел после точки. Это были не совсем те советы по python, которые ожидало большинство пользователей, поэтому в Python 3 изменилось поведение чисел с плавающей точкой.

print(1 / 3) # Python 2
# 0
print(1 / 3) # Python 3
# 0.3333333333333333
print(1 // 3) # Python 3
# 0

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

from __future__ import division
print(1 / 3)# Python 2
# 0.3333333333333333
print(1 // 3)# Python 2
# 0
print(1 / 3) # Python 3
# 0.3333333333333333
print(1 // 3)# Python 3
# 0

Оригинал

Другие материалы по теме:

30429

Комментарии

Рекомендуем