Search code examples
python-3.xpython-2.7scopethread-safetypython-multithreading

Python class scope and threading


I am a little confused about python class scope. Also about its relation to threading. Below is a minimal working example. I created an instance of B and passed it to the instance of A.

  1. The way I understand it A should be making its own local copy of B_instance. Apparently this is not happening since every time I modify an attribute of B_instance by any imaginable means I see the change in both A_instance and B_instance (prints 1 -6). Does this mean that A_instance.other_class is regarded as global? Is there a way to make A_instance.other_class local? so that modifying it wouldn´t change B_instance itself.

  2. The second part of the question is related to threading. I know that access to class attributes is not thread-safe so I am using locks. However if you take a look at the print statements I would expect "lock released" to be printed before "modified by main thread". What am I missing? Both locks seem to be locking something else because apparently the lock in the main thread can be acquired even though the one in A_instance is still in effect. I feel like this contradicts my findings from the first part.

  3. Is it possible to acquire a lock for the whole object, not just its attribute or method?


class A:

    def __init__(self, other_class):
        self.lock = threading.Lock()
        self.other_class = other_class

    def increment_B(self):
        self.other_class.B_attr_1 += 1

    def set_B(self):
        self.other_class.B_attr_1 = 10

    def thread_modify_attr(self):
        self.lock.acquire()
        self.other_class.B_attr_1 = "modified by class"
        time.sleep(10)
        self.lock.release()
        print("lock released")


class B:

    def __init__(self):
        self.B_attr_1 = 0


if __name__ == "__main__":

    B_instance = B()

    A_instance = A(B_instance)

    print("1:", B_instance.B_attr_1)

    A_instance.other_class.B_attr_1 += 1
    print("2:", B_instance.B_attr_1)

    A_instance.other_class.B_attr_1 = 10
    print("3:", B_instance.B_attr_1)

    A_instance.increment_B()
    print("4:", B_instance.B_attr_1)

    A_instance.set_B()
    print("5:", B_instance.B_attr_1)

    B_instance.B_attr_1 = 0
    print("6:", A_instance.other_class.B_attr_1)

    lock = threading.Lock()

    t = threading.Thread(target=A_instance.thread_modify_attr)
    t.start()
    print("thread started")

    print(B_instance.B_attr_1)

    lock.acquire()
    B_instance.B_attr_1 = "modified by main thread"
    lock.release()

    print(B_instance.B_attr_1)
    print("done")

    t.join()

Results:

1: 0
2: 1
3: 10
4: 11
5: 10
6: 0                                                                                                             
thread started                                                                                                      
modified by class                                                                                                          
modified by main thread
done
lock released

If somebody knows about some good place to read about details of python scoping I would appreciate it.


Solution

    1. No. When you create an instance of B and pass that to the A constructor, a reference to the same B instance is passed. There is no copy made. If you want that, you would have to make a copy of it yourself. One way is with copy.deepcopy (although note that there are certain types that cannot be copied -- a threading.Lock instance being one of them). Essentially the new reference (that you store in A's self.other_class) is another name for the same instance.

    2. The reason your two blocks of code are able to execute at the same time is that you have created two different locks. You create one in the class A constructor -- that one is being locked/unlocked by thread_modify_attr. The other one is being created in the main thread code just before the sub-thread is created and is being locked and unlocked by the main thread. If you want the two to utilize the same lock, pass (a reference to) the lock into the thread function as an argument. Because they are not the same lock, there is nothing preventing both threads from executing concurrently.

    3. Locks can protect whatever you want them to protect. It's the responsibility of your code to determine what is protected by the lock. That being said, you could create a lock in the constructor of your class. Then whenever anything references the class instance, you could use the lock associated with the instance to synchronize access. A common pattern is to centralize control of object resources within the class. So that an outside agent simply asks the object to do something (by calling one of its methods). The object then uses the lock internally to protect its data structures.

    Python scoping rules are described here