Holy Python – Telegram
Holy Python
476 subscribers
28 links
Станьте "папой" питона!
Download Telegram
#ошибки

Введение

Сегодня я бы хотел рассмотреть топ 3 самых частых проблем в коде новичков.

F строки .format или % в тексте для компьютера

Мы питон программисты, привыкли форматировать текст с помощью ф строк, .format или % - это очень удобно, а в случае с ф строками ещё и очень быстро.
Если с текстом, который получает человек всё довольно просто, то с текстом для компьютера могут возникнуть проблемы.
Весь текст, ожидаемый компьютером текст должен быть отформатирован согласно определённым правилам, поэтому мы
не можем без последствий вставить произвольную строку. Что я подразумеваю под "текстом для компьютера"?
URL, SQL, любые языки разметки, регулярные выражения, код на питоне и других ЯПах, команды shell. О каких последствиях идёт речь?
Последствия могут быть разные, от банального краша программы, до серьёзной утечки данных.

Разработка не тестируемого кода

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

1. Использование глобальных переменных

Казалась бы, они же упрощают жизнь, но нет, они приводят к усложнению кода,
неконтролируемому доступу и склеиванию с кодом инициализации. Так например, рассматривая код одного пет проекта, я
наткнулся на то, как человек из кнопки, делает SQL запрос.

2. config.py, settings.py, CFG, load_config

Я уверен, что все мои читатели когда-то создавали файлик и/или класс конфигурации и пихали туда
все константы и настройки. Так вот друзья, так делать вредно, ведь когда вы запустите тесты,
ваши настройки будут уже прочитаны, также для тестирования кода придётся часто их изменять.

3. Использование синглтона

Синглтон очень спорный паттерн, который лучше не использовать в коде дабы избежать проблем в будущем.

Данный паттерн нарушает принцип единой ответственности(так как кроме выполнения своей задачи, ещё и следит за наличием только одного экземпляра) и полностью противоречит природе тестов.

У каждого паттерна можно найти свои плюсы и минусы, однако синглтон - это исключение, так как его плюсы на самом деле и являются минусами.

Вывод: Не стоит использовать данный паттерн.

4. Использование функций высшего порядка там где они не нужны.

Вложенные функции невозможно протестировать и переиспользовать, поэтому использовать их нужно осторожно.

Всего я выделил 3 основных кейса, когда их использование необходимо:

1. Создание декоратора
2. Создание замыкания
3. Исключения, где без их использования нельзя создать нужное поведение в программе.

Использование ООП, где оно не нужно или использование псевдо ООП

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

class DeleteFile:
def delete(self):
....

Или процедурные куски кода засовываются в классы:

class Int_input(object):

def new(cls, quest_text, error_text: str = 'Вы ввели некорректное число'):
return cls.int_input(quest_text, error_text)

@staticmethod
def int_input(quest_text, error_text):
while True:
a = input(quest_text)
try:
return int(a)
except ValueError:
print(error_text)


Вывод: Если вы пишите маленький калькулятор на 20 строк или генератор паролей на 2, то не надо использовать ООП!

Дополнительные материалы

https://t.me/advice17/6
https://news.1rj.ru/str/advice17/5
https://github.com/bomzheg/aiogram_template/blob/master/app/
👍6🤮51🤯1
#паттерны

Введение

Сегодня мы рассмотрим паттерны проектирования.

Паттерн проектирования - это повторяемая архитектурная конструкция, представляющая собой решение проблемы проектирования в рамках некоторого часто возникающего контекста.

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

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

Виды паттернов

Паттерны делятся на 3 группы:

1. Поведенческие - паттерны проектирования, определяющие алгоритмы и способы реализации взаимодействия различных объектов и классов.
2. Порождающие - паттерны проектирования, которые имеют дело с процессом создания объектов.
3. Структурные - паттерны проектирования, в которых рассматривается вопрос о том, как из классов и объектов образуются более крупные структуры.

Когда использовать паттерн?

С паттернами нужно обращаться осторожно, ведь при не правильном использовании они могут очень сильно навредить.

Не нужно использовать паттерны просто так, чтобы они были в коде. Также, не стоит использовать паттерн, когда
он сильно усложняет весь код или приводит к нарушению какого-либо принципа ООП.

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

Анти-паттерны

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

То анти-паттерны - это шаблоны ошибок, которые совершаются при решении различных задач.

Ресурсы по изучению паттернов

Легендарная книга Design Patterns от банды четырёх: https://www.ozon.ru/product/patterny-obektno-orientirovannogo-proektirovaniya-gamma-erih-helm-richard-174491298/?sh=EDT-TraCsw
5👍2
#паттерны

Введение

Сегодня мы рассмотрим наш первый паттерн проектирования - стратегия.

Классификация

Тип: Поведенческий

Определение: Стратегия - это поведенческий паттерн проектирования, который определяет семейство схожих алгоритмов и помещает каждый из них в собственный класс, после чего алгоритмы можно взаимозаменять прямо во время исполнения программы.

Грубо говоря, его задача - выделить схожие алгоритмы, решающие конкретную задачу. 

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

Как работает стратегия

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

Стратегии реализуют различные варианты алгоритма.

