Технологические заметки
10 subscribers
30 photos
1 video
20 links
Пишу блог для себя.
Основной контент: парсинги, автоматизация и аналитика.
Стек технологий: Python, VBA и пр.
Download Telegram
#ТаковПуть

Продолжаем примеры упаковки распаковки переменных

Упаковка в list и dict:

a = [1, 2]
b = {3, 4}
c = (5, 6)

*list_data1, = *a, *b, *c
print(list_data1)
#Output: [1, 2, 3, 4, 5, 6]

*list_data2, = a, b, c
print(list_data2)
#Output: [[1, 2], {3, 4}, (5, 6)]

dict_data = dict(list_data2)

print(dict_data)
#Output: {1: 2, 3: 4, 5: 6}
👍1
#ВДзене

Друзья, коротко делюсь мыслями о новых статьях в Дзене


# 6 Работа с Git на Windows
В своих статьях я часто ссылаюсь на GitLab. Решил написать статью про установку, настройку и работу с Git и GitLab

Думаю, что надо написать ещё несколько статей про Git, CI/CD, ведение документации и автоматизацию с GitLab.

#git


#  7 Создание XLSB файла

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

Скоро выпущу статью про скрапинг террористов, где эту программу применю.

#python #excel #pywin32


#8 Про совместную работу в Jupyter Notebook

Статья написана по памяти. На одной из моих работ было реализовано пространство для совместной работы в Jupyter, решил поделиться решением.

Единое пространство для работы на Python – очень полезная вещь. Как минимум, это позволяет подсматривать за кодингом коллег, что полезно в обучающих целях.

#python #team

Предыдущие статьи.
👍1
#ЧистыйКот

Короткие функции

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

Старайтесь, чтобы ваши функции содержали до 15 строк (а лучше до 5) и выполняли только одну задачу.

К примеру, в моем недавнем проекте я мог бы создать один метод, который бы создавал Application, Workbook и Sheet эксель файла.

def _create_app( self ) -> None:
    self.xl_app = ...
    ...
    self.xl_wb = ...
self.xl_sheet = ...

Но для большей наглядности я разделил три действия на три метода.

def _create_new_app( self ) -> None:     
self.xl_app = ...
self.xl_app.Visible = True
self.xl_app.ScreenUpdating = True

def _create_new_wb( self ) -> None:
self.xl_wb = ...

def _activate_sheet( self ) -> None:
self.xl_sheet = ...
#ВДзене

Друзья, коротко делюсь мыслями о новых статьях в Дзене

Все программы в статьях намеренно написаны в разных архитектурных решениях.

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

# 9 Скрапинг перечня террористов ФЛ

В рамках статьи наполняем эксель файл данными с сайта.

#python #selenium #excel #pywin32

# 10 Скрапинг перечня террористов ЮЛ

В рамках статьи разворачиваем БД, которую наполняем данными с сайта.

#python #selenium #PostgreSQL #pyodbc

# 11 Нахождение террориста в ЧС ФЛ

Показываю интересный подход к поиску человека в ЧС, когда в рамках одного ФИО делаем сотни различных проверок.

#python #excel #re #pyxlsb #pydantic

# 12 Нахождение террориста в ЧС ЮЛ

Показываю немного другой подход к поиску в черном списке.

#python #PostgreSQL #re #pyodbc #pydantic

🎯 В планах: на базе этих программ сделать API, а на базе него web-сервис с UI и авторизацией.

Предыдущие статьи.
#ТаковПуть

Уникальные элементы list. ч.1

Довольно часто требуется получить уникальный набор элементов списка.

Возьмём список:
l = ["Атос", "Портос", "Арамис", "д’Артаньян", "д’Артаньян"]

Самый простой и популярный способ - это получить set элементов:
u = set(l)
print(u)
# Output:
# {'д’Артаньян', 'Арамис', 'Атос', 'Портос'}

Проблема в том, что теряется порядок записей.

Можно проитерироваться по элементам list и добавлять уникальные элементы в другой list:
u = list()
[u.append(k) for k in l if k not in u]
print(u)
# Output:
# ['Атос', 'Портос', 'Арамис', 'д’Артаньян']

