Pythonic способ сбросить переменные объекта?

("переменные" здесь относятся к "именам", я думаю, не совсем уверен в определении, которое используют питонисты)

У меня есть объект и несколько методов. Все эти методы нуждаются и все изменяют переменные объекта. Как я могу, в наиболее питоническом и в лучшем случае, соблюдая методы ООП, добиться того, чтобы объектные переменные использовались методами, но также сохраняли их исходные значения для других методов?

Должен ли я копировать объект каждый раз, когда вызывается метод? Должен ли я сохранять исходные значения и иметь метод reset() для их сброса каждый раз, когда они нужны методу? Или есть еще лучший способ?

EDIT: меня попросили ввести псевдокод. Поскольку меня больше интересует понимание концепции, а не просто конкретное решение проблемы, с которой я сталкиваюсь, я попытаюсь привести пример:

class Player():
    games = 0
    points = 0
    fouls = 0
    rebounds = 0
    assists = 0
    turnovers = 0
    steals = 0

    def playCupGame(self):
        # simulates a game and then assigns values to the variables, accordingly
        self.points = K #just an example

    def playLeagueGame(self):
        # simulates a game and then assigns values to the variables, accordingly
        self.points = Z #just an example
        self.rebounds = W #example again

    def playTrainingGame(self):
        # simulates a game and then assigns values to the variables, accordingly
        self.points = X #just an example
        self.rebounds = Y #example again

Выше приведен мой класс для объекта Player (например, предположим, что он играет в баскетбол). Этот объект имеет три различных метода, которые присваивают значения статистике игроков.

Итак, допустим, у команды две игры в лиге, а затем кубковая игра. Я должен был бы сделать эти звонки:

p.playLeagueGame()
p.playLeagueGame()
p.playCupGame()

Очевидно, что при втором и третьем звонках необходимо сбросить ранее измененную статистику игрока. Для этого я могу либо написать метод сброса, который устанавливает все переменные обратно в 0, либо копировать объект для каждого вызова, который я делаю. Или сделать что-то совершенно другое.

Вот где лежит мой вопрос, какой лучший подход, python и oop мудрый?

ОБНОВЛЕНИЕ: я подозреваю, что слишком усложнил это, и я могу легко решить свою проблему, используя локальные переменные в функциях. Однако что произойдет, если у меня есть функция внутри другой функции, могу ли я использовать локальные переменные внешней функции внутри внутренней?


person Community    schedule 01.02.2011    source источник
comment
Не могли бы вы объяснить свое намерение, желаемое поведение? Это цепочка методов, как в jQuery?   -  person Pēteris Caune    schedule 01.02.2011
comment
Возможно, какой-нибудь псевдокод поможет объяснить, почему необходимо изменить объектные переменные, но вернуть их обратно в конце функции, а не просто использовать локальные переменные.   -  person tkerwin    schedule 01.02.2011
comment
Я читал это несколько раз и до сих пор не уверен, как определить, когда метод захочет видеть обновленные значения членов, а когда нет. Похоже, ему нужен шаблон Agent, Facade или MVC.   -  person Mike DeSimone    schedule 01.02.2011


Ответы (10)


Не уверен, что это достаточно "Pythonic", но вы можете определить "сбрасываемый" декоратор для метода __init__, который создает копию __dict__ объекта и добавляет метод reset(), который переключает текущий __dict__ на исходный.

Изменить. Вот пример реализации:

def resettable(f):
    import copy

    def __init_and_copy__(self, *args, **kwargs):
        f(self, *args)
        self.__original_dict__ = copy.deepcopy(self.__dict__)

        def reset(o = self):
            o.__dict__ = o.__original_dict__

        self.reset = reset

    return __init_and_copy__

class Point(object):
    @resettable
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __str__(self):
        return "%d %d" % (self.x, self.y)

class LabeledPoint(Point):
    @resettable
    def __init__(self, x, y, label):
        self.x = x
        self.y = y
        self.label = label

    def __str__(self):
        return "%d %d (%s)" % (self.x, self.y, self.label)

