Подтвердить что ты не робот

Насколько опасно настраивать себя.__ class__ на что-то еще?

Скажем, у меня есть класс, который имеет подклассы числа.

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

Итак, насколько опасно это делать? Кажется странным, но неправильно ли это делать? Несмотря на способность изменять тип во время выполнения, является ли это особенностью языка, которого следует полностью избегать? Почему или почему нет?

(В зависимости от ответов я расскажу о более конкретном вопросе о том, что я хотел бы сделать, и если есть лучшие альтернативы).

4b9b3361

Ответ 1

Вот список вещей, которые я могу придумать, сделать это опасным, в грубом порядке от худшего до наименее плохого:

  • Это может смутить кого-то, кто читает или отлаживает ваш код.
  • Вы не получили бы правильный метод __init__, поэтому у вас, вероятно, не будет все инициализированы все переменные экземпляра (или даже вообще).
  • Различия между 2.x и 3.x достаточно значительны, что может быть болезненным для порта.
  • Имеются некоторые случаи кросс-класса с classmethods, дескрипторы с ручным кодированием, привязки к порядку разрешения метода и т.д., и они отличаются между классическими и новыми классами (и, опять же, между 2.x и 3. х).
  • Если вы используете __slots__, все классы должны иметь одинаковые слоты. (И если у вас есть совместимые, но разные слоты, возможно, они работают сначала, но делают ужасные вещи...)
  • Определения специальных методов в классах нового стиля могут не изменяться. (Фактически, это будет работать на практике со всеми текущими реализациями Python, но это не документировано для работы, поэтому...)
  • Если вы используете __new__, все будет работать так, как вы наивно ожидали.
  • Если классы имеют разные метаклассы, все становится еще более запутанным.

Между тем, во многих случаях, когда вы думаете, что это необходимо, есть лучшие варианты:

  • Используйте factory, чтобы создать экземпляр соответствующего класса динамически, вместо создания базового экземпляра, а затем перетащить его в производный.
  • Используйте __new__ или другие механизмы, чтобы подключить конструкцию.
  • Перепроектируйте вещи так, чтобы у вас был один класс с определенным поведением, управляемым данными, вместо злоупотребления наследованием.

Как наиболее распространенный конкретный случай последнего, просто поместите все "переменные методы" в классы, экземпляры которых хранятся как элемент данных "родительский", а не в подклассы. Вместо изменения self.__class__ = OtherSubclass просто выполните self.member = OtherSubclass(self). Если вам действительно нужны методы для магического изменения, автоматическая пересылка (например, через __getattr__) является гораздо более распространенной и питонической идиомой, чем изменение классов "на лету".

Ответ 2

Назначение атрибута __class__ полезно, если у вас есть приложение, работающее на длительное время, и вам нужно заменить старую версию какого-либо объекта более новой версией того же класса без потери данных, например. после некоторого reload(mymodule) и без перезагрузки неизменных модулей. Другой пример: если вы реализуете постоянство - что-то похожее на pickle.load.

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

Ответ 3

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

Таким образом, изменение __class__ объектов среди набора классов, которые были специально разработаны, которые будут использоваться таким образом, могут быть совершенно точными. Я знал, что вы можете сделать это в течение длительного времени, но я никогда не нашел использования для этой техники, где лучшее решение не было spring одновременно. Поэтому, если вы думаете, что у вас есть прецедент, пойдите для этого. Просто будьте ясны в ваших комментариях/документации, что происходит. В частности, это означает, что реализация всех задействованных классов должна учитывать все их инварианты/предположения/etc, а не возможность рассматривать каждый класс изолированно, поэтому вы должны быть уверены, что любой, кто работает на любом из этот код знает об этом!

Ответ 4

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

Прежде всего, причина, по которой я смотрю этот пост, - это то, что я сделал именно это, а __slots__ не нравится. (да, мой код является допустимым прецедентом для слотов, это чистая оптимизация памяти), и я пытался обойти проблему с слотами.

Я впервые увидел это в Повар-книге Alex Martelli Python (1-е изд.). В 3-е изд., Это рецепт 8.19 "Реализация состояний с заданными объектами или проблемы с машиной". Хорошо осведомленный источник, Python-мудрый.

Предположим, у вас есть объект ActiveEnemy, который отличается от поведения InactiveEnemy, и вам нужно быстро переключаться между ними. Может быть, даже DeadEnemy.

Если InactiveEnemy был подклассом или родным братом, вы можете переключать атрибуты класса. Точнее, точная родословная имеет меньшее значение, чем методы и атрибуты, которые соответствуют коду, вызывающему его. Подумайте о Java интерфейсе или, как говорили несколько человек, ваши классы должны быть разработаны с учетом этого использования.

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

Но я использовал это довольно успешно на Python 2.x и никогда не имел никаких необычных проблем с ним. Лучше всего сделать с общим родителем и небольшими поведенческими различиями в подклассах с теми же сигнатурами метода.

Нет проблем, пока не будет проблема с __slots__, которая блокирует его только сейчас. Но слоты - это боль в шее в целом.

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

