Мой ответ - очень простой ответ на ваш вопрос, поэтому мне интересно, пропустил ли я что-то. По сути, я думаю, что вам следует избегать хранения текущего состояния внешних объектов в вашем модуле.
Вам нужно где-то хранить состояние (если вызывался change_behavior
и, возможно, некоторые другие данные). У вас есть два основных варианта: сохранить состояние в модуле или сохранить состояние в самом потоке. Помимо проблем, которые у вас возникали при сохранении состояния в модуле, можно ожидать, что модуль (в основном) не будет иметь состояния, поэтому я думаю, что вам следует придерживаться последнего и хранить данные в потоке.
Версия 1
Если вы сохраняете состояние в поле, вы рискуете столкнуться между именем создаваемого вами атрибута и именами существующих атрибутов, но если документация ясна и если вы выбираете хорошее имя, это должно не будет проблемой.
Простое подтверждение концепции, без setattr
или hasattr
(я не проверял исходный код CPython, но, возможно, странное поведение исходит от setattr
):
module1.py
import threading
import random
import time
_lock = threading.Lock()
def do_something():
with _lock:
t = threading.current_thread()
try:
if t._my_module_s:
print(f"DoB ({t})")
else:
print(f"DoA ({t})")
except AttributeError:
t._my_module_s = 0
print(f"DoA ({t})")
time.sleep(random.random()*2)
def change_behavior():
with _lock:
t = threading.current_thread()
print(f"Change behavior of: {t}")
t._my_module_s = 1
test1.py
import random
import threading
from module1 import *
class MyThread(threading.Thread):
def __init__(self):
threading.Thread.__init__(self)
def run(self):
n = random.randint(1, 10)
for i in range(n):
do_something()
change_behavior()
for i in range(10-n):
do_something()
thread_1 = MyThread()
thread_2 = MyThread()
thread_1.start()
thread_2.start()
thread_1.join()
thread_2.join()
Выход 1
DoA (<MyThread(Thread-1, started 140155115792128)>)
DoA (<MyThread(Thread-2, started 140155107399424)>)
DoA (<MyThread(Thread-1, started 140155115792128)>)
DoA (<MyThread(Thread-1, started 140155115792128)>)
Change behavior of: <MyThread(Thread-1, started 140155115792128)>
DoB (<MyThread(Thread-1, started 140155115792128)>)
DoB (<MyThread(Thread-1, started 140155115792128)>)
DoA (<MyThread(Thread-2, started 140155107399424)>)
DoB (<MyThread(Thread-1, started 140155115792128)>)
DoA (<MyThread(Thread-2, started 140155107399424)>)
DoB (<MyThread(Thread-1, started 140155115792128)>)
DoA (<MyThread(Thread-2, started 140155107399424)>)
DoA (<MyThread(Thread-2, started 140155107399424)>)
DoB (<MyThread(Thread-1, started 140155115792128)>)
DoA (<MyThread(Thread-2, started 140155107399424)>)
Change behavior of: <MyThread(Thread-2, started 140155107399424)>
DoB (<MyThread(Thread-2, started 140155107399424)>)
DoB (<MyThread(Thread-1, started 140155115792128)>)
DoB (<MyThread(Thread-1, started 140155115792128)>)
DoB (<MyThread(Thread-2, started 140155107399424)>)
DoB (<MyThread(Thread-2, started 140155107399424)>)
DoB (<MyThread(Thread-2, started 140155107399424)>)
Версия 2
Если вы уверены, что конечный пользователь будет использовать ваш модуль внутри потоков, вы можете предоставить ему / ей удобный способ сделать это. Идея состоит в том, чтобы обрабатывать темы самостоятельно Просто поместите пользовательскую функцию в поток и сохраните состояние потока в этом потоке, как описано выше. Разница в том, что вы являетесь владельцем дочернего класса Thread
и избегаете риска конфликта имен. Плюс, на мой взгляд, код становится чище:
module2.py
import threading
import random
import time
_lock = threading.Lock()
def do_something():
with _lock:
t = threading.current_thread()
t.do_something() # t must be a _UserFunctionWrapper
time.sleep(random.random()*2)
def change_behavior():
with _lock:
t = threading.current_thread()
t.change_behavior() # t must be a _UserFunctionWrapper
def wrap_in_thread(f):
return _UserFunctionWrapper(f)
class _UserFunctionWrapper(threading.Thread):
def __init__(self, user_function):
threading.Thread.__init__(self)
self._user_function = user_function
self._s = 0
def change_behavior(self):
print(f"Change behavior of: {self}")
self._s = 1
def do_something(self):
if self._s:
print(f"DoB ({self})")
else:
print(f"DoA ({self})")
def run(self):
self._user_function()
test2.py
import random
from module2 import *
def user_function():
n = random.randint(1, 10)
for i in range(n):
do_something() # won't work if the function is not wrapped
change_behavior()
for i in range(10-n):
do_something()
thread_1 = wrap_in_thread(user_function)
thread_2 = wrap_in_thread(user_function)
thread_1.start()
thread_2.start()
thread_1.join()
thread_2.join()
Выход 2
DoA (<_UserFunctionWrapper(Thread-1, started 140193896072960)>)
DoA (<_UserFunctionWrapper(Thread-2, started 140193887680256)>)
DoA (<_UserFunctionWrapper(Thread-2, started 140193887680256)>)
Change behavior of: <_UserFunctionWrapper(Thread-1, started 140193896072960)>
DoB (<_UserFunctionWrapper(Thread-1, started 140193896072960)>)
DoB (<_UserFunctionWrapper(Thread-1, started 140193896072960)>)
DoA (<_UserFunctionWrapper(Thread-2, started 140193887680256)>)
DoA (<_UserFunctionWrapper(Thread-2, started 140193887680256)>)
DoB (<_UserFunctionWrapper(Thread-1, started 140193896072960)>)
DoB (<_UserFunctionWrapper(Thread-1, started 140193896072960)>)
DoA (<_UserFunctionWrapper(Thread-2, started 140193887680256)>)
DoB (<_UserFunctionWrapper(Thread-1, started 140193896072960)>)
DoA (<_UserFunctionWrapper(Thread-2, started 140193887680256)>)
DoB (<_UserFunctionWrapper(Thread-1, started 140193896072960)>)
DoA (<_UserFunctionWrapper(Thread-2, started 140193887680256)>)
DoB (<_UserFunctionWrapper(Thread-1, started 140193896072960)>)
DoA (<_UserFunctionWrapper(Thread-2, started 140193887680256)>)
DoA (<_UserFunctionWrapper(Thread-2, started 140193887680256)>)
Change behavior of: <_UserFunctionWrapper(Thread-2, started 140193887680256)>
DoB (<_UserFunctionWrapper(Thread-2, started 140193887680256)>)
DoB (<_UserFunctionWrapper(Thread-1, started 140193896072960)>)
DoB (<_UserFunctionWrapper(Thread-1, started 140193896072960)>)
Недостатком является то, что вы должны использовать поток, даже если он вам не нужен.