Часто используемый синтаксис Python в науке о данных (продвинутый уровень)

В эти дни я читал книгу Data Science from Scrach (PDF-версия), и это отличное, доступное для понимания введение в науку о данных. Одна из глав этой книги посвящена базовому синтаксису Python и продвинутым функциям, часто используемым в науке о данных. Мне понравилось, как четко и лаконично это было изложено, поэтому я решил перевести этот материал и сохранить его здесь в качестве заметки.
Часто используемый синтаксис Python в науке о данных (базовый уровень)
Часто используемый синтаксис Python в науке о данных (продвинутый уровень)

Эта глава посвящена продвинутому синтаксису и функциям Python, которые очень полезны при обработке данных (на основе Python 2.7).

Сортировка Sorting

Если вы хотите отсортировать список в Python, можно использовать метод sort() для самого списка. Если же вы не хотите изменять исходный список, воспользуйтесь функцией sorted(), которая вернет новый, уже отсортированный список:

x = [4,1,2,3]
y = sorted(x)       # y = [1,2,3,4], x не изменяется
x.sort()            # теперь x = [1,2,3,4]
# Методы sort или sorted по умолчанию сортируют списки по возрастанию.

Чтобы отсортировать список по убыванию, укажите параметр reverse = True.

Также можно задать собственную функцию сортировки, чтобы список упорядочивался по определенному ключу:

# Сортировка по абсолютному значению в порядке убывания
x = sorted([-4,1,-2,3], key=abs, reverse=True) # is [-4,3,-2,1]
# Сортировка по частоте встречаемости слов в порядке убывания
wc = sorted(word_counts.items(),
key=lambda (word, count): count,
reverse=True)

Списковые включения List Comprehensions

Часто возникает ситуация, когда нужно извлечь определенные элементы из списка для создания нового, либо изменить значения некоторых элементов, либо сделать и то, и другое. Идиоматический способ решения этой задачи в Python — это списковые включения (List Comprehensions):

even_numbers = [x for x in range(5) if x % 2 == 0]  # [0, 2, 4]
squares = [x * x for x in range(5)]                 # [0, 1, 4, 9, 16]
even_squares = [x * x for x in even_numbers]        # [0, 4, 16]

Аналогично, вы можете превратить список в словарь или множество:

square_dict = { x : x * x for x in range(5) }       # { 0:0, 1:1, 2:4, 3:9, 4:16 }
square_set = { x * x for x in [1, -1] }             # { 1 }

Если вам не нужны сами элементы списка, можно использовать нижнее подчеркивание в качестве переменной:

zeroes = [0 for _ in even_numbers] # такой же длины, как и список even_numbers

Списковые включения поддерживают множественные циклы for:

pairs = [(x, y)
    for x in range(10)
    for y in range(10)]    # Всего 100 пар: (0,0) (0,1) ... (9,8), (9,9)

Последующий цикл for может использовать результаты предыдущего for-цикла:

increasing_pairs = [(x, y)                      # Содержит только пары, где x < y
                    for x in range(10)          # range(lo, hi) эквивалентно
                    for y in range(x + 1, 10)]  # [lo, lo + 1, ..., hi - 1]

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

Генераторы и итераторы Generators and Iterators

Проблема со списками заключается в том, что они могут быстро разрастаться. Например, range(1000000) создаст список из миллиона элементов. Если обрабатывать данные по одному, это может занять слишком много времени (или привести к исчерпанию памяти). При этом вам, возможно, понадобятся лишь первые несколько элементов, и тогда остальные вычисления окажутся излишними.

Генераторы позволяют вам итерировать только по тем данным, которые действительно нужны. Вы можете создать генератор, используя функцию и выражение yield:

def lazy_range(n):
    """ленивая версия range"""
    i = 0
    while i < n:
        yield i
        i += 1

Дополнение от переводчика:
Генератор — это особый вид итератора, и ключевую роль в его работе играет выражение yield. Оно служит точкой приостановки и возобновления выполнения генератора, позволяя присваивать значения выражению yield и возвращать его значение. Любая функция, содержащая оператор yield, считается генератором. Когда генератор приостанавливает работу, он сохраняет текущее состояние выполнения и восстанавливает его при следующем вызове, чтобы получить следующее итерируемое значение. Использование итерации по списку может занять много адресного пространства, в то время как генератор занимает примерно одно адресное пространство, что позволяет значительно экономить память.

