Welcome, guest | Sign In | My Account | Store | Cart
1

Use of metaclasses to hide access to class constructor.

Python, 48 lines
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
"""
USAGE:
class A:
	__metaclass__ = Singleton
	def __init__(self):
		self.a=1
		
a=A()
b=A()
a is b #true

You don't have access to the constructor, 
you only can call a factory that returns always the same instance.
"""

_global_dict = {}

def Singleton(name, bases, namespace):
	class Result:pass
	Result.__name__ = name
	Result.__bases__ = bases
	Result.__dict__ = namespace
	_global_dict[Result] = Result()
	return Factory(Result)


class Factory:
	def __init__(self, key):
		self._key = key
	def __call__(self):
		return _global_dict[self._key]
		
def test():
	class A:
		__metaclass__ = Singleton
		def __init__(self):
			self.a=1	
	a=A()
	a1=A()
	print "a is a1", a is a1
	a.a=12
	a2=A()
	print "a.a == a2.a == 12", a.a == a2.a == 12
	class B:
		__metaclass__ = Singleton
	b=B()
	a=A()
	print "a is b",a==b

Metaclasses are a powerfull tool to change behaviour to classes. To use metaclasses you must provide a callable object that accepts 3 arguments: name, a tuple of base classes and a namespace. class Dummy:pass def x(*args): print args return Dummy

class A(B): a=1 __metaclass__ = x

When the code is compiled x is called and prints: ('A',(B,),{'a':1}) if you do:

A

The big problem is that a class A can't be subclassed. If you want to subclass: class _A: def a(self):return "a" class A(_A): ___metaclass__=Singleton class _B(_A): def b(self):return "b" class B(_B): __metaclass__ = Singleton

9 comments

Steve Alexander 12 years, 2 months ago  # | flag

This simplifies things and allows subclassing.

_global_dict = {}

class Singleton:
    def __init__(self, name, bases, namespace):
        ns=[item for item in namespace.iteritems()]
        ns.sort()
        key=(name,bases,tuple(ns))
        if key in _global_dict:
            self._obj = _global_dict[key]
        else:
            self._obj = _global_dict[key] = type(name,bases,namespace)

    def __call__(self):
        return self._obj
Steve Alexander 12 years, 2 months ago  # | flag

Even better: this is probably as simple as it gets...

class Singleton:
    def __init__(self, name, bases, namespace):
        self._obj=type(name,bases,namespace)

    def __call__(self):
        return self._obj
Steve Alexander 12 years, 2 months ago  # | flag

...and it doesn't work. The previous examples give the correct test output, but don't function as actual classes. Doh!

I'll have a think about this and post back once I've tested it properly.

Sorry for the noise!

Steve Alexander 12 years, 2 months ago  # | flag

This one works! Silly really... in the last example, I was returning a class rather than an object from my factory.

class Singleton:
    def __init__(self, name, bases, namespace):
        self._obj=type(name,bases,namespace)()

    def __call__(self):
        return self._obj
Ivan Vigasin 12 years, 2 months ago  # | flag

Class variables and __metaclass__.

What's wrong with the following code?
>>> class A:
...   __metaclass__ = Singleton
...   var = 1
...   def __init__(self):
...     A.var = A.var + 1
...
Traceback (most recent call last):
  File "", line 1, in ?
  File "", line 3, in __init__
  File "", line 5, in __init__
NameError: global name 'A' is not defined
>>> class A:
...   var = 1
...   def __init__(self):
...     A.var = A.var + 1
...
>>>
Steve Alexander 12 years, 1 month ago  # | flag

Here's a version that copes with your code. The problem is that the Singleton is trying to construct an instance of your class before the class has finished being fully defined. Hence, you don't get access to a bound variable for your class.

Here's a version that gets around that problem, although it adds a line or two more of code:

class Singleton(type):

    _obj = None

    def __init__(self, name, bases, namespace):
        type.__init__(self,name,bases,namespace)

    def __call__(self):
        if self._obj is None:
            self._obj = type.__call__(self)
        return self._obj
Tim Keating 10 years, 11 months ago  # | flag

One small recommended change. Use 'super' to make your metaclass safe for multiple inheritance:

class Singleton(type):

    _obj = None

    def __init__(self, name, bases, namespace):
        super(Singleton, self).__init__(name,bases,namespace)

    def __call__(self):
        if self._obj is None:
            self._obj = type.__call__(self)
        return self._obj
Michele Simionato 10 years, 9 months ago  # | flag

This is not inheritance safe. If you do

class C:
    __metaclass__=Singleton

class D(C):
    pass

c=C()
d=D()

print d

you will see that d is None.

Here is a Singleton metaclass that works:

class Singleton(type):
    def __init__(cls,name,bases,dic):
        super(Singleton,cls).__init__(name,bases,dic)
        cls.instance=None
    def __call__(cls,*args,**kw):
        if cls.instance is None:
            cls.instance=super(Singleton,cls).__call__(*args,**kw)
        return cls.instance

Michele

Philip Mountifield 1 year, 7 months ago  # | flag

There is an even easier way too, which is also inheritance safe:

class Singleton(type):
    def __call__(self, *args, **kwargs):
        if 'instance' not in self.__dict__:
            self.instance = super(Singleton, self).__call__(*args, **kwargs)
        return self.instance

Use it like normal:

class A(object):
    __metaclass__ = Singleton

class B(A):
    pass

The use of self.__dict__ is very important to make this work correctly. It ensures we only create one instance per class definition and do not see the instance of a parent class by mistake.

Add a comment

Sign in to comment

Created by Andres Tuells on Thu, 13 Dec 2001 (PSF)
Python recipes (4190)
Andres Tuells's recipes (7)

Required Modules

  • (none specified)

Other Information and Tasks