Клиент должен создать объект стратегию и передать её в контекст.

Чтобы сменить стратегию нужно будет всего лишь изменить объект стратегию в контексте.

Клиент будет вызывать нужные ему методу у контекста, а контекст будет делегировать их на стратегии.

Важное уточнение: Все стратегии должны быть унаследованы от одного абстрактного класса, чтобы контекст был независимым.

Плюсы данного паттерна:

Возможность замены алгоритмов на лету.

Реализует принцип открытости/закрытости

Уходит от наследования к делегированию

Предоставляет разделение между моделью и представлением

Минусы данного паттерна:

Клиент должен знать в чем разница между стратегиями.

Усложняет программу, из-за добавления новых классов.

Пример:

Возьмём довольно просто пример. Пусть у нас есть некий элемент, который нужно провалидировать по определённому правилу, однако каким будет данное правило неизвестно, при этом надо написать код так, чтобы в будущем он не нарушал принцип открытости/закрытости и его было бы удобно расширять.

Задача

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

Дополнительные материалы

https://refactoring.guru/ru/design-patterns/strategy

https://habr.com/ru/post/487858/
5👍1
#паттерны

Введение

Сегодня мы рассмотрим паттерн проектирования "Фабричный метод".

Классификация

Тип
: Порождающий

Определение: Фабричный метод - это порождающий паттерн проектирования, который определяет общий интерфейс для создания объектов в родителе, позволяя подклассам изменять тип создаваемых объектов.

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

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

Из чего состоит и как работает паттерн фабричный метод

1. Абстрактный класс/Интерфейс "создатель" - содержит фабричный метод и метод содержащий базовую бизнес логику, которая основана на объектах, возвращаемых фабричным методом. Метод с бизнес логикой обычно содержит вызов фабричного метода и некую работу с объектом, который вернул фабричный метод:

class BaseCreator(ABC):
@abstractmethod
def fabric_method(self):
"""
Return concrete product.
"""

def operation(self):
# Here some buiesness logic.
product = self
.fabric_method()

2. Абстрактный класс/Интерфейс "продукт" - методы которого реализуют конкретные продукты:

class BaseProduct(ABC):
@abstractmethod
def operation(self):
pass


3. Конкретные классы создатели и конкретные классы продуктов:

class ConcreteProduct1(BaseProduct):
def operation(self):
return "{Result of the ConcreteProduct1}"

class ConcreteProduct2(BaseProduct):
def operation(self):
return "{Result of the ConcreteProduct2}"

class ConcreteFactory1(BaseFactory):
def fabric_method(self):
return ConcreteProduct1()

class ConcreteFactory2(BaseFactory):
def fabric_method(self):
return ConcreteProduct2()


Плюсы данного паттерна:

Избавляет класс от привязки к конкретным классам продуктов.

Упрощает поддержку кода.

Реализует принцип открытости/закрытости

Минусы данного паттерна:

Может привести к созданию больших параллельных иерархий классов.

Пример и задача:

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

Пример из реального кода

Данный пример предоставил @Tishka17

https://github.com/Tishka17/dataclass_rest/blob/master/dataclass_rest/base.py#L24

Вот его объяснение какую задачу фабричный метод решает в данном случае: Это универсальный клиент для рестапишек. Ты реализуешь конкретные методы путем наследования и соответсвенно есть фабричный метод для создания конвертера (Factory из dataclass-factory)
👍6🔥6
Понятны ли вам посты по паттернам? Если нет, напишите в комментариях почему.
Anonymous Poll
20%
Да
20%
Нет
59%
Я оладушек
👍7
Навигация

Посты про ООП - #ООП
Посты про SOLID - #SOLID
Посты про ошибки и антипаттерны - #ошибки
Посты про паттерны проектирования - #паттерны
Посты про тайпхинты - #тайпхинты
Посты с задачами - #задача
Посты с исследованиями - #исследование
🔥9👍3
#паттерны

Введение

Сегодня мы рассмотрим паттерн проектирования "Строитель".

Классификация

Тип: По
рождающий

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

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

Данный паттерн позволяет пошагово инициализировать большие объекты!

Главное отличие «строителя» от «фабрики» — это то, что она используется, когда создание занимает один шаг, а «строитель» применяется при множестве шагов.

Из чего состоит и как работает паттерн строитель

1. Абстрактный класс/Интерфейс "Builder", содержит методы, которые должны быть реализованы в классе строитель.


class BaseBuilder(ABC):
@abstractmethod
def test1(self):
"""Return Test1 object"""

@abstractmethod
def test2(self):
"""Return Test1 object"""

@abstractmethod
def create_test(self):
"""Create test object"""


2.
Класс описывающий сложный объект который мы хотим создать.

class MyTest:
def init(self, test1: object, test2: object):
self.test1 = test1
self.test2 = test2

# Any methods that these object need.
def do(self):
...



3. Классы описывающие объекты, которые нужны для создания сложного объекта.


class Test1:
... # Here some logic

class Test2:
... # Here some logic



4. Класс строителя


class Builder(BaseBuilder):
def test1(self):
return Test1()

def test2(self):
return Test2()

def build_test(self):
test1 = self.test1()
test2 = self.test2()
return MyTest(test1, test2)