Следующий цикл будет потреблять по одному значению из yield до тех пор, пока они не закончатся:

for i in lazy_range(10):
    do_something_with(i)

(На самом деле, в Python есть встроенная функция, аналогичная _lazy_range_, называемая xrange в Python 2 и range в Python 3, которая ведет себя лениво.) Это означает, что вы можете создать бесконечную последовательность:

def natural_numbers():
    """Возвращает 1, 2, 3, ..."""
    n = 1
    while True:
        yield n
        n += 1

Однако не рекомендуется использовать такие выражения без логики выхода из цикла.

TIP

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

Второй способ создания генераторов: с помощью генераторного выражения в скобках:

lazy_evens_below_20 = (i for i in lazy_range(20) if i % 2 == 0)

Мы знаем, что метод items() в словарях возвращает список всех пар ключ-значение, но чаще для итерации мы используем метод-генератор iteritems(), который генерирует и возвращает пары ключ-значение по одной.

Случайность Randomness

При изучении науки о данных нам часто потребуется генерировать случайные числа, для этого достаточно импортировать модуль random:

import random
four_uniform_randoms = [random.random() for _ in range(4)]
# [0.8444218515250481,        # random.random() генерирует случайное число
# 0.7579544029403025,         # случайное число нормализовано, находится в диапазоне от 0 до 1
# 0.420571580830845,          # Эта функция является наиболее часто используемой для генерации случайных чисел
# 0.25891675029296335]

Если вы хотите получить воспроизводимые результаты, можно настроить модуль random так, чтобы он генерировал псевдослучайные (то есть детерминированные) числа, основываясь на внутреннем состоянии, заданном random.seed:

random.seed(10)           # set the seed to 10
print random.random()     # 0.57140259469
random.seed(10)           # reset the seed to 10
print random.random()     # 0.57140259469 again

Иногда мы также используем функцию random.randrange для генерации случайного числа в указанном диапазоне:

random.randrange(10)      # Выбирает случайное число из range(10) = [0, 1, ..., 9]
random.randrange(3, 6)    # Выбирает случайное число из range(3, 6) = [3, 4, 5]

Есть и другие удобные методы, например, random.shuffle перемешивает элементы в списке, создавая новый, случайно упорядоченный список:

up_to_ten = range(10)
random.shuffle(up_to_ten)
print up_to_ten
# [2, 5, 1, 9, 7, 3, 8, 6, 4, 0] (Ваш результат, скорее всего, будет отличаться)

Если вы хотите случайно выбрать один элемент из списка, можно использовать метод random.choice:

my_best_friend = random.choice(["Alice", "Bob", "Charlie"]) # Я получил "Bob"

Если вы хотите сгенерировать случайную последовательность, но при этом не изменять исходный список, используйте метод random.sample:

lottery_numbers = range(60)
winning_numbers = random.sample(lottery_numbers, 6) # [16, 36, 10, 6, 25, 9]

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

four_with_replacement = [random.choice(range(10))
                         for _ in range(4)]
# [9, 4, 4, 2]

Регулярные выражения Regular Expressions

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

import re
print all([                                 # Все следующие утверждения возвращают True, потому что
    not re.match("a", "cat"),               # * 'cat' не начинается с 'a'
    re.search("a", "cat"),                  # * 'cat' содержит букву 'a'
    not re.search("c", "dog"),              # * 'dog' не содержит букву 'c'
    3 == len(re.split("[ab]", "carbs")),    # * разделяет слово на три части ['c','r','s'] по 'a' или 'b'
    "R-D-" == re.sub("[0-9]", "-", "R2D2")  # * заменяет цифры дефисом
    ])                                      # Выводит True

Объектно-ориентированное программирование Object-Oriented Programming

