Попробуйте поискать в Интернете «Паттерны проектирования на Python» — и получите целую простыню туториалов, демонстрирующих, как в точности воспроизвести на Python паттерны проектирования из книги «Банды четырёх». Там же будут диаграммы классов, иерархии фабрик и столько шаблонного кода, что выхлопа хватит, чтобы отопить маленькую деревню. Так вам внушают, будто вы пишете «серьёзный» код. Умно. Профессионально. Готово для корпоративного использования.

Но вот в чём проблема: большинство из этих паттернов решают проблемы, которые в Python просто отсутствуют. Паттерны разрабатывались для таких языков как Java и C++, где для выполнения самых базовых вещей требуется настоящая эквилибристика — нет ни функций первого класса, ни динамической типизации, ни модулей в качестве пространств имён. Разумеется, вам потребуется Фабрика или Синглтон, если без них в вашем языке просто не с чем работать.

Слепо копировать эти паттерны в Python — не признак большого ума. Из-за них ваш код сложнее читать, тестировать, а также объяснять очередному бедняге, которому этот код придётся поддерживать. Возможно, через три месяца этим беднягой станете вы..

В этом посте мы разберём несколько классических паттернов «Банды четырёх» (GOF), которые при разработке на Python лучше забыть. Для каждого из этих паттернов мы рассмотрим:

  1. Как он обычно (и при этом неудачно) реализуется в Python,

  2. Почему такой код пробуждает воспоминания о том, как писали на Java в 2001 году

  3. Как выглядит нормальная альтернатива на Python — поскольку, да, почти всегда можно сделать проще.

Начнём с самого большой головной боли: порождающих паттернов — то есть, с целой категории решений для тех проблем, которые в Python уже решены.

Синглтон: когда вам нужна глобальная переменная, но надо, чтобы она выглядела красиво

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

class Singleton:
    _instance: "Singleton" | None = None

    def __new__(cls, *args, **kwargs):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
        return cls._instance

Выглядит умно — пока не попытаешься этим пользоваться.

s1 = Singleton(name="Alice", age=30)
s2 = Singleton(name="Bob", age=25)

print(s1.name)  # 'Алиса'
print(s2.name)  # Тоже 'Алиса'!

Что случилось? Оказывается, вы всё время получаете один и тот же экземпляр, какие бы параметры вы ни передавали. При втором вызове к Singleton(name="Bob", age=25) не создаётся ничего нового — код просто тихо переиспользует оригинальный объект с его исходными атрибутами. Никаких предупреждений. Никакой ошибки. Просто бесшумно делается ерунда.

Но всё становится ещё хуже при попытке унаследовать от этого класса:

class DBConnection:
    _instance = None
    def __new__(cls, *args, **kwargs):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
        return cls._instance

class MySqlConnection(DBConnection): ...
class PostGresConnection(DBConnection): ...

conn1 = MySqlConnection()
conn2 = PostGresConnection()

Логично было бы ожидать, что здесь будет два отдельных объекта, по одному для каждого из подклассов. Но нет — и conn1, и conn2 — это один и тот же экземпляр. Всё дело в том, что _instance находится в базовом классе, а не в каждом подклассе отдельно. Поэтому поздравляю: вы написали идеальную коробку сюрпризов. PostGresConnection() может вернуть MySqlConnection, а MySqlConnection() может выдать вам PostGresConnection. Всё зависит от того, который из них вы инстанцировали первым.

Надеюсь, в вашем приложении нормально играть в рулетку.

# settings.py
from typing import Final

class Settings: ...

settings: Final[Settings] = Settings() # добавляем в настройки typing.Final, в таком случае механизм проверки типов пожалуется, если кто-то попытается переприсвоить объект настроек.

Почему синглтон был уместен в C++

Будем честны: синглтон появился не на пустом месте. Он зародился на диких просторах C++ — языка без нормальной системы модулей, где только в самом приблизительном виде существовали пространства имён.

В C++ код живёт в заголовочных файлах и файлах исходников, и вся эта информация перемешивается в ходе компиляции. Невозможно чётко выразить: «это значение является приватным в рамках этого файла» или «этот глобальный объект существует только однажды», приходится импровизировать. Язык предоставляет вам глобальные переменные, которые быстро превращаются в путаницу, если тщательно не контролировать их инициализацию и время жизни.