Порядок сохранен, но надо заранее создать пустой list.

А можно применить магию. И преобразовать list в dict и вернуть list ключей:
u = [*dict.fromkeys(l)]
print(u)
# Output:
# ['Атос', 'Портос', 'Арамис', 'д’Артаньян']

Результат тот же, а кода меньше.
👍1
#ТаковПуть

Уникальные элементы list. ч.2

Возьмём список из первой части и для каждого имени создадим экземпляр класса Person, который будет содержать только это имя:
class Person:
def __init__(self, name):
self._name = name

def __repr__(self):
return self._name

names = ["Атос", "Портос", "Арамис", "д’Артаньян", "д’Артаньян"]
persons = []

for name in names:
persons.append(Person(name))

print(persons)
# Output:
# [Атос, Портос, Арамис, д’Артаньян, д’Артаньян]

Все примеры из ч.1 не будут работать 😱
К примеру:
u = set(persons)
print(u)
# Output:
# {Портос, Атос, д’Артаньян, Арамис, д’Артаньян}

Не только порядок потерялся, еще и дубли не убрались 😢

Чтобы исправить ситуацию, необходимо доработать класс Person

class Person:
def __init__(self, name):
self._name = name

def __repr__(self):
return self._name

def __eq__(self, other):
if isinstance(other, Person):
return self._name == other._name
else:
return False

def __hash__(self):
return hash(self._name)

Проверяем:
u = [*dict.fromkeys(persons)]
print(u)
# Output:
# ['Атос', 'Портос', 'Арамис', 'д’Артаньян']

Дубли убрались и порядок остался 😎
👍1
#ТаковПуть

Возможности конструкции match-case в Python, ч.1

Конструкция представляет собой более мощный инструмент, чем аналоги в других языках.
Ниже приведен пример обработки словаря.
def match_dict( data : dict ) -> None:
match data:
case {"name": name, "age": age}:
print(f"Имя: {name}, Возраст: {age}")
case {"name": name}:
print(f"Имя: {name}")
case _:
print("Имя не найдено")

Подадим словарь, где будет ключ name
d = {"id": 1, "name": "Вася"}
match_dict(d)
# Output:
# Имя: Вася

Сработал второй блок case.
Первый case был пропущен, т.к. нет поля age
Как видно из примера, словарь не должен на 100% совпадать по ключам, что делает конструкцию match-case очень интересным инструментом в разработке и отладке программ.

Теперь добавим ключ age
d = {"id": 1, "name": "Вася", "age": 19}
match_dict(d)
# Output:
# Имя: Вася, Возраст: 19

Сработал первый блок case.

Теперь подадим организацию, а не физ лицо
d = {"id": 1, "orgName": "ООО Ромашка", "inn": "1234567890"}
match_dict(d)
# Output:
# Имя не найдено

Сработал дефолтный блок case
Спасибо разработчикам Python за такой прекрасный инструмент, который убирает из кода громоздкие конструкции if-else
👍1
#ТаковПуть

Возможности конструкции match-case в Python, ч.2

В примере ниже показано, как можно с помощью конструкции match-case обрабатывать разные типы переменной
type NestedIntItem = int | list[NestedIntItem]

def print_list( data: NestedIntItem ) -> None:
match data:
case int():
print(data)
case list():
for item in data:
print_list(item)
case _:
raise TypeError(f"Expected int or list, got {type(data)}")

nested_list: NestedIntItem = [5,
[3, 7],
[9, [0, 1]],
2,
8,
[4, 6]
]
print_list(nested_list)
# output
# 5
# 3
# 7
# 9
# 0
# 1
# 2
# 8
# 4
# 6

Конструкцией type мы создали пользовательский тип.
Причем этот тип рекурсивный, т.е. ссылается сам на себя.
Фактически был создан новый тип: Целое число или список, содержащий числа и другие списки.