Плюсы данного паттерна

Позволяет создавать объекты пошагово.

Сокращает количество написанного кода и повышает читаемость

Изолирует код реализующий конструирование и представление.

Минусы данного паттерна

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

2. Алгоритм создания сложного объекта не должен зависеть от того, из каких частей состоит объект и как они стыкуются между собой.

Пример и задача:

Сегодня возьмем довольно простой пример - машина. Вам нужно реализовать паттерн строитель который будет собирать машину.

Дополнительные материалы

https://m.youtube.com/watch?v=x5yGIuf13Rk1
https://bit.ly/3O7V8oB
👍3
#паттерны

Введение

Сегодня мы рассмотрим паттерн проектирования "Прототип".

Классификация

Тип: Порождающий

Определение: Прототип - это порождающий паттерн проектирования, который позволяет копировать объекты не вдаваясь в подробности их реализации.

Из чего состоит и как работает данный паттерн

1. Абстрактного класса/Интерфейс всех прототипов. Практически всегда содержит только один абстрактный метод - clone.


class BasePrototype(ABC):
@abstractmethod
def clone(self) -> object:
...



2. Конкретный прототип. Реализует операцию клонирования самого себя.


class Test(BasePrototype):
def init(self, test1, test2):
self.test1 = test1
self.test2 = test2

def clone(self):
return Test(self.test1, self.test2)


3. Клиент. Создаёт копии объектов.


test = Test(1, 2)
test2 = test.clone()



4. Опц
ионально - хранилище прототипов. В нём удобно хранить вариации объектов, которые по разному настроены.

В питоне данный паттерн с нуля писать не нужно - есть готовые методы copy.copy() и copy.deepcopy(), рекомендую использовать их.

Плюсы данного паттерна

1. Уменьшает количество кода, ускоряет создание объектов

2. Позволяет клонировать объекты без привязки к конкретному классу.

Минусы данного паттерна

1. Прямое использование в клиенте может ухудшить читаемость кода.

Пример и задача

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

Дополнительные материалы

https://m.youtube.com/watch?v=Wm27d6Nn6VU
👍31
#паттерны

Введение

Сегодня мы рассмотрим паттерн проектирования "Адаптер".

Классификация

Тип:
Структурный

Определение: Адаптер — это структурный паттерн проектирования, который позволяет объектам с несовместимыми интерфейсами работать вместе.
Адаптер выступает прослойкой между объектами превращая вызовы одного в понятные для другого.

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

Из чего состоит и как работает паттерн адаптер

1. Клиент. Описывает бизнес логику программы. Работает с адаптером и другими объектами.


adapter = Adapter()
result = adapter.do() + "5"


2. Интерфейс с которым может работать клиентский код.


class Test1:
def do(self) -> str:
return "3"


3. Класс, который имеет нужную клиенту логику, однако клиент не может с ним общаться, так как интерфейс данного класса ему не понятен.


class Test2:
def specific_do(self) -> float:
return 3.4


4. Адаптер - класс который помогает клиенту использовать функционал незнакомого ему сервиса в правильном формате. Реализует клиентский интерфейс. Содержит ссылку на незнакомый сервис. Адаптер при получении вызовов от клиента через клиентский интерфейс и переводит их в вызовы незнакомого сервиса в правильном формате.


class Adapter(Test1, Test2):
def do(self) -> str:
return f"Translated: {round(self.specific_do())}"


Уточнение: Приложение должно использовать объект адаптера только через клиентский интерфейс. Это позволит легко создавать и изменять адаптеры в будущем.

Плюсы данного паттерна

1. Скрывает все "низкоуровневые" преобразования интерфейсов от клиента. Реализует принцип абстракция.

Минусы данного паттерна

Таковых я не обнаружил

Пример и задача

В качестве примера возьмём класс с методом do который возвращает небольшой текст. Также есть класс с методом secret_do который возвращает другую строку, зашифрованную шифром Цезаря со сдвигом 5, которая ещё и полностью развёрнута.

В виде клиента выступает функция которая постит текст в ваш блог.(Можете просто создать функцию которая выводит переданный в неё текст). Естественно она должна принимать только расшифрованный текст.

Реализуйте адаптер для второго класса, который спасёт ваш блог от зашифрованных постов.

Пример из реального кода

https://github.com/aiogram/aiogram/blob/b190bbba1915ed3b7f311a780f34723ebd6b5acd/aiogram/contrib/fsm_storage/redis.py#L280

Вот его объяснение какую задачу решает адаптер в данном случае:

Здесь адаптер дает возможность контроллеру хранилища (RedisStorage) работать с первой версией редиса, т.е. адаптирует aioredis-v1, там еще есть адаптер для aioredis-v2, но он в отличие от первой версии адаптирует только создание клиента редиса


Дополнительные материалы

https://habr.com/ru/post/85095/
👍52
Введение

Давайте напишем простейшую программу на питоне:

print(0.1 + 0.2)

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

0.30000000000000004

И
это проблема не питона, вы можете запустить такую же программу на js или c++. Вывод будет таким же.

Что же происходит на самом деле?

Давайте разбираться.

IEEE

