2025, Dec 29 09:02

Почему аргументы по умолчанию в Python «застывают» и как исправить

Разбираем, почему аргументы по умолчанию в Python «застывают», как глобальные переменные ломают логику, и показываем паттерн со состоянием экземпляра.

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

Воспроизводим проблему

Рассмотрим два файла: они задают цвет, «меняют» его, а затем рисуют, используя параметр по умолчанию, который будто бы должен отражать текущий цвет.

# file_a.py
shade = (0, 0, 0)

class Sketch:
    def setShade(new_shade):
        global shade
        shade = (new_shade)
    def drawDisk(x, y, radius, col=shade):
        pass  # представьте здесь код рисования, который использует col
# file_b.py
from file_a import *

obj = Sketch()
Sketch.setShade((255, 255, 255))
Sketch.drawDisk(0, 0, 5)

Несмотря на попытку сменить цвет, отрисовка по‑прежнему использует чёрный. Печать переменной после «изменения» тоже показывает исходное значение.

Что на самом деле происходит и почему всё ломается

Аргументы по умолчанию в Python вычисляются один раз — в момент определения функции. В примере выше значение по умолчанию для col в drawDisk привязано к shade на этапе определения, то есть к (0, 0, 0). Позднейшие обновления shade на уже зафиксированное значение по умолчанию не влияют. К тому же shade используется как глобальная переменная, а методы вызываются через класс, а не через экземпляр, что делает управление состоянием запутанным и хрупким. Функция, которая «меняет» цвет, тоже не является методом экземпляра, поэтому она не управляет состоянием конкретного объекта и лишь усугубляет путаницу.

Надёжное решение: состояние экземпляра вместо глобалей и «замороженных» значений по умолчанию

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

# file_core.py
class Painter:
    def __init__(self):
        self.tint = (0, 0, 0)

    def setTint(self, new_tint):
        self.tint = new_tint

    def drawCircle(self, x, y, r):
        # используйте self.tint для рисования
        # например, вызов рисования в pygame примет self.tint
        pass
# run_paint.py
from file_core import Painter

c = Painter()
c.setTint((255, 255, 255))
c.drawCircle(0, 0, 5)

В этой версии цвет хранится в экземпляре и берётся в момент вызова, поэтому с «застывшим» значением по умолчанию бороться не приходится.

Если хотите иногда переопределять цвет

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

# file_core.py
class Painter:
    def __init__(self):
        self.tint = (0, 0, 0)

    def setTint(self, new_tint):
        self.tint = new_tint

    def drawCircle(self, x, y, r, col=None):
        if col is None:
            col = self.tint
        # рисуем, используя col
        pass
# run_paint.py
from file_core import Painter

c = Painter()

# По умолчанию используется цвет экземпляра
c.setTint((255, 255, 255))
c.drawCircle(0, 0, 5)

# Переопределяет цвет только для этого вызова
c.drawCircle(10, 10, 8, col=(128, 128, 128))

Здесь col определяется во время вызова. Если ничего не передано, метод берёт текущий цвет экземпляра. Если указать явный цвет, только этот вызов использует переопределение.

Почему это важно

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

Выводы

Если значение по умолчанию зависит от текущего состояния, не привязывайте его как аргумент по умолчанию. Откладывайте решение до времени выполнения — обычно с помощью специального значения вроде None с возвратом к состоянию экземпляра. Избегайте глобалей для общего состояния в объектно‑ориентированном коде и объявляйте методы как полноценные методы экземпляра, чтобы поток данных был прозрачен. Так код рисования — для окружностей или чего угодно ещё — честно использует именно тот цвет, который задуман.