Функция принимает на вход этот тип данных.
С помощью конструкции match-case маршрутизируется логика программы
Как итог, программа рекурсивно печатает все числа во вложенных списках.
👍1
#ТаковПуть

Валидация полей класса. ч.1 Подготовка.


Есть множество различных вариаций, как валидировать поля.
Тут показан один из самых надежных.

Для усложнения примера возьмем не один класс, а два класса.
Класс Client будет наследоваться от BaseModel библиотеки pydantic.
Класс ClientData будет наследовать поля из класса Client и все возможности класса BaseModel.

Так как мы наследуемся от BaseModel, у нас появляется в руках мощный инструмент валидации полей.

Напишем базовый код:
from pydantic import BaseModel, ValidationError, field_validator

class Client(BaseModel):
name: str

class ClientData(Client):
ids: list[int]
phone_type: str
phone: str

# Тут будем писать весь дальнейший код

try:
product =ClientData(name="Peter", ids=[100], phone_type="mobile", phone="9123456")
print(product)
except ValidationError as err:
print(err)

# output
# name='Peter' ids=[100] phone_type='mobile' phone='9123456'


Пока у нас нет валидации полей, кроме валидации типов.
Например, в поле ids мы не сможем написать str

В следующей части реализуем 3 разных варианта валидации.
👍1
#ТаковПуть

Валидация полей класса. ч.2 Валидация

1. Преобразование типа
Если в поле ids передадим не list, а int, программа сама должна преобразовать int в list
@field_validator('ids', mode="before")
@classmethod
def check_id( cls, value ):
if not isinstance(value, list):
return [value]
else:
return value

Очень важно указать mode="before", если указать mode="after", то программа упадет, т.к. ids обязан быть list.
Разница между mode="before" и mode="after" в том, что при mode="before" программа вызывает метод ДО проверки типов класса, что позволяет методу работать с любым типом данных, а не только тем, что обязан подаваться на вход.

2. Валидируем несколько полей один за другим.
@field_validator('name', 'phone', 'phone_type', mode="after")
@classmethod
def check_len( cls, value, info ):
if len(value) <= 2:
raise ValueError(f'Поле {info.field_name} должно быть длинее 2х символов')
return value

Цикл скрыт от нас. Но он есть и поочередно проверяет на кол-во символов сначала поле name, потом phone_type, потом phone.
Так как именно в таком порядке поля инициализированы в классе ClientData.
В методе появилось поле info, оно содержит объект ValidationInfo.
Он содержит информацию о полях класса ClientData, которые уже прошли валидацию.
И содержит информацию о поле, которое проходит валидацию.

3. Валидируем поле, опираясь на ранее провалидированное поле
@field_validator('phone')
@classmethod
def check_phone( cls, value, info ):
phone_type = info.data['phone_type']
if phone_type == 'mobile':
if not value.startswith("9"):
raise ValueError('Мобильный должен начинаться с 9')
return value

К моменту, когда начинается валидация поля phone, уже провалидировано поле phone_type.
Поэтому в рамках примера мы можем реализовать проверку типа: если это мобильный телефон, то он должен начинаться на 9
Также в декораторе не указали mode, это значит, что используем дефолтный, т.е. mode="after".
👍1
#ТаковПуть

Класс контейнер

Создаем контейнер Orders, который будет содержать список из классов Order

Контейнер будет:
- итерируемый
- с поддержкой срезов
- с разными поисками элементов контейнера
- с проверкой типов
- с возможностью добавления элементов
- с проверкой размерности контейнера

Код широкий, лучше просматривать горизонтально.

from typing import Sequence, Iterator, Optional, List
from dataclasses import dataclass

@dataclass
class Order:
id: int

def __repr__(self) -> str:
return f"order_id={self.id}"


class Orders:
def __init__(self, orders: Sequence[Order]) -> None:
if not all(isinstance(o, Order) for o in orders):
raise TypeError("Все элементы должны быть типа Order")
self._orders = list(orders)

