Изоляция переменных экземпляра класса Python

Я программист-самоучка, и я недавно изучал питон. Я столкнулся с какой-то странной проблемой, но, по-моему, это просто результат того, что я не знаю синтаксиса и/или потока программы python.

У меня есть один класс под названием Test, который находится в файле TestClass.py. `

class Test:

    __tags = {}
    __fields = {}

    def __init__(self, tags: dict={}, fields: dict={}):
        self.__tags = tags
        self.__fields = fields

    def setTag(self, key, value):
        self.__tags[key] = value

    def getTag(self, key):
        return self.__tags[key]

    def setField(self, key, value):
        self.__fields[key] = value

    def getField(self, key):
        return self.__fields[key]


    def getAll(self):
        return [
            {
                'tags': self.__tags,
                'fields': self.__fields
            }
        ]

Я тестирую функциональность этого класса в файле, содержащем процедурный код, test.py

import TestClass

t1 = TestClass.Test()
t1.setTag('test1', 'value1')
t1.setField('testfield', 'fieldvalue')

t2 = TestClass.Test()
t2.setTag('test2', 'value2')

print(t1.getAll())
print(t2.getAll())

Операторы print, где вещи становятся странными. Выход должен быть:

[{'tags': {'test1': 'value1'}, 'fields': {'testfield': 'fieldvalue'}}]
[{'tags': {'test2': 'value2'}, 'fields': {}}]

Но фактический вывод...

[{'tags': {'test2': 'value2', 'test1': 'value1'}, 'fields': {'testfield': 'fieldvalue'}}]
[{'tags': {'test2': 'value2', 'test1': 'value1'}, 'fields': {'testfield': 'fieldvalue'}}]

Почему?

Изменить: Python 3.5

Ответы

Ответ 1

Вы просто упали не в одном, а в двух хорошо известных "ловушках" Python для новичков.

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

from typing import Optional 


class Test:
    def __init__(self, tags: Optional(dict)=None, fields: Optional(dict)=None):
        self.__tags = tags or {}
        self.__fields = fields or {}
        ...
    ...

Теперь понимаем "почему так?":
Код Python, включая выражения, присутствующие либо на уровне модуля, либо внутри тела класса, либо в объявлении функции или метода обрабатывается только один раз - когда этот модуль загружается первым.

Это означает пустые словари, которые вы создавали в своем классе, и параметры по умолчанию уровня __init__, созданные в настоящее время как словарь, и повторно используемые каждый раз при создании экземпляра класса.

Первая часть состоит в том, что атрибуты, объявленные непосредственно в классе класса в Python, являются атрибутами класса, что означает, что они будут доступны для всех экземпляров этого класса. Если вы назначаете атрибут self.attribute = XXX внутри метода, вы создаете атрибут экземпляра.

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

Обычный шаблон, чтобы избежать этого, заключается в том, чтобы установить параметры по умолчанию на None или другое значение дозорного значения, и внутри тела функции проверить: если значение не было отправлено этим параметрам, просто создайте новый новый словарь (или другой изменяемый объект). Это создается, когда функция фактически выполняется и уникальна для этого запуска. (И, если вы назначаете их атрибуту экземпляра с self.attr = {}, уникальным для этого экземпляра, конечно)

Что касается ключевого слова or, которое я предложил в своем ответе self.__tags = tags or {} - он попросит шаблон, распространенный в старом Python (до того, как мы получили inine if), но все же полезный, в котором ярлыки операторов "или", а также  в выражениях, подобных obj1 or obj2, возвращает первый операнд, если он оценивает "истинное" значение, или возвращает второй атрибут (если он не является правдивым, не имеет значения, значение истины второго параметра все равно имеет значение), То же выражение, использующее встроенное выражение "if", будет: self.__tags = tags if tags else {}.

Также приятно отметить, что хотя шаблон добавления двух __ атрибутов имен для того, чтобы иметь то, что упоминается в старых учебниках как атрибуты "private", это не хороший шаблон программирования, и его следует избегать, Python фактически не реализует частный или защищенный доступ к атрибутам - то, что мы используем, является соглашением, которое, если определенный атрибут, имя метода или функции начинается с _ (одно подчеркивание), оно предназначено для личного использования того, кто его закодировал там, и изменение или вызов этих может иметь необъяснимое поведение в будущих версиях кода, которые управляют этими атрибутами, - но ничто в коде не мешает вам сделать это.

Однако для двойного префикса подчеркивания существует побочный эффект: во время компиляции атрибуты класса с префиксом __ переименовываются, а __xxx переименовывается в _<classname>__xxx - все вхождения в тело класса переименовываются одинаково, и код за пределами тела класса может обращаться к нему обычно, просто записывая полное искаженное имя. Эта функция предназначена для того, чтобы базовые классы сохраняли атрибуты и методы, которые не должны переопределяться в подклассах, по ошибке или простота использования имени атрибута (но не для целей "безопасности" ).

Учебники и тексты старого языка обычно объясняют эту функцию как способ "частных атрибутов" в Python - на самом деле это неверно.