p = Point(1, 2)

print p # 1 2

p.x = 15
p.y = 25

print p # 15 25

p.reset()

print p # 1 2

p2 = LabeledPoint(1, 2, "Test")

print p2 # 1 2 (Test)

p2.x = 3
p2.label = "Test2"

print p2 # 3 2 (Test2)

p2.reset()

print p2 # 1 2 (Test)

Edit2: добавлен тест с наследованием

person PaoloVictor    schedule 01.02.2011
comment
В Python они называются декораторами, а не аннотациями. - person Rafe Kettler; 01.02.2011
comment
+1 Да! Я всегда их путаю. Забавно, что при реализации я продолжаю думать о декораторах, но все равно печатаю аннотации. Угу. - person PaoloVictor; 01.02.2011
comment
-1, Использование __dict__ в качестве источника всех атрибутов объекта ненадежно. Он не учитывает наследование или метаклассы. - person Apalala; 02.02.2011
comment
@Apalala Не учитывает наследование? Как именно? Не могли бы вы показать мне пример, показывающий, как эта реализация нарушена? Я обновил код с помощью теста, использующего наследование, и он, похоже, работал нормально. - person PaoloVictor; 02.02.2011
comment
def __getattribute__(self,name): return 0 среди прочего. См. goo.gl/3hWLt. См. раздел «Реализация дескрипторов», чтобы узнать о другом способе, которым атрибуты класса, полученные через его экземпляры, могут отличаться от объектов, фактически хранящихся в dict класса. Если атрибут класса не найден, а класс объекта имеет метод __getattr__(), который вызывается для выполнения поиска. - person Apalala; 02.02.2011
comment
-(-1) за старание. Обратите внимание, что это также не работает для подклассов некоторых встроенных типов, например, в class MyList(list): .... Как сказано в документации к модели данных, __dict__ — это деталь реализации, и она не всегда используется. Я не уверен, что этот подход будет работать безупречно, даже если вы измените реализацию, чтобы использовать что-то вроде setattr. Проблема в том, что не существует универсального способа узнать, что означает сбросить объект, поэтому он должен определяться на основе класса за классом. - person Apalala; 02.02.2011
comment
Если у других людей возникают проблемы с вызовом reset(), вызывающим исключение, если он вызывается более одного раза, см. Ответ, который я предоставил. - person Brett Stottlemyer; 17.01.2017
comment
Если B наследуется от A, а я украшаю A.__init__() @resettable, должен ли я ожидать, что B будет правильно сброшен? - person Teodoro; 09.04.2020

Я не уверен насчет «pythonic», но почему бы просто не создать метод reset в вашем объекте, который выполняет все необходимые сбросы? Вызовите этот метод как часть вашего __init__, чтобы вы не дублировали данные (т.е. всегда (повторно) инициализируйте его в одном месте - метод reset)

person Bryan Oakley    schedule 01.02.2011
comment
+1, хотя замена __dict__ умна и все такое, это самый явный (и, следовательно, самый питонический ИМХО) метод. Неизменяемый объект также является хорошим вариантом, но только в том случае, если вашим методам не нужно изменять значения при вычислении. - person senderle; 02.02.2011
comment
+1 Забудьте о pythonic. Значение reset() должно быть определено классом, который в нем нуждается. - person Apalala; 02.02.2011

Я бы создал default dict как элемент данных со всеми значениями по умолчанию, затем сделал __dict__.update(self.default) во время __init__, а затем еще раз в какой-то момент, чтобы вернуть все значения обратно.

В более общем смысле вы можете использовать хук __setattr__, чтобы отслеживать каждую переменную, которая была изменена, а затем использовать эти данные для их сброса.

person richo    schedule 02.02.2011

Похоже, вы хотите знать, должен ли ваш класс быть неизменяемым объектом. Идея состоит в том, что однажды созданный неизменяемый объект не может/не должен/не будет изменен.

В Python встроенные типы, такие как экземпляры int или tuple, являются неизменяемыми, что обеспечивается языком :