def __getitem__(self, key: int | slice) -> Order | List[Order]:
if isinstance(key, slice):
return self._orders[key]
if not isinstance(key, int):
raise TypeError("Индекс должен быть целым числом или срезом")
if not 0 <= key < len(self._orders):
raise IndexError(f"Индекс {key} вне диапазона [0, {len(self._orders)})")
return self._orders[key]

def __len__(self) -> int:
return len(self._orders)

def __iter__(self) -> Iterator[Order]:
return iter(self._orders)

def __contains__(self, item: object) -> bool:
return item in self._orders

def get_by_id(self, id: int) -> Optional[Order]:
return next((o for o in self._orders if o.id == id), None)

def add_order(self, order: Order) -> None:
if not isinstance(order, Order):
raise TypeError("Можно добавлять только объекты Order")
self._orders.append(order)


if __name__ == "__main__":
orders = Orders([Order(1), Order(2), Order(3)])

print("Все заказы:")
for order in orders:
print(order)

print("Первый заказ:", orders[0])

print("Количество заказов:", len(orders))

print("Есть ли заказ с id=2:", Order(2) in orders)

print("Поиск заказа с id=3:", orders.get_by_id(3))

print("Поиск несуществующего заказа:", orders.get_by_id(99))

# Добавление нового заказа
orders.add_order(Order(4))
print("После добавления:", [o.id for o in orders])

print("Последние 2 заказа:")
for order in orders[-2:]:
print(order)
#ТаковПуть

Структурная типизация и причём тут утки. ч.1🦆🦆🦆

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

Структурная типизация - это продвинутая версия классической утиной типизации:
"Если это ходит как утка и крякает как утка, то это утка"

Пример:
class Bird:
def quack(self):
print("Кря!")

class Person:
def quack(self):
print("Кря!")

def speak(obj):
obj.quack()

speak(Bird()) # Кря!
speak(Person()) # Кря!

Как видно на примере, т.к. у Person есть метод quack, то метод speak прекрасно его применил и человек крякнул.😜

Структурная типизация работает на схожем подходе. Для её реализации нам необходимо создать класс, наследуемый от Protocol

Теперь посмотрим работу с таким классом:
from typing import Protocol, List
import statistics

class Stat(Protocol):
def __call__(self, items: List[int]) -> float:
...

def avg( items: List[int] ) -> float:
return sum(items) / len(items)

def med(items: List[int]) -> float:
return statistics.median(items)

def hello(items: List[int]) -> str:
return "Hello"

def stat(op: Stat, items: List[int]) -> float:
return op(items)

# Список int для примера
numbers = [1, 2, 3, 4, 5]

avg = stat(avg, numbers)
median = stat(med, numbers)
hi = stat(hello, numbers) # ошибка: Expected type 'Stat', got '(items: list[int]) -> str' instead

print(f"Среднее: {avg}") # Среднее: 3.0
print(f"Медиана: {median}") # Медиана: 3

Функции avg и med реализуют интерфейс метода call, поэтому они прекрасно работают.
А вот функция hello возвращает не float, а str.
Так как мы работаем в Python, а это язык с динамической типизацией, то функция hello прекрасно отработает и ошибки не будет.
Но анализаторы кода (типа mypy) выдадут ошибку.
Желательно писать код так, чтобы анализаторы кода не жаловались:)
#ТаковПуть

Структурная типизация и причём тут утки. ч.2 🦆🦆🦆

Если в прошлый раз мы работали с функциями, тут поработаем с классами.

Пусть будет класс Order, реализация в нем нам неважна, поэтому оставим его пустым.
class Order:
pass

order = Order()

Теперь реализуем класс OrderStorage, у которого будет метод save
from typing import Protocol

class OrderStorage(Protocol):
def save(self, order: Order) -> None:
pass

Создадим 2 класса, которые будут реализовывать структуру класса OrderStorage
class DBStorage:
def save(self, order: Order) -> None:
print("Заказ сохранили в БД")

class FileStorage:
def save(self, order: Order) -> None:
print("Заказ сохранили в файл")

db_storage = DBStorage()
file_storage = FileStorage()

Создадим функцию, которая будет принимать на вход Order и класс, который будет реализовывать структуру класс OrderStorage
save_order(order, db_storage)
save_order(order, file_storage)

