How can I rewrite this for greater maintainability without sacrificing flexibility?
You don't. The flexibility is precisely what causes the problem. If any code anywhere may change what attributes an object has, maintainability is already in pieces. Ideally, every class has a set of attributes that's set in stone after __init__
and the same for every instance. Not always possible or sensible, but it should the case whenever you don't have really good reasons for avoiding it.
one idea is to pre-initialize the class with all the attributes that one expects to encounter
That's not a good idea. Sure, then the attribute is there, but may have a bogus value, or even a valid one that covers up for code not assigning the value (or a misspelled one). AttributeError
is scary, but getting wrong results is worse. Default values in general are fine, but to choose a sensible default (and decide what is required) you need to know what the object is used for.
What if I don't know what my attributes are a priori?
Then you're screwed in any case and should use a dict or list instead of hardcoding attribute names. But I take it you meant "... at the time I write the container class". Then the answer is: "You can edit files in lockstep, duh." Need a new attribute? Add a frigging attribute to the container class. There's more code using that class and it doesn't need that attribute? Consider splitting things up in two separate classes (use mixins to stay DRY), so make it optional if it makes sense.
If you're afraid of writing repetive container classes: Apply metaprogramming judiciously, or use collections.namedtuple
if you don't need to mutate the members after creation (your FP buddies would be pleased).
You should look at all of the common object creation patterns and make an assessment based upon the state of your logic, time you have to implement the change, and so forth. Also, rather than have the participating objects know how to construct each other, you should consider Inversion of Control.
Its hard to be specific without more info, but given that you are talking about a complex graph I would think that Abstract Factory or Builder would be more useful here than just a simple Factory depending on how different each of your scenarios is.
If you have some initial decisions to make that influence creation of the graph but the differences are nuances, look at Builder...particularly if there are some structural differences between the "products" (such as, some participating objects are optional or in some scenarios participating objects drag in another set of collaborators).
If the differences between the scenarios are more distinct / elaborate and take the form of "families" of objects that should be used together, look at Abstract Factory with a few different factory implementations.
If you actually just have a few specific flavors of this graph and want to pick and choose between them dynamically, you might be able to just go with Prototype; treat your current implementation as a prototype, code up your alternate implementation(s) as another prototype, and just clone the appropriate prototype as needed.
Best Answer
As others already mentioned, preventing this would require a special case for
__init__
, and one of the design principles of Python is to avoid special cases, see the Zen of Python:This design is also in line with other design decisions, e.g, the lack of static type checking or access control modifiers. Python tends to empower its users, not to enforce things.
You can easily prevent this for any particular class by using
__slots__
. However, there is no way to change this globally.While this can in theory cause errors, I wouldn't worry about it much. For example, a typo could mean that you are by accident create a new attribute when you meant to assign to an existing attribute. However, good unit testing coverage will usually catch this.