Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How restrict creation of objects of one class to instances of another in Python?

I have encountered a conundrum. I think I should know how to solve this — I am extremely knowledgeable and experienced in Python — but I can't figure it out. It seems like what I am struggling with should be addressed by a design pattern that is a variation on the factory theme, but I can't recall one. (At the end of this writeup I suggest a technical solution, but it seems an implausible one.) I hope you find this discussion interesting in its own right, but I look forward to hearing some suggestions for solutions to my problem.

Given classes, A, B, C, and D, I want to limit creation of B instances to methods of A, C instances to methods of B, and D instances to methods of C — in other words A instances are the factories for B instances, B instances the factories for C instances, and C instances the factories for D instances. (I also want each instance of D to store which instance of C created it, each instance of C to store which instance of B created it, and each instance of B to store which instance of A created it, but this is easily arranged by having each instance provide itself as an __init__ argument when it creates an instance of the next class down in the hierarchy.)

These are model-layer classes that an application manipulates. There is no problem with the application manipulating instances of B, C, or D, it just shouldn't create them directly — all it should be able to create directly are instances of A (and A might even be a singleton). There are various kinds of validation, management, tracking, auditing, etc. that could be implemented in the methods of one class that create instances of the next.

An example might be a Operating System that can create Filesystems that can create Directories that can create Files, but application code would have to follow that chain. For example, even if it had its hands on a Directory, it shouldn't be able to create a File giving File.__init__ the Directory instance even though that is what instances of Directory would do when asked to create a File.

I am looking for a design solution backed up some implementation, not something user-proof -- I understand the futility of the latter.

The only thing I have thought of so far is:

  1. begin all class name except A with an underscore
  2. access _B() only from A instances, _C() only from B instances, and _D() only from _C instances
  3. rely on application-layer programmers to respect this arrangement and directly create instances only of the (possibly singleton) class A

Module-level "hiding" by omitting the class from the module's __all__ list is insufficient, because the only affects import * constructions — another module could still reach the class by import module then referencing module.class.

(This is all vaguely reminiscent of the C++ problems that require two classes to be friends because they participate in a two-way relationship: instances of each class must be able to reference the methods of the other class that manage that the other side of the relationship.)

The solution that may best conform to the semantics and pragmatics of Python is to define D inside C, C defined inside B, and B defined inside A. This seems an awfully ugly way to cram multiple modules worth of code into a single very large class. (And imagine if the chain went down a few more levels.) This is an extension of more common uses of nested classes and maybe the only technically sound way of doing this, but I have never seen this sort of Russian-doll class structure.

Ideas?

like image 555
Mitchell Model Avatar asked Aug 15 '16 05:08

Mitchell Model


Video Answer


2 Answers

Python is not very good at completely hiding it's objects ... Even if you decide to do the "Russian doll" closure to attempt to hide a class definition, a user can still get at it...

class A(object):
    def create_b(self):
        class B(object):
            pass
        return B()

a = A()
b = a.create_b()
b_cls = type(b)
another_b = b_cls()

Simply put -- If a user has access to the source code (and they can get it via inspect or at the very least, they can get the bytecode via dis which to a trained eye is good enough), then they have the power to create instances of your class themselves if they're motivated enough.

Generally though, it's good enough to just document the class as being something that the user shouldn't instantiate themselves -- If they break that trust, it's their fault when their program crashes and burns (and has some nasty side-effect that causes their computer to catch fire after clearing their hard-drive). This is a pretty central python philosophy, stated nicely in the python mail archives:

Nothing is really private in python. No class or class instance can keep you away from all what's inside (this makes introspection possible and powerful). Python trusts you. It says "hey, if you want to go poking around in dark places, I'm gonna trust that you've got a good reason and you're not making trouble."

After all, we're all consenting adults here.

C++ and Java don't have this philosophy (not to the same extent). They allow you create private methods and static members.

Perl culture is like python in this respect, but Perl expresses the sentiment a bit differently. As the Camel book puts it,

"a Perl module would prefer that you stayed out of its living room because you weren't invited, not because it has a shotgun."

But the sentiment is identical.

like image 170
mgilson Avatar answered Oct 26 '22 23:10

mgilson


Just from the top of my head:

def B(object):
    pass

def D(object):
    pass

def bound(object):
    if type(object) is C:
        assert isinstance(D)
    if type(object) is A:
        assert isinstance(B)
    else:
        assert false

@bound
def C(D):
    pass

@bound
def A(B):
    pass
like image 39
noumenal Avatar answered Oct 26 '22 23:10

noumenal