В C++ не было модулей (до версии C++20) или полноценных систем пакетов, поэтому Синглтон был умным приёмом, гарантировавшим, что будет существовать ровно один экземпляр класса. Синглтон спасал от таких кошмаров как дублирование глобальных значений и множественные определения. Таким образом, в языке были вынуждены изобрести паттерн для обработки таких сущностей, которые в Python выражаются просто как объект на уровне модуля.

// logger.h

#ifndef LOGGER_H
#define LOGGER_H

class Logger {
public:
    void log(const char* msg);
};

extern Logger globalLogger; // Declaration
#endif

// logger.cpp

#include "logger.h"
#include <iostream>

Logger globalLogger; // Definition

void Logger::log(const char* msg) {
    std::cout << msg << std::endl;
}

// main.cpp

#include "logger.h"

int main() {
    globalLogger.log("Starting the app");
    return 0;
}

Здесь globalLogger определяется в одной единице трансляции (logger.cpp), но, если вы случайно определите его сразу в нескольких местах, то компоновщик будет ругаться на дублирование символов. Управлять таким глобальным состоянием непросто — и паттерн Синглтон заключает эту идею в класс, который сам управляет своим единственным экземпляром, поэтому не приходится беспокоиться о множественных определениях.

Таким образом, Синглтон можно считать пластырем для C++, понадобившимся в C++ из-за отсутствия модульности и из-за того, что не было налажено чистое управление глобальным состоянием — это совсем не святой Грааль проектирования программ.

Python-альтернатива: просто пользуйтесь модулями (серьёзно)

Если в Python вам нужен глобальный единственный экземпляр, то не нужно заново изобретать велосипед, создавая сложные классы-синглтоны. Python уже предоставляет вам всё, что нужно — в форме модулей.

Просто создавайте объект на уровне модуля — и он гарантированно будет синглтоном, пока этот модуль импортируется:

# settings.py
from typing import Final

class Settings: ...

settings: Final[Settings] = Settings() # добавляем в настройки typing.Final, в таком случае механизм проверки типов пожалуется, если кто-то попытается переприсвоить объект настроек.

Хотите повременить с созданием? Пользуйтесь замыканиями

Ладно, допустим, вы хотите отложить создание объекта до тех пор, пока он действительно не понадобится — это называется «ленивая инициализация». Всё равно нет необходимости в паттерне синглтон.

Воспользуйтесь для сохранения экземпляра простой функцией с замыканием и внутренней переменной:

def _settings():
    settings: Settings = Settings()

    def get_settings() -> Settings:
        return settings

    def set_settings(value: Settings) -> None:
        nonlocal settings
        settings = value

    return get_settings, set_settings

get_settings, set_settings = _settings()

Пример этого паттерна с github

Такой подход особенно полезен, когда ваш объект настроек зависит от значений, доступных только во время выполнения – например, от пути к файлу окружения (env_file: Path). При ленивой инициализации с использованием замыканий можно отложить создание экземпляра Settings до тех пор, пока у вас не будет всей нужной информации, а не делать это принудительно во время импорта.

Паттерн Cтроитель: по-взрослому переусложняем создание объектов

Если вы увлекаетесь паттернами проектирования, то, вероятно, имели дело с паттерном Строитель, прославляемым как красивый инструмент для пошагового создания сложных объектов. В таких языках как Java или C++, где у конструкторов не может быть аргументов по умолчанию, и при этом всё держится на неизменяемости объектов, такой паттерн действительно в каком-то смысле оправдан. .

Но в Python? Ох, ребята. Зачастую попадаются строители, которые выглядят вот так:

class CarBuilder:
    def __init__(self):
        self._color = None
        self._engine = None

    def set_color(self, color: str) -> "CarBuilder":
        self._color = color
        return self

    def set_engine(self, engine: str) -> "CarBuilder":
        self._engine = engine
        return self

    def build(self) -> "Car":
        return Car(color=self._color, engine=self._engine)

class Car:
    def __init__(self, color: str, engine: str):
        self.color = color
        self.engine = engine

car = (
    CarBuilder()
    .set_color("red")
    .set_engine("V8")
    .build()
)

Подобный код создаёт у вас впечатление, будто вы знаете, что делаете… пока вы не поймёте, что просто переизобрели именованные аргументы со сцеплением методов и дополнительными классами. Весь этот шаблонный код нужен вам лишь для того, чтобы не использовать принятые в Python аргументы по умолчанию или аргументы ключевых слов?

Мои поздравления! Вам только что удалось при помощи строителя обойти проблему, которую Python решает прямо «из коробки».

Паттерн Строитель часто требуется в тех случаях, когда у параметров нет значений по умолчанию:

