Reputation: 105
I understood that classes are instances of metaclasses and that __new__
is running before __init__
, because, you must create an instance before initializing it.
Imagine now the following :
import time
class ConfigurationsMeta(type):
def __new__(cls, name, bases, attr):
# Potentially a long task here (eg: Getting value from a web service)
time.sleep(2)
# Which class inherit from me (debug)
print(f'Class {name}')
config = super().__new__(cls, name, bases, attr)
#Set a variable to be propagated (Variable coming from web service)
setattr(config, "URL", "https://stackoverflow.com/")
return config
class Foo(metaclass=ConfigurationsMeta):
def __init__(self):
print(f'{__class__.__name__} : {self.URL}')
class Bar(Foo):
def __init__(self):
print(f'{__class__.__name__} : {self.URL}')
class Baz(Bar):
def __init__(self):
print(f'{__class__.__name__} : {self.URL}')
e = Foo()
s = Bar()
c = Baz()
Nice because URL is well propagated as I do have
Foo : https://stackoverflow.com/
Bar : https://stackoverflow.com/
Baz : https://stackoverflow.com/
I do have now something I don't understand very well :
Class Foo
is written after 2 sec
Class Bar
is written after another 2 sec
Class Baz
is written finally after another 2 sec
So metaclass is executed three times.
This must explain that as __new__
is responsible for creating classes, it must be run every time, so three times.
Am I right?
How can I avoid it and make it run only once?
Upvotes: 1
Views: 358
Reputation: 110271
The other answers cover why you don't need a metaclass here.
This answer is to explain briefly what metaclass' __new__
does:
it does build your class - it is called by the Python runtime itself when it processes a class <name>(bases, ...): <body>
statement. No, you can't have a class without calling
a metaclass __new__
method - or at least "the root of all metaclasses", type
's __new__
method.
That said, if for some reason you'd need to have a long task that could be run just once for all classes you create, all you'd have to do would be cache the results of the long task, and used the cached values in subsequent calls.
If for some reason the value can not be cached, and must be performed in the metaclass, you'd have to arrange for your classes bodies themselves to be executed in different treads or using an asyncio loop. More elegant forms might include instantiating a concurrent.futures.ThreadPoolExecutor inside the metaclass __new__
when it is called for the first time (and hold it alive as a metaclass attribute), and after calling type.__new__
for each class submit the part that takes long as a future.
As you can see, since what you need is just set a class attribute, you should avoid doing this as a metaclass.
Still, the design could be like:
from concurrent.futures import ThreadPoolExecutor
TIMEOUT = 5
class PostInitParallelMeta(type):
executor = None
def __init__(cls, name, bases, ns, **kw):
super().__init__(name, bases, ns, **kw)
mcls = cls.__class__
if not mcls.executor:
mcls.executor = ThreadPoolExecutor(20)
mcls.executor.submit(cls._post_init)
class Base(metaclass=PostInitParallelMeta):
_initalized = False
def _post_init(cls, url):
# do long calculation and server access here
result = ...
cls.url = result
cls._initialized = True
def __init__(self, *args, **kw):
super.__init__(*args, **kw)
counter = 0
while not self.__class__._initialized:
time.sleep(0.2)
counter += 0.2
if counter > TIMEOUT:
raise RunTimeError(f"failed to initialize {self.__class__}")
class Foo(Base):
# set any parameters needed to the initilization task
# as class attributes
...
PS - I just wrote this and realized the code in the metaclass can safely be put in Base's __init_subclass__
method - no need for a metaclass at all even for this.
Upvotes: 0
Reputation: 11590
As mentioned already, you do not need metaclasses.
And you have got an excellent and flexible answer already, which uses __init_subclass__
.
A flat simple approach is to set the shared attribute in the superclass and let the instances of the subclasses find it (as they normally do up along the chain of instance, class, superclasses)
class Configurations:
URL = 'https://stackoverflow.com/'
class Foo(Configurations):
def __init__(self):
print(f'{__class__.__name__} : {self.URL}')
class Bar(Foo):
def __init__(self):
print(f'{__class__.__name__} : {self.URL}')
class Baz(Bar):
def __init__(self):
print(f'{__class__.__name__} : {self.URL}')
e = Foo()
s = Bar()
c = Baz()
Or, if the configuration is more complicated, use a class method to keep the code tidy
class Configurations:
@classmethod
def create_cfg(cls):
cls.URL = 'https://stackoverflow.com/'
...
Configurations.create_cfg()
e = Foo()
s = Bar()
c = Baz()
Both ways initialise the configuration once only and produce
Foo : https://stackoverflow.com/
Bar : https://stackoverflow.com/
Baz : https://stackoverflow.com/
Upvotes: 0
Reputation: 531055
You don't really need a metaclass here. Assuming you want URL
to be a class attribute, not an instance attribute, you just need to define a base class with a suitable __init_subclass__
definition. The URL should be retrieved first and passed as an argument to __init_subclass__
(via a keyword argument in the class
statement).
class Base:
def __init_subclass__(cls, /, url=None):
super().__init_subclass__(cls)
if url is not None:
cls.URL = url
some_url = call_to_webservice()
class Foo(Base, url=some_url):
pass
class Bar(Foo):
pass
class Baz(Bar):
pass
If URL
should be an instance attribute, then replace __init_subclass__
with __init__
:
some_url = call_to_webservice()
class Base:
def __init__(self, /, url):
self.url = url
class Foo(Base):
pass
f = Foo(some_url)
Upvotes: 1