Как и многие другие языки, Python позволяет определять классы, которые инкапсулируют данные, и функции для работы с ними. Иногда мы используем их, чтобы сделать наш код более чистым и лаконичным. Проще всего объяснить это на примере с большим количеством комментариев. Предположим, у нас нет встроенного типа set в Python; мы могли бы захотеть создать свой собственный класс Set. Какими функциями должен обладать такой класс? Например, имея Set, мы должны иметь возможность добавлять в него элементы, удалять их и проверять, содержит ли он определенное значение. Итак, мы создадим все эти функции в качестве методов класса. Таким образом, мы сможем обращаться к этим методам через точку после объекта Set:

# По соглашению, имена классов записываются в _PascalCase_ (Верхний Верблюжий Регистр)
class Set:
    # Это методы класса
    # Каждый метод класса имеет первым параметром "self" (еще одно соглашение)
    # "self" соответствует конкретному объекту Set, с которым мы работаем

    def __init__(self, values=None):
        """Это функция-конструктор
        Вызывается каждый раз, когда вы создаете новый Set
        Можно вызывать так:
        s1 = Set() # пустое множество
        s2 = Set([1,2,2,3]) # инициализирует множество заданными значениями"""
        self.dict = {} # Каждый экземпляр Set имеет свой собственный атрибут dict
        # Мы используем этот атрибут для отслеживания членов
        if values is not None:
            for value in values:
            self.add(value)

    def __repr__(self):
        """Это строковое представление объекта Set
        Вы можете получить его, набрав объект в командной строке Python или передав объект в функцию str()"""
        return "Set: " + str(self.dict.keys())

    # Мы будем представлять принадлежность к множеству, используя элементы как ключи в self.dict и устанавливая значение True
    def add(self, value):
        self.dict[value] = True

    # Если аргумент является ключом в словаре, соответствующее значение находится в Set
    def contains(self, value):
        return value in self.dict

    def remove(self, value):
        del self.dict[value]

Затем мы можем использовать Set следующим образом:

s = Set([1,2,3])
s.add(4)
print s.contains(4)     # True
s.remove(3)
print s.contains(3)     # False

Функциональные инструменты Functional Tools

Частичное применение функций partial

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

def exp(base, power):
    return base ** power

Мы хотим использовать её для создания функции, которая принимает одну переменную и выдает результат степенной функции exp(2, power) с основанием 2.

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

def two_to_the(power):
  return exp(2, power)

Более элегантное решение — использовать метод functools.partial:

from functools import partial
two_to_the = partial(exp, 2)      # Теперь эта функция принимает только один аргумент
print two_to_the(3)               # 8

Если указаны имена, partial также можно использовать для заполнения других параметров:

square_of = partial(exp, power=2)
print square_of(3)                # 9

Если вы попытаетесь неправильно использовать параметры внутри функции, программа быстро станет запутанной, поэтому старайтесь избегать такого поведения.

Отображение map

Иногда мы также используем функции map, reduce и filter в качестве альтернативы списковым включениям:

def double(x):
    return 2 * x

xs = [1, 2, 3, 4]
twice_xs = [double(x) for x in xs]      # [2, 4, 6, 8]
twice_xs = map(double, xs)              # То же самое
list_doubler = partial(map, double)     # Функция для удвоения списка
twice_xs = list_doubler(xs)             # Также [2, 4, 6, 8]

Метод map также можно использовать для отображения функций с несколькими аргументами на несколько списков:

def multiply(x, y): return x * y

products = map(multiply, [1, 2], [4, 5])  # [1 * 4, 2 * 5] = [4, 10]

Фильтр filter

Аналогично, filter выполняет ту же функцию, что и if в списковых включениях:

def is_even(x):
    """Возвращает True, если x четное, и False, если x нечетное"""
    return x % 2 == 0

x_evens = [x for x in xs if is_even(x)]   # [2, 4]
x_evens = filter(is_even, xs)             # То же самое
list_evener = partial(filter, is_even)    # Эта функция реализует фильтрацию
x_evens = list_evener(xs)                 # Также [2, 4]

Свертка reduce

Метод reduce последовательно объединяет первый и второй элементы списка, затем объединяет результат с третьим элементом и повторяет этот процесс до тех пор, пока не будет получен единственный результат:

x_product = reduce(multiply, xs)          # = 1 * 2 * 3 * 4 = 24
list_product = partial(reduce, multiply)  # Эта функция выполняет свертку списка
x_product = list_product(xs)              # Также 24

