Поширений синтаксис Python у науці про дані (просунутий рівень)
Останніми днями я читав книгу Data Science from Scratch (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) equals
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
Однак не рекомендується використовувати такі оператори без логіки виходу з циклу.
ПОРАДА
Одним з недоліків ітерації за допомогою генераторів є те, що елементи можна перебрати лише один раз від початку до кінця. Якщо ви хочете виконати кілька ітерацій, вам доведеться щоразу створювати новий генератор або використовувати список.
Другий спосіб створення генератора: використання виразу включення у дужках:
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) # встановлює початкове значення на 10
print random.random() # 0.57140259469
random.seed(10) # скидає початкове значення на 10
print random.random() # 0.57140259469 знову
Іноді ми також використовуємо функцію 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 дозволяє визначати класи, які інкапсулюють дані, та функції, що оперують цими даними. Іноді ми використовуємо їх, щоб зробити наш код зрозумілішим та лаконічнішим. Мабуть, найпростіше пояснити це, побудувавши приклад з великою кількістю коментарів. Припустимо, що в 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
Ми хочемо використати її для створення функції, яка приймає одну змінну та повертає результат степеня з основою 2, тобто exp(2, power).
Звісно, ми могли б визначити нову функцію за допомогою 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)
Аналогічно, фільтр реалізує функціонал 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)
Іноді виникають ситуації, коли під час ітерації по списку потрібно одночасно використовувати як елемент, так і його індекс:
# Не дуже "пітонічно" (не дуже лаконічно й елегантно)
for i in range(len(documents)):
document = documents[i]
do_something(i, document)
# Також не дуже "пітонічно" (не дуже лаконічно й елегантно)
i = 0
for document in documents:
do_something(i, document)
i += 1
Найлаконічніший підхід — використовувати метод enumerate, який генерує кортежі (index, element):
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 "позиційні аргументи:", args
print "іменовані аргументи:", kwargs
magic(1, 2, key="word", key2="word2")
# Результат виводу:
# позиційні аргументи: (1, 2)
# іменовані аргументи: {'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 у науці про дані (базовий рівень)