Но для управления очень специфическими условиями, известными заранее? Как конечная машина, которую клиенты должны понимать полностью? Тогда это довольно чертовски близко к магии, со всем риском, который приходит с ним. Это довольно элегантно.

Вопросы Python 3? Протестируйте его, чтобы увидеть, работает ли он, но Cookbook использует синтаксис Python 3 print (x) в своем примере FWIW.

Ответ 5

Насколько "опасно" это зависит, в первую очередь, от того, что должен был сделать подкласс при инициализации объекта. Вполне возможно, что он не будет правильно инициализирован, запустив только базовый класс __init__(), и что-то не удастся позже из-за, скажем, неинициализированного атрибута экземпляра.

Даже без этого в большинстве случаев использования это кажется плохой практикой. Легче всего просто создать нужный класс.

Ответ 6

Вот пример того, как вы могли бы сделать то же самое, не меняя __class__. Цитируя @unutbu в комментариях к вопросу:

Предположим, вы моделировали клеточные автоматы. Предположим, что каждая ячейка может находиться в одном из 5 этапов. Вы можете определить 5 классов Stage1, Stage2 и т.д. Предположим, что каждый класс Stage имеет несколько методов.

class Stage1(object):
  …

class Stage2(object):
  …

…

class Cell(object):
  def __init__(self):
    self.current_stage = Stage1()
  def goToStage2(self):
    self.current_stage = Stage2()
  def __getattr__(self, attr):
    return getattr(self.current_stage, attr)

Если вы разрешаете изменять __class__, вы можете мгновенно дать ячейке все методы нового этапа (такие же имена, но другое поведение).

То же самое для изменения current_stage, но это совершенно нормальная и питоническая вещь, которая никого не путает.

Кроме того, он позволяет не изменять некоторые специальные методы, которые вы не хотите изменять, просто переопределив их в Cell.

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

Если вы откажетесь изменить __class__, вам может потребоваться включить атрибут stage и использовать множество операторов if или переназначить множество атрибутов, указывающих на разные функции этапа

Да, я использовал атрибут stage, но это не недостаток - это очевидный видимый способ отслеживать, что такое текущий этап, лучше для отладки и для удобочитаемости.

И нет ни одного оператора if или любого переназначения атрибута, кроме атрибута stage.

И это всего лишь один из нескольких способов сделать это без изменения __class__.

Ответ 7

В комментариях я предложил моделировать клеточные автоматы в качестве возможного варианта использования для динамических __class__ s. Попробуем немного изложить идею:


Использование динамического __class__:

class Stage(object):
    def __init__(self, x, y):
        self.x = x
        self.y = y

class Stage1(Stage):
    def step(self):
        if ...:
            self.__class__ = Stage2

class Stage2(Stage):
    def step(self):
        if ...:
            self.__class__ = Stage3

cells = [Stage1(x,y) for x in range(rows) for y in range(cols)]

def step(cells):
    for cell in cells:
        cell.step()
    yield cells

Из-за отсутствия лучшего термина, я собираюсь назвать это

Традиционный способ: (в основном код abarnert)

class Stage1(object):
    def step(self, cell):
        ...
        if ...:
            cell.goToStage2()

class Stage2(object):
    def step(self, cell):
        ...
        if ...:        
            cell.goToStage3()

class Cell(object):
    def __init__(self, x, y):
        self.x = x
        self.y = y
        self.current_stage = Stage1()
    def goToStage2(self):
        self.current_stage = Stage2()
    def __getattr__(self, attr):
        return getattr(self.current_stage, attr)

cells = [Cell(x,y) for x in range(rows) for y in range(cols)]

def step(cells):
    for cell in cells:
        cell.step(cell)
    yield cells

Сравнение:

  • Традиционный способ создает список экземпляров Cell, каждый с текущий атрибут этапа.

    Динамический способ __class__ создает список экземпляров, которые подклассы Stage. Нет необходимости в текущем этапе атрибут, поскольку __class__ уже служит этой цели.

  • Традиционный способ использует методы goToStage2, goToStage3,... ступеней переключения.

    Динамический __class__ способ не требует таких методов. Ты только переназначить __class__.

  • Традиционный способ использует специальный метод __getattr__ для делегирования некоторый метод вызывает соответствующий экземпляр этапа, хранящийся в self.current_stage.

    Динамический __class__ способ не требует такого делегирования. экземпляры в cells уже являются объектами, которые вы хотите.

  • Традиционный способ должен передать Cell в качестве аргумента для Stage.step. Это можно назвать cell.goToStageN.

    Динамический __class__ путь не должен пропускать ничего. объект, с которым мы имеем дело, имеет все, что нам нужно.


Вывод:

Оба способа можно заставить работать. Насколько я могу представить себе, как будут реализованы эти две реализации, мне кажется, что динамическая реализация __class__ будет

  • более простой (нет Cell класс),

  • более элегантные (не уродливые методы goToStage2, ни мозговые тизеры, как почему вам нужно написать cell.step(cell) вместо cell.step()),

  • и легче понять (нет __getattr__, никакого дополнительного уровня косвенность)