Недавно я начал оттачивать владение языком программирования Python.
Я хотел изучить продвинутые паттерны, идиомы и методы
программирования. Начал я с чтения книг по продвинутому Python, но
информация, похоже, не откладывалась в голове без применения
навыков. Хотелось иметь возможность задавать вопросы эксперту, пока
учусь, а такую возможность трудно найти! Тогда ко мне и пришла
идея: что, если я найду проект с открытым и достаточно продвинутым
кодом и напишу документацию и тесты? Я сделал ставку, что это
заставит меня изучать все очень глубоко, а поддерживающие проект
люди оценит мою работу и будут готовы ответить на мои вопросы.
Предыстория
Поиском такого проекта и написанием документации с тестами к нему я
занимался целый месяц и такое обучение было самым эффективным из
всех, что я пробовал. Я обнаружил, что написание документации
заставило меня глубоко понять не только то, что делает код, но и
то,
почему код работает именно так, как он работает, а
также исследовать крайние случаи во время написания тестов. Самое
главное, что я мог задавать вопросы, когда застрял, а люди были
готовы посвятить мне дополнительное время, зная, что их разъяснения
служило тому, чтобы сделать код доступнее! Оказывается, выбранная
мной библиотека,
fastcore одна
из самых увлекательных в Python, с которыми я работал. Ее цели и
задачи действительно уникальны.
fastcore это
основа многих проектов
fast.ai. Самое
главное: fastcore расширяет Python, стремясь к устранению
шаблонного кода и добавлению полезной функциональности для общих
задач. В этом посте я выделю некоторые из моих любимых инструментов
fastcore вместо того, чтобы делиться тем, что я узнал о языке. Моя
задача вызвать интерес к этой библиотеке, и, надеюсь, мотивировать
вас к ознакомлению с документацией после прочтения статьи.
Чем интересна fastcore?
- Ознакомление с идеями из других языков прямо в
Python: Я постоянно слышу, что полезно изучать другие
языки, чтобы стать лучшим программистом. Мне было трудно изучать
другие языки с практической точки зрения, потому что я не мог
применять их на работе. Fastcore расширяет Python, чтобы включить в
него паттерны из разных языков: Julia, Ruby и Haskell. Теперь,
когда я понимаю эти инструменты, у меня появилась мотивация изучать
другие языки.
- Новый набор прагматичных инструментов:
fastcore включает в себя утилиты, позволяющие писать более
лаконичный выразительный код и, возможно, решать новые задачи.
- Изучение Python: fastcore расширяет Python, в
этом процессе проявляются многие продвинутые понятия. Для
мотивированных людей это прекрасный способ увидеть многое о
внутренней работе языка.
Пройдемся по fastcore
ураганом
Вот некоторые привлекшие мое внимание вещи, которые возможно
сделать с помощью fastcore.
Делаем kwargs
прозрачными
Я немного поеживаюсь каждый раз, когда вижу функцию с аргументом
kwargs
. Это потому, что kwargs
означает обфускацию API. Мне нужно прочитать исходный код, чтобы
понять, какие параметры допустимы. Посмотрим на пример ниже:
def baz(a, b=2, c =3, d=4): return a + b + cdef foo(c, a, **kwargs): return c + baz(a, **kwargs)inspect.signature(foo)
<Signature (c, a, **kwargs)>
Без чтения исходного кода может быть трудно узнать, что
foo
также принимает дополнительные
параметры
b
и
d
. Это можно исправить с
помощью
delegates
:
def baz(a, b=2, c =3, d=4): return a + b + c@delegates(baz) # this decorator will pass down keyword arguments from bazdef foo(c, a, **kwargs): return c + baz(a, **kwargs)inspect.signature(foo)
<Signature (c, a, b=2, d=4)>
Поведение этого декоратора настраивается. Например, вы можете
передать аргументы и сохранить при этом
kwargs
:
@delegates(baz, keep=True)def foo(c, a, **kwargs): return c + baz(a, **kwargs)inspect.signature(foo)
<Signature (c, a, b=2, d=4, **kwargs)>
Аргументы можно исключать. Например, ниже мы исключаем из
делегирования аргумент
d
:
def basefoo(a, b=2, c =3, d=4): pass@delegates(basefoo, but= ['d']) # exclude `d`def foo(c, a, **kwargs): passinspect.signature(foo)
<Signature (c, a, b=2)>
Возможно делегирование между классами:
class BaseFoo: def __init__(self, e, c=2): pass@delegates()# since no argument was passsed here we delegate to the superclassclass Foo(BaseFoo): def __init__(self, a, b=1, **kwargs): super().__init__(**kwargs)inspect.signature(Foo)
<Signature (a, b=1, c=2)>
Для получения дополнительной информации прочтите
документацию о delegates
.
Избегаем шаблонного кода при установке
атрибутов экземпляра
Вы когда-нибудь задумывались, можно ли избежать шаблонного кода,
связанного с установкой атрибутов в
__init__
?
class Test: def __init__(self, a, b ,c): self.a, self.b, self.c = a, b, c
Ой! Это было больно. Посмотрите на все эти повторяющиеся имена
переменных. Неужели действительно нужно повторять всё это при
определении класса? Уже нет! Посмотрите на
store_attr:
class Test: def __init__(self, a, b, c): store_attr()t = Test(5,4,3)assert t.b == 4
Вы также можете исключить определенные атрибуты:
class Test: def __init__(self, a, b, c): store_attr(but=['c'])t = Test(5,4,3)assert t.b == 4assert not hasattr(t, 'c')
Есть гораздо больше способов настройки и использования
store_attr
, чем я показываю. Ознакомьтесь с
документацией для получения более подробной информации.
P.S. Вы можете подумать, что
классы данных тоже позволяют избежать такого шаблонного кода.
Хотя в некоторых случаях это верно,
store_attr
более
гибок.
1
Например,
store_attr
не полагается на наследование, а
это значит, что вы не застрянете, используя множественное
наследование, когда применяете его со своими собственными классами.
Кроме того, в отличие от классов данных,
store_attr
не
требует python 3.7 или выше. Вы можете использовать
store_attr
в любой момент в жизненном цикле объекта и
в любом месте вашего класса, чтобы настроить поведение в том, как и
когда хранятся переменные. Подробности
здесь.
Избегаем бойлерплейта
подклассов
Одна вещь, которую я ненавижу в python связанный с подклассами
шаблонный код
__super__ ().__init__ ()
. Например:
class ParentClass: def __init__(self): self.some_attr = 'hello'class ChildClass(ParentClass): def __init__(self): super().__init__()cc = ChildClass()assert cc.some_attr == 'hello' # only accessible b/c you used super
Мы можем избежать такого кода, используя метакласс
PrePostInitMeta. Как? Определив новый класс под названием
NewParent
обертку вокруг
ParentClass
:
class NewParent(ParentClass, metaclass=PrePostInitMeta): def __pre_init__(self, *args, **kwargs): super().__init__()class ChildClass(NewParent): def __init__(self):passsc = ChildClass()assert sc.some_attr == 'hello'
Диспетчеризация типа
Диспетчеризация типа или
множественная диспетчеризация позволяет изменить поведение
функции в зависимости от типов получаемых входных данных. Это
характерная особенность некоторых языков программирования, таких
как Julia. Вот
концептуальный пример того, как работает множественная
диспетчеризация в Julia. В зависимости от типов входных данных
x
и
y
возвращаются разные значения:
collide_with(x::Asteroid, y::Asteroid) = ... # deal with asteroid hitting asteroidcollide_with(x::Asteroid, y::Spaceship) = ... # deal with asteroid hitting spaceshipcollide_with(x::Spaceship, y::Asteroid) = ... # deal with spaceship hitting asteroidcollide_with(x::Spaceship, y::Spaceship) = ... # deal with spaceship hitting spaceship
Диспетчеризация типа может быть особенно полезна в Data Science,
где возможно разрешить различные типы ввода (т.е. массивы numpy и
фреймы данных Pandas) в обрабатывающей данные функции. Типовая
диспетчеризация позволяет иметь общий API у выполняющих похожие
задачи функций. К сожалению, Python не поддерживает такую
функциональность из коробки. К счастью, у нас есть декоратор
@typedispatch. Этот декоратор полагается на подсказки типа,
чтобы маршрутизировать входные данные к правильной версии
функции:
@typedispatchdef f(x:str, y:str): return f'{x}{y}'@typedispatchdef f(x:np.ndarray): return x.sum()@typedispatchdef f(x:int, y:int): return x+y
Ниже показывается диспетчеризации типа при работе для функции
f
:
f('Hello ', 'World!')
'Hello World!'
f(2,3)
5
f(np.array([5,5,5,5]))
20
У этой функциональности есть ограничения (также как у других
способов использования этой функции) и о них
вы
можете прочитать здесь. В процессе изучения типовой
диспетчеризации я также нашел библиотеку Python под названием
multipledispatch, написанную
Mathhew
Rocklin создателем Dask.
После применения этой функции я теперь мотивирован изучать такие
языки, как Julia, чтобы обнаружить, каких еще парадигм мне может не
хватать.
Лучшая версия
functools.partial
functools.partial
отличная утилита, создающая функции
из других функций и позволяющая устанавливать значения по
умолчанию. Возьмем, к примеру, функцию, которая фильтрует список
таким образом, чтобы он содержал значения больше либо равное
val
:
test_input = [1,2,3,4,5,6]def f(arr, val): "Filter a list to remove any values that are less than val." return [x for x in arr if x >= val]f(test_input, 3)
[3, 4, 5, 6]
Из этой функции вы можете создать новую функцию с помощью
partial
, которая устанавливает значение по умолчанию:
5
:
filter5 = partial(f, val=5)filter5(test_input)
[5, 6]
Одна из проблем с
partial
заключается в том, что она
удаляет исходную строку документации и заменяет ее общей строкой
документации:
filter5.__doc__
'partial(func, *args, **keywords) - new function with partial application\n of the given arguments and keywords.\n'
fastcore.utils.partialler исправляет это и обеспечивает
сохранение строки документации таким образом, чтобы новый API был
прозрачным:
filter5 = partialler(f, val=5)filter5.__doc__
'Filter a list to remove any values that are less than val.'
Композиция функций
Распространенный в функциональных языках программирования метод
композиция функций, когда вы связываете несколько функций вместе,
чтобы достичь определенного результата. Это особенно полезно в
различных преобразований данных. Рассмотрим игрушечный пример, где
у меня три функции: первая удаляет элементы списка меньше 5 (из
предыдущего раздела), вторая добавляет 2 к каждому числу, третья
суммирует все числа:
def add(arr, val): return [x + val for x in arr]def arrsum(arr): return sum(arr)# See the previous section on partialleradd2 = partialler(add, val=2)transform = compose(filter5, add2, arrsum)transform([1,2,3,4,5,6])
15
Почему это полезно? Вы можете подумать, что я могу сделать то же
самое вот так:
arrsum(add2(filter5([1,2,3,4,5,6])))
Все верно! Однако, композиция дает удобный интерфейс на случай,
когда вы захотите сделать что-то такое:
def fit(x, transforms:list): "fit a model after performing transformations" x = compose(*transforms)(x) y = [np.mean(x)] * len(x) # its a dumb model. Don't judge me return y# filters out elements < 5, adds 2, then predicts the meanfit(x=[1,2,3,4,5,6], transforms=[filter5, add2])
[7.5, 7.5]
Более подробную информацию о
compose
читайте
в
документации.
__repr__
, но
полезнее
В Python
__repr__
помогает получить информацию об
объекте для логирования и отладки. Ниже приведено то, что вы
получите по умолчанию, когда определите новый класс. Примечание: мы
используем
store_attr
, который обсуждался выше.
class Test: def __init__(self, a, b=2, c=3): store_attr() # `store_attr` was discussed previouslyTest(1)
<__main__.Test at 0x7ffcd766cee0>
Мы можем использовать
basic_repr, чтобы быстро получить более разумное значение по
умолчанию:
class Test: def __init__(self, a, b=2, c=3): store_attr() __repr__ = basic_repr('a,b,c')Test(2)
Test(a=2, b=2, c=3)
Обезьяньи патчи через
декоратор
Это может быть удобно для
обезьяньих патчей с помощью декоратора, что особенно полезно,
когда вы хотите пропатчить импортируемую внешнюю библиотеку. Мы
можем использовать
декоратор @patch
из
fastcore.foundation
вместе с подсказками типа примерно
так:
class MyClass(int): pass @patchdef func(self:MyClass, a): return self+amc = MyClass(3)
Теперь в
MyClass
есть дополнительный метод под
названием
func
:
mc.func(10)
13
Я еще не убедил вас? Тогда покажу вам еще один пример такого патча
в следующем разделе.
pathlib.Path
Увидев
эти расширения в
pathlib.path
, вы больше никогда
не будете работать с vanilla pathlib! В pathlib добавлен ряд
дополнительных методов, таких как:
Path.readlines
: то же, что with open
('somefile', 'r') as f: f.readlines ()
Path.read
: то же, что with open ('somefile',
'r') as f: f.read ()
Path.save
: сохраняет файл как
pickle
Path.load
: загружает файл pickle
Path.ls
: показывает содержимое пути в виде
списка.
- и так далее.
Подробнее об этом здесь. Вот демонстрация
ls
:
from fastcore.utils import *from pathlib import Pathp = Path('.')p.ls() # you don't get this with vanilla Pathlib.Path!!
(#7) [Path('2020-09-01-fastcore.ipynb'),Path('README.md'),Path('fastcore_imgs'),Path('2020-02-20-test.ipynb'),Path('.ipynb_checkpoints'),Path('2020-02-21-introducing-fastpages.ipynb'),Path('my_icons')]
Подождите! Что здесь происходит? Мы только что импортировали
pathlib.Path
почему мы получили новую
функциональность? Потому, что импортировали модуль
fastcore.utils
, который патчит
pathlib.Path
с помощью упомянутого выше декоратора
@patch
. Чтобы довести дело до конца и показать, чем
полезен
@patch
, я пойду дальше и прямо сейчас добавлю
еще один метод в
Path
:
@patchdef fun(self:Path): return "This is fun!"p.fun()
'This is fun!'
Волшебно, правда? Вот почему я пишу об этом!
Еще более лаконичный способ написать
лямбду
Self
с заглавной буквы S это еще более лаконичный
способ написания вызывающих методы объекта лямбд. Например,
создадим лямбду для получения суммы массива Numpy:
arr=np.array([5,4,3,2,1])f = lambda a: a.sum()assert f(arr) == 15
Вы можете таким же образом использовать
Self
:
f = Self.sum()assert f(arr) == 15
Давайте создадим лямбду, которая будет делать группировку и
возвращать максимальный элемент фрейма данных Pandas:
import pandas as pddf=pd.DataFrame({'Some Column': ['a', 'a', 'b', 'b', ], 'Another Column': [5, 7, 50, 70]})f = Self.groupby('Some Column').mean()f(df)
Подробнее о
Self
читайте в
документации.
Функции блокнота
Они просты, но удобны и позволяют узнать, выполняется ли код в
блокноте Jupyter, Colab или через оболочку IPython:
from fastcore.imports import in_notebook, in_colab, in_ipythonin_notebook(), in_colab(), in_ipython()
(True, False, True)
Это полезно, когда вы отображаете определенные типы визуализаций,
индикаторов прогресса или анимации в вашем коде, которые, возможно,
захотите изменить или переключить в зависимости от окружения.
Замена стандартного списка
Вы можете быть довольно счастливы со стандартным
list
в Python. Это одна из тех ситуаций, когда вы не знаете, что вам
нужен список лучше, пока кто-то не покажет его вам.
L
как раз такой список с большим количеством доработок.
Лучший способ описать
L
сделать вид, что у
list
и
numpy
родился милый ребенок.
Определите список (посмотрите на приятный
__repr__
,
показывающий длину списка!)
L(1,2,3)
(#3) [1,2,3]
Перемешайте список:
p = L.range(20).shuffle()p
(#20) [8,7,5,12,14,16,2,15,19,6...]
Индекс [прим.перев. скорее позиция
position] в
списке:
p[2,4,6]
(#3) [5,14,2]
L
имеет разумные умолчания, например, вот добавление
элемента в список:
1 + L(2,3,4)
(#4) [1,2,3,4]
L
может гораздо больше. Читайте
документацию, чтобы узнать больше.
Но подождите Это еще не всё!
Есть еще кое-что, что я хотел бы показать вам, но это никак не
вписываются в пост. Вот список некоторых любимых вещей, которые я
не показывал в этом посте:
Утилиты
Раздел
Utilites
содержит множество шорткатов для выполнения общих задач или
предоставляет дополнительный интерфейс к стандартному
интерфейсу.
-
mk_class: быстро добавляет кучу атрибутов в класс
-
wrap_class: добавление новых методов в класс с помощью простого
декоратора
- groupby:
похоже на
groupby
из Scala
- merge:
слияние словарей
-
fasttuple: кортеж на стероидах
-
Infinite Lists: полезно для увеличения размеров массивов и
тестирования
- chunked:
упаковка и организация элементов
Многопроцессорная обработка
Раздел
Многопроцессорная обработка расширяет соответствующую
библиотеку Python, предлагая такие возможности:
- Шкала прогресса
- Возможность сделать паузу, чтобы смягчить состояние гонки с
помощью внешних сервисов
- Пакетная обработка для каждого воркера, если у вас нет
векторизованных операций для выполнения в блоках (chunk)
Прокачивать себя в Python стало проще, ведь специально для
хабравчан мы сделали промокод
HABR, дающий дополнительную
скидку 10% к скидке указанной на баннере.
Рекомендуемые статьи