За счет того, что DBStorage и FileStorage реализуют структуру OrderStorage, то функция save_order прекрасно обрабатывает эти классы.

Полный код:
from typing import Protocol

class Order:
    pass

order = Order()

class OrderStorage(Protocol):
    def save(self, order: Order) -> None:
        pass

class DBStorage:
    def save(self, order: Order) -> None:
        print("Заказ сохранили в БД")

class FileStorage:
    def save(self, order: Order) -> None:
        print("Заказ сохранили в файл")

db_storage = DBStorage()
file_storage = FileStorage()

save_order(order, db_storage)
save_order(order, file_storage)

Ниже представлен аналогичный пример, но в другой реализации:
from __future__ import annotations
from typing import Protocol

class OrderStorage(Protocol):
def save(self, order: Order) -> None:
pass

class Order:
def __init__(self, *storages: OrderStorage) -> None:
self.storages = storages

def save_order(self) -> None:
for storage in self.storages:
storage.save(self)

class DBStorage:
def save(self, order: Order) -> None:
print("Заказ сохранили в БД")

class FileStorage:
def save(self, order: Order) -> None:
print("Заказ сохранили в файл")

db_storage = DBStorage()
file_storage = FileStorage()

order = Order(db_storage, file_storage)
order.save_order()

Строка
from __future__ import annotations

нужна, чтоб код запустить в одном файле.
#ЗаЧаем

Цитата из книги "Вы, конечно, шутите, мистер Фейнман!", посвящена участнику проекта Манхеттен

А что касается мистера Френкеля, который затеял всю эту деятельность, то он начал страдать от компьютерной болезни — о ней сегодня знает каждый, кто работал с компьютерами. Это очень серьезная болезнь, и работать при ней невозможно. Беда с компьютерами состоит в том, что ты с ними играешь. Они так прекрасны, столько возможностей — если четное число, делаешь это, если нечетное, делаешь то, и очень скоро на одной-единственной машине можно делать все более и более изощренные вещи, если только ты достаточно умен.


В ответ на эту цитату, цитата преподавателя МГУ Столярова Андрея Викторовича из книги "Программирование. Введение в профессию"

Как ни странно, именно эта “болезнь” превращает человека в программиста. Если хотите стать программистом — постарайтесь подхватить болезнь, описанную Фейнманом.
#ТаковПуть

Типизация.

Можно написать простой метод
def print_phone(phone = None):
if phone:
print(f"Phone number is {phone}")
else:
print("No phone number")
print_phone() #No phone number

А можно написать аналогичный метод с типизацией
def print_phone(phone: str | None = None) -> None:
if phone:
print(f"Phone number is {phone}")
else:
print("No phone number")
print_phone() #No phone number

Типизация призвана повысить читаемость кода.
Также типизация существенно ускоряет программу
#ТаковПуть

В Python есть прекрасные функции: all() и any(), которые делают код чище и эффективнее.

Классический пример, который показывает базовую работу этих функций:
nums = [2, 4, 5, 6]
any_1 = any(n % 2 == 0 for n in nums)
all_1 = all(n % 2 == 0 for n in nums)

print(any_1) #True
print(all_1) #False

any() находит первый подходящий элемент, all() проверяет, что все элементы удовлетворяют условию

Пример ниже показывает вызов функций в выражении, что удобно для комплексных проверок, т.е. когда много проверок внутри одной проверки.
def check_1():
return True

def check_2():
return True

def check_3():
return False

def check_4():
return True

checks_2 = [check_1, check_2, check_3, check_4]


any_2 = any(check() for check in checks_2)
all_2 = all(check() for check in checks_2)

print(any_2) #True
print(all_2) #False

Примером ниже показываю работу с генераторами:
def checks_3():
yield True
yield True
yield False
yield True

any_3 = any(check for check in checks_3())
all_3 = all(check for check in checks_3())

print(any_3) #True
print(all_3) #False

На самом деле, последний yield даже не вызвался. Это связано с тем, что функции all() и any() - ленивые.

