У меня есть два класса конфигурации, базовый класс Config и подкласс ProdConfig, код показан ниже:

class Config:
    URL = "http://127.0.0.1"
    ANOTHER_URL = URL + "/home"

class ProdConfig(Config):
    URL = "http://test.abc.com"
    # ANOTHER_URL = URL + "/home"

print(Config.URL) # "http://127.0.0.1"
print(Config.ANOTHER_URL) # "http://127.0.0.1/home"
print(ProdConfig.URL) # "http://test.abc.com"
print(ProdConfig.ANOTHER_URL) # http://127.0.0.1/home

Если я не переопределяю переменную ANOTHER_URL в ProdConfig или объявляю ее как @property, значение совпадает с базовым классом, я получил его, поскольку значение назначается при импорте базового класса, но могу ли я получить его с помощью новое значение выровнять с новым URL? Есть ли способ использовать уловки Metaclass или setattr или getattr для решения этой проблемы ??

Большое спасибо!

1
Xu Wang 20 Фев 2020 в 04:05

2 ответа

Лучший ответ

Как отмечено в вопросе и последующем обсуждении, приложенном к нему, использование property не работает непосредственно с определением класса, и это заставляет подклассы также определять свойство, чтобы поддерживать определение «протокол» для каждого атрибута, может стать громоздким. Было также предложено использовать форматную строку, но у нас все еще будет та же проблема с присваиванием, выполненным в определении класса для стандартного метакласса type, в том, что он не будет пересчитан. Рассмотрим альтернативный подход:

class Config:
    URL = "http://127.0.0.1"
    ANOTHER_URL = f"{URL}/home"

class ProdConfig(Config):
    URL = "http://test.abc.com"

Выполнение следующего не даст желаемого результата:

>>> conf = ProdConfig()                           
>>> print(conf.URL)                               
http://test.abc.com
>>> print(conf.ANOTHER_URL)                       
http://127.0.0.1/home

Просто из-за того, что ANOTHER_URL не был переназначен в области ProdConfig. Однако эту проблему можно решить с помощью следующего метакласса:

class ConfigFormatMeta(type):

    def __init__(cls, name, bases, attrs):
        # create and store a "private" mapping of original definitions,
        # for reuse by subclasses
        cls._config_map = config_map = {}
        # merge all config_maps of base classes.
        for base_cls in bases:
            if hasattr(base_cls, '_config_map'):
                config_map.update(base_cls._config_map)

        # update the config_map with original definitions in the newly
        # constructed class, filter out all values beginning with '_'
        config_map.update({
            k: v for k, v in vars(cls).items() if not k.startswith('_')})

        # Now assign the formatted attributes to the class
        for k in config_map:
            # Only apply to str attributes; other types of attributes
            # on the class will need additional work.
            if isinstance(config_map[k], str):
                setattr(cls, k, config_map[k].format(**config_map))

        super().__init__(name, bases, attrs)

После этого попробуйте создать класс Config, используя новый метакласс:

class Config(metaclass=ConfigFormatMeta):
    URL = 'http://example.com'
    ANOTHER_URL = '{URL}/home'


class ProdConfig(Config):
    URL = 'http://abc.example.com'

Теперь попробуйте это снова:

>>> conf = ProdConfig()
>>> print(conf.URL)
http://abc.example.com
>>> print(conf.ANOTHER_URL)
http://abc.example.com/home

Обратите внимание, что ANOTHER_URL не был переопределен в области действия ProdConfig, однако желаемое поведение переопределения только URL для получения ожидаемого значения 'http://abc.example.com/home' было достигнуто.

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

1
metatoaster 20 Фев 2020 в 03:01

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

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

>>> class Config:
...     LOCALHOST    = "http://127.0.0.1"
...     DEFAULT_HOME = "/home"
...     def __init__(self):
...         self._home = self.DEFAULT_HOME
... 
...     @property
...     def url(self):
...         return self.LOCALHOST + self.home
... 
...     @property
...     def home(self):
...         return self._home
... 
...     @home.setter
...     def home(self, h):
...         self._home = h
...         
>>> class ProdConfig(Config):
... 
...     def __init__(self, home=None):
...         super().__init__()
...         if home:
...             self.home = home
>>> 
>>> c = Config()
>>> d = ProdConfig()
>>> e = ProdConfig("/someotherhome")
>>> 
>>> c.url
'http://127.0.0.1/home'
>>> d.url
'http://127.0.0.1/home'
>>> e.url
'http://127.0.0.1/someotherhome'
>>> 

Идея, которую я изложил выше, состояла в том, чтобы показать хорошую практику предоставления подклассам доступа к их унаследованному состоянию, не требуя чувствительного доступа непосредственно к переменным базового класса. Закрытая переменная базового класса _home доступна через свойство подклассом. Используя другой язык, я бы, вероятно, объявил home как свойство protected, а <base>._home - как private.

0
Todd 20 Фев 2020 в 04:17