Чем отличается целое число от дробного с точки зрения хранения?

Правильно в дробном числе есть точка. Точку не возможно представить с помощью нулей и единиц, поэтому нужна другая, специальная форма хранения такого числа. Данную задачу решил "Институт инженеров электроники и электротехники"(IEEE), он создал стандарт хранения дробных чисел - IEEE 754.

Как перевести дробное число из десятичной системы счисления в двоичную?

Перед тем как перейти к изучению стандарта IEEE 754 давайте рассмотрим как перевести дробное число из десятичной системы счисления в двоичную.

В качестве примера возьмём число 3.25. Чтобы перевести его в двоичную систему счисления, нужно перевести целую и дробную часть в двоичную систему поотдельности.

Начнём с целой части. Здесь все просто, мы делим число на 2 пока последнее частное не станет меньше двух. Последнее частное и все остатки записываем в обратном порядке.

Пример:

3 ÷ 2 = 1 (1)

3 в двоичной системе счисления - это 11

Перейдём к дробной части. Здесь вместо деления нам нужно умножение. Мы умножаем дробную часть на 2 пока не получим в ответе единицу. А затем просто склеиваем целые части ответов.

Пример:

0.25 * 2 = 0.5
0.5 * 2 = 1.0

0.25 в двоичной системе - это 01

IEEE 754

Итак, мы успешно перевели дробное число в двоичный вид: 11.01

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


(-1)^s*1.M*10^E

s
- знак числа
m - мантиса(дробная часть числа)
e - это количество знаков на которое нужно сдвинуть точку, чтобы перед ней осталась только одна единица.

Чтобы сохранить число в такой форме стандарт предлагает 3 основных формата хранения:

1. Одинарный - 32 бит
2. Двойной - 64 бит
3. Четырехкратный - 128 бит

Чем больше размер тем выше точность хранимого числа.

В качестве простого примера рассмотрим 32 битный формат.

Первый бит - бит знака. Если он равен нулю то число положительное, если он равен единице то число отрицательное.

Следующие 8 бит - выделены для хранения степени. Чтобы определить знак степени, нам нужно прибавить 127 к значению степени. Если число будет больше 127, то степень положительная, иначе - отрицательная.

Оставшиеся 23 бита - выделены для хранения мантиссы. Если мантисса меньше 23 бит, оставшиеся пространство заполняют нулями.

Вы можете спросить, а где хранится целая часть и основание степени?

Ответ прост - целая часть всегда равна единице, а основание степени всегда равно десяти => для экономии памяти их можно не хранить!

Давайте сохраним наше число в памяти.

1. Сдвинем точку на 1 знак влево 1.101. e = 1
2. Первый бит знака равен нулю, так как число положительное
3. e + 127 = 128 => число положительное.
4. Переведём 128 в двоичную систему счисления.

128 ÷ 2 = 64 (0)
64 ÷ 2 = 32 (0)
32 ÷ 2 = 16 (0)
16 ÷ 2 = 8 (0)
8 ÷ 2 = 4 (0)
4 ÷ 2 = 2 (0)
2 ÷ 2 = 1 (0)

128 в двоичной системе счисления -
10000000
5. Запишем 10000000 в 8 бит предназначеные для степени.
6. Запишем 101 в 23 бита предназначеные для хранения дробной части. Оставшиеся биты заполним нулями.
7. Мы сохранили наше число в памяти!

Вот результат выполнения всех шагов:


0 10000000 10100000000000000000000


Отлично, теперь попробуем перевести сохранённое число обратно:

s = 0 (знак положительный)
m = 101
e = 1

-1^s * 1.m * 10^e = 11.01

Осталось перевести 11.01 в десятичную систему счисления и мы получим наше число - 3.25.

Проблема

Не всегда всё происходит настолько гладко. Некоторые дробные числа просто нельзя перевести в двоичную систему счисления.

Пример:
👍6
0.8 * 2 = 1.6
0.6 * 2 = 1.2
0.2 * 2 = 0.4
0.4 * 2 = 0.8
...

Поэтому приходится записывать в дробную часть столько, сколько возможно.

После сохранения числа 0.8 в память. Мы получим, вот такой результат:


0 01111110 10011001100110011001101


А
при переводе данного числа обратно мы получаем непонятное число с большим количеством нулей и цифр:

0.800000011920928955078125

Теперь вы знаете, как на самом деле хранятся дробные числа и почему 0.1 + 0.2 != 0.3 с точки зрения компьютера.

Дополнительные материалы

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

Пример:


print(0.8) -> 0.8
print(f"{0.8:.20f}") -> 0.80000000000000004441


Когда нужны точные вычисления(например в области финансов), рекомендую использовать стандартную библиотеку decimal.

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

https://docs.python.org/3/library/decimal.html
https://0.30000000000000004.com/
🔥7👍4
Введение

Наверняка если вы изучаете питон вы слышали об одной достаточно известной аббревиатуре - GIL,
если вы немного углублялись в тему вы могли узнать что это штука как-то связана с потокам

Сегодня мы попробуем разобраться что это, как это работает и зачем это нужно питону.

GIL