>>> a=(1, 2, 3, 1, 2, 3)
>>> a[0] = 9
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'tuple' object does not support item assignment

В качестве другого примера, каждый раз, когда вы добавляете два целых числа, создается новый экземпляр:

>>> a=5000
>>> b=7000
>>> d=a+b
>>> d
12000
>>> id(d)
42882584
>>> d=a+b
>>> id(d)
42215680

id()функция возвращает адрес int объекта 12000. И каждый раз, когда мы добавляем a+b, создается новый экземпляр объекта 12000.

Определяемые пользователем неизменяемые классы должны применяться вручную или просто выполняться по соглашению с комментарием исходного кода:

class X(object):
    """Immutable class. Don't change instance variables values!"""
    def __init__(self, *args):
        self._some_internal_value = ...

    def some_operation(self, arg0):
        new_instance = X(arg0 + ...)
        new_instance._some_internal_operation(self._some_internal_value, 42)
        return new_instance

    def _some_internal_operation(self, a, b):
        """..."""

В любом случае можно создавать новый экземпляр для каждой операции.

person vz0    schedule 01.02.2011
comment
Спасибо за этот ответ, очень полезный! - person ; 03.02.2011

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

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

# The Memento design pattern
class Scores(object):
    ...

class Player(object):
    def __init__(self,...):
        ...
        self.scores = None
        self.history = []
        self.reset()

    def reset(self):
        if (self.scores):
            self.history.append(self.scores)
        self.scores = Scores()
person Apalala    schedule 02.02.2011
comment
любой декоратор, чтобы решить эту проблему? потому что вызов метода сброса для каждого класса многословен, особенно если класс имеет много атрибутов - person TomSawyer; 17.05.2019
comment
@Том Сойер. В других ответах есть декораторы, которые делают копии объекта __dict__. Я думаю, что они слишком широки по своему охвату, но в них есть идея. Может работать копирование только тех атрибутов, которые не начинаются с _ и т. д. Все зависит от семантики рассматриваемой проблемы. - person Apalala; 17.05.2019

Похоже, в целом ваш дизайн нуждается в некоторой доработке. Как насчет класса PlayerGameStatistics, который будет отслеживать все это, а класс Player или Game будет содержать коллекцию этих объектов?

Также показанный вами код является хорошим началом, но не могли бы вы показать больше кода, взаимодействующего с классом Player? Мне просто трудно понять, почему один объект Player должен иметь PlayXGame методов - один Player не взаимодействует с другими Player во время игры, или почему конкретный Player играет в игру?

person user470379    schedule 02.02.2011

Простой метод сброса (вызывается в __init__ и вызывается повторно при необходимости) имеет большой смысл. Но вот решение, которое я считаю интересным, хотя и несколько замысловатым: создать менеджер контекста. Мне интересно, что люди думают об этом...

from contextlib import contextmanager

@contextmanager
def resetting(resettable):
    try:
        resettable.setdef()
        yield resettable
    finally:
        resettable.reset()

class Resetter(object):
    def __init__(self, foo=5, bar=6):
        self.foo = foo
        self.bar = bar
    def setdef(self):
        self._foo = self.foo
        self._bar = self.bar
    def reset(self):
        self.foo = self._foo
        self.bar = self._bar
    def method(self):
        with resetting(self):
            self.foo += self.bar
            print self.foo

r = Resetter()
r.method()    # prints 11
r.method()    # still prints 11

Чтобы перепроектировать, вы можете создать декоратор @resetme

def resetme(f):
    def rf(self, *args, **kwargs):
        with resetting(self):
            f(self, *args, **kwargs)
    return rf

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

@resetme
def method(self):
    self.foo += self.bar
    print self.foo
