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

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


# 4 Создание службы PostgreSQL

Статья может быть полезна при необходимости установки нескольких версий PostgreSQL на одной машине или для персональных настроек.
В статье рассматриваются особенности работы с кодировками в Windows. Только из-за кодировок я бы не рекомендовал никому ставить PostgreSQL на Windows, хотя статья посвящена именно установке на нем.

#PostgreSQL #cmd


#  5 Создание БД PostgreSQL

Данная статья является продолжением предыдущей. Тут описывается создание bat-файла, который будет создавать БД, пользователя, таблицы и заполнять таблицы данными.
Плюс рассматривается установка драйвера и подключение к БД через host+port с помощью pyodbc на Python.

#PostgreSQL #python #pyodbc


P.S.
В других статьях я буду прикреплять готовый bat-файл со всеми необходимыми
установками для поднятия БД, а за разъяснениями работы батника буду ссылаться на эти две статьи.


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

Модульность

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

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

Пакет — это папка, которая содержит файл __init__.py, который призван собирать все модули в папке в единую структуру.

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

Не забывайте разбивать код на модули и пакеты. Это поможет вам создавать более качественный и поддерживаемый код.
#ТаковПуть

Для любителей экзотики, можно писать python скрипты и запускать их на языке VBA.

Для этого необходимо установить плагин ExcelPython и подключить его в редакторе VBA через Tools->References
#ТаковПуть

Присваивания значений переменным

Все примеры ниже приведут к идентичному результату

x = 1
y = 2

x, y = 1, 2
x, y = [1, 2]
x, y = (1, 2)
x, y = {1, 2}
#ТаковПуть

Объединяем списки (list)

fruits = ['яблоко','мандарин']
vegetables = ['лук', 'помидор']

meal = [*fruits, *vegetables]
print(meal)

#Output:
#['яблоко', 'мандарин', 'лук', 'помидор']
👍1
#ТаковПуть

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

Упаковка в 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

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