Как работают ленивые функции показываю в примере ниже:
def checks_4():
print("#1") # any() остановится здесь и не пойдет дальше
yield True

print("#2")
yield True

print("#3") # all() остановится здесь и не пойдет дальше
yield False

print("#4")
yield True

print("any")
any(check for check in checks_4())

print("all")
all(check for check in checks_4())

# Output:
# any
# #1
# all
# #1
# #2
# #3

🎯 Наглядная демонстрация принципа "короткого замыкания". any() выполнила только первую проверку, all() остановилась на третьей, не проверяя четвертую. Это важно для снижения вычислительной нагрузки и ускорения кода.
👍1
#ТаковПуть

Кратко о bool

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

Несмотря на невозможность наследоваться от bool, можно использовать магические методы для превращения своего класса в bool. 🪄🧙‍♂

class IsEmpty:
def __init__(self, sequence):
self._sequence = sequence

def __bool__(self):
return not bool(self._sequence)


# Примеры разных последовательностей
sec_1 = [1, 2, 3]
sec_2 = {"a":1, "b":2}
sec_3 = {}

# три разных варианта получить bool
result_1 = IsEmpty(sec_1).__bool__()
result_2 = IsEmpty(sec_2)
result_3 = IsEmpty(sec_3)

print(result_1) # False
print(bool(result_2)) # False

if result_3: # автоматом вызовется __bool__
print("Пустой") # вызовет этот вариант
else:
print("Не пустой")

Жизненный пример, где в классе магический метод bool не будет лишним
class DatabaseConnection:
def __bool__(self):
return self.is_connected()
...

# такой вариант неидеоматичен
with DatabaseConnection(...) as conn:
    if conn.is_connected():
        ...

# такой вариант предпочтительнее
with DatabaseConnection(...) as conn:
if conn:
...


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

def bool_class(cls):
def wrapper(*args):
result = cls(*args)
return bool(result)
return wrapper

@bool_class
class IsEmpty:
def __init__(self, sequence):
self._sequence = sequence
def __bool__(self):
return not bool(self._sequence)


# Примеры разных последовательностей
sec_1 = [1, 2, 3]
sec_2 = {"a":1, "b":2}
sec_3 = {}

result_1 = IsEmpty(sec_1)
result_2 = IsEmpty(sec_2)
result_3 = IsEmpty(sec_3)

print(result_1) # False
print(result_2) # False
print(result_3) # True

# причем принтуются именно булевые значения
print(result_3 is True) # True

Я не знаю, где такое может пригодится, но если очень хочется, чтоб вызов класса возвращал bool, то есть техническая возможность такое сделать.
#ТаковПуть

Практикуемся с типами

Начнем с примера:
type Order = str
order: Order = "123"
print(type(order)) # <class 'str'>

В примере создаётся тип-алиас Order, который является просто псевдонимом для str

Это делается для того, чтобы не просто у некой переменной был тип, а чтобы он был осмысленный.

Продолжим. Сделаем не просто алиас, а еще и произведем проверку на наполнение.
from typing import NewType

Order = NewType('Order', str)

def validate_order(order_str: str) -> Order:
if not order_str.isdigit():
raise ValueError("Ожидается число")
return Order(order_str)

order: Order = validate_order("123")
print(type(order)) # <class 'str'>, но тип аннотирован как Order

Теперь создадим новый тип Order, который наследуется от str
Order = type('Order', (str,), {})
order: Order = Order("123")
print(type(order)) # <class '__main__.Order'>
print(isinstance(order, str)) # True

Создали новый тип, а не просто алиас и при этом сохранили всю функциональность str

Расширим пример. Добавим метод для нашего типа
def order_format(self):
return f"Order: {self}"

Order = type('Order', (str,), {'format': order_format})
order = Order("123")
print(order.format()) # "Order: 123"

Т.е. был создан пользовательский тип данных со встроенным методом!
👍1
#ТаковПуть

Фишки работы с Enum, ч.1

