Programmer's Python - Inside Class |
Written by Mike James | |||
Tuesday, 29 April 2025 | |||
Page 2 of 2
SingletonSingletons are classes that you can only instantiate once, i.e. there can only be one instance. You can argue about whether or not singletons are a good design pattern, but they do make a good example of how to use __new__. To create a singleton class all we have to do is keep track in __new__ of whether or not an instance already exists. Consider, for example: class MyClass: __instance=None def __new__(cls): if(cls.__instance==None): cls.__instance=object.__new__(cls) return cls.__instance The logic is very simple, use a private class attribute to store the instance when the class is called. If the class is called a second time then no new instance is created and the original is returned. You can check this by adding an initializer: def __init__(self,value): self.MyAttribute = value print("__init__ called")
Now when you try: myObj1=MyClass(42) print(myObj1.MyAttribute) #42 myObj2=MyClass(43) print(myObj1.MyAttribute) #43 You can see that there is only one object and myObj1 and myObj2 reference it as myObj1.MyAttribute is changed to 43 after myObj2 attempts to create a new instance. However, notice that this also reveals a fairly obvious problem. Every time the class is called __new__ returns an instance of MyClass, even if it is a preexisting instance, and so, by the standard rules, __init__ is called even though the instance might have already been initialized. If this is what you want then no problem, but if you don’t want __init__ to change the instance’s attributes you either have to throw an exception or return something that isn’t an instance of MyClass. For example: def __new__(cls,value): if(cls.__instance is not None): return None cls.__instance=object.__new__(cls) return cls.__instance Now if you try it you will discover that you don’t get an instance the second time the class is called and __init__ isn’t called to change the value of MyAttribute. You can generalize the singleton to a class that will only allow you to create a set number of objects quite easily. There are many ways of creating a singleton and using __new__ probably isn’t the best way of doing the job, arguably metaclasses, introduced in the next chapter are, but it is a simple example of how customizing instance creation can be used. Customizing Immutable Data ObjectsThe documentation states that one of the main uses of __new__ is to allow the customization of immutable types. To demonstrate this the next example creates a sorted tuple. The idea for the example is that you call the class with a set of positional arguments which are converted into a tuple with all of the values sorted into order. You can’t do this in the __init__ method because once a tuple instance is created you can’t change it, it’s immutable, and __init__ can’t return a new tuple because it doesn’t return anything. You have to use __new__ to create the instance that you want: class MySortedTuple(): def __new__(cls, *args): T = tuple(sorted(args)) return T This works as: myTuple = MySortedTuple(6,3,4) print(myTuple) prints 3,4,6 There is a small problem. If you add an __init__ you will find it isn’t called. The reason is that T is a tuple, not a MySortedTuple. One direct attempt at a solution is to use the tuple’s __new__ to create an instance that is a MySortedTuple i.e.: T = tuple.__new__(cls, T) If you are wondering why this might work it is because the tuple constructor accepts an iterable as a parameter. That is: tuple((1,2,3)) returns a tuple with elements 1,2,3 in that order. This means when you call: tuple.__new__(MySortedTuple,T) what you get is an instance of a tuple with the same elements as T but with its __class__ set to MySortedTuple. Despite this being good reasoning, if you try this you will find it doesn’t work because MySortedTuple isn’t a subtype of tuple. The correct solution is to make it a subtype of tuple by inheriting tuple, see Chapter 12. Now the new class works including calling __init__: class MySortedTuple(tuple): def __new__(cls, *args): T=tuple(sorted(args)) T = tuple.__new__(cls, T) return T def __init__(self,*args): print("init called") A more direct version of the new class that doesn’t use an intermediate tuple is: class MySortedTuple(tuple): def __new__(cls, *args): return tuple.__new__(cls, sorted(args)) def __init__(self,*args): print("init called")
Inheriting An InstanceYou may notice that the argument in the last section about calling the tuple implementation of __new__ to get an instance is a general idea. If you are creating a new type based on an existing type then you need an instance of the existing type but with its __class__ changed so that you can build extra features on it. That is when you create a subclass with a custom __new__ you need to call the superclass version of __new__ to get a suitable instance. The usual way of calling __new__ on the superclass is to use the super function which is described in detail in Chapter 12. In most simple situations that the superclass of MyClass is object and so our previous examples fit this pattern by default. How Instances Become FunctionsThe final question we have to answer in the use of a class is how is it that we can call a class as if it was a function? The answer is that there is a magic __call__ method and any object which has it defined can be called like a function – it is a callable. This is the basic idea, but there is a subtle extra condition. The __call__ has to be defined on the object’s class object. The Python documentation says that magic methods aren’t guaranteed to work if they are defined on the object rather than its class. This is the case for __call__. If you want an object to be callable you have to define __call__ on its class. For example: class MyClass: def __call__(self,*args, **kwargs): print("class call") If you now create an instance it can be called like a function: myObject=MyClass() myObject() and you will see class call printed. Being able to create an instance that is a function is very useful, but notice that as it is the class __call__ method that is actually called, all of the instances of a class are restricted to use the same function. This is usually a reasonable way to organize things. In general adding methods of any kind to an instance is not best practice. To modify the way an instance works the best option is to create a subclass, i.e. use inheritance and override the methods you want to change, see Chapter 11. See the next chapter to discover more on how class objects become callable. Summary
Programmer's Python
|
|||
Last Updated ( Tuesday, 29 April 2025 ) |