CD86
CD86

Reputation: 1089

identity of a python object

Often times I find myself in a situation in which I want to inject a sort of 'identity' into an instance of a custom class and depending on the kind, the instance should exhibit differing behaviour.

class Foo:
    def __init__(self, identity):
       self.identity = identity

    def bar(self):
       if self.identity = 'A':
           do_something
       elif self.identity = 'B':
           sth_else

So right now I am using string comparison for this task but coming from C++ and its notorious striving for type-safety this seems odd to me. What is the most pythonic way of achieving what I want?

Upvotes: 0

Views: 400

Answers (2)

Duncan
Duncan

Reputation: 95712

Use inheritance:

class Foo:
    ...

class Foo_A(Foo):
    def bar(self):
       do_something

class Foo_B(Foo):
    def bar(self):
       sth_else

Then just construct a Foo_A or Foo_B instance as appropriate.

If you want to change the 'identity' of the object during its lifecycle then identity would be the wrong name for it.

Usually if you want an object with behaviour 'A' you create a Foo_A and then later when you want an object with behaviour 'B' you throw away the original object and create Foo_B instance. Make objects immutable if at all reasonable, you will thank yourself later if you do that now.

As suggested in the comments you can mutate the type of the class to change the behaviour of overridden methods. That works in Python and for some problems is the cleanest solution, but it is rare. Usually it is better to aggregate the variable objects (but better still just to not mutate the original objects). Here's a possible solution using aggregation:

class Foo:
    def __init__(self, identity):
        self.identity = identity

    @property
    def identity(self):
        return self._processor.identity

    @identity.setter
    def identity(self, identity):
        self._processor = FooProcessor(identity, self)

    def bar(self):
        self._processor.bar()

class FooProcessor:
    def __new__(cls, identity, foo):
        for subclass in cls.__subclasses__():
            if identity == subclass.identity:
                return object.__new__(subclass)
        raise RuntimeError(f'unknown identity {identity}')

    def __init__(self, identity, foo):
        assert identity == self.identity
        self.foo = foo

class Foo_A(FooProcessor):
    identity = 'A'
    def bar(self):
        print(f'do_something with {self.foo}')

class Foo_B(FooProcessor):
    identity = 'B'
    def bar(self):
        print(f'something else with {self.foo}')


foo = Foo('A')
foo.bar()
foo.identity = 'B'
foo.bar()
assert foo.identity == 'B'

And here's a version that mutates __class__:

class Foo:
    def __init__(self, identity):
        self.identity = identity

    @property
    def identity(self):
        return self._identity

    @identity.setter
    def identity(self, identity):
        for subclass in Foo.__subclasses__():
            if identity == subclass._identity:
                self.__class__ = subclass
                return
        raise RuntimeError(f'unknown identity {identity}')

class Foo_A(Foo):
    _identity = 'A'
    def bar(self):
        print(f'do_something with {self} {self.identity}')

class Foo_B(Foo):
    _identity = 'B'
    def bar(self):
        print(f'something else with {self} {self.identity}')


foo = Foo('A')
foo.bar()
foo.identity = 'B'
foo.bar()
assert foo.identity == 'B'

Output is something like:

do_something with <__main__.Foo_A object at 0x10c022af0> A
something else with <__main__.Foo_B object at 0x10c022af0> B

Upvotes: 1

AKX
AKX

Reputation: 169268

With the

I cant, because I need to dynamically switch between those at runtime

going against inheritance, maybe you could split that dynamic behavior into behavior classes such as

class BaseFooBehavior:
    @classmethod
    def bar(cls, foo):
        raise NotImplementedError('...')


class FooBehavior1(BaseFooBehavior):
    @classmethod
    def bar(cls, foo):
        return foo.a * foo.b


class FooBehavior2(BaseFooBehavior):
    @classmethod
    def bar(cls, foo):
        return str(foo.a) * foo.b


class Foo:
    behavior_cls = FooBehavior1  # default behavior
    def __init__(self, a, b):
        self.a = a
        self.b = b

    def bar(self):
        return self.behavior_cls.bar(self)


foo = Foo(a=42, b=12)
print(foo.bar())
foo.behavior_cls = FooBehavior2  # change the instance's behavior
print(foo.bar())

printing out

504
424242424242424242424242

Upvotes: 0

Related Questions