1"""Thread-local objects. 2 3(Note that this module provides a Python version of the threading.local 4 class. Depending on the version of Python you're using, there may be a 5 faster one available. You should always import the `local` class from 6 `threading`.) 7 8Thread-local objects support the management of thread-local data. 9If you have data that you want to be local to a thread, simply create 10a thread-local object and use its attributes: 11 12 >>> mydata = local() 13 >>> mydata.number = 42 14 >>> mydata.number 15 42 16 17You can also access the local-object's dictionary: 18 19 >>> mydata.__dict__ 20 {'number': 42} 21 >>> mydata.__dict__.setdefault('widgets', []) 22 [] 23 >>> mydata.widgets 24 [] 25 26What's important about thread-local objects is that their data are 27local to a thread. If we access the data in a different thread: 28 29 >>> log = [] 30 >>> def f(): 31 ... items = sorted(mydata.__dict__.items()) 32 ... log.append(items) 33 ... mydata.number = 11 34 ... log.append(mydata.number) 35 36 >>> import threading 37 >>> thread = threading.Thread(target=f) 38 >>> thread.start() 39 >>> thread.join() 40 >>> log 41 [[], 11] 42 43we get different data. Furthermore, changes made in the other thread 44don't affect data seen in this thread: 45 46 >>> mydata.number 47 42 48 49Of course, values you get from a local object, including a __dict__ 50attribute, are for whatever thread was current at the time the 51attribute was read. For that reason, you generally don't want to save 52these values across threads, as they apply only to the thread they 53came from. 54 55You can create custom local objects by subclassing the local class: 56 57 >>> class MyLocal(local): 58 ... number = 2 59 ... initialized = False 60 ... def __init__(self, **kw): 61 ... if self.initialized: 62 ... raise SystemError('__init__ called too many times') 63 ... self.initialized = True 64 ... self.__dict__.update(kw) 65 ... def squared(self): 66 ... return self.number ** 2 67 68This can be useful to support default values, methods and 69initialization. Note that if you define an __init__ method, it will be 70called each time the local object is used in a separate thread. This 71is necessary to initialize each thread's dictionary. 72 73Now if we create a local object: 74 75 >>> mydata = MyLocal(color='red') 76 77Now we have a default number: 78 79 >>> mydata.number 80 2 81 82an initial color: 83 84 >>> mydata.color 85 'red' 86 >>> del mydata.color 87 88And a method that operates on the data: 89 90 >>> mydata.squared() 91 4 92 93As before, we can access the data in a separate thread: 94 95 >>> log = [] 96 >>> thread = threading.Thread(target=f) 97 >>> thread.start() 98 >>> thread.join() 99 >>> log 100 [[('color', 'red'), ('initialized', True)], 11] 101 102without affecting this thread's data: 103 104 >>> mydata.number 105 2 106 >>> mydata.color 107 Traceback (most recent call last): 108 ... 109 AttributeError: 'MyLocal' object has no attribute 'color' 110 111Note that subclasses can define slots, but they are not thread 112local. They are shared across threads: 113 114 >>> class MyLocal(local): 115 ... __slots__ = 'number' 116 117 >>> mydata = MyLocal() 118 >>> mydata.number = 42 119 >>> mydata.color = 'red' 120 121So, the separate thread: 122 123 >>> thread = threading.Thread(target=f) 124 >>> thread.start() 125 >>> thread.join() 126 127affects what we see: 128 129 >>> mydata.number 130 11 131 132>>> del mydata 133""" 134 135from weakref import ref 136from contextlib import contextmanager 137 138__all__ = ["local"] 139 140# We need to use objects from the threading module, but the threading 141# module may also want to use our `local` class, if support for locals 142# isn't compiled in to the `thread` module. This creates potential problems 143# with circular imports. For that reason, we don't import `threading` 144# until the bottom of this file (a hack sufficient to worm around the 145# potential problems). Note that all platforms on CPython do have support 146# for locals in the `thread` module, and there is no circular import problem 147# then, so problems introduced by fiddling the order of imports here won't 148# manifest. 149 150class _localimpl: 151 """A class managing thread-local dicts""" 152 __slots__ = 'key', 'dicts', 'localargs', 'locallock', '__weakref__' 153 154 def __init__(self): 155 # The key used in the Thread objects' attribute dicts. 156 # We keep it a string for speed but make it unlikely to clash with 157 # a "real" attribute. 158 self.key = '_threading_local._localimpl.' + str(id(self)) 159 # { id(Thread) -> (ref(Thread), thread-local dict) } 160 self.dicts = {} 161 162 def get_dict(self): 163 """Return the dict for the current thread. Raises KeyError if none 164 defined.""" 165 thread = current_thread() 166 return self.dicts[id(thread)][1] 167 168 def create_dict(self): 169 """Create a new dict for the current thread, and return it.""" 170 localdict = {} 171 key = self.key 172 thread = current_thread() 173 idt = id(thread) 174 def local_deleted(_, key=key): 175 # When the localimpl is deleted, remove the thread attribute. 176 thread = wrthread() 177 if thread is not None: 178 del thread.__dict__[key] 179 def thread_deleted(_, idt=idt): 180 # When the thread is deleted, remove the local dict. 181 # Note that this is suboptimal if the thread object gets 182 # caught in a reference loop. We would like to be called 183 # as soon as the OS-level thread ends instead. 184 local = wrlocal() 185 if local is not None: 186 dct = local.dicts.pop(idt) 187 wrlocal = ref(self, local_deleted) 188 wrthread = ref(thread, thread_deleted) 189 thread.__dict__[key] = wrlocal 190 self.dicts[idt] = wrthread, localdict 191 return localdict 192 193 194@contextmanager 195def _patch(self): 196 impl = object.__getattribute__(self, '_local__impl') 197 try: 198 dct = impl.get_dict() 199 except KeyError: 200 dct = impl.create_dict() 201 args, kw = impl.localargs 202 self.__init__(*args, **kw) 203 with impl.locallock: 204 object.__setattr__(self, '__dict__', dct) 205 yield 206 207 208class local: 209 __slots__ = '_local__impl', '__dict__' 210 211 def __new__(cls, *args, **kw): 212 if (args or kw) and (cls.__init__ is object.__init__): 213 raise TypeError("Initialization arguments are not supported") 214 self = object.__new__(cls) 215 impl = _localimpl() 216 impl.localargs = (args, kw) 217 impl.locallock = RLock() 218 object.__setattr__(self, '_local__impl', impl) 219 # We need to create the thread dict in anticipation of 220 # __init__ being called, to make sure we don't call it 221 # again ourselves. 222 impl.create_dict() 223 return self 224 225 def __getattribute__(self, name): 226 with _patch(self): 227 return object.__getattribute__(self, name) 228 229 def __setattr__(self, name, value): 230 if name == '__dict__': 231 raise AttributeError( 232 "%r object attribute '__dict__' is read-only" 233 % self.__class__.__name__) 234 with _patch(self): 235 return object.__setattr__(self, name, value) 236 237 def __delattr__(self, name): 238 if name == '__dict__': 239 raise AttributeError( 240 "%r object attribute '__dict__' is read-only" 241 % self.__class__.__name__) 242 with _patch(self): 243 return object.__delattr__(self, name) 244 245 246from threading import current_thread, RLock 247