Enum используется для создания групп связанных констант и повышения читабельности кода.
Можно сравнить 2 примера, чтоб понять, зачем применяется Enum
result == 1
result == Results.SUCCESS

Второй пример читабельней. Т.к. в первом не понятно, что означает цифра 1.

Для работы с Enum необходимо создать класс, который будет наследоваться от Enum
Реализуем класс со статусами заказа
from enum import Enum

class OrderStatus(Enum):
CREATED = "created"
PROCESSING = "processing"
SHIPPED = "shipped"
DELIVERED = "delivered"
CANCELLED = "cancelled"

Интересное в таком классе то, что объекты в нем итерируются и его объекты являются экземплярами этого же класса

С итерируемостью всё просто:
for status in OrderStatus:
print(status) # возвращает объект enum
print(status.value) # возвращает значение объекта enum

Теперь давайте создадим две похожие функции.
Обе будут принимать на вход экземпляр класса OrderStatus.
Правда, возвращать они будут разные вещи: значение объекта enum, другая сам объект enum
def get_order_status_value(order_status: OrderStatus) -> str:
return order_status.value

def get_order_status_enum(order_status: OrderStatus) -> OrderStatus:
return order_status

print("delivered" == get_order_status_value(OrderStatus.DELIVERED))
print("delivered" == get_order_status_enum(OrderStatus.DELIVERED))

# Output:
# True
# False

Как видим, мы можем подать OrderStatus.DELIVERED в функцию
, т.к. OrderStatus.DELIVERED - является экземпляром класса OrderStatus
Что визуально непривычно.

Также видно, когда мы сравниваем объект со строкой, то ожидаемо получаем False

Однако. Мы можем переделать наш класс. И отнаследоваться не только от Enum, но и от str
class OrderStatus(str, Enum):
CREATED = "created"
PROCESSING = "processing"
SHIPPED = "shipped"
DELIVERED = "delivered"
CANCELLED = "cancelled"

Проверим, как изменились результаты сравнения:
print("delivered" == get_order_status_value(OrderStatus.DELIVERED))
print("delivered" == get_order_status_enum(OrderStatus.DELIVERED))

# Output:
# True
# True

Как видим, теперь сравнивание строки с объектом привело к True. Это связано с тем, что объект отнаследован от str.
Также отпадает необходимость в наличии функции get_order_status_value.
#ТаковПуть

Фишки работы с Enum, ч.2

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

Пояснения будут в самом коде.

from enum import Enum

class OrderStatus(str, Enum):
CREATED = "created"
PROCESSING = "processing"
SHIPPED = "shipped"
DELIVERED = "delivered"
CANCELLED = "cancelled"

@classmethod
def get_next_status( cls, current_status: 'OrderStatus' ) -> 'OrderStatus':
status_flow = {
cls.CREATED: cls.PROCESSING,
cls.PROCESSING: cls.SHIPPED,
cls.SHIPPED: cls.DELIVERED
}
return status_flow.get(current_status, current_status)

def is_final( self ) -> bool:
return self in {self.DELIVERED, self.CANCELLED}

@classmethod
def from_string( cls, value: str ) -> 'OrderStatus':
try:
return cls(value.lower())
except ValueError:
raise ValueError(f"Invalid status: {value}. Valid statuses are: {', '.join([s.value for s in cls])}")

# Пусть будет некий заказ, кроторый будет представлен в виде dict, и этот dict будет содержать текущий статус заказа
order = {"status": "created"}

# Проверяем статус заказа, не является ли он CREATED
print(order["status"] == OrderStatus.CREATED) # True

# Зная текущий статус, можем получить следующий
next_status = OrderStatus.get_next_status(OrderStatus.CREATED)
order["status"] = next_status.value
print(order["status"]) # "processing"

# Пробуем найти экземпляр класса OrderStatus по строке
status_enum = OrderStatus.from_string(order["status"])
print(status_enum) # OrderStatus.PROCESSING

# Проверка конечного статуса
print(OrderStatus.DELIVERED.is_final()) # True

Фактически тут представлены все те вещи, которые описывались в ч. 1, но представлены в более интересном виде.
🔥1