1

I am trying to create a client class that is able to be instantiated with connection information and other attributes about how the client is connecting and interacting with a service. The client class would have inner classes that represent objects in the service. These objects could be instantiated in a couple of different ways:

  1. The outer, client class has a factory method that creates the inner class, like client.make_someobject() - this would pass an instance of itself to the new object, so the object would know about and can use the connection information without the caller explicitly passing the connection in.
  2. An existing object in the service can be pulled in by writing client.SomeObject(some_id)

My question is mostly related to the second scenario. When creating an instance of an inner class directly, without a factory method that can just pass in self, how could I ensure that the new instance of the inner class knows about the attributes of the outer, client class?

Illustrative example:

class Client():
    def __init__(self, client_attr):
        self.client_attr = client_attr

    def make_serviceobject(self):
        return ServiceObject._make_serviceobject(self)

    class ServiceObject():
        def __init__(self, id,client=None):
            self.id = id
            if client:
                self.client = client
            # ...

        @classmethod
        def _make_serviceobject(cls, client):
            id = 'some_id'
            return cls(id, client=client)

my_client = Client(some_attr)
# now, how can this new ServiceObject know about the my_client attributes and methods?
my_existing_resource = my_client.ServiceObject(some_id) 
# I am trying to avoid this: 
my_existing_resource = my_client.ServiceObject(some_id, client=my_client) 

natebay
  • 53
  • 5
  • Did you mean to put a `@classmethod` in front of: `def _make_serviceobject(cls, client):`? – quamrana Aug 05 '21 at 15:03
  • Nested classes do not allow this behavior: https://stackoverflow.com/questions/2024566/how-to-access-outer-class-from-an-inner-class – 7evy Aug 05 '21 at 15:04
  • Aside from decorating your classmethod explicitly as a `@classmethod`, I don't see any issue with the code you posted. In the future, if something goes wrong with code that you wrote, you should a) attempt to diagnose the problem yourself; b) clearly explain what happened when you tried running the code (including complete error tracebacks, formatted as code), and exactly how that is different from what you wanted to happen. – Karl Knechtel Aug 05 '21 at 15:05
  • @7evy that question is about doing it *implicitly*; this code is trying to make an *explicit* work-around for that, using ordinary composition and delegation. – Karl Knechtel Aug 05 '21 at 15:05
  • Oh, wait, I think I see what you mean now. Hold on a moment. – Karl Knechtel Aug 05 '21 at 15:07
  • The main purpose of having inner classes IMO is to encapsulate data so you have fewer interactions and smaller interfaces to reason about. If the inner class can see everything in the outer class, and the outer class can see everything in the inner class, and one gets implicitly created by the other, why not just have a single class? – Samwise Aug 05 '21 at 15:08
  • Python's classes are not as flexible as, say, Java ones. What you're trying to do can be achieved with inheritance, but like Samwise said, this isn't the purpose of nested classes. – 7evy Aug 05 '21 at 15:11
  • @Samwise exactly in order to have smaller interfaces. Client code that gets an inner class instance via the factory method only has the inner class' interface exposed, even if it is granted access to an associated outer class instance's data in order to implement that interface. This could also be implemented by having two *separate* classes that relate to each other by composition. However, nesting the definitions can be a useful bit of namespacing. As the Zen tells us, namespaces are a honking good idea. – Karl Knechtel Aug 05 '21 at 15:25
  • @quamrana yep that was a typo, I made the edit – natebay Aug 05 '21 at 15:40
  • My understanding of Python is that you can do anything as long as you are crazy enough. I might be going into crazy territory here - I already explored metaclasses :( but decided that complexity may not be worth the effort. Ultimately I am trying to create a package that can be imported and a user of the package can very cleanly and clearly implement it for the purpose of interacting with a service. I took inspiration from AWS's boto3 where resource clients can be made - see this good context https://boto3.amazonaws.com/v1/documentation/api/latest/guide/resources.html – natebay Aug 05 '21 at 15:52

1 Answers1

0

You're pretty close, but there are a few issues here:

  1. Class methods (where you expect a class instance to be passed as the first parameter, conventionally named cls, rather than an object instance conventionally named self) need to be decorated with @classmethod. However, there isn't really a reason to set up a hidden class method within the inner class; since it's logic that only gets used by the outer class, and is accessible (via the Client.make_serviceobject interface) from the outer class, it logically belongs in the outer class. (There also isn't a reason to use classmethod anyway - as opposed to staticmethod - because there is no hope of this working polymorphically - the inner class is specific to this outer class.)

  2. my_client.ServiceObject (that is, the class name of the inner class, looked up via an outer class instance) just gets you that class itself, rather than anything associated with or bound to the outer class instance.

  3. It makes no sense to offer a default None for the inner class' client (i.e., containing instance), because a) there will never be a None value (we only intend to create the instance from the outer class) and b) the outer class instance is presumably necessary for the inner class' functionality (otherwise, why do any of this setup work at all rather than just having two separate classes?)

  4. You apparently want to supply the ID from the calling code, so the internal code can't just make one up.

To fix these problems, we simply:

  1. Make the outer class' interface do the work needed to create an instance of the inner class, and have it accept a parameter for the ID.

  2. Have it do so directly, via the constructor.

  3. Have calling code use that interface.

I would also mark the inner class' name to indicate that it isn't intended to be dealt with directly.

It looks like:

class Client():
    def __init__(self, client_attr):
        self.client_attr = client_attr

    def make_serviceobject(self, id):
        # any logic necessary to compute the ID goes here.
        return _ServiceObject(id, self)

    class _ServiceObject():
        def __init__(self, id, client):
            self.id = id
            self.client = client
            # ...

my_client = Client(some_attr)
my_existing_resource = my_client.make_serviceobject('some_id') 
# assert my_existing_resource.client == my_client
Karl Knechtel
  • 62,466
  • 11
  • 102
  • 153
  • Perhaps I am being fussy and maybe idyllic, but the point of being able to do `my_client.ServiceObject('id')` instead of `my_client.some_factory_method()` is to make it clear to the caller that they are creating an instance of a class. Also, by calling a _ method under ServiceObject, all the logic for creating the ServiceObject and about that object is organized in the most relatable namespace. I don't want Client to own methods that make ServiceObject related calls. While your solution works fine, it is not quite ideal (which is also because the library I'm writing won't be used by just me) – natebay Aug 05 '21 at 15:46
  • Nothing prevents you from naming the factory method in a way that makes it look like a class name. – Karl Knechtel Jul 01 '22 at 13:07