GIL(Глобальная блокировка интерпретатора) - это один из способов синхронизации потоков, который позволяет только одному потоку управлять интерпретатором Python. Задача GIL сделать интерпретатор CPython потокобезопасным.

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

Казалось бы, зачем вводить такие ограничения? Это же может очень сильно навредить производительности многопоточных программ?

Да, это очень резонные вопросы к данному подходу, GIL и вправду очень часто является "бутылочным горлышком" производительности многопоточных программ. Однако данный механизм не был добавлен просто так, дело в том, что GIL решает одну довольно серьёзную проблему о которой мы поговорим ниже.

Проблема гонки

У каждого объекта в питоне есть свой "счётчик ссылок", который отслеживает количество ссылок указывающий на данный объект.

Если счётчик = 0, то память занятая объектом высвобождается.

Пример:


import sys

x = 3
a = x
print(sys.getrefcount(x)) # 38 refs
del a
print(sys.getrefcount(x)) # 37 refs

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

Именно эту проблему в случае питона и решает GIL.

Если нет параллельных потоков, то и никаких проблем с ними не будет)

Как работает GIL?

Когда какой-либо поток захватывает GIL, остальные потоки которые хотят захватить его вынуждены ждать. Переключение между потоками может происходить: когда активный поток пытается осуществить ввод-вывод, по исчерпании лимита выполненных инструкций, либо по таймеру

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

Почему в питоне используется именно GIL и его не удалили/заменили?

1. GIL очень прост в реализации
2. GIL повышает производительность однопоточных программ.

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

Плюс, даже если будет найдено адекватное решение проблемы гонки, которое удовлетворяет всем требованиям питона, оно сломает существующие расширения на C, которые зависят от GIL.

Достаточное количество разработчиков предпринимали попытки убрать GIL, однако ничего толкового из этого не вышло.

Дополнительные материалы

https://wiki.python.org/moin/GlobalInterpreterLock
https://towardsdatascience.com/python-gil-e63f18a08c65
👍73👎1
#паттерны

Введение

Сегодня мы рассмотрим паттерн проектирования "Абстрактная фабрика".

Классификация

Тип: Порождающий

Определение: Абстрактная фабрика - это порождающий паттерн проектирования, который предоставляет интерфейс для создания семейств взаимосвязанных или взаимозависимых объектов, не специфицируя их конкретных классов.

Грубо говоря, абстрактная фабрика - это "фабрика фабрик", данный паттерн позволяет решить проблему создания целых семейств связанных объектов, без указания конкретных классов продуктов.
С помощью абстрактной фабрики вы можете предоставить библиотеку объектов не расскрывая их реализацию.

Из чего состоит и как работает данный паттерн

1. Абстрактного класса/Интерфейса абстрактной фабрики. Содержит абстрактные методы которые возвращают абстрактные продукты, связанные одной конпцецией.

class AbstractFactory(ABC):
@abstractmethod
def create_product_a(self):
...

@abstractmethod
def create_product_b(self):
...


2. Конретные фабрики. Конкретные фабрики реализут операции которые создают конкретные продукты.

class ConcreteFactory1(AbstractFactory):
def create_product_a(self):
return ConcreteProductA1()

def create_product_b(self):
return ConcreteProductB1()

class ConcreteFactory2(AbstractFactory):
def create_product_a(self):
return ConcreteProductA2()

def create_product_b(self):
return ConcreteProductB2()


3. Абстрактные классы продуктов. Реализуют интерфейс для всех конретных продуктов своего семейства.

class AbstractProductA(ABC):
@abstractmethod
def very_important_super_function_a(self) -> str:
...

class AbstractProductB(ABC):
@abstractmethod
def very_important_super_function_b(self) -> str:
...


4. Конкретные продукты. Реализуют абстрактные продукты. Продукты одного семейства не могут взаимодействовать, с продуктами другого семейства.

class ConcreteProductA1(AbstractProductA):
def very_important_super_function_a(self) -> str:
return "Product A1"


class ConcreteProductA2(AbstractProductA):
def very_important_super_function_a(self) -> str:
return "Product A2"

class ConcreteProductB1(AbstractProductB):
def very_important_super_function_b(self) -> str:
return "Product B1"


class ConcreteProductB2(AbstractProductB):
def very_important_super_function_b(self) -> str:
return "Product B2"


5. Клиент. Клиентский код работает исключительно с абстрактной фабрикой и абстрактными продуктами.

def client(factory: AbstractFactory) -> str:
return factory.create_product_a().very_important_super_function_a()

for factory in (ConcreteFactory1, ConcreteFactory2, ...):
client(factory)


Плюсы данного паттерна

1. Реализует принцип открытости/закрытости.

2. Упращает поддержку кода.

3. Выделяет код производства продуктов в одно место, упрощая поддержку кода.

Минусы данного паттерна

1. Снижает читаемость программы из-за введения множества дополнительных классов.

Пример и задача

Дед Мороз и Пасхальный кролик не успевают сделать игрушки к празднику, которые они будут дарить детям.
Все существует 3 вида игрушек:

1. Лошадка
2. Зайчик
3. Шарик

Всё было бы просто однако Деду Морозу нужны игрушки в новогоднем стиле, а пасхальному кролику в пасхальном.
Создайте абстрактную фабрику игрушек и спасите эти праздники!

