10

I want to create a class in python, which should work like this:

  1. Data assigned, maybe bound to a variable (eg a = exampleclass(data) or just exampleclass(data))

  2. Upon being inserted data, it should automatically determine some properties of the data, and if some certain properties are fullfilled, it will automatically...

  3. ... change class to another class

The part 3 is the part that i have problem with. How do i really change the class inside of the class? for example:

If I have two classes, one is Small_Numbers, and the other is Big_numbers; now I want any small_number smaller than 1000 to be transferred into a Big_number and vice versa, testcode:

a = Small_number(50)
type(a) # should return Small_number.
b = Small_number(234234)
type(b) # should return Big_number.
c = Big_number(2)
type(c) # should return Small_number.

Is this possible to do?

Martin Geisler
  • 72,968
  • 25
  • 171
  • 229
user1187139
  • 385
  • 2
  • 3
  • 7

3 Answers3

15

Why not using a factory method? This one will decide which class to instanciate depending on the passed data. Using your example:

def create_number(number):
    if number < 1000:
        return SmallNumber(number)
    return BigNumber(number)
juliomalegria
  • 24,229
  • 14
  • 73
  • 89
11

Don't. Use a factory function instead.

def create_number(source):
    if source < 1000:
       return Small_number(source)
    else:
       return Big_number(source)

a = create_number(50)
b = create_number(234234)
c = create_number(2)
Cat Plus Plus
  • 125,936
  • 27
  • 200
  • 224
11

Using a factory method is the usual way to solve this, especially since instantiating a class is indistinguishable from calling a function in Python.

However, if you really want, you can assign to self.__class__:

THRESHOLD = 1000

class Small(object):
    def __init__(self, n):
        if n < THRESHOLD:
            self.n = n
        else:
            self.__class__ = Big
            self.__init__(n)

class Big(object):
    def __init__(self, n):
        if n < THRESHOLD:
            self.__class__ = Small
            self.__init__(n)
        else:
            self.n = n

This works as expected:

>>> a = Small(100)
>>> type(a)
<class 'Small'>
>>> b = Small(1234)
>>> type(b)
<class 'Big'>
>>> c = Big(2)
>>> type(c)
<class 'Small'>

If assigning to self.__class__ seems too strange, then you can override __new__ instead. This method is called before __init__ is called and it can be used to pick the class to instantiate:

THRESHOLD = 1000

class Switcher(object):
    def __new__(cls, n):
        if n < THRESHOLD:
            new_cls = Small
        else:
            new_cls = Big
        instance = super(Switcher, new_cls).__new__(new_cls, n)
        if new_cls != cls:
            instance.__init__(n)
        return instance

class Small(Switcher):
    def __init__(self, n):
        self.n = n

class Big(Switcher):
    def __init__(self, n):
        self.n = n
Martin Geisler
  • 72,968
  • 25
  • 171
  • 229
  • 1
    This approach is very cool, but it doesn't work 100% of the time: you can get "TypeError: \_\_class\_\_ assignment: 'X' object layout differs from 'Y'" from time to time, if your class bases aren't carefully chosen. – Roman Susi Feb 04 '12 at 20:10
  • @RomanSusi: good point! I can see [in the Python source](http://hg.python.org/cpython/file/443772a82dcd/Objects/typeobject.c#l3146) that you will get this error if the classes use different `__slots__`. This is also mentioned at the [bottom of this section](http://docs.python.org/reference/datamodel.html#slots) of the manual. – Martin Geisler Feb 04 '12 at 21:06
  • I think I got many good answers to this question, but I loved this one! especially the first half. I think it's wonderful solution and works just like I want it to. Btw this works without (object) for python 3 – user1187139 Feb 06 '12 at 15:29
  • 2
    I highly recommend *not* to change the object's type (i.e., not to assign to `__class__`). See [this comment from the docs](http://docs.python.org/reference/datamodel.html#id5), and consider that special methods may work unpredictably after you do that. Even if you think you know exactly what will happen, you're ok with that, you may get screwed with the next version of Python - after all, there's no clear promise from Python how exactly it will (or will not) work. – max Oct 25 '12 at 06:53
  • 1
    @max: Yeah, the solution involving `__class__` assignment is also my least favorite. – Martin Geisler Oct 25 '12 at 07:18
  • 2
    The solution in which you override `__new__` needs to either check that new_cls is not a subclass of cls (`if not issubclass(client_cls, cls)`), or else do something other than calling `__init__` as this will cause the `__init__` for the subclass to be called twice. You can test this by adding a print statement into the `__init__` for each of the subclasses. – Silfheed Mar 28 '13 at 21:41