- Регистрация
- 21.07.20
- Сообщения
- 40.408
- Реакции
- 1
- Репутация
- 0
You must be registered for see links
Недавно я начал оттачивать владение языком программирования Python. Я хотел изучить продвинутые паттерны, идиомы и методы программирования. Начал я с чтения книг по продвинутому Python, но информация, похоже, не откладывалась в голове без применения навыков. Хотелось иметь возможность задавать вопросы эксперту, пока учусь, а такую возможность трудно найти! Тогда ко мне и пришла идея: что, если я найду проект с открытым и достаточно продвинутым кодом и напишу документацию и тесты? Я сделал ставку, что это заставит меня изучать все очень глубоко, а поддерживающие проект люди оценит мою работу и будут готовы ответить на мои вопросы.
Предыстория
Поиском такого проекта и написанием документации с тестами к нему я занимался целый месяц и такое обучение было самым эффективным из всех, что я пробовал. Я обнаружил, что написание документации заставило меня глубоко понять не только то, что делает код, но и то, почему код работает именно так, как он работает, а также исследовать крайние случаи во время написания тестов. Самое главное, что я мог задавать вопросы, когда застрял, а люди были готовы посвятить мне дополнительное время, зная, что их разъяснения служило тому, чтобы сделать код доступнее! Оказывается, выбранная мной библиотека,
You must be registered for see links
— одна из самых увлекательных в Python, с которыми я работал. Ее цели и задачи действительно уникальны.
You must be registered for see links
— это основа многих проектов
You must be registered for see links
. Самое главное: 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 + c
def foo(c, a, **kwargs):
return c + baz(a, **kwargs)
inspect.signature(foo)
Без чтения исходного кода может быть трудно узнать, что 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 baz
def foo(c, a, **kwargs):
return c + baz(a, **kwargs)
inspect.signature(foo)
Поведение этого декоратора настраивается. Например, вы можете передать аргументы и сохранить при этом kwargs:
@delegates(baz, keep=True)
def foo(c, a, **kwargs):
return c + baz(a, **kwargs)
inspect.signature(foo)
Аргументы можно исключать. Например, ниже мы исключаем из делегирования аргумент d:
def basefoo(a, b=2, c =3, d=4): pass
@delegates(basefoo, but= ['d']) # exclude `d`
def foo(c, a, **kwargs): pass
inspect.signature(foo)
Возможно делегирование между классами:
class BaseFoo:
def __init__(self, e, c=2): pass
@delegates()# since no argument was passsed here we delegate to the superclass
class Foo(BaseFoo):
def __init__(self, a, b=1, **kwargs): super().__init__(**kwargs)
inspect.signature(Foo)
Для получения дополнительной информации прочтите
You must be registered for see links
.Избегаем шаблонного кода при установке атрибутов экземпляра
Вы когда-нибудь задумывались, можно ли избежать шаблонного кода, связанного с установкой атрибутов в __init__?
class Test:
def __init__(self, a, b ,c):
self.a, self.b, self.c = a, b, c
Ой! Это было больно. Посмотрите на все эти повторяющиеся имена переменных. Неужели действительно нужно повторять всё это при определении класса? Уже нет! Посмотрите на
You must be registered for see links
: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 == 4
assert not hasattr(t, 'c')
Есть гораздо больше способов настройки и использования store_attr, чем я показываю. Ознакомьтесь с
You must be registered for see links
для получения более подробной информации.P.S. Вы можете подумать, что
You must be registered for see links
тоже позволяют избежать такого шаблонного кода. Хотя в некоторых случаях это верно, store_attr более гибок.
You must be registered for see links
Например, store_attr не полагается на наследование, а это значит, что вы не застрянете, используя множественное наследование, когда применяете его со своими собственными классами. Кроме того, в отличие от классов данных, store_attr не требует python 3.7 или выше. Вы можете использовать store_attr в любой момент в жизненном цикле объекта и в любом месте вашего класса, чтобы настроить поведение в том, как и когда хранятся переменные. Подробности
You must be registered for see links
.Избегаем бойлерплейта подклассов
Одна вещь, которую я ненавижу в 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
Мы можем избежать такого кода, используя метакласс
You must be registered for see links
. Как? Определив новый класс под названием NewParent — обертку вокруг ParentClass:class NewParent(ParentClass, metaclass=PrePostInitMeta):
def __pre_init__(self, *args, **kwargs): super().__init__()
class ChildClass(NewParent):
def __init__(self)ass
sc = ChildClass()
assert sc.some_attr == 'hello'
Диспетчеризация типа
Диспетчеризация типа или
You must be registered for see links
позволяет изменить поведение функции в зависимости от типов получаемых входных данных. Это характерная особенность некоторых языков программирования, таких как Julia. Вот
You must be registered for see links
того, как работает множественная диспетчеризация в Julia. В зависимости от типов входных данных x и y возвращаются разные значения:collide_with(x::Asteroid, y::Asteroid) = ...
# deal with asteroid hitting asteroid
collide_with(x::Asteroid, y::Spaceship) = ...
# deal with asteroid hitting spaceship
collide_with(x::Spaceship, y::Asteroid) = ...
# deal with spaceship hitting asteroid
collide_with(x::Spaceship, y::Spaceship) = ...
# deal with spaceship hitting spaceship
Диспетчеризация типа может быть особенно полезна в Data Science, где возможно разрешить различные типы ввода (т.е. массивы numpy и фреймы данных Pandas) в обрабатывающей данные функции. Типовая диспетчеризация позволяет иметь общий API у выполняющих похожие задачи функций. К сожалению, Python не поддерживает такую функциональность из коробки. К счастью, у нас есть декоратор
You must be registered for see links
. Этот декоратор полагается на подсказки типа, чтобы маршрутизировать входные данные к правильной версии функции:@typedispatch
def f(x:str, y:str): return f'{x}{y}'
@typedispatch
def f(x:np.ndarray): return x.sum()
@typedispatch
def 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
У этой функциональности есть ограничения (также как у других способов использования этой функции) и о них
You must be registered for see links
. В процессе изучения типовой диспетчеризации я также нашел библиотеку Python под названием
You must be registered for see links
, написанную
You must be registered for see links
— создателем 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'
You must be registered for see links
исправляет это и обеспечивает сохранение строки документации таким образом, чтобы новый 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 partialler
add2 = 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 mean
fit(x=[1,2,3,4,5,6], transforms=[filter5, add2])
[7.5, 7.5]
Более подробную информацию о compose читайте
You must be registered for see links
.__repr__, но полезнее
В Python __repr__ помогает получить информацию об объекте для логирования и отладки. Ниже приведено то, что вы получите по умолчанию, когда определите новый класс. Примечание: мы используем store_attr, который обсуждался выше.
class Test:
def __init__(self, a, b=2, c=3): store_attr() # `store_attr` was discussed previously
Test(1)
Мы можем использовать
You must be registered for see links
, чтобы быстро получить более разумное значение по умолчанию: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)
Обезьяньи патчи через декоратор
Это может быть удобно для
You must be registered for see links
с помощью декоратора, что особенно полезно, когда вы хотите пропатчить импортируемую внешнюю библиотеку. Мы можем использовать
You must be registered for see links
из fastcore.foundation вместе с подсказками типа примерно так:class MyClass(int): pass
@patch
def func(self:MyClass, a): return self+a
mc = MyClass(3)
Теперь в MyClass есть дополнительный метод под названием func:
mc.func(10)
13
Я еще не убедил вас? Тогда покажу вам еще один пример такого патча в следующем разделе.
pathlib.Path
Увидев
You must be registered for see links
в 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: показывает содержимое пути в виде списка.
- и так далее.
You must be registered for see links
. Вот демонстрация ls:from fastcore.utils import *
from pathlib import Path
p = 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:
@patch
def fun(selfath): 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 pd
df=pd.DataFrame({'Some Column': ['a', 'a', 'b', 'b', ],
'Another Column': [5, 7, 50, 70]})
f = Self.groupby('Some Column').mean()
f(df)
Another Column |
---|