Используйте класс в контексте другого модуля - PullRequest
7 голосов
/ 21 сентября 2011

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

Пример

Этот пример только для примера:

# module_a.py

my_global = []

class A:
    def __init__(self):
        my_global.append(self)

class B:
    def __init__(self):
        my_global.append(self)

В этом примере, если я создам экземпляр A через A(), он вызовет append для объекта с именем my_global. Но теперь я хочу создать новый модуль, импортировать B в него, и B использовать my_global из модуля, в который он был импортирован, вместо my_global из модуля B, который был первоначально определен.

# module_b.py

from module_a import B

my_global = []

Относящиеся

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

Update0

  • Приведенный выше пример только для иллюстрации того, чего я пытаюсь достичь.
  • Поскольку для классов нет переменной области (в отличие, скажем, от C ++), я думаю, что ссылка на глобальное отображение не сохраняется в классе, а вместо этого прикрепляется к каждой функции, когда она определена.

Update1

Пример был запрошен из стандартной библиотеки:

Многие (может быть, все?) Классы в модуле threading используют глобальные переменные, такие как _allocate_lock, get_ident и _active, определенные здесь и здесь . Нельзя изменить эти глобальные переменные, не изменив их для всех классов в этом модуле.

Ответы [ 7 ]

5 голосов
/ 09 октября 2011

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

Полагаю, вы знакомы с sys.modules, и что, если вы удалите модуль оттуда, Python забудет, что он был импортирован, но старые объекты, ссылающиеся на него, продолжат это делать. При повторном импорте будет создана новая копия модуля.

Хакерское решение вашей проблемы могло бы быть примерно таким:

import sys
import threading

# Remove the original module, but keep it around
main_threading = sys.modules.pop('threading')

# Get a private copy of the module
import threading as private_threading

# Cover up evidence by restoring the original
sys.modules['threading'] = main_threading

# Modify the private copy
private_threading._allocate_lock = my_allocate_lock()

А теперь у private_threading.Lock глобалы полностью отделены от threading.Lock!

Само собой разумеется, модуль не был написан с учетом этого, и особенно с системным модулем, таким как threading, вы можете столкнуться с проблемами. Например, threading._active должен содержать все запущенные потоки, но с этим решением ни у _active не будет их всех. Кодекс также может съесть ваши носки и поджечь ваш дом и т. Д. Испытайте строго.

1 голос
/ 07 октября 2011

«Нельзя изменить эти глобальные переменные, не изменив их для всех классов в этом модуле». В этом корень проблемы, не правда ли, и хорошее объяснение проблемы с global переменными в целом. Использование globals в потоке привязывает его классы к этим глобальным объектам.

К тому времени, когда вы собираете что-то, чтобы найти и монтируете патчи для каждого использования глобальной переменной в отдельном классе из модуля, вы еще впереди, просто пытаясь переопределить код для собственного использования?

Единственный обходной путь, который "может" пригодиться в вашей ситуации, это что-то вроде mock . Декораторы патчей / менеджеры контекста Mock (или что-то подобное) могут быть использованы для замены глобальной переменной на время жизни данного объекта. Он хорошо работает в очень контролируемом контексте модульного тестирования, но при любых других обстоятельствах я бы не стал его рекомендовать и подумал бы просто о переопределении кода в соответствии с моими потребностями.

1 голос
/ 07 октября 2011

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

def migrate_class(cls, globals):
    """Recreates a class substituting the passed-in globals for the
    globals already in the existing class.  This proof-of-concept
    version only goes one-level deep (i.e. properties and other nested
    functions are not changed)."""
    name = cls.__name__
    bases = cls.__bases__
    new_dict = dict()
    if hasattr(cls, '__slots__'):
        new_dict['__slots__'] = cls.__slots__
        for name in cls.__slots__:
            if hasattr(cls, name):
                attr = getattr(cls, name)
                if callable(attr):
                    closure = attr.__closure__
                    defaults = attr.__defaults__
                    func_code = attr.__code__
                    attr = FunctionType(func_code, globals)
                new_dict[name] = attr
    if hasattr(cls, '__dict__'):
        od = getattr(cls, '__dict__')
        for name, attr in od.items():
            if callable(attr):
                closure = attr.__closure__
                defaults = attr.__defaults__
                kwdefaults = attr.__kwdefaults__
                func_code = attr.__code__
                attr = FunctionType(func_code, globals, name, defaults, closure)
                if kwdefaults:
                    attr.__kwdefaults__ = kwdefaults
            new_dict[name] = attr
    return type(name, bases, new_dict)

После прохождения этого упражнения мне действительно интересно, зачем вам это нужно?

0 голосов
/ 11 октября 2011

Если вы используете Python 3, вы можете создать подкласс B и переопределить атрибут __globals__ метода __init__ следующим образом:

from module_a import B

function = type(lambda: 0)  # similar to 'from types import FunctionType as function', but faster
my_global = []


class My_B (B):
    __init__ = function(B.__init__.__code__, globals(), '__init__',  B.__init__.__defaults__, B.__init__.__closure__)
0 голосов
/ 08 октября 2011

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

Я бы попытался переопределить A и B (возможно, путем их подкласса) в моем собственном модуле и со всеми ссылками на my_global заменен введенной зависимостью от A и B, которую я здесь назову реестром.

class A(orig.A):

    def __init__(self, registry):
        self.registry = registry
        self.registry.append(self)

    # more updated methods

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

my_registry = []
def A_in_my_registry():
    return A(my_registry)

Если внешний код создает для вас экземпляры orig.A, и вы предпочитаете иметь новые экземпляры A, вы должны надеяться, что внешний код можно настроить с заводами. Если нет, производные от внешних классов и обновите их, чтобы вместо них использовать (недавно введенные) фабрики. .... И промыть повторить для создания этих обновленных классов. Я понимаю, что это может быть утомительно или почти невозможно в зависимости от сложности внешнего кода, но большинство стандартных библиотек довольно плоские.

-

Редактировать: Monkey patch std lib code.

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

import orig

class A(orig.A):

    def __init__(self, registry=orig.my_globals):
        self.registry = registry
        self.registry.append(self)

    # more updated methods

orig.A = A

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

0 голосов
/ 21 сентября 2011

Глобалы редко бывают хорошей идеей.

Неявные переменные редко бывают хорошей идеей.

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

Кроме того, вы не хотите A.__init__() делать что-либо «на уровне класса», например, обновлять какую-то загадочную коллекцию, которая существует для класса в целом. Это часто плохая идея.

Вместо того, чтобы связываться с неявной коллекцией на уровне класса, вам нужна Factory in module_a, которая (1) создает A или B экземпляров и (b) обновляет явную коллекцию.

Затем вы можете использовать эту фабрику в module_b, за исключением другой коллекции.

Это может улучшить тестируемость, выставив неявную зависимость.

module_a.py

class Factory( object ):
    def __init__( self, collection ):
        self.collection= collection
    def make( self, name, *args, **kw ):
        obj= eval( name )( *args, **kw )
        self.collection.append( obj )
        return obj

module_collection = []
factory= Factory( module_collection )

module_b.py

module_collection = []
factory = module_a.Factory( module_collection )

Теперь клиент может сделать это

import module_b
a = module_b.factory.make( "A" )
b = module_b.factory.make( "B" )
print( module_b.module_collection )

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

Смысл в том, чтобы сделать коллекцию явной через фабричный класс.

0 голосов
/ 21 сентября 2011

ИМХО невозможно переопределить глобальные переменные ...

...