Zen of Python
20.1K subscribers
1.29K photos
179 videos
36 files
3.3K links
Полный Дзен Пайтона в одном канале

Разместить рекламу: @tproger_sales_bot

Правила общения: https://tprg.ru/rules

Другие каналы: @tproger_channels

Сайт: https://tprg.ru/site

Регистрация в перечне РКН: https://tprg.ru/xZOL
Download Telegram
Почему некоторые исключения не попадают в лог и как это исправить

logging — это уже целый стандарт записи ошибок в Python. Ваше приложение запускается, сообщения попадают в лог. Но вдруг в продакшене приложение внезапно «падает», а в логах — тишина. Знакомо?

Если да — вы столкнулись с одной из малозаметных, но опасных особенностей Python — «непойманные исключения» (uncaught exceptions).

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

Рассмотрим следующий код:


import logging

logger = logging.getLogger(__name__)
logging.basicConfig(filename="output.log", level=logging.INFO)

logger.info("Application started")

1 / 0 # деление на ноль


В консоли вы увидите traceback:


Traceback (most recent call last):
...
ZeroDivisionError: division by zero


А в output.log будет только:


INFO:__main__:Application started


Никакой информации об ошибке. И это сгенерирует вам часы работы.

Почему так происходит?

Библиотека logging в Python не логирует ошибки сама по себе: она просто предоставляет инструменты для записи. Если программа падает из-за исключения, и это исключение не обрабатывается в try / except, то встроенный модуль никак не участвует в этом процессе. Потому что стандартный Python-интерпретатор выводит непойманные исключения напрямую в stderr, минуя logging.

Плохое (но распространённое) решение

Один из способов «поймать всё» — обернуть main() в try / except:


def main():
logger.info("Application started")
1 / 0

try:
main()
except Exception as e:
logger.exception("Unhandled exception:")


Это сработает: logger.exception() запишет ошибку и трейсбек. Но есть минусы:

