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

Продолжаем обсуждать GIL


В Python 3.13 появилась возможность не только отключить GIL при сборке из исходников, что может оказаться для многих сложным, но для нашего с вами удобства при установке Python 3.13 появилась опция, которая позволяет установить два интерпретатора на компьютер: с GIL и без него. (см. скриншот)

На примере ниже видно, на сколько Python быстрее без GIL (python3.13t). На скриншоте результат.

from threading import Thread
from time import time


def count(n):
while n > 0:
n -= 1

def run_threads():
args = (10**8,)
thread1 = Thread(target=count, args=args)
thread2 = Thread(target=count, args=args)
thread1.start()
thread2.start()
thread1.join()
thread2.join()

def main():
start_time = time()
run_threads()
finish = time() - start_time
print(f"Время выполнения: {finish:.2f} сек")


if __name__ == "__main__":
main()
1
#ЧистыйКот

Точка входа

В Python-программе, как правило, выделяется один основной файл, который служит точкой входа для выполнения программы. Традиционно этот файл располагается в корне проекта и носит имя main.py.
Расположение в корне и наименование не являются строго обязательными.
Например, у меня не прижилось наименование main.py, я называю такой файл app.py.
Для себя я выработал определенный стиль написания программ, в частности, даже если весь проект на Python состоит из одного файла, я создаю второй файл с названием app.py.
│ app.py
│ my_prog.py

Из файла app.py я вызываю основную программу, которая лежит в первом файле.
Файл app.py обычно содержит примерно такой код:
from my_prog import MyProg

if __name__ == '__main__':
prog = MyProg()
prog.run()

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

Рассмотрим варианты работы с Excel из Python. Часть 1.

1) Самый простой способ это использовать внешние библиотеки. К примеру:
pip install openpyxl

2) Можно подключиться к объектной модели Excel, Outlook, Word и другим личным программам компании Microsoft напрямую!

pip install pywin32

3) Если у вас есть доступ к Visual Studio, вы можете установить интерпретатор IronPython и подключаться к библиотекам Excel, Word и пр через промежуточный язык CRL.
pip install clr

Подключение к библиотеке Excel
import clr
clr.AddReference("Microsoft.Office.Interop.Excel")
import Microsoft.Office.Interop.Excel as Excel

Продолжение следует..
#ТаковПуть

Рассмотрим варианты работы с Excel на Python. Часть 2

4) Так как в Windows есть встроенный интерпретатор C#.
Можно написать на языке C# dll, которая будет взаимодействовать с объектами excel, word и т.д. И при сборке dll указать зависимость с нужной библиотекой:
%WINDIR%\CSC.EXE /r:Microsoft.Office.Interop.Excel.dll "путь к файлу .cs"


Так как интерпретатор CPython не умеет работать с C#, есть 2 варианта:

4.1) Подключиться через библиотеку, которая умеет обрабатывать C#:
pip install pythonnet

Эта библиотека внутри содержит класс clr
import clr


4.2) либо если есть доступ к Visual Studio и он используется для разработки на языке C++ и при этом нет возможности установить IronPython. Необходимо написать обёртку на C++ к C#.
#using "наша dll на cs"

И с компилировать на компиляторе cl
cl /clr /LD "наша обёртка на плюсах.cpp"

Результирующую dll можно будет подключить к python через встроенную библиотеку:
from ctype import CDLL
#ВДзене

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


# 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) выдадут ошибку.
Желательно писать код так, чтобы анализаторы кода не жаловались:)