TL;DR
My answer is that you should prefer option 1 for it's simplicity and better OOP design, and avoid premature optimization.
The Rest
I think the efficiency question here is dwarfed by how difficult it will be in the future to maintain your second option. If one object needs to use attributes of another object (your example code uses a form of composition), then it should have those objects as attributes, rather than creating extra references directly to the object attributes it needs. Your first option is the way to go. The first option supports encapsulation, option 2 very clearly violates it. (Granted, encapsulation isn't as strongly enforced in Python as some langauages, like Java, but it's still a good principle to follow).
The only efficiency-related reason you should prefer number two is if you find your code is slow, you profile, and your profiling shows that these extra lookups are indeed your bottleneck. Then you could consider sacrificing things like ease of maintenance for the speed you need. It is possible that the extra layer of references (foo = self.classa.bar()
vs. foo = self.bar()
) could slow things down if you're using them in tight loops, but it's not likely.
In fact, I would go one step further and say you should modify your code so that OtherClass
actually instantiates the object it needs, rather than having them passed in. With Option 1, if I want to use OtherClass
, I have to do this:
classa = ClassA(class_a_init_args)
classb = ClassC(class_b_init_args)
classc = ClassC(class_c_init_args)
otherclass_obj = OtherClass(classa_obj, classb_obj, classc_obj)
That's too much setup required just to instantiate OtherClass
. Instead, change OtherClass
to this:
def OtherClass(object):
def __init__(self, classa_init_args, classb_init_args, classc_init_args):
self.classa = ClassA(class_a_init_args)
self.classb = ClassC(class_b_init_args)
self.classc = ClassC(class_c_init_args)
Now instantiating an OtherClass
object is simply this:
otherclass_obj = OtherClass(classa_init_args, classb_init_args, classc_init_args)
If possible, another option may be possible to reconfigure your class so that you don't even have to instantiate the other classes! Have a look at Class Attributes and the classmethod decorator. That allows you to do things like this:
class foo(object):
bar = 2
@classmethod
def frobble(self):
return "I didn't even have to be instantiated!"
print(foo.bar)
print(foo.frobble())
This code prints this:
2
I didn't even have to be instantiated!
If your OtherClass
uses attributes or methods of classa
, classb
, and classc
that don't need to be tied to an instance of those classes, consider using them directly via class methods and attributes instead of instantiating the objects. That would actually save you the most memory by avoiding the creation of entire objects.