Как перезаписать импортированный класс python для всех вызовов

Я создаю python-packages/MyLibPackage, который я буду импортировать в свои проекты.

MyLibPackage.____init____.py включает mymodiciation.py. Кроме того, папка MyLibPackage содержит другой файл: base_classes.py(= внешний проект)

mymodiciation.py импортирует "from base_classes import *".

Цель: Я могу импортировать MyLibPackage, который имеет все классы из base_classes (= внешний проект). И если мне нужно модифицировать некоторые классы или функции, я могу перезаписать это в mymodiciation.py. Это работает, но у меня проблема. Например:

Я перезаписываю эти классы в mymodiciation.py:

class Bookcollection(Bookcollection):
   new_member = "lalala"


class user(user):
   def get_books(self):
      return Bookcollection()

если:

from MyLibPackage import *
x = user()
books = x.get_books()

то объект Bookcollection имеет свойство "new_member". Хорошо! Но если я сделаю это:

from MyLibPackage import *
x = shelf() #this class is not overwritten and used also the object "Bookcolelction"
books = x.get_books()

тогда объект Bookcollection НЕ имеет свойство "new_member", потому что он инстанс с MyLibPackage.base_classes.Bookcollection, а не с моим перезаписанным классом MyLibPackage.mymodiciation.Bookcollection

Как я могу сказать: если я перезаписываю класс в mymodiciation, то MyLibPackage должен использовать это, хотя при вызове cames из MyLibPackage.base_classes.shelf(get_books).

Ответ 1

То, что вы хотите сделать, называется "исправление обезьян" и не имеет ничего общего с объектной ориентацией.

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

Может быть, лучше использовать такую инфраструктуру, как Zope Component Architecture, которая позволяет помечать классы интерфейсами и предоставляет объекты-адаптеры, чтобы вы могли без проблем использовать один объект, поскольку для него имеется некоторый интерфейс, для которого он изначально не был предназначен.

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

Вы делаете именно это: меняете класс в модуле, к которому он принадлежит. В Python это можно сделать, просто присвоив вашему новому классу желаемое имя в модуле происхождения:

import base_classes

class Bookcollection(base_classes.Bookcollection):
   new_member = "lalala"

base_classes.Bookcollection = Bookcollection

(Чтобы подобные вещи работали, вы должны избегать "из x import *" в любом проекте, превышающем один скрипт - в этом случае у вас было 2 переменные с одинаковым именем и разными значениями в коде: основа класс и унаследованный класс, например. Пространства имен Python позволяют избежать этого).

Таким образом, это изменит класс Bookcollection в модуле base_class - НО только для кода, который будет ссылаться на него с этого момента и далее в вашей цепочке выполнения. Если класс "x" в вашем примере определен в модуле "base_classes" или определен иным образом до импорта "MyModule", он получит ссылку на старый класс "Bookcollection".

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

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

 import base_classes

 base_classes.Bookcollection.new_member = "lalala"

 def new_bookcol_method(self):
      pass

 # to replace or register a method in the other class:
 base_classes.Bookcollection.__dict__["old_bookcol_method"] = new_bookcol_method

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

В общем, вы должны либо сделать, как подсказывает @jamesj в своем ответе, и использовать разные классы, либо, если вам нужно динамическое поведение, использовать для этого поддерживаемую среду, например Zope Component Architecture. И какой бы подход вы ни выбрали, пишите юнит-тесты.

Ответ 2

Наличие классов с таким же именем, как правило, является плохой идеей, поскольку вы получаете конфликты пространства имен. Я бы рекомендовал изменить ваш MyLibPackage.base_classes.Bookcollection на MyLibPackage.base_classes.BaseBookcollection или аналогичный. Это должно работать так, как ожидалось.