Избегайте __new__ звонить каждый раз - PullRequest
1 голос
/ 23 января 2020

Я понял, что классы являются экземплярами метаклассов и что __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 se c

    Class Bar записывается после еще 2 se c

    Class Baz записывается окончательно после еще 2 se c

Таким образом, метакласс выполняется три раза. Это должно объяснить, что поскольку __new__ отвечает за создание классов, его нужно запускать каждый раз, то есть три раза.

Я прав?

Как мне избежать этого и заставить его работать только один раз?

Ответы [ 3 ]

1 голос
/ 23 января 2020

Вам не нужен метакласс здесь. Предполагая, что вы хотите, чтобы 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)
0 голосов
/ 24 января 2020

Другие ответы охватывают, почему вам не нужен метакласс здесь. Этот ответ должен кратко объяснить, что делает метакласс __new__: он создает ваш класс - он вызывается самой средой выполнения Python, когда обрабатывает оператор class <name>(bases, ...): <body>. Нет, у вас не может быть класса без вызова метода метакласса __new__ - или хотя бы "метода root всех метаклассов", type * __new__.

Тем не менее, если по какой-то причине вам понадобится длинная задача, которая может быть запущена только один раз для всех создаваемых вами классов, все, что вам нужно будет сделать, - это кэшировать результаты длинной задачи и использовать кэшированные значения в последующих вызовах. .

Если по какой-то причине значение не может быть кэшировано и должно быть выполнено в метаклассе, вам нужно будет организовать, чтобы сами тела ваших классов выполнялись в разных шагах или с использованием асинхронного l * 1025. *. Более элегантные формы могут включать создание экземпляра concurrent.futures.ThreadPoolExecutor внутри метакласса __new__, когда он вызывается впервые (и удерживает его в качестве атрибута метакласса), и после вызова 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 - Я только что написал это и понял, что код в метаклассе можно безопасно поместить в метод __init_subclass__ Base - метакласс вообще не нужен, даже для этого.

0 голосов
/ 23 января 2020

Как уже упоминалось, вам не нужны метаклассы.

И у вас уже есть превосходный и гибкий ответ, который использует __init_subclass__.

Плоский простой подход заключается в установке атрибут shared в суперклассе, и пусть экземпляры подклассов находят его (как это обычно происходит в цепочке экземпляров, классов, суперклассов)

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/
...