person senderle    schedule 02.02.2011
comment
Я думаю, что это слишком сложно. def reset(self):... совершенно очевиден и не требует дополнительного кода. Все остальное только усложняет ситуацию без всякой выгоды, IMO. - person Bryan Oakley; 02.02.2011
comment
Что ж, я думаю, что есть некоторый выигрыш в том, что вместо возврата к какому-то фиксированному состоянию, которое нельзя легко изменить, значения foo и bar возвращаются в свое состояние непосредственно перед вызовом метода. Если вам нужна эта функциональность в большом классе с множеством методов, этот метод избавит вас от необходимости постоянно копировать состояние. Тем не менее, я во многом с вами согласен :). Я просто подумал, что это веселое упражнение. - person senderle; 02.02.2011

Мне понравился (и попробовал) лучший ответ от PaoloVictor. Однако я обнаружил, что он «сбрасывает» себя, т. Е. Если вы вызываете reset () во второй раз, он выдает исключение.

Я обнаружил, что он работал многократно со следующей реализацией

def resettable(f):
    import copy

    def __init_and_copy__(self, *args, **kwargs):
        f(self, *args, **kwargs)
        def reset(o = self):
            o.__dict__ = o.__original_dict__
            o.__original_dict__ = copy.deepcopy(self.__dict__)
        self.reset = reset
        self.__original_dict__ = copy.deepcopy(self.__dict__)
    return __init_and_copy__
person Brett Stottlemyer    schedule 17.01.2017

Мне кажется, что вам нужно переработать свою модель, чтобы, по крайней мере, включить отдельный класс «PlayerGameStats».

Что-то вроде:

PlayerGameStats = collections.namedtuple("points fouls rebounds assists turnovers steals")

class Player():
    def __init__(self):
        self.cup_games = []
        self.league_games = []
        self.training_games = []

def playCupGame(self):
    # simulates a game and then assigns values to the variables, accordingly
    stats = PlayerGameStats(points, fouls, rebounds, assists, turnovers, steals)
    self.cup_games.append(stats)

def playLeagueGame(self):
    # simulates a game and then assigns values to the variables, accordingly
    stats = PlayerGameStats(points, fouls, rebounds, assists, turnovers, steals)
    self.league_games.append(stats)

def playTrainingGame(self):
    # simulates a game and then assigns values to the variables, accordingly
    stats = PlayerGameStats(points, fouls, rebounds, assists, turnovers, steals)
    self.training_games.append(stats)

И чтобы ответить на вопрос в вашем редактировании, да, вложенные функции могут видеть переменные, хранящиеся во внешних областях. Подробнее об этом можно прочитать в руководстве: http://docs.python.org/tutorial/classes.html#python-scopes-and-namespaces

person ncoghlan    schedule 02.02.2011

спасибо за хороший вклад, так как у меня была похожая проблема. Я решаю это с помощью хука в методе init, так как я хотел бы иметь возможность сбросить до любого начального состояния, которое было у объекта. Вот мой код:

import copy
_tool_init_states = {}

def wrap_init(init_func):
    def init_hook(inst, *args, **kws):
        if inst not in _tool_init_states:
            # if there is a class hierarchy, only the outer scope does work
            _tool_init_states[inst] = None
            res = init_func(inst, *args, **kws)
            _tool_init_states[inst] = copy.deepcopy(inst.__dict__)
            return res
        else:
            return init_func(inst, *args, **kws)
    return init_hook

def reset(inst):
    inst.__dict__.clear()
    inst.__dict__.update(
        copy.deepcopy(_tool_init_states[inst])
    )

class _Resettable(type):
    """Wraps __init__ to store object _after_ init."""
    def __new__(mcs, *more):
        mcs = super(_Resetable, mcs).__new__(mcs, *more)
        mcs.__init__ = wrap_init(mcs.__init__)
        mcs.reset = reset
        return mcs

class MyResettableClass(object):
    __metaclass__ = Resettable
    def __init__(self):
        self.do_whatever = "you want,"
        self.it_will_be = "resetted by calling reset()"

Чтобы обновить начальное состояние, вы можете создать какой-нибудь метод, например reset(...), который записывает данные в _tool_init_states. Я надеюсь, что это поможет кому-то. Если это возможно без метакласса, дайте мне знать.

person Heiner    schedule 19.05.2014