Пример из реального кода

Пример из реального кода предоставил @Tishka17. Он разработал замечательный фреймворк aiogram_dialog для разработки интерактивных диалогов и меню в телеграмм ботах, как обычное приложение с графическим интерфейсом.

Вот здесь он использует данный паттерн: https://github.com/Tishka17/aiogram_dialog/blob/develop/aiogram_dialog/manager/manager_middleware.py#L23

Вот его объяснение, какую задачу он решает в данном случае:
5👍3👎1
В библиотеке aiogram dialog есть диалог менеджер, он реализует базовую функциональность управления диалогами: старт разным способом, доступ к контексту и т.п.
Также есть менеджер - это временный объект. Он создаётся на время обработки конкретного события. Соответственно, его надо постоянно пересоздавать.
В какой-то момент я решил что неплохо дать возможность что-то в менеджере переопределить, соответственно нужно и фабрику менять.
Для решения этой проблемы абстрактная фабрика подошла лучше всего.
10👎1
6👍2👎1
Введение

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

Сегодня мы разберёмся, как правильно искать и исправлять ошибки в программе.

План исправления ошибки

Представим вы запустили вашу программу и увидели что она работает неверно. Данные которые мы должны получать на выходе и данные которые возврашает программа не совпадают. Что делать в данном случае? Следуйте этому плану поиска и исправления бага:

0. Проверьте правильность "ожидаемых данных", возможно программа делает все правильно, а ошибка содержится в них.

1. Стабилизация ошибки(если возможно) - некоторые ошибки возникают не постоянно, их нужно стабилизировать - сделать так чтобы одна и таже ошибка появлялась всегда.

2. Поиск места возникновения ошибки
    a. Пробуем первые входные данные, анализируем выдвигаем гипотезы. Создаем новые входные данные, передаём в программу, на основе всех результатов ищем корреляции, проводим дебаг и выдвигаем гипотезы.
   b. Выдвигаем гипотезы, на основе предыдущих данных, тестируем, если гипотеза подтвердилась ищем причину возникновения ошибки путем дебага, затем приступаем к пункту 3 иначе повторяем процесс.

3. Исправление ошибки. Мы уже знаем проблемное место - 80% работы выполнено, теперь остаётся исправить эту ошибку.

4. Тестируем новое решение.

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

6. Ищем схожие возможные проблемы в программе.

Типичные антипаттерны

1. Виноваты все кроме моей программы. Иногда люди начинают обвинять все подряд кроме своей программы(интерпретатор, библиотеку, IDE). И так бывает очень сложно найти и исправить баг в программе, но если ты считаешь что проблема не ней, то это вообще не реально.

2. Исправление конкретного случая возникновения проблемы. Вместо поиска и исправления всей проблемы, новички часто пишут условие которое обрабатывает конкретный случай и возвращают "правильные данные". Такой подход приводит к страшным последствиям в будущем.

3. Поиск гаданием. Вместо применения научного метода в данном случае человек расставляет принты и "трай эксепты" по всей программе, а потом гадает где же проблема. Крайне не эффективный подход.

P. S. Если вы знаете, как дополнить пост, в частности пункт 1 и часть "типичные антипаттерны", добро пожаловать в комментарии.
👍64
#ошибки

Введение

Сегодня мы поговорим о минусах и вреде такого известного фреймворка как Django.

Немного уточнений:

1. Этот пост был сделан для того чтобы вы ознакомились с минусами данного фреймворка, я не заставляю вас не использовать его.
2. С данным фреймворком ознакомится в любом случае придется, так как бизнес все ещё его использует, однако предлагать ее внедрение в новый проект - плохая идея.
3. Если начать с джанги и не смотреть в сторону других фреймворков, будет складываться ощущение, что в ней все сделано правильно.

Проблемы архитектуры

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

