PEP 8, виртуальные окружения, модули, файлы, документация
Хороший Python-код — читаем, следует конвенциям, грамотно организован. PEP 8 — не догма, но общий язык команды.
# snake_case — переменные, функции, методы, модули
user_name = "Alice"
def calculate_total(items): ...
class UserAccount: ... # PascalCase — классы
MAX_RETRY_COUNT = 3 # UPPER_SNAKE_CASE — константы
_private_var = "internal" # _ — соглашение о приватности
__name_mangled = "mangled" # __ — name mangling в классах
# Имена: описательные, не сокращения
# ❌ calc, n, usr, tmp
# ✅ calculate_total, count, user, temp_file# Отступы: 4 пробела (не табуляция)
def function():
if condition:
do_something()
# Максимум 79 символов в строке (PEP 8) или 88/99 (Black)
# Перенос с обратным слешем или скобками (предпочтительно):
result = (
some_very_long_variable
+ another_very_long_variable
+ yet_another_variable
)
# Пробелы вокруг операторов:
x = 5 + 3 # ✅
x=5+3 # ❌
func(a=1, b=2) # ✅ keyword args — без пробелов
func(a =1, b= 2) # ❌
# Пустые строки:
# 2 перед определением класса или функции на верхнем уровне
# 1 внутри класса между методамиpip install black isort ruff
black myfile.py # форматирует код по стандарту Black
isort myfile.py # сортирует импорты
ruff check myfile.py # линтер: находит проблемы
ruff check --fix . # автоисправление# Правильный порядок (isort это делает автоматически):
# 1. Стандартная библиотека
import os
import sys
from pathlib import Path
from typing import Optional
# 2. Сторонние пакеты
import requests
import fastapi
from pydantic import BaseModel
# 3. Локальные модули
from myapp.models import User
from .utils import helper_function
# Не используйте wildcard импорты (засоряют namespace):
from os import * # ❌ — что именно импортировано?
# Alias для длинных имён:
import numpy as np
import pandas as pd
from datetime import datetime as dt# Проблема:
# a.py: from b import B
# b.py: from a import A ← ImportError!
# Решение 1: Перенести импорт внутрь функции
def create_b():
from b import B # импорт при вызове, не при загрузке модуля
return B()
# Решение 2: TYPE_CHECKING
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from b import B # импорт только при статическом анализе
def process(b: "B") -> None: # строковая аннотация
pass
# Решение 3: Реструктуризация кода
# Вынести общие части в третий модуль common.py__all__ — публичный API модуля# mymodule.py
__all__ = ["PublicClass", "public_function"]
class PublicClass: ...
def public_function(): ...
def _private_function(): ... # не экспортируется
# from mymodule import * — импортирует только то что в __all__venv — стандартный инструмент# Создание
python -m venv .venv
# Активация
source .venv/bin/activate # Linux/macOS
.venv\Scripts\activate # Windows
# Деактивация
deactivate
# Управление зависимостями
pip install requests
pip freeze > requirements.txt
pip install -r requirements.txtpoetry — современный менеджер зависимостейpip install poetry
poetry new myproject # создать проект
poetry init # инициализировать в существующем
poetry add requests # добавить зависимость
poetry add --dev pytest # dev-зависимость
poetry remove requests # удалить
poetry install # установить все из pyproject.toml
poetry update # обновить зависимости
poetry run python script.py # запустить в окружении
poetry shell # активировать оболочку# pyproject.toml
[tool.poetry]
name = "myproject"
version = "0.1.0"
[tool.poetry.dependencies]
python = "^3.11"
requests = "^2.31"
pydantic = "^2.0"
[tool.poetry.group.dev.dependencies]
pytest = "^7.4"
black = "^23.0"
mypy = "^1.5"pathlib — современный подходfrom pathlib import Path
# Создание пути
home = Path.home()
project = Path("/Users/alice/projects/myapp")
config = project / "config" / "settings.yaml" # / — оператор!
# Информация
print(config.name) # "settings.yaml"
print(config.stem) # "settings"
print(config.suffix) # ".yaml"
print(config.parent) # /Users/alice/projects/myapp/config
print(config.exists()) # True/False
print(config.is_file()) # True/False
print(config.is_dir()) # True/False
# Чтение / запись
text = config.read_text(encoding="utf-8")
config.write_text("key: value\n", encoding="utf-8")
data = config.read_bytes()
# Обход директории
for py_file in project.rglob("*.py"):
print(py_file)
for item in project.iterdir():
if item.is_dir():
print(f"Dir: {item.name}")
# Создание
Path("/tmp/new_dir").mkdir(parents=True, exist_ok=True)
# Переименование / перемещение
old = Path("old_name.txt")
new = old.rename("new_name.txt")
old.replace("/other/dir/file.txt") # перезаписывает если существует# Всегда указывайте encoding
with open("data.txt", encoding="utf-8") as f:
content = f.read()
# Большой файл — читаем построчно
with open("huge.log", encoding="utf-8") as f:
for line in f: # итерация без загрузки всего файла
process(line.rstrip("\n"))
# Бинарные файлы
with open("image.png", "rb") as f:
header = f.read(8) # первые 8 байт
# Запись
with open("output.txt", "w", encoding="utf-8") as f:
f.write("Hello\n")
f.writelines(["line1\n", "line2\n"])
# Дозапись
with open("log.txt", "a", encoding="utf-8") as f:
f.write("New log entry\n")import json
data = {"name": "Alice", "age": 30, "scores": [1, 2, 3]}
# Сериализация
json_str = json.dumps(data)
json_pretty = json.dumps(data, indent=2, ensure_ascii=False) # unicode сохраняется
# Десериализация
parsed = json.loads(json_str)
# Файлы
with open("data.json", "w", encoding="utf-8") as f:
json.dump(data, f, indent=2, ensure_ascii=False)
with open("data.json", encoding="utf-8") as f:
loaded = json.load(f)
# Кастомный encoder для нестандартных типов
import datetime
class DateTimeEncoder(json.JSONEncoder):
def default(self, obj):
if isinstance(obj, datetime.datetime):
return obj.isoformat()
return super().default(obj)
json.dumps({"ts": datetime.datetime.now()}, cls=DateTimeEncoder)import csv
# Запись
with open("data.csv", "w", newline="", encoding="utf-8") as f:
writer = csv.DictWriter(f, fieldnames=["name", "age", "email"])
writer.writeheader()
writer.writerow({"name": "Alice", "age": 30, "email": "a@b.com"})
writer.writerows([
{"name": "Bob", "age": 25, "email": "b@b.com"},
{"name": "Charlie", "age": 35, "email": "c@b.com"},
])
# Чтение
with open("data.csv", encoding="utf-8") as f:
reader = csv.DictReader(f)
for row in reader:
print(row["name"], row["age"])
# row — OrderedDict: {"name": "Alice", "age": "30", ...}
# Внимание: все значения — строки! Нужно явное приведение типовpip install pyyamlimport yaml
config_text = """
database:
host: localhost
port: 5432
name: mydb
debug: true
allowed_hosts:
- localhost
- 127.0.0.1
"""
# Безопасная загрузка (не выполняет Python-код)
config = yaml.safe_load(config_text)
print(config["database"]["port"]) # 5432 (int, не строка!)
# Из файла
with open("config.yaml", encoding="utf-8") as f:
config = yaml.safe_load(f)
# Сохранение
with open("output.yaml", "w", encoding="utf-8") as f:
yaml.dump(config, f, allow_unicode=True, default_flow_style=False)
# ⚠️ НЕ используйте yaml.load() без Loader — это опасно!
# yaml.load(data) # ❌ может выполнить произвольный код
# yaml.safe_load(data) # ✅ безопасноimport tomllib # встроен в Python 3.11+
with open("pyproject.toml", "rb") as f: # обязательно "rb"!
data = tomllib.load(f)
# Или из строки:
text = """
[tool.mypy]
strict = true
python_version = "3.12"
"""
config = tomllib.loads(text)
print(config["tool"]["mypy"]["strict"]) # Truedef calculate_bmi(weight_kg: float, height_m: float) -> float:
"""Вычисляет индекс массы тела (ИМТ).
Args:
weight_kg: Вес в килограммах.
height_m: Рост в метрах. Должен быть положительным.
Returns:
ИМТ = weight_kg / height_m².
Raises:
ValueError: Если height_m <= 0.
Examples:
>>> calculate_bmi(70, 1.75)
22.857142857142858
"""
if height_m <= 0:
raise ValueError(f"height_m must be positive, got {height_m}")
return weight_kg / height_m ** 2doctest — тесты в документацииdef add(a: int, b: int) -> int:
"""Складывает два числа.
>>> add(2, 3)
5
>>> add(-1, 1)
0
>>> add(0, 0)
0
"""
return a + b
# Запуск doctests:
# python -m doctest module.py -v
# pytest --doctest-modules module.pyimport logging
# Базовая конфигурация
logging.basicConfig(
level=logging.DEBUG,
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
)
# Правильный способ: logger для каждого модуля
logger = logging.getLogger(__name__)
def process_order(order_id: int) -> None:
logger.debug(f"Processing order {order_id}")
try:
result = execute_order(order_id)
logger.info(f"Order {order_id} completed: {result}")
except Exception:
logger.exception(f"Failed to process order {order_id}")
raise
# Уровни: DEBUG < INFO < WARNING < ERROR < CRITICAL
# Структурированное логирование
import json
logging.basicConfig(handlers=[logging.StreamHandler()])
class JSONFormatter(logging.Formatter):
def format(self, record):
return json.dumps({
"time": self.formatTime(record),
"level": record.levelname,
"message": record.getMessage(),
"module": record.module,
})__name__ == "__main__"# module.py
def main():
print("Running as main program")
def helper():
return 42
# Этот блок выполняется только при прямом запуске:
# python module.py
# НЕ выполняется при: import module
if __name__ == "__main__":
main()
# Зачем: позволяет использовать файл и как модуль, и как скрипт
# import module → можно использовать helper()
# python module.py → запускается main()import os
from typing import Optional
# os.environ — словарь переменных окружения
database_url = os.environ["DATABASE_URL"] # KeyError если нет
database_url = os.environ.get("DATABASE_URL", "sqlite:///dev.db") # с дефолтом
debug = os.getenv("DEBUG", "0") == "1" # bool
# python-dotenv для .env файлов
# pip install python-dotenv
from dotenv import load_dotenv
load_dotenv(".env") # загружает .env в os.environ
# Pydantic Settings — лучший способ (FastAPI)
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
database_url: str
redis_url: str = "redis://localhost:6379"
debug: bool = False
api_key: Optional[str] = None
class Config:
env_file = ".env"
settings = Settings() # автоматически читает из переменных окружения# enumerate — индекс + элемент
for i, item in enumerate(["a", "b", "c"], start=1):
print(f"{i}. {item}")
# zip — параллельный обход (останавливается на коротком)
names = ["Alice", "Bob", "Charlie"]
scores = [85, 92, 78]
for name, score in zip(names, scores):
print(f"{name}: {score}")
# zip_longest (заполняет None или custom fillvalue)
from itertools import zip_longest
# sorted с key
users = [{"name": "Charlie", "age": 30}, {"name": "Alice", "age": 25}]
by_name = sorted(users, key=lambda u: u["name"])
by_age_desc = sorted(users, key=lambda u: u["age"], reverse=True)
# map, filter (предпочитайте comprehensions)
squares = list(map(lambda x: x**2, range(10)))
evens = list(filter(lambda x: x % 2 == 0, range(10)))
# Лучше:
squares = [x**2 for x in range(10)]
evens = [x for x in range(10) if x % 2 == 0]
# any / all
has_admin = any(u["role"] == "admin" for u in users)
all_active = all(u["active"] for u in users)
# vars / dir / type / id
obj = SomeObject()
print(vars(obj)) # obj.__dict__
print(dir(obj)) # все атрибуты и методы
print(type(obj)) # <class 'SomeObject'>
print(id(obj)) # адрес в памяти
# isinstance vs type
isinstance(True, int) # True — bool наследует int
type(True) is int # False — точный тип boolos.path.join(), os.path.exists(), os.listdir() в эквивалент на pathlib.read_config(path: str) -> dict читающий JSON/YAML/TOML в зависимости от расширения файла.calculate() и блоком if __name__ == "__main__". Проверьте что он работает и как import, и как скрипт.pydantic_settings, создайте Settings класс для приложения. Добавьте валидацию: port должен быть от 1024 до 65535.Вопросы ещё не добавлены
Вопросы для этой подтемы ещё не добавлены.