Построение сложных объектов пошагово. Fluent Interface через метод __enter__ и контекстные менеджеры. Dataclasses и attrs для Builder.
Сложные объекты строятся пошагово, а не создаются одним монструозным конструктором.
Builder — порождающий паттерн для пошагового создания сложных объектов, позволяющий получать разные представления одного объекта.
# ❌ ПЛОХО: Конструктор с кучей параметров
class Query:
def __init__(self, table, columns=None, where=None, order_by=None,
limit=None, offset=None, join=None, group_by=None,
having=None, distinct=False):
self.table = table
self.columns = columns or '*'
self.where = where
# ... ещё 10 параметров ...
# Вызов выглядит ужасно:
query = Query(
table="users",
columns=["id", "name"],
where="age > 18",
order_by="name",
limit=10,
offset=0,
distinct=True
)# ✅ ХОРОШО: Builder для пошагового построения
class Query:
def __init__(self):
self._table = None
self._columns = '*'
self._where = None
self._order_by = None
self._limit = None
self._offset = None
self._distinct = False
def table(self, name: str) -> "Query":
self._table = name
return self
def select(self, *columns: str) -> "Query":
self._columns = columns if columns else '*'
return self
def where(self, condition: str) -> "Query":
self._where = condition
return self
def order_by(self, column: str) -> "Query":
self._order_by = column
return self
def limit(self, n: int) -> "Query":
self._limit = n
return self
def offset(self, n: int) -> "Query":
self._offset = n
return self
def distinct(self) -> "Query":
self._distinct = True
return self
def build(self) -> str:
parts = [f"SELECT {'DISTINCT ' if self._distinct else ''}{self._columns}"]
parts.append(f"FROM {self._table}")
if self._where:
parts.append(f"WHERE {self._where}")
if self._order_by:
parts.append(f"ORDER BY {self._order_by}")
if self._limit:
parts.append(f"LIMIT {self._limit}")
if self._offset:
parts.append(f"OFFSET {self._offset}")
return " ".join(parts)
# Использование
query = (Query()
.table("users")
.select("id", "name")
.where("age > 18")
.order_by("name")
.limit(10)
.build())
print(query)
# SELECT id, name FROM users WHERE age > 18 ORDER BY name LIMIT 10Fluent Interface — стиль проектирования API, где методы возвращают self, позволяя цепочечные вызовы.
builder.select().from_().where()class Email:
def __init__(self):
self._from = None
self._to = []
self._cc = []
self._subject = None
self._body = None
self._attachments = []
def from_(self, email: str) -> "Email":
self._from = email
return self
def to(self, *emails: str) -> "Email":
self._to.extend(emails)
return self
def cc(self, *emails: str) -> "Email":
self._cc.extend(emails)
return self
def subject(self, subject: str) -> "Email":
self._subject = subject
return self
def body(self, text: str) -> "Email":
self._body = text
return self
def attach(self, *files: str) -> "Email":
self._attachments.extend(files)
return self
def build(self) -> dict:
return {
"from": self._from,
"to": self._to,
"cc": self._cc,
"subject": self._subject,
"body": self._body,
"attachments": self._attachments,
}
# Использование — читается как предложение!
email = (Email()
.from_("noreply@example.com")
.to("user@example.com", "admin@example.com")
.cc("boss@example.com")
.subject("Welcome!")
.body("Hello and welcome!")
.attach("guide.pdf", "terms.pdf")
.build())from dataclasses import dataclass, field
from typing import Optional, List
@dataclass
class HTTPRequest:
url: str
method: str = "GET"
headers: dict = field(default_factory=dict)
body: Optional[str] = None
timeout: int = 30
class RequestBuilder:
VALID_METHODS = {"GET", "POST", "PUT", "DELETE", "PATCH"}
def __init__(self):
self._url = None
self._method = "GET"
self._headers = {}
self._body = None
self._timeout = 30
def url(self, url: str) -> "RequestBuilder":
if not url.startswith(("http://", "https://")):
raise ValueError("URL must start with http:// or https://")
self._url = url
return self
def method(self, method: str) -> "RequestBuilder":
method = method.upper()
if method not in self.VALID_METHODS:
raise ValueError(f"Invalid method: {method}")
self._method = method
return self
def header(self, key: str, value: str) -> "RequestBuilder":
self._headers[key] = value
return self
def body(self, body: str) -> "RequestBuilder":
if self._method == "GET":
raise ValueError("GET request cannot have a body")
self._body = body
return self
def timeout(self, seconds: int) -> "RequestBuilder":
if seconds <= 0:
raise ValueError("Timeout must be positive")
self._timeout = seconds
return self
def build(self) -> HTTPRequest:
if not self._url:
raise ValueError("URL is required")
return HTTPRequest(
url=self._url,
method=self._method,
headers=self._headers,
body=self._body,
timeout=self._timeout,
)
# Использование
request = (RequestBuilder()
.url("https://api.example.com/users")
.method("POST")
.header("Content-Type", "application/json")
.header("Authorization", "Bearer token")
.body('{"name": "Alice"}')
.timeout(60)
.build())from contextlib import contextmanager
class TransactionBuilder:
def __init__(self, db):
self.db = db
self._operations = []
def insert(self, table: str, **data):
self._operations.append(("insert", table, data))
return self
def update(self, table: str, where: str, **data):
self._operations.append(("update", table, where, data))
return self
def delete(self, table: str, where: str):
self._operations.append(("delete", table, where))
return self
def execute(self):
with self.db.transaction():
for op in self._operations:
if op[0] == "insert":
self.db.insert(op[1], **op[2])
elif op[0] == "update":
self.db.update(op[1], op[2], **op[3])
elif op[0] == "delete":
self.db.delete(op[1], op[2])
@contextmanager
def transaction_builder(db):
"""Контекстный менеджер для builder"""
builder = TransactionBuilder(db)
try:
yield builder
builder.execute()
except Exception:
# Автоматический rollback при ошибке
db.rollback()
raise
# Использование
with transaction_builder(db) as tx:
tx.insert("users", name="Alice", email="alice@example.com") \
.update("accounts", "user_id=1", balance=100) \
.delete("sessions", "expired=true")
# Все операции выполняются в транзакцииclass HTMLBuilder:
def __init__(self, tag: str):
self.tag = tag
self.attrs = {}
self.children = []
self.text = None
def attr(self, name: str, value: str) -> "HTMLBuilder":
self.attrs[name] = value
return self
def add(self, child: "HTMLBuilder") -> "HTMLBuilder":
self.children.append(child)
return self
def text_(self, text: str) -> "HTMLBuilder":
self.text = text
return self
def build(self) -> str:
attrs_str = " ".join(f'{k}="{v}"' for k, v in self.attrs.items())
opening = f"<{self.tag} {attrs_str}>" if attrs_str else f"<{self.tag}>"
if self.text:
return f"{opening}{self.text}</{self.tag}>"
if not self.children:
return f"{opening}</{self.tag}>"
children_html = "".join(child.build() for child in self.children)
return f"{opening}{children_html}</{self.tag}>"
# DSL для HTML
def div() -> HTMLBuilder:
return HTMLBuilder("div")
def a(href: str = None) -> HTMLBuilder:
builder = HTMLBuilder("a")
if href:
builder.attr("href", href)
return builder
def span() -> HTMLBuilder:
return HTMLBuilder("span")
# Использование
html = (div()
.attr("class", "container")
.add(
div().attr("class", "header")
.add(span().text_("Welcome!"))
)
.add(
a(href="/login").text_("Login")
)
.build())
print(html)
# <div class="container"><div class="header"><span>Welcome!</span></div><a href="/login">Login</a></div>from dataclasses import dataclass, field
from typing import Optional, List
@dataclass
class Pizza:
size: str = "M"
dough: str = "thin"
sauce: Optional[str] = None
toppings: List[str] = field(default_factory=list)
cheese: bool = True
class PizzaBuilder:
def __init__(self):
self._pizza = Pizza()
def size(self, size: str) -> "PizzaBuilder":
self._pizza.size = size
return self
def dough(self, dough: str) -> "PizzaBuilder":
self._pizza.dough = dough
return self
def sauce(self, sauce: str) -> "PizzaBuilder":
self._pizza.sauce = sauce
return self
def topping(self, topping: str) -> "PizzaBuilder":
self._pizza.toppings.append(topping)
return self
def no_cheese(self) -> "PizzaBuilder":
self._pizza.cheese = False
return self
def build(self) -> Pizza:
return self._pizza
# Использование
pizza = (PizzaBuilder()
.size("L")
.dough("thick")
.sauce("tomato")
.topping("mozzarella")
.topping("basil")
.topping("pepperoni")
.build())
print(pizza)
# Pizza(size='L', dough='thick', sauce='tomato',
# toppings=['mozzarella', 'basil', 'pepperoni'], cheese=True)# ❌ ПЛОХО: Builder для простого объекта
class UserBuilder:
def __init__(self):
self._name = None
self._email = None
def name(self, name: str):
self._name = name
return self
def email(self, email: str):
self._email = email
return self
def build(self):
return User(self._name, self._email)
# Зачем, если можно:
@dataclass
class User:
name: str
email: str
user = User(name="Alice", email="alice@example.com")Правило: Используйте Builder, когда:
| Паттерн | Когда использовать | Pythonic-реализация |
|---|---|---|
| Builder | Сложные объекты с многими параметрами | Методы возвращают self |
| Fluent Interface | Читаемость API как предложения | Цепочки вызовов |
| Context Manager | Автоматический cleanup/rollback | @contextmanager |
Главный принцип: Builder превращает конструктор с 10 параметрами в читаемую цепочку вызовов.
Изучите тему Prototype и копирование объектов для создания объектов через копирование.
Вопросы ещё не добавлены
Вопросы для этой подтемы ещё не добавлены.