public class Car {
    private final String color;
    private final String engine;

    private Car(Builder builder) {
        this.color = builder.color;
        this.engine = builder.engine;
    }

    public static class Builder {
        private String color;   // нет значения по умолчанию
        private String engine;  // нет значения по умолчанию

        public Builder setColor(String color) {
            this.color = color;
            return this;
        }

        public Builder setEngine(String engine) {
            this.engine = engine;
            return this;
        }

        public Car build() {
            // Возможно, вы захотите добавить здесь валидацию
            return new Car(this);
        }
    }

    public static void main(String[] args) {
        Car car = new Car.Builder()
            .setColor("Red")
            .setEngine("V8")
            .build();
    }
}

В Java конструкторы не могут иметь значения по умолчанию для параметров, а когда опций много, перегрузка методов быстро становится неудобной. Паттерн Строитель решает эту проблему, обеспечивая пошаговое построение с опциональными параметрами.

Python-альтернатива: пользуйтесь аргументами по умолчанию и фабричными функциями — никаких строителей не требуется

Так как же создавать в Python сложные объекты без всех этих церемоний. Просто: всего лишь используем язык по назначению.

1. Пользуемся аргументами по умолчанию как человек разумный

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

class Car:
    def __init__(self, color: str = "black", engine: str = "V4"):
        self.color = color
        self.engine = engine

car = Car(color="red", engine="V8")

Бум. Удобочитаемо, лаконично и бесконечно проще в тестировании. Хотите автомобиль по умолчанию? Просто назовите его Car(). Хотите автомобиль с тюнингом? Передайте ему аргументы. Готово.

2. Хотите что-то ещё более причудливое? Используйте фабричную функцию с перегрузками

Если хотите полнее контролировать код, или вам нужна более серьёзная поддержка при редактировании (например, разные комбинации аргументов), то фабричная функция с typing.overload обеспечит вам нужную гибкость и избавит от необходимости писать целый класс Builder:

from typing import overload

class Car:
    def __init__(self, color: str, engine: str):
        self.color = color
        self.engine = engine

@overload
def make_car() -> Car: ...
@overload
def make_car(color: str) -> Car: ...
@overload
def make_car(color: str, engine: str) -> Car: ...

def make_car(color: str = "black", engine: str = "V4") -> Car:
    return Car(color=color, engine=engine)

car1 = make_car()
car2 = make_car("red")
car3 = make_car("blue", "V8")

Получается чистая логика, удобное автозавершение прямо в IDE, а также ноль шаблонного кода. Представляете — мы справились без Строителя, решив его задачи при помощи одних лишь функций и умолчаний. Кто бы мог подумать?

Комментарии (7)


  1. Frokich
    20.08.2025 13:43

    Действительно полезная статья


  1. Andrey_Solomatin
    20.08.2025 13:43

    Python-альтернатива: просто пользуйтесь модулями (серьёзно)

    Если у вас в sys.path есть вложенные пути, то можно один и тот же модуль заимпортить два раза и это будут разные модули. За много лет, я только один раз с таким сталкивался.


  1. Andrey_Solomatin
    20.08.2025 13:43

    Заступлюсь за строителя, иногда он удобен и уместен. Например в ArgumentParser.

    Странно, что вы про декоратор не сказали.


  1. ceveru
    20.08.2025 13:43

    Вроде, Java поддерживает функции первого порядка.

    public int cumulativeScore(Function<String, Integer> wordScore, List<String> words) { ... }
    ...
    Function<String, Integer> scoreFunction = w -> score(w);
    ...
    public Function<String, String> createGreeter(String greeting) {
        return (String name) -> greeting + ", " + name + "!";
    }


    Ну, и Java Platform Module System как раз реализует механизм "модулей, как пространства имен".


  1. Tishka17
    20.08.2025 13:43

    Билдер в питоне действительно часто лишний из-за наличия keyword-аргументов функции, но на самом деле это очень удобная вещь, когда речь идёт о конструировании со сложной логикой. Query builder из ORM это буквально тот же паттерн.

    Синглтон же - просто антипаттерн как и всякие мутабельные статические переменные (только хуже).


    1. outlingo
      20.08.2025 13:43

      Согласен про билдер, он позволяет делегировать или отложить вычисление аргументов. Автор походу просто не писал сколь-нибудь сложной логики с асинхронным параллельным выполнением запросов.


  1. Dhwtj
    20.08.2025 13:43

    Хоронили GoF, порвали два баяна