-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy path7.code_styles.qmd
More file actions
465 lines (343 loc) · 35.1 KB
/
7.code_styles.qmd
File metadata and controls
465 lines (343 loc) · 35.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
---
title: "CodeStyle"
author: "Lev Kovalenko"
format:
revealjs:
theme: dark
self-contained: true
echo: true
source: true
jupyter: "epml"
---
## Почему это важно?
:::: {.columns}
::: {.column width="50%"}
Начинающий программист
{fig-align="center"}
:::
::: {.column width="50%"}
Опытный программист
{fig-align="center"}
:::
::::
::: {.notes}
Потому что датасаентисты работают с кодом. В процессе работы приходиться писать и дебажить код, изучать библиотеки, примеры использования инструментов, а также читать и разбирать код коллег. Это все может занимать значительную долю времени. Для экономии своего времени и времени коллег (ну кому охота полдня читать код?) приходиться договариваться о стиле и структуре кода и приводить код к одному виду. Это ключевой фактор, который характеризует читабельность кода.
В процессе сбора материала для презентации я набрел на исследования, в которых датчики следили за движениями глаз начинающего и опытного программистов. [Code style как стандарт разработки](https://habr.com/ru/company/manychat/blog/468953/)
Обратите внимание, чем занимается опытный программист. Он выделяет блоки кода, декомпозирует их и читает поблочно, выделяя ключевые части и анализируя их работу. Начинающий же мечется построчно, просто пытается понять, что тут вообще происходит.
Видео довольно старое, от 2012 года, и запись была направлена на исследование процессов мозга программиста. Чуть подробнее можно почитать [тут](https://synesthesiam.github.io/pages/research.html).
:::
## Основная мотивация появления CodeStyle
- Ускорить понимания и ревью кода.
- Уменьшить стилистическое разнообразие кода.
- Уменьшить сложность кода.
- Запретить использование плохих практик.
## Читаемость кода
Есть 2 способа ускорить чтение и понимания кода:
1. Постоянно наращивать базу знаний разработчиков по тому, как может выглядеть код.
2. Привести весь код к одному стандарту, задать тот самый code style
::: {.notes}
Конечно стоит идти по второму пути, поскольку это снизит нагрузку на разработчика, выделили следующие поинты, которые даст повышение читабельности кода:
- Если вы обеспечили историческое написание понятного кода, то, сколько бы не приходило и не уходило разработчиков, вы всегда имеете равное качество кода, что позволяет проекту динамично расти вне зависимости от его объема.
- Если ваш код написан понятно, новый специалист быстро разберется и начнет приносить пользу. Есть такое понятие, как точка самоокупаемости сотрудника. Это точка, с которой сотрудник начинает приносить только пользу. А если код написан понятно, новым сотрудникам научится читать ваш код, без необходимости детально разбираться. И чем быстрее он это сделает, тем быстрее перестанет задавать тонны вопросов остальным специалистам, отнимая у них время. А время специалистов, которые прошли точку самоокупаемости, значительно дороже для команды и компании с точки зрения потенциальной ценности приносимой продукту.
- Не нужно будет постоянно спотыкаться о чью-то оригинальность и специфическое оформление.
- Ваш мозг меньше сопротивляется, т.к. не нужно вникать в чужую стилистику. Ментальных ресурсов на чтение понятного кода нужно намного меньше.
- Очень скоро, после прибытия в новую компанию, программист начнет делиться впечатлениями с бывшими коллегами и друзьями. И он либо скажет, что тут все круто, либо выделит негативные моменты в работе с кодом.
- Задача программиста лежит на более низком уровне, чем будущее всей компании, но важно донести понимание, что понятность и читаемость кода сейчас влияет на динамику дальнейшей разработки.
:::
# [Pep8](https://peps.python.org/pep-0008/){preview-link="true"}
::: {.notes}
Основным инструментом датасаентиста обычно является python. Поэтому дальше будем обсуждать codestyle непосредственно этого языка. В питоне уже задан code style, описывается он в pep8.
- Структура файла (line length, indentation, blanc lines, break lines, imports, encoding, tabs or spaces)
- Разделение пробелами выражений
- Комментарии (docstrings, block and inline comments)
- Naming convention (переменные, константы, типы, функции, классы, методы)
- Аннотации к функциям и переменным
- Другие рекомендации
Но pep8 все равно оставляет некоторую свободу в форматировании, выставлении отступов и переносов строк. Поэтому в команде обычно приходиться создавать дополнительные договоренности.
:::
## {background-image="images/cs_my_own_codestyle.png" background-size="70%"}
::: {.notes}
Собственно, действительно можно собрать свой собственный набор стилей для написания кода. И для этого есть множество удобных инструментов, которые позволяют автоматизировать множество проверок стиля и дополнить их написанием своих собственных.
:::
## AutoFormatters
{fig-align="center"}
::: {.notes}
На сегодняшний день есть несколько популярных автоформаттеров для питона. Yapf, Autopep8 и Black. Какой выбирать решайте сами.
- Yapf - google
- Autopep8 - Hideo Hattori
- Black - Python Software Foundation
В качестве инструмента форматирования согласно PEP 8 используют autopep8 (есть и другие, но этот самый популярный). Он запускается через командную строку, поддерживает множество аргументов. Но ожидать, что ваш Python-код станет повсюду единообразным не стоит, поскольку PEP 8 не дает строгих рекомендаций. Но уж лучше с ним, чем без него.
YAPF является не официальным продуктом от Google, но кодом, который, так случилось, владеет Google. YAPF основан на таком инструменте форматирования кода Си-подобных языков, как clang-format. Он берет код и форматирует его в соответствии с заданными настройками, причем отформатирует код даже в случае, если он не нарушает PEP 8. Например, вы можете задать необходимую компоновку словарей, а все остальное сделать как в Black. Эти настройки сохраняются и передаются другим программистам.
Black является наиболее строгим инструментом форматирования. С одной стороны, это помогает придерживаться одного стиля форматирования кода, с другой, некоторые правила могут вызывать внутреннее отторжение. Кроме того, в отличие от YAPF, он не поддерживает множество настроек. По факту Black имеет две перенастраиваемые опции:
- изменение допустимой длины строки (по умолчанию стоит 88),
- разрешение использования одинарных кавычек (по умолчанию разрешаются только двойные).
- Поддержка isort
Как привести код в порядок мы обсудили, теперь же поговорим о том, как отслеживать качество кода и находить отклонения от code style.
:::
## Linters {.scrollable}
|Linter|Category|Description|
|------|--------|-----------|
|[Pylint](https://www.pylint.org/)|Logical & Stylistic|Checks for errors, tries to enforce a coding standard, looks for code smells|
|[PyFlakes](https://github.com/PyCQA/pyflakes)|Logical|Analyzes programs and detects various errors|
|[Pycodestyle](https://github.com/PyCQA/pycodestyle)|Stylistic|Checks against some of the style conventions in PEP8|
|[Pydocstyle](https://github.com/PyCQA/pydocstyle)|Stylistic|Checks compliance with Python docstring conventions|
|[Bandit](https://github.com/PyCQA/bandit)|Logical|Analyzes code to find common security issues|
|[MyPy](http://mypy-lang.org/)|Logical|Checks for optionally-enforced static types|
::: {.notes}
Теперь давайте обсудим их, зачем же их так много.
Они разбиваются на две группы: Logical и Stylistic.
Первая позволяет находить, паттерны опасного кода и потенциальное неоднозначное поведение.
Стилистический линтеринг предназначен для проверки стилистических договоренностей.
- Pylint сложный и мощный анализатор. Который умеет многое из коробки.
- PyFlake базовый flake linter который анализирует программу и ищет потенциальные ошибки
- Pycodestyle - проверка на соглашение по pep8
- Pydocstyle - проверка docstrings по соглашению
- Bandit - проверка на безопасность кода, уязвимости в используемых библиотеках и прочее
- MyPy - проверка типов на основе аннотаций
Эти инструменты направлены на проверку соглашений по написанию кода, которые были заключены, на поиск плохих практик, ошибок.
:::
## [Flake8 plugins](https://github.com/DmytroLitvinov/awesome-flake8-extensions)
{fig-align="center"}
::: {.notes}
Хочется отметить среди линтеров flake8. Это по-настоящему мощный инструмент который объединяет в себе все другие за счет расширений и плагинов. Вы видите наиболее популярную часть плагинов и расширений для flake8.
То есть за счет компоновки плагинов вы можете настроить все необходимые вам инструменты в одной библиотеке и проверять все одной командой, при этом за вас соберут все дублирующиеся ошибки и варнинги и выведут в приоритетном порядке.
Если сравнивать с pylint это инструмент, который включает в себя сразу большое количество проверок, но не дает гибкого интерфейса для настройки и расширения проверок, то flake8 его во много выигрывает за счет огромной экосистемы плагинов, которые увеличивают его мощь и строгость.
Один из принципов python dzen звучит что complex is better than complicated. Это как раз и применимо в этом сравнении, составное лучше сложного.
:::
## Анализаторы кода - [Radon](https://radon.readthedocs.io/en/latest/)
- Цикломатическая сложность
- Метрика Холстеда
- Индекс поддерживаемости кода
::: {.notes}
А как вообще команде проверить что договорённости, которые она заключила помогают в достижении изначальных целей? Как проверить читабельность кода?
Для этого существуют анализаторы кода, например, Radon. Он позволяет посчитать некоторые метрики:
- Цикломатическая сложность - Чем больше в коде переходов (if-else), циклов, генераторов, обработчиков исключений и логических операторов — тем больше у программы вариантов исполнения и тем сложнее удержать в голове различные состояния системы. Метрика, которая измеряет сложность кода, опираясь на количество этих операций, называется цикломатической сложностью программы. При подсчете ее с помощью радона вы получите список файлов, классов, методов и функций в вашем проекте и их индекс сложности, от очень простого до очень сложного. Индекс укажет на перегруженные логикой места, которые можно разбить на куски помельче, упростить или переписать (если есть такая возможность — алгоритм может быть очень сложным сам по себе и попытки его разбить на куски могут только ухудшить понимабельность кода).
- Метрика Холстеда - Тут считается количество уникальных операторов и операндов в коде и их общее количество. Полученные значения подставляются в формулы и получается набор чисел, который описывает сложность программы и количество усилий, предположительно затраченное на написание и понимание кода.
- Индекс поддерживаемости кода - Этот индекс говорит нам о том, насколько сложно будет поддерживать или редактировать кусок программы. Этот параметр рассчитывается на основе чисел, полученных из метрик, посчитанных выше.
:::
## Mypy проверка типов
:::: {.columns}
::: {.column width="40%"}
From Python...
```python
def fib(n):
a, b = 0, 1
while a < n:
yield a
a, b = b, a+b
```
:::
::: {.column width="60%"}
...to statically typed Python
```python
def fib(n: int) -> Iterator[int]:
a, b = 0, 1
while a < n:
yield a
a, b = b, a+b
```
:::
::::
::: {.notes}
Как вы знаете в питоне есть аннотации типов. Mypy использует для статической проверки типов и собственно проверяет везде ли и корректно ли они указаны.
Mypy имеет мощную современную систему типов с такими функциями, как двунаправленный вывод типов, обобщения, вызываемые типы, абстрактные базовые классы, множественное наследование и типы кортежей.
Зачем это DS? У них же один dataframe или numpy array. На самом деле такой подход увеличивает читаемость кода, сразу понятно какой dataframe к вам пришел, какие стобцы в нем есть и каких типов. Есть разные библиотеки для этого, например, pandera, но об этом мы поговорим позже.
Основная цель добавления статической типизации или аннотирования типов заключается в повышении читаемости, вам и вашим коллегам не придётся догадываться что и какого типа нужно передать той или иной функции. Да и такой подход позволит избавится от части ошибок, которые вы можете допустить в коде.
:::
## Pre-commit автоматизация
.pre-commit-config.yaml
```yaml
default_stages: [commit, push]
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v3.2.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: check-added-large-files
- id: check-ast
- id: check-merge-conflict
- repo: https://github.com/python/black
rev: 22.3.0
hooks:
- id: black
- repo: https://github.com/pycqa/isort
rev: 5.10.1
hooks:
- id: isort
name: isort (python)
- repo: https://github.com/PyCQA/flake8
rev: 4.0.1
hooks:
- id: flake8
additional_dependencies: [
yastyleguide==0.1.0
]
```
::: {.notes}
Итак, теперь мы знаем, как использовать линтеры и автоформатеры, и поняли, насколько они полезны! Следующий шаг — начать применять их в наших проектах. Это можно сделать с помощью хуков перед комитом. Хуки перед комитом позволяют нам проверять наш код на наличие проблем со стилем и форматированием каждый раз, когда происходит коммит изменения, тем самым обеспечивая сохранение единого стиля на протяжении всего проекта.
То есть в репозиторий не попадает плохого кода. Мы не даем человеку его писать и комитить, а уже потом когда-нибудь рефакторить. Мы делаем условие что в репозиторий код попадает как минимум в соответствии ос стилем и уровнем качества удовлетворяющим линтеры.
Это также снимает с самого разработчика ответственность за запуск различных линтеров и форматеров, облегчает рутинную работу так сказать.
:::
## Выводы
- Читаемость и качество важны.
- За качеством нужно следить.
- Нужно автоматизировать рутинные операции.
# Как это работает?
::: {.notes}
Давайте посмотрим, как все эти линтеры работают. Для начала познакомимся с внутренним устройством python.
Это поможет вам в дальнейшем автоматизировать проверки кода на наличие плохих практик, отклонений от codestyle.
:::
## Как работает python?
{fig-align="center"}
::: {.notes}
Python не преобразует свой код в машинный код, который может понять аппаратное обеспечение. Он фактически преобразует его в то, что называется байт-кодом. Таким образом, внутри Python компиляция происходит, но не на машинном языке. Он транслирует в байтовый код (.pyc или .pyo), и этот байтовый код не может быть понят процессором. Поэтому нам нужен интерпретатор, называемый виртуальной машиной python, для выполнения байт-кодов.
Исходный код Python выполняет следующие действия для создания исполняемого кода:
- Шаг 1: Транслятор Python читает исходный код или инструкцию Python. Затем проверяется правильность форматирования инструкции, т. е. проверяется синтаксис каждой строки. Если он обнаруживает ошибку, он немедленно останавливает перевод и показывает сообщение об ошибке.
- Шаг 2: Если ошибки нет, т. е. если инструкция или исходный код Python правильно отформатированы, то компилятор переводит их в эквивалентную форму на промежуточном языке, называемом «Байт-код».
- Шаг 3: Затем байт-код отправляется на виртуальную машину Python (PVM), которая является интерпретатором Python. PVM преобразует байт-код Python в машинно-исполняемый код. Если во время этой интерпретации возникает ошибка, то преобразование останавливается с сообщением об ошибке.
Давайте повнимательнее разберемся с шагами 1 И 2, как именно питон проверяет синтаксис каждой строки и в какую структуру данных преобразует код для перевода его в byte code.
:::
## Abstract syntax tree
\
\
{fig-align="center"}
::: {.notes}
Абстрактное синтаксическое дерево (AST) — это структура данных, используемая для рассуждения о грамматике языка программирования в контексте инструкций, представленных в исходном коде.
Такой подход используется и в python для преобразования кода в байт-коды.
- При наличии некоторого текстового исходного кода компилятор сначала размечает текст, чтобы идентифицировать ключевые слова языка программирования, переменные, литералы и т. д. Каждая лексема представляет собой «атом» инструкции.
- Затем токены перестраиваются в AST, дерево, узлы которого являются «атомами» инструкций, и ограничивает отношения между атомами на основе грамматики языка программирования. Например, AST делает явным наличие вызова функции, соответствующих входных аргументов, инструкций, составляющих функцию, и т. д.
- Затем компилятор может применить несколько оптимизаций к AST и в конечном итоге преобразовать его в двоичный код.
Собственно, большинство анализаторов кода используют AST для анализа. В нем можно рассматривать взаимодействия нод с друг-другом, сравнивать это с паттернами, искать плохие практики и даже генерировать код в нужном стиле. В питоне есть специальный модуль `ast` для этого.
:::
## [AST](https://docs.python.org/3/library/ast.html){preview-link="true"}
```{python}
import ast
code = "one_plus_two = 1 + 2"
tree = ast.parse(code)
print(ast.dump(tree, indent=4))
```
::: {.notes}
Тут чуть-чуть углубимся в сам питон. В нем есть модуль ast, который мы можем импортить и использовать.
Сначала это может быть неочевидно, но результат, сгенерированный на ast.dump()самом деле, представляет собой дерево:
- Слова, начинающиеся с заглавной буквы, являются узлами дерева.
- Атрибутами узлов являются либо ребра дерева, либо метаданные.
:::
## [AST](https://docs.python.org/3/library/ast.html){preview-link="true"}
{fig-align="center"}
::: {.notes}
Давайте переработаем вывод в диаграмму со следующими соглашениями:
- Один прямоугольник для каждого узла, выделенный жирным шрифтом соответствующий тип узла.
- Атрибуты узлов, собирающие метаданные, отображаются синим цветом.
- Другие атрибуты узла аннотируются их типом.
- Узлы связаны на основе их атрибутов.
Имея под рукой эту визуализацию, мы можем наблюдать несколько вещей.
- Корнем дерева является Module узел.
- У Module есть атрибут body - список инструкций в модуле.
- Наш пример состоит из одной операции присваивания и, следовательно, Module.body содержит только один Assign узел.
- Операция присваивания имеет правую часть, определяющую операцию для выполнения, и левую часть, определяющую место назначения операции.
- Справа у нас бинарная операция, которая содержит два операнда(константы) и операцию - сложение.
- Слева же имя переменной и ссылка ctx на переменную в программе.
Собственно осознав эту структуру данных мы можем по ней передвигаться.
:::
## AST visit functions
```{python}
import ast
code = "one_plus_two = 1 + 2"
tree = ast.parse(code)
for node in ast.walk(tree):
print(node.__class__.__name__)
```
```{python}
for name, value in ast.iter_fields(tree):
print(name, value)
```
```{python}
for node in ast.iter_child_nodes(tree):
print(node.__class__.__name__)
```
::: {.notes}
Давайте рассмотрим функции для итерирования по дереву. Например функция walk позволяет совершать обход в глубину ast. А функции iter_fields, iter_child_nodes просмотреть поля ноды или просмотреть ее потомков, они не рекурсивны и применяются только к одному узлу дерева.
С помощью них можно анализировать ast дерево, но это не очень удобно, поэтому предлагаю взглянуть на NodeVisitor.
:::
## [NodeVisitor](https://docs.python.org/3/library/ast.html#ast.NodeVisitor){preview-link="true"}
```{python}
import ast
class BinOpVisitor(ast.NodeVisitor):
def visit_BinOp(self, node):
print(f"found BinOp at line: {node.lineno}")
self.generic_visit(node)
code = "one_plus_two = 1 + 2"
tree = ast.parse(code)
BinOpVisitor().visit(tree)
```
::: {.notes}
Это инструмент для рекурсивного спуска по ast. При этом использует `generic_visit`, то есть смотрит есть ли у него функции `visit_<NODE_NAME>` и если есть вызывает ее. Соответственно вам нужно определить такие функция для определенных nodes и вы сможете проверять различные конструкции в коде использую такой класс.
:::
## А что если написать свой линтер?
```{python}
from typing import Any, Generator, Tuple
__version__ = "0.1.0"
ERROR = Tuple[int, int, str, Any]
ERROR_GENERATOR = Generator[ERROR, None, None]
class NoConstantPlugin:
name = __name__
version = __version__
def __init__(self, tree: ast.AST):
self._tree = tree
def run(self) -> ERROR_GENERATOR:
visitor = Visitor()
visitor.visit(self._tree)
for line, col, msg in visitor.errors:
yield line, col, msg, type(self)
```
:::{.notes}
Это сделать достаточно легко, как я говорил ранее flake8 имеет огромное количество плагинов и написать их не составляет труда. Давайте напишем плагин, который будет запрещать использовать константы отличные от 0, 1, 10 и 100.
Для этого вам нужно создать класс, в конструктор которого будет передаваться abstract syntax tree. А также он будет иметь метод-генератор, который будет выдавать ошибки при их наличии.
Осталось только разобраться с классом visitor и зарегистрировать наш плагин.
:::
## Plugin visitor
```{python}
import ast
class Visitor(ast.NodeVisitor):
errors: list[tuple[int, int, str]] = []
def visit_Constant(self, node: ast.AST):
if node.value not in (0, 1, 10, 100):
self.errors.append((node.lineno, node.col_offset, "NAC100 Not allowed CONSTANT in code"))
self.generic_visit(node)
```
Теперь запустим
```{python}
code = "one_plus_two = 1 + 2"
tree = ast.parse(code)
next(NoConstantPlugin(tree).run())
```
::: {.notes}
Для класса visitor мы можем воспользоваться ast.NodeVisitor и написать функцию для посещения node Constant. В ней сделаем проверку на значение константы, если оно отлично, то добавляем в список ошибку, если нет, то все верно и мы продолжаем итерироваться по ast.
Теперь нужно сделать так что бы наш код мог использоваться flake8.
:::
## Добавляем плагин Flake8
setup.py
```python
flake8_entry_point = # ...
setuptools.setup(
# snip ...
entry_points={
flake8_entry_point: [
'NAC = flake8_example:NoConstantPlugin',
],
},
# snip ...
)
```
pyproject.toml
```toml
[tool.poetry.plugins."flake8.extension"]
NAC = "flake8_example:NoConstantPlugin"
```
:::{.notes}
Что бы его зарегистрировать надо добавить в информацию о проекте flake8 entry point, в котором указано где определен ваш класс плагина.
На слайде показано как это сделать если вы используете традиционный setup.py и как если вы используете pyptoject.
После этого ваш пакет можно собрать, опубликовать и устанавливать вместе с flake8 и другими плагинами для анализа кода.
:::