Перечисление enumerate

Иногда возникает ситуация, когда при итерации по списку нужно одновременно использовать как сам элемент, так и его индекс:

# Не очень Pythonic (не так лаконично и изящно)
for i in range(len(documents)):
    document = documents[i]
    do_something(i, document)

# Тоже не очень Pythonic (не так лаконично и изящно)
i = 0
for document in documents:
    do_something(i, document)
    i += 1

Самый лаконичный способ — использовать метод enumerate, который генерирует кортежи (индекс, элемент):

for i, document in enumerate(documents):
    do_something(i, document)

Аналогично, если нужен только индекс:

for i in range(len(documents)): do_something(i)   # Не лаконично
for i, _ in enumerate(documents): do_something(i) # Лаконично

В дальнейшем мы будем часто использовать этот метод.

Сжатие и распаковка аргументов zip and Argument Unpacking

Сжатие zip

Мы часто сжимаем два или более списков. Сжатие — это фактически преобразование нескольких списков в один список из соответствующих кортежей:

list1 = ['a', 'b', 'c']
list2 = [1, 2, 3]
zip(list1, list2)       # получим [('a', 1), ('b', 2), ('c', 3)]

Распаковка аргументов Argument Unpacking

Если списки имеют разную длину, процесс сжатия остановится по концу самого короткого списка. Вы также можете использовать забавный трюк для “распаковки” списков:

pairs = [('a', 1), ('b', 2), ('c', 3)]
letters, numbers = zip(*pairs)

Здесь звездочка * используется для распаковки аргументов, она берет элементы pairs и передает их как отдельные аргументы в zip. Следующий вызов имеет тот же эффект:

zip(('a', 1), ('b', 2), ('c', 3))  # возвращает [('a','b','c'), ('1','2','3')]

Распаковку аргументов также можно использовать с другими функциями:

def add(a, b): return a + b

add(1, 2)           # Возвращает 3
add([1, 2])         # Вызовет ошибку
add(*[1, 2])        # Возвращает 3

Хотя это и не всегда практично, это хороший способ сделать код более лаконичным.

Передача произвольного количества аргументов args and kwargs

Предположим, мы хотим создать функцию высшего порядка, которая принимает старую функцию и возвращает новую, где новая функция — это старая функция, умноженная на 2:

def doubler(f):
    def g(x):
      return 2 * f(x)
    return g

Пример работы:

def f1(x):
    return x + 1

g = doubler(f1)
print g(3)        # 8 (== ( 3 + 1) * 2)
print g(-1)       # 0 (== (-1 + 1) * 2)

Однако, если передаваемых аргументов больше одного, этот метод становится неудобным:

def f2(x, y):
    return x + y

g = doubler(f2)
print g(1, 2) # Вызовет ошибку TypeError: g() takes exactly 1 argument (2 given)

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

def magic(*args, **kwargs):
    print "unnamed args:", args
    print "keyword args:", kwargs
magic(1, 2, key="word", key2="word2")
# Результат вывода:
# unnamed args: (1, 2)
# keyword args: {'key2': 'word2', 'key': 'word'}

Когда мы определяем функцию таким образом, args (сокращение от arguments) — это кортеж, содержащий неименованные аргументы, а kwargs (сокращение от keyword arguments) — это словарь, содержащий именованные аргументы.

Их также можно использовать в случаях, когда передаваемые аргументы представляют собой список (или кортеж) или массив:

def other_way_magic(x, y, z):
    return x + y + z

x_y_list = [1, 2]
z_dict = { "z" : 3 }
print other_way_magic(*x_y_list, **z_dict)    # 6

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

def doubler_correct(f):
    """Работает независимо от того, что такое f"""
    def g(*args, **kwargs):
        """Независимо от количества параметров, эта функция корректно передаст их в f"""
        return 2 * f(*args, **kwargs)
    return g

g = doubler_correct(f2)
print g(1, 2) # 6

Добро пожаловать в мир науки о данных!

Дзынь! Поздравляем, вы открыли дверь в новый мир! Теперь можно с удовольствием приступать к делу~

Связанные статьи:

Часто используемый синтаксис Python в науке о данных (базовый уровень)