— Вы можете пропустить системные исключения (KeyboardInterrupt и проч., если ловите Exception, а не BaseException;
— Такой код трудно масштабировать: оборачивать каждый main() в каждом скрипте — дублирование;
— Это маскирует архитектурные проблемы: непойманные исключения — это чаще всего баг, а не ожидаемое поведение.


Правильное решение: sys.excepthook

Python дает глобально обрабатывать непойманные исключения — sys.excepthook:


import sys
import logging

logger = logging.getLogger(__name__)
logging.basicConfig(filename="output.log", level=logging.INFO)

def handle_uncaught_exception(exc_type, exc_value, exc_traceback):
logger.critical(
"Uncaught exception. Application will terminate.",
exc_info=(exc_type, exc_value, exc_traceback)
)

sys.excepthook = handle_uncaught_exception

logger.info("Application started")

1 / 0


Теперь, если запустить скрипт:

— В output.log появится подробный трейсбек ошибки;
— Вы будете уверены, что даже критические ошибки попадут в лог, прежде чем приложение завершится.

Когда в Python возникает исключение, и его никто не перехватывает, вызывается sys.excepthook(type, value, traceback). По умолчанию она просто печатает детали в stderr. Но вы можете управлять процессом:

— Логировать ошибки;
— Отправлять оповещения (например, в Telegram или на почту);
— Снимать дампы или делать очистку.

#основы
🔥183👍2😎1
​​5 архитектурных ошибок, которые мы совершаем при старте проектов

Многие из нас с головой уходят в реализацию идеи, не задавая себе главный вопрос: а что будет, когда проект вырастет? 

Аспекты вроде масштабирования, как и фундамент дома, нужно продумывать сначала, иначе потом вас ждет не апгрейд, а перестройка с нуля. А еще именно в самом начале проекта закладывается почва для ада зависимостей: спонтанные решения, быстрые фиксы, «временные» костыли — всё это превращается в хаос, который сложно контролировать.

В статье Tproger 5 самых частых архитектурных ошибок, которые мешают проектам расти и развиваться.

#основы
@zen_of_python
🙊 — Если сам так писал
2👍2🎉1
isinstance(): Проверка типов

В динамически типизированных языках нам особенно важно знать тип объекта, которым мы оперируем. С этим помогают две встроенные функции — type() и isinstance(), и мы поговорим сегодня о второй из них.


isinstance(object, classinfo)


object: объект, тип которого вы хотите проверить
classinfo: класс, тип или кортеж типов


# Является ли 42 целочисленным значением?
isinstance(42, int) # True

# Относится ли "hello" к одному из типов str / list (логическое «ИЛИ»)?
isinstance("hello", (str, list)) # True



isinstance() vs type()

Поначалу может показаться, что type() делает то же самое:


type(42) == int # True


Но isinstance(), в свою очередь, учитывает наследование:


class Animal:
pass

class Dog(Animal):
pass

dog = Dog()

type(dog) == Animal # False
isinstance(dog, Animal) # True


Это делает isinstance() предпочтительным выбором при работе с иерархиями классов.

#основы
👍13
Вы точно знаете отличия между or и and?

Каким будет результат выражения:


5 or 0


Большинство людей уверены, что Python вернёт True. Но если запустить код, мы увидим 5. Это происходит, потому что Python возвращает само значение операнда, если оно Truthy / Falsy.


Truthy / Falsy

Любой объект Python — либо «трушный», либо «ложный»:

— Falsy: 0, "", [], {}, None, 0.0
— Truthy: всё остальное

Оператор bool(obj) конвертирует объект в логическое значение, но сами and / or возвращают ненулевые сущности: последний вычисленный операнд.


Поведение or

Оператор возвращает первый Truthy операнд, если таковой есть; иначе — последний Falsy.


5 or 0 # → 5 (первый truthy)
0 or 7 # → 7 (второе значение, truthy)
0 or '' or None # → None (все falsy, возвращается последний)


Идея: достаточно одной истины, и дальше Python не продолжает (Short‑Circuit Evaluation).


Поведение and

Оператор возвращает первый Falsy операнд, если он встретится; иначе — последний Truthy.


5 and 0 # → 0 (первый Falsy)
5 and 7 # → 7 (оба Truthy, возвращаем последний)
0 and 5 # → 0 (первый Falsy — возвращён)


Логика: and требует, чтобы обе стороны были истинными, иначе выражение — ложь.


Приоритет not, and, or

Операторы имеют встроенный приоритет:

1. not (наивысший)
2. and
3. or (наинизший)


True or False and False # → True, т. к. это эквивалентно True or (False and False)
(not False) or True # → True, сначала выполняется not.


Рекомендации

— Используйте скобки, чтобы явно показывать порядок операций;
— Не пишите слишком длинные цепочки с and / or без промежуточных переменных;
— Именованные логические условия помогают читать код.

#основы
7👍6
Byte of Python | Бесплатный учебник, ставший классикой

Сейчас лето, и при должном везении ваш работодатель слегка расслабился. Можно и книжку почитать.
«Укус питона» — известный бесплатный учебник, который можно скачать в форматах .pdf / .epub вместе с Python-скриптами прямо из репозитория автора Сварупа.

Спасибо @Chellbas за рекомендацию.
#основы
@zen_of_python
8
argparse: зачем нужен и при чем здесь sys.argv[]?

При создании скриптов, которые запускаются из командной строки, часто возникает необходимость принимать аргументы. Чтобы эффективно разбирать и обрабатывать эти параметры, в стандартной библиотеке Python есть модуль argparse. В этом лонгриде мы покажем на примере, как его использовать, покажем взаимосвязь с sys.argv.


➡️ Какую роль играет sys.argv?

sys.argv — это список, содержащий аргументы командной строки, с которыми был запущен скрипт. Первый элемент списка sys.argv[0] — это имя самого скрипта.

Например, если вы вызовете скрипт так:


python script.py filename.txt -v


то выведется такой перечень:


import sys

print(sys.argv) # ['script.py', 'filename.txt', '-v']


Однако sys.argv — это просто список строк, и если работать с ним вручную, то придётся самостоятельно обрабатывать порядок, типы данных, проверять правильность и т.п., что может стать сложной задачей. Здесь на помощь приходит argparse. Он автоматизирует парсинг аргументов и предоставляет удобные способы проверки параметров CLI.


➡️ argparse: как с ним обращаться

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

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


import argparse

# Создаём парсер
parser = argparse.ArgumentParser(description='Обработка файла.') # Создаем объект ArgumentParser;

# Добавляем позиционный аргумент 'filename'
parser.add_argument('filename', help='имя файла для обработки') # Добавляем описание каждого аргумента или опции

# Разбираем аргументы командной строки
args = parser.parse_args() # Разберём аргументы sys.argv

# Fргументы доступны как атрибуты: args.filename
print(f'Обрабатываем файл: {args.filename}')


Теперь, если запустить скрипт так:


python script.py example.txt


Программа выведет:


Обрабатываем файл: example.txt


Если попытаться запустить без аргумента:


python script.py


то argparse автоматически покажет сообщение об ошибке и краткую справку:


usage: script.py [-h] filename
script.py: error: the following arguments are required: filename


Документация
#основы
@zen_of_python
🙏 — Если спасибо за такой контент
Please open Telegram to view this post
VIEW IN TELEGRAM
🙏12
Паттерн Mediator | Что это и зачем нужен

Mediator — паттерн проектирования, который вводит объект-посредник для координации взаимодействий между другими объектами. Вместо того, чтобы объекты напрямую вызывали методы друг друга и пытались «договариваться», они отправляют сообщения посреднику, а он решает, кто и как должен на них отреагировать. Классическая аналогия — диспетчерская в аэропорту: пилоты не связываются друг с другом напрямую, а говорят с диспетчером.

При прямой связи «каждый с каждым» количество зависимостей растёт как квадрат числа компонентов: изменения в одном классе часто заставляют править десятки других. Посредник помещает логику взаимодействия в одну точку:


class Mediator:
"""Интерфейс медиатора."""
def notify(self, sender, event):
raise NotImplementedError

class CourseMediator(Mediator):
"""Конкретный медиатор — координатор курсов и пользователей."""
def __init__(self):
self.users = []

def register(self, user):
self.users.append(user)
user.mediator = self

def notify(self, sender, course_name):
# В простом варианте медиатор просто логирует сообщение
print(f"[{sender}] выбрал курс: {course_name}")
# Можно добавить дополнительную логику: фильтрация, отправка уведомлений и т.п.

class User:
def __init__(self, name):
self.name = name
self.mediator = None

def send_course(self, course_name):
if not self.mediator:
raise RuntimeError("User не зарегистрирован у медиатора")
self.mediator.notify(self, course_name)

def __str__(self):
return self.name


m = CourseMediator()
u1 = User("Майкл"); u2 = User("Оля")
m.register(u1); m.register(u2)
u1.send_course("DSA")
u2.send_course("Software Development")


User не знает про других пользователей. Вся координация — в CourseMediator. Такой подход облегчает изменение логики (например, добавить рассылку уведомлений другим пользователям) без модификации User. Медиатор снижает связанность (Coupling) между компонентами и упрощает поддержку.


Плюсы

1️⃣ Централизация логики взаимодействия (проще править и тестировать).
2️⃣ Снижение связности между компонентами.
3️⃣ Легче добавлять новые стратегии взаимодействия, не меняя классы коллег.

Минусы

1️⃣ Риск «божественного объекта» (God Object): медиатор может накопить слишком много логики и стать сложным.
2️⃣ Централизация порождает узкое место — медиатор становится более сложным и менее прозрачным.

#основы
@zen_of_python
Please open Telegram to view this post
VIEW IN TELEGRAM
👍85
@pytest.mark.parametrize: Как параметризировать тесты

Тестирование кода может быть утомительным процессом. Когда у вас есть множество похожих тестовых случаев, написание отдельных функций для каждого часто приводит к дублированию кода. Именно здесь на помощь приходит функция @pytest.mark.parametrize.

Начнем с простого примера. У нас есть функция add_nums(), которая складывает числа из списка:


def add_nums(numbers):
return sum(numbers)


Без parametrize тесты могли бы выглядеть так:


def test_123():
assert add_nums([1, 2, 3]) == 6

def test_negatives():
assert add_nums([1, 2, -3]) == 0

def test_empty():
assert add_nums([]) == 0


Что не так с этим подходом? Дублирование кода: каждая тестовая функция повторяет одну и ту же структуру. Вместо написания трех отдельных функций, мы можем создать одну параметризованную функцию:


import pytest

@pytest.mark.parametrize(
"nums, expected_total",
[
([1, 2, 3], 6),
([1, 2, -3], 0),
([], 0),
]
)
def test_add_nums(nums, expected_total):
assert add_nums(nums) == expected_total


1. @pytest.mark.parametrize — это специальный декоратор pytest
2. Параметры "nums, expected_total" — имена параметров функции
3. Тестовые данные — список кортежей, где каждый содержит значения для одного теста

Pytest автоматически вызывает вашу функцию с каждым набором параметров:


# Первый вызов
test_add_nums([1, 2, 3], 6)

# Второй вызов
test_add_nums([1, 2, -3], 0)

# Третий вызов
test_add_nums([], 0)


Результат: 3 отдельных теста, каждый из которых может пройти или упасть.


Кастомные ID для тестов

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


@pytest.mark.parametrize(
"nums, expected_total",
[
([1, 2, 3], 6),
([1, 2, -3], 0),
([], 0),
],
ids=["positive_numbers", "mixed_numbers", "empty_list"]
)
def test_add_nums(nums, expected_total):
assert add_nums(nums) == expected_total


Теперь при запуске тестов вы увидите:

test_add_nums[positive_numbers] PASSED
test_add_nums[mixed_numbers] PASSED
test_add_nums[empty_list] PASSED



Вложенная параметризация

Можно комбинировать несколько параметризаций:


@pytest.mark.parametrize("x", [1, 2, 3])
@pytest.mark.parametrize("y", [10, 20])
def test_multiply(x, y):
assert x * y == x * y


Это создаст 6 тестов: (1,10), (1,20), (2,10), (2,20), (3,10), (3,20).

#основы
@zen_of_python
👍131🔥1
Паттерн Flyweight | как экономить память и избегать дублирования кода

Flyweight («вес мухи») — один из структурных паттернов, предназначенный для оптимизации расходования памяти. Суть — разделять состояния объектов на:

➡️ Внутреннее (intrinsic): общие, неизменяемые компоненты, которые можно разделять между объектами;
➡️ Внешнее (extrinsic) — уникальные, изменяемые данные, передаваемые в объект лишь в контексте его использования.

Это позволяет хранить меньше объектов при одинаковом поведении. Стоит задуматься об этом паттерне, если требуется создать множество объектов с частично общими данными.

Пример
Представь, что у нас лес в игре из 100К деревьев. У каждого дерева есть:

— Внутреннее состояние: текстура, цвет листвы, форма кроны, высота модели. Это разделяемые каждым деревом в лесу свойства;
— Внешнее состояние: координаты на карте, текущее состояние (здорово/повалено). Такое уникально для каждого дерева.

Если бы мы для каждого дерева хранили копию текстуры и модели, мы бы потратили гигабайты памяти. Flyweight избавляет от проблемы:


Наивный вариант (без Flyweight)


class Tree:
def __init__(self, texture, color, shape, x, y):
self.texture = texture
self.color = color
self.shape = shape
self.x = x
self.y = y

def draw(self):
print(f"Drawing {self.color} {self.shape} at ({self.x}, {self.y})")


# создаём 100.000 деревьев, каждое хранит одинаковую текстуру и форму
forest = [
Tree("oak_texture.png", "green", "oak", x, y)
for x, y in zip(range(1000), range(1000))
]


Каждый объект Tree хранит одинаковые данные (oak_texture.png, "oak", "green"), хотя это лишнее.


С Flyweight


# Общие характеристики
class TreeType:
def __init__(self, texture, color, shape):
self.texture = texture
self.color = color
self.shape = shape

def draw(self, x, y):
# внешние данные передаются параметром
print(f"Drawing {self.color} {self.shape} at ({x}, {y})")


# Фабрика для переиспользования типов деревьев
class TreeFactory:
_tree_types = {}

@classmethod
def get_tree_type(cls, texture, color, shape):
key = (texture, color, shape)
if key not in cls._tree_types:
cls._tree_types[key] = TreeType(texture, color, shape)
return cls._tree_types[key]


# Контекст: хранит только уникальные данные (extrinsic)
class Tree:
def __init__(self, x, y, tree_type):
self.x = x
self.y = y
self.tree_type = tree_type

def draw(self):
self.tree_type.draw(self.x, self.y)


# создаём 100ю000 деревьев, но реально разных TreeType всего 2-3
forest = []
for i in range(100000):
if i % 2 == 0:
tree_type = TreeFactory.get_tree_type("oak_texture.png", "green", "oak")
else:
tree_type = TreeFactory.get_tree_type("pine_texture.png", "darkgreen", "pine")
forest.append(Tree(i, i * 2, tree_type))

# Нарисуем первые пять
for tree in forest[:5]:
tree.draw()


⚡️ У нас 100 000 объектов Tree, но всего 2 объекта `TreeType` (oak и pine). Экономия памяти огромная: вместо хранения 100.000 текстур хранится только 2.


Недостатки паттерна

— Усложнение структуры кода;
— Возможны сложности с сопровождением, особенно при неправильном управлении extrinsic;
— Повышен риск связности — общие объекты могут влиять на многие части системы .

#основы
@zen_of_python
Please open Telegram to view this post
VIEW IN TELEGRAM
22🆒1
Mixins | Что это и как использовать

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

В отличие от других ЯП (Ruby, Dart и проч.), которые поддерживают этот паттерн явно через специализированный синтаксис, Python полагается на множественное наследование как на механизм для реализации этой концепции.

Представьте, что вы создаете классы Animal и Vehicle с различными форматами данных. Реализация одной и той же функции serialize() приводит к дублированию кода:


# Плохо: Дублирование кода
class Animal:
def __init__(self, name, species):
self.name = name
self.species = species

def serialize(self) -> dict:
return vars(self)

class Vehicle:
def __init__(self, make, model, year):
self.make = make
self.model = model
self.year = year

def serialize(self) -> dict: # Дублирование!
return vars(self)

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

def serialize(self) -> dict: # Дублирование!
return vars(self)


Решение через примеси:


# Миксин обычно предоставляет одну конкретную функцию
class SerializableMixin:
def serialize(self) -> dict:
if hasattr(self, "__slots__"):
return {
name: getattr(self, name)
for name in self.__slots__
}
else:
return vars(self)

# Классы используют mixin без странной иерархии
class Animal:
def __init__(self, name, species):
self.name = name
self.species = species

class Vehicle:
def __init__(self, make, model, year):
self.make = make
self.model = model
self.year = year

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

# Применяем mixin к нужным классам
class SerializableAnimal(Animal, SerializableMixin):
pass

class SerializableVehicle(Vehicle, SerializableMixin):
pass

class SerializablePerson(Person, SerializableMixin):
pass



Когда использовать Mixins

1️⃣ Когда нужно переиспользовать функциональность между несвязанными классами
2️⃣ Когда нужно создать модульный и композируемый код
3️⃣ Когда нужно расширить функциональность сторонних библиотек


Когда НЕ стоит использовать Mixins

1️⃣ Когда функциональность специфична для одного класса
2️⃣ Когда создается слишком сложная иерархия наследования

#основы
@zen_of_python
🤓 — Если изучил досконально
Please open Telegram to view this post
VIEW IN TELEGRAM
🆒5🤓311