1. Нарушены многие принципы написания кода и архитектуры(SOLID, CCP, принцип ацикличности зависимостей)
2. Используются некоторые антипаттерны(например: глобальные переменные, хранение настроек ввиде большого количества переменных в одном файле, подробнее о таких антипаттернах в: https://news.1rj.ru/str/holy_python/46)
3. Сильная связность компонентов, отсутствие гибкости, зависимость от преждевременных решений. При замене какого-либо решения используемого Django, сломаются другие компоненты. Пример: Если мы поменяем Django ORM на SQLAlchemy, то сломается Django Admin.

Проблемы ORM

Django ORM, содержит массу серьёзных проблем, которые усложняют разработку.

Отсутствует:

1. Иерархические и рекурсивные запросы в SQL(CTE)
2. Возможность кастомизации group by
3. Виртуальные ключи
4. Возможность разделения таблиц на группы.
5. Виртуальные таблицы
6. Триггеры(особые разновидности хранимых процедур, которые автоматически выполняются при возникновении события на сервере базы данных)
7. Функции
8. Поддержка only по умолчанию
9. Асинхронность для БД(пытаются добавить ввиде костылей в 4.x)

Другие проблемы:

1. Используется спорный паттерн ActiveRecord. Ведёт к нарушению принципа SRP, сильно связан с бизнес логикой. Если вы захотите использовать другую абстракцию для хранения данных придётся проводить рефакторинг.

Заключение

В данном посте, я постарался отметить основные минусы данного фреймворка. Пользоваться ли им или нет - решать вам.

P. S. Если вы знаете другие значительные минусы Django добро пожаловать в комментарии.

Дополнительные материалы

https://www.djangoproject.com
https://habr.com/ru/post/198450
https://news.1rj.ru/str/rudepython/168523
👍143🔥1
#чистаяархитектура

Введение

Сегодня я хотел бы поговорить о трёх, часто забываемых принципах чистой архитектуры. Речь пойдёт о триаде принципов для компонентов(минимальных единиц развёртывание, с точки зрения питона, это один/несколько файлов с колом): CCP, CRP, REP.

CCP

Расшифровка: Common Closure Principle – принцип согласованного изменения.

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

Давайте кратко сформулируем этот принцип:

Собирайте вместе все, что изменяется по одной причине и в одно время. Разделяйте все, что изменяется в разное время и по разным причинам.

CRP

Расшифровка: Common Reuse Principle - принцип совместного повторного использования.

CRP - указывает, что в компонент должны включаться классы и модули, используемые совместно.
Данный принцип предполагает, что в компоненте должны быть классы, имеющие множество зависимостей друг от друга. Как пример абстрактные фабрики и их фасад.

Давайте кратко сформулируем этот принцип:

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

REP

REP - Release Equivalence Principle - Принцип эквивалентности повторного использования и выпуска. Данный принцип означает, что эффективно переиспользовать можно только компоненты, релизнутые через системы версионирования. Без номера версии невозможно гарантировать совместимость всех компонентов, а разработчики не смогут чётко увидеть когда версия появилась и какие изменения в ней произошли. С точки зрения архитектуры принцип означает, что классы и модули, составляющие компонент, должны принадлежать связной группе. Очень похоже на предыдущие принципы, не так ли? Но тут появляется ещё одно условие. Классы и модули, объединяемые в компонент, должны выпускаться вместе.

Давайте кратко сформулируем этот принцип:

1. Единица повторного использования = единице выпуска
2. Эффективно можно использовать только компоненты, имеющие номер версии.

Противоречия

Внимательный читатель заметил что все 3 принципа одновременно соблюсти невозможно. Принципы CCP и REP стремятся сделать компонент как можно мельче, а принцип CRP как можно крупнее. Наша задача понять когда, какой принцип важнее. Для этого я выделил набор правил.

1. В фреймворках/библиотеках должен соблюдаться именно CCP, соблюдение CRP приведёт к каше, так как в фреймворках/библиотеках важнее всего чёткость компонентов и структура.
2. В больших проектах соблюдение CCP приведёт к слишком большому количеству компонентов, что только усложнит разработку.
3. На начальной стадии развития проекта стоит соблюдать CCP, а в дальнейшем постепенно смещать фокус на REP и/или CRP.
4. Слишком много зависимостей между компонентами приводит к их неустойчивости(об этом в следующем посте), что усложняет разработку.

Заключение

Чтобы хорошо спроектировать свой проект пользуйтесь выше перечисленными правилами и старайтесь искать золотую середину между этими принципами.

Дополнительные материалы

https://books.google.com.tr/books/about/%D0%A7%D0%B8%D1%81%D1%82%D0%B0%D1%8F_%D0%B0%D1%80%D1%85%D0%B8%D1%82%D0%B5%D0%BA%D1%82%D1%83%D1%80%D0%B0_%D0%98%D1%81.html?id=d6JSDwAAQBAJ&printsec=frontcover&source=kp_read_button&hl=ru&newbks=1&newbks_redir=0&gboemv=1&redir_esc=y
👍92
Хотите ли вы на канале видеть статьи от контрибьютера cpython @backinblacknext?
В частности, статьи про эталонную реализацию интерпретатора питона - CPython
Anonymous Poll
79%
Да
21%
Нет
By: quantum super position, MVC

Введение

Сегодня мы научимся добавлять свою функцию в builtins¹ эталонной реализации языка программирования Python - CPython.

Перед тем как приступить, давайте определимся какую функцию мы хотим добавить.

В builtins присутствует функция abs, её задача - вызывать у объекта dunder-method(магический метод) abs.

Обычно abs используют для того, что бы вернуть модуль какого-то числа.

Помимо dunder-method'a abs у числовых типов обычно присутсвует neg, который отрицает число.

Но в builtins функции neg которая бы вызывала этот dunder нет!

Именно реализацией этого мы с вами сегодня и займемся.

Замечание: Стоит отметить, что на самом деле number может вызывать neg, но отдельной функции под это нет. Видимо это сделанно из соображений красоты.

Реализация

Начнём с установки. Склонируем репозиторий интерпретатора:

git clone --depth 1 --branch 3.11 https://github.com/python/cpython

Затем перейдем в директорию cpython:

cd cpython

Теперь откроем Objects/abstract.c и реализуем функцию PyNumber_Negate:

PyObject * PyNumber_Negate(PyObject *object) {
if (object == NULL) {
return null_error();
}
PyNumberMethods *methods = Py_TYPE(object)->tp_as_number;
if (methods && methods->nb_negative) {
PyObject *res = methods->nb_negative(object);
assert(_Py_CheckSlotResult(object, "neg", res != NULL));
return res;
}
return type_error("bad operand type for neg(): '%.200s'", object);
}

Эта функция получается в виде аргументов указатель на PyObject,
далее проверяем, не указывает ли object на NULL
Следущим шагом мы получаем методы object.
"if" здесь нам нужен что бы проверить, есть ли метод nb_negative и neg у объекта.
Если таковых нет, то возвращаем исключение type_error("bad operand type for neg(): '%.200s'", object)

Иначе возвращаем ng_negative объекта, можно считать что это neg.

Теперь откроем заголовочный файл Include/abstract.h и объявим наш PyNumber_Negate:

PyAPI_FUNC(PyObject *) PyNumber_Negate(PyObject *o);

Регистрация

Для начала откроем Python/bltinmodule.c и реализуем функцию builtin_neg:

static PyObject *
builtin_neg(PyObject *modle, PyObject *x){
return PyNumber_Negate(x);
}


Это и есть реализация нашего будущего neg.
Она вызывает функцию PyNumber_Negate, которая в свою очередь и делает все проверки, и в случае их успеха, возвращает нам neg.

Затем открываем файл Python/clinic/bltinmodule.c.h, тут мы обьявим макрос BUILTIN_NEG_METHODDEF.

Но перед этим стоит написать небольшую документацию нашего метода, которая будет отображаться при вызове help(neg):

PyDoc_STRVAR(builtin_negdoc,
"neg($module, x, /)\n"
"--\n"
"\n"
"Return the negate value of the argument.");

Теперь объявим наш макрос:

#define BUILTIN_NEG_METHODDEF \
{"neg", (PyCFunction)builtin_neg, METH_O, builtin_negdoc},
В дефайне макроса, как мы видим мы передаём название билтина neg, и функцию реализующую его (PyCFunction)builtin_neg
Теперь вернемся к Python/bltinmodule.c
Здесь нам нужно найти static PyMethodDef builtin_methods[], и добавить в него наш макрос BUILTNI_NEG_METHODDEF
После добавления, static PyMethodDef builtin_methods[] будет выглядеть как-то так:

static PyMethodDef builtin_methods[] = {
{"build_class", _PyCFunction_CAST(builtin_build_class),
METH_FASTCALL | METH_KEYWORDS, build_class_doc},
BUILTIN_IMPORT_METHODDEF
BUILTIN_ABS_METHODDEF
BUILTIN_ALL_METHODDEF
BUILTIN_ANY_METHODDEF
BUILTIN_ASCII_METHODDEF
BUILTIN_BIN_METHODDEF
BUILTIN_NEG_METHODDEF // а вот и наш макрос!
// здесь еще куча макросов и остального
};

Мы добавили нашу функцию в builtins питона! Осталось всё это запустить.

Сборка

Если у вас UNIX like OS, то для того что бы начать сборку вам нужно ввести в терминал следующее:
./configure --with-py-debug
Флаг --with-py-debug означает что мы собираем Python для дебага, и там не будет некоторых оптимизаций, что, конечно, ускорит сборку.
Далее вводим make -j 2 за что отвечает -j 2 можно прочитать в man make.
👍64
Если же у вас Windows, нужно ввести следующее:
PcBuild/build.bat -c Debug
Здесь мы опять же собираем питон с флагом -c Debug, что бы сборка была быстрее.

Замечание: Сборка процесс не быстрый. Эти команды могут выполняться достаточно долго, всё зависит от вашего железа.

Протестируем то что у нас получилось

создадим в корне директории cpython файл test_interpreter.py, и протестируем наш neg()

help(neg)

Запустим наш test_interpreter.py с помощью нашего нового интерпретатора
./python test_interpreter.py
Help on built-in function neg in module builtins:

neg(x, /)
Return the negate value of the argument.
Прекрасно! Та самая небольшая документация что мы писали, работает.
Теперь изменим test_interpreter.py:
print(neg(1))
print(neg(-1))

./python test_interpreter.py
-1
1
Отлично. Всё работает как нужно. Теперь давайте проверим как это будет работать
с классами, которые определит пользователь.
class SomeClass:
def init(self, number: int) -> None:
self.number = number
def neg(self):
return -2 * self.number
Мы реализовали neg у класса, который будет возвращать -2 * number
Давайте проверим!

test = SomeClass(5)
print(neg(test))

./python test_interpreter.py
-10
Чудно! Всё работает ровно так как мы ожидали.
Теперь, попробуйте реализовать какую-нибудь не столь бесполезную функцию как та что описана в статье.

1. builtins - встроенные в интерпретатор функции и типы, которые всегда доступны, например print или bool

Дополнительные материалы

https://github.com/python/cpython

Если у вас возникли трудности, вы можете взглянуть на этот форк CPython: https://github.com/Eclips4/cpython_articles/tree/3.11
В частности, на этот(https://github.com/Eclips4/cpython_articles/commit/97965a553096e1e6121ec032ab26a2794a40f315) коммит.
👍9👎3🥰2🎉2🔥1