【问题标题】:Avoid __new__ to be called every time避免每次都调用 __new__
【发布时间】:2023-03-22 06:53:01
【问题描述】:

我知道类是元类的实例,__new____init__ 之前运行,因为您必须在初始化之前创建一个实例。

现在想象一下:

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()
  1. 很好,因为 URL 像我一样传播得很好

    Foo : https://stackoverflow.com/
    Bar : https://stackoverflow.com/
    Baz : https://stackoverflow.com/
    
  2. 我现在确实有一些我不太了解的东西:

    Class Foo 在 2 秒后写入

    Class Bar 在 2 秒后写入

    Class Baz 在 2 秒后终于写入

所以元类被执行了 3 次。 这必须说明,由于__new__负责创建类,所以每次都要运行,所以3次。

我说的对吗?

我怎样才能避免它并让它只运行一次?

【问题讨论】:

  • __new__ 不应从 Web 服务中获取值...
  • 我说的对吗? - 是的。我怎样才能避免它并让它只运行一次? - 只有一个类的元类是ConfigurationsMeta(或使用缓存)

标签: python metaclass


【解决方案1】:

这里你真的不需要元类。假设您希望 URL 成为 class 属性,而不是实例属性,您只需要使用合适的 __init_subclass__ 定义来定义基类。应首先检索 URL,并将其作为参数传递给 __init_subclass__(通过 class 语句中的关键字参数)。

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

如果URL 应该是实例属性,则将__init_subclass__ 替换为__init__

some_url = call_to_webservice()

class Base:
    def __init__(self, /, url):
        self.url = url

class Foo(Base):
    pass

f = Foo(some_url)

【讨论】:

  • 我的想法是一样的,但甚至避免__init_subclass__,只是将 URL 存储在超类中(硬连线),因为 OP 似乎希望它只被共享和初始化一次
  • 可能。我提到__init_subclass__ 主要是为了演示元类通常对于类定制来说是多余的。
  • 赞成。注意:我认为它不会改变任何东西,但是这三个类是在一个链中继承的,每个类都来自前一个。它们并非都直接从 Base 继承
  • 有一个显着的区别:这意味着url 只需传递给Foo,因为BarBaz 将继承URL 一旦Foo 设置它。
  • (尽管当Foo.__init_subclass__ 解析为Base.__init_subclass_ 并且url 参数不是由BarBaz 提供时,这要求url 不是必需的参数。)
【解决方案2】:

如前所述,您不需要元类。

您已经得到了一个出色而灵活的答案,它使用__init_subclass__

一个简单的方法是在超类中设置共享属性并让子类的实例找到它(就像它们通常沿着实例、类、超类的链一样)

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()

或者,如果配置比较复杂,使用类方法保持代码整洁

class Configurations:

    @classmethod
    def create_cfg(cls):
        cls.URL = 'https://stackoverflow.com/'
...

Configurations.create_cfg()
e = Foo()
s = Bar()
c = Baz()

两种方式都只初始化一次配置并产生

Foo : https://stackoverflow.com/
Bar : https://stackoverflow.com/
Baz : https://stackoverflow.com/

【讨论】:

    【解决方案3】:

    其他答案涵盖了为什么您在这里不需要元类。 这个答案是为了简要解释元类'__new__ 的作用: 它确实构建了您的 class - 它在处理 class <name>(bases, ...): <body> 语句时由 Python 运行时本身调用。不,不打电话就不能上课 一个元类__new__ 方法——或者至少是“所有元类的根”,type__new__ 方法。

    也就是说,如果出于某种原因,您需要一个长任务,而您创建的所有类只能运行一次,您所要做的就是缓存长任务的结果,并使用后续调用中的缓存值。

    如果由于某种原因无法缓存该值,并且必须在元类中执行,则您必须安排您的类主体本身以不同的方式执行或使用异步循环。更优雅的形式可能包括在第一次调用元类 __new__ 时实例化一个 concurrent.futures.ThreadPoolExecutor(并将其作为元类属性保持活动状态),并在为每个类调用 type.__new__ 后提交需要很长的未来。

    如您所见,由于您只需要设置类属性,因此应避免将其作为元类进行。

    不过,设计可能是这样的:

    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 - 我刚刚写了这个,并意识到元类中的代码可以安全地放在 Base 的 __init_subclass__ 方法中 - 甚至不需要元类。

    【讨论】:

      猜你喜欢
      • 2020-08-25
      • 2014-10-31
      • 2021-12-16
      • 1970-01-01
      • 1970-01-01
      • 2021-05-19
      • 2010-10-22
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多