1

I have two config classes, base class Config and subclass ProdConfig, code shows below:

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

If I do not override the variable ANOTHER_URL in ProdConfig or declare it as @property, the value is same with base class, I got it cus value is assigned when base class imported, but can I get it with new value align to new URL? Is there a way to use Metaclass or setattr or getattr tricks to solve this problem??

Thank you very much!

Xu Wang
  • 344
  • 1
  • 9
  • By `proerty` do you mean the `@property` decorator? Even that one will not solve your problem when accessing the assignment of `ANOTHER_URL` at the class level - a `property` only has an effect on instances of classes in general (i.e. accessing `ProdConfig.ANOTHER_URL` will return the exact assignment there, which would _be_ the `property` object definition, not the value your application may expect). Metaclass _may_ help with this, but its additional complexity may result in additional complications. – metatoaster Feb 20 '20 at 01:21
  • In any case, if you are thinking of using metaclasses to implement class property, [this thread](https://stackoverflow.com/questions/5189699/how-to-make-a-class-property) has a solution, but note the additional complexity required. – metatoaster Feb 20 '20 at 01:25
  • Yes, Thanks, it is `@property` and I edited it. Though it will not solve my problem directly, but I can initialize `Config` or `ProdConfig` and then use it as an object, maybe it`s ugly as a Config class I think – Xu Wang Feb 20 '20 at 01:30
  • Then simply define a function named `ANOTHER_URL` that `return self.URL + '/home'` and decorate that with the `@property` decorator. – metatoaster Feb 20 '20 at 01:32
  • Yes, it\`s may fine, but I think it\`s not beautiful of writing method in a config class, and if I override field in the subclass, I think that it\`s too much writing not necessary, so this is what I confuse... – Xu Wang Feb 20 '20 at 01:40
  • Without using properties you can't make a dynamic attribute. They get their value when the class is defined and don't change unless you reassign them. – Barmar Feb 20 '20 at 01:43
  • The problem as described isn't strictly unsolvable in Python - I can see the lack of aesthetics with usage of `property` methods and the difficulty to reassign subsequent values in subclasses; this is a legitimate question if posed with all of these specific requirements. – metatoaster Feb 20 '20 at 01:45
  • Is there still no way of using `metaprogramming` in Python? @Barmar – Xu Wang Feb 20 '20 at 01:45
  • @XuWang would using the [`format()`](https://docs.python.org/3/library/stdtypes.html#str.format) syntax to define config be satisfiable? Example: user might define `ANOTHER_URL = "{URL}/home"` at the class (without needing the user to also manually call `.format(...)`), and the correct value will be produced when read. If this is the case it may make the problem more amenable for solving. – metatoaster Feb 20 '20 at 01:48
  • @metatoaster You mean this?`ANOTHER_URL = '{}/home'.format(URL)`, I defined in `Config` and still not override it in the subclass, it not works. – Xu Wang Feb 20 '20 at 01:53
  • No, not quite, what you described is exactly what I don't want either, I mean all users have to do is define everything without the `.format` bit, and then achieve what you desire through a metaclass. All I am asking is whether or not that particular format string is acceptable, to avoid having to deal with additional operation with operators (because with operators such as `+`, now you are forced to deal with either lambdas or AST manipulation or bytecode, which significantly more _complicated_ than dealing with a single literal format string). – metatoaster Feb 20 '20 at 01:55
  • @metatoaster Thank you for your tips, now I am going to try to use `"f{abc}"` format string now – Xu Wang Feb 20 '20 at 02:00
  • I don't get what you're aiming for here. If you want a local variable to the URL, you can just pass a URL in to the constructor and set a local instance variable. – Todd Feb 20 '20 at 03:16

2 Answers2

1

As noted in the question and the following discussion attached to it, the usage of property does not work on the class definition directly, and that forcing subclasses to also define the property to maintain the definition "protocol" for every attribute may become cumbersome. A usage of format string was also suggested, but we will still have the same issue with the assignment done at class definition for the standard type metaclass is that it will not be recalculated. Consider the alternative approach:

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

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

Running the following will fail to produce the desired result:

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

Simply due to the fact that ANOTHER_URL was not reassigned in the scope of ProdConfig. However, this problem can be solved using the following metaclass:

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)

With that, try basing the Config class using the new metaclass:

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


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

Now try this again:

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

Note that ANOTHER_URL was not redefined in the scope of ProdConfig, yet the desired behavior of having only the URL be redefined to have the expected value of 'http://abc.example.com/home' was achieved.

Also it is worth noting that using metaclasses will interfere with multiple inheritance with other classes that have a different base metaclass, and that there is a bit of duplication with the original mapping that is slightly hidden via the _config_map attribute on the class itself, so please treat this mostly as a proof of concept.

metatoaster
  • 17,419
  • 5
  • 55
  • 66
0

If you want a way to inherit the default home directory of your site, but also be able to override it when you create instances of the subclass. You could do it this way.

This shows how a subclass can inherit default state, or customize its state depending on the parameter passed to the constructor.

>>> 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'
>>> 

The idea I had above was to show a good practice for providing subclasses access to their inherited state without necessitating sensitive access directly to the base class's variables. The private _home base class variable is accessed through a property by the subclass. Using another language, I'd probably declare home as a protected property, and <base>._home as private.

Todd
  • 4,669
  • 1
  • 22
  • 30