Наследование классов, где дочерними элементами являются простые классы, состоящие только из переменных

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

Вот подход, который я использовал до сих пор:

class YamlPatch:
    def __init__(self, kind, name, namespace, op, path, value):
        target = {
            "kind": kind,
            "name": name,
            "namespace": namespace
        },
        scalar=[{
            "op": op,
            "path": path,
            "value": value
        }]

        self.yaml = (target, scalar)

class PatchA(YamlPatch):
    def __init__(self, name):
        namespace = "my-namespace"
        kind = "test"
        op = "replace"
        path = "/test"
        value = "hello"

        super().__init__(kind, name, namespace, op, path, value)

class PatchB(YamlPatch):
    def __init__(self, path):
        namespace = "my-namespace"
        name = "my-name"
        kind = "test"
        op = "replace"
        value = "hello"

        super().__init__(kind, name, namespace, op, path, value)

### Insert 4 or 5 other types of patches here...

patches = []
patches.append(PatchA("hello").yaml)
for app in ["app1", "app2"]:
    patches.append(PatchB(f"/{app}").yaml)

print(patches)

### output: [(({'kind': 'test', 'name': 'hello', 'namespace': 'my-namespace'},), [{'op': 'replace', 'path': '/test', 'value': 'hello'}]), (({'kind': 'test', 'name': 'my-name', 'namespace': 'my-namespace'},), [{'op': 'r
eplace', 'path': '/app1', 'value': 'hello'}]), (({'kind': 'test', 'name': 'my-name', 'namespace': 'my-namespace'},), [{'op': 'replace', 'path': '/app2', 'value': 'hello'}])]

Это кажется беспорядочным и повторяющимся, особенно когда вы добавляете подсказки и комментарии. Не очень СУХОЙ. Некоторые значения являются довольно распространенными значениями по умолчанию, и необходимость __init__ затем super() в каждом дочернем классе (патче) неприятна.

Я пробовал использовать классы данных, но поскольку необходимые «входные» аргументы для дочерних классов разные, мне пришлось бы использовать аргумент kw_only, который сложно запомнить при таком большом количестве разных патчей (например, PatchA(value = "blah") или это было PatchA(name = "blah"), я могу' не помнишь?).

Короче говоря, я ищу самый быстрый и эффективный способ написания кода, который позволит мне ссылаться на запоминающееся и простое имя (здесь я назвал их PatchA и PatchB, но в реальном коде они будут чем-то уникальным и очевидным для пользователя). сопровождающие) и вернуть правильно отформатированный патч YAML. Например. print(PatchA).

Я использую Python 3.11.

--- ИЗМЕНИТЬ ДЛЯ РАЗЪЯСНЕНИЯ

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

Вот реалистичный (хотя и сокращенный) пример:

class YamlPatch:
  yaml = ...

class PackageApplicationFromGit(YamlPatch):
  path = "/spec/path"
  name = f"application-{application}"
  value = f"/some/applications/path/{application}"

class AppsGitRepoBranchPatch(YamlPatch):
  kind = "GitRepository"
  path = "/spec/ref/branch"
  value = "my-branch-name"

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

🤔 А знаете ли вы, что...
С Python можно создавать ботов для социальных сетей и мессенджеров.


1
63
2

Ответы:

Решено

Вы можете использовать dataclass и исключить подклассы.

from dataclasses import dataclass


@dataclass
class YamlPatch:
  namespace :str = "my-namespace"
  kind      :str = "test"
  name      :str = "my-name"
  op        :str = "replace"
  path      :str = "/test"
  value     :str = "hello"

  def __post_init__(self) -> None:
    target = {
      "namespace": self.namespace ,
      "kind"     : self.kind      ,
      "name"     : self.name      ,
    }
    
    scalar=[{
      "op"   : self.op    ,
      "path" : self.path  ,
      "value": self.value ,
    }]
    
    self.yaml = (target, scalar)
    
  def __str__(self) -> str:
    return str(self.yaml)


if __name__ == "__main__":
  patches = [YamlPatch(name = "hello").yaml]
  
  for app in ("app1", "app2"):
    patches.append(YamlPatch(path=f"/{app}").yaml)
  
  print(*patches, sep = "\n")

Честно говоря, единственное, что dataclass здесь действительно делает для вас, — это избавляет вас от необходимости выполнять всю стандартную работу в методе __init__ для преобразования аргументов конструктора в свойства. Это также делает его намного чище, поскольку вам не понадобится конструктор с шестью типизированными аргументами по умолчанию. Если ваши экземпляры YamlPatch должны быть более динамичными, вы можете реорганизовать __post_init__ в property.

from dataclasses import dataclass


@dataclass
class YamlPatch:
  namespace :str = "my-namespace"
  kind      :str = "test"
  name      :str = "my-name"
  op        :str = "replace"
  path      :str = "/test"
  value     :str = "hello"

  @property
  def yaml(self) -> tuple:
    target = {
      "namespace": self.namespace ,
      "kind"     : self.kind      ,
      "name"     : self.name      ,
    }
    
    scalar=[{
      "op"   : self.op    ,
      "path" : self.path  ,
      "value": self.value ,
    }]
    
    return (target, scalar)
    
  def __str__(self) -> str:
    return str(self.yaml)

Я пробовал использовать классы данных, но поскольку необходимые «входные» аргументы поскольку дочерние классы разные, мне придется использовать kw_only аргумент, который сложно запомнить при таком большом количестве разных патчи (например, PatchA(value="blah") или это был PatchA(name="blah"), я не помню?).

В вашем примере все экземпляры имеют одинаковые свойства, независимо от того, как вы подклассифицировали супер, поэтому ваш аргумент KW_ONLY не имеет смысла. Что касается запоминания значений отдельных патчей, вам обязательно следует их запомнить. Кажется, вы ищете систему, в которой вы произвольно просто соединяете вещи и/или имеете множество произвольных классов, названных в честь значения, каждый из которых принимается. Это излишне раздуто, и у вас будет столько же проблем с отслеживанием всех классов, сколько и с отслеживанием необходимого значения. Короче говоря: ленивый подход более громоздкий, чем просто отсутствие лени. Вы уже доказали это себе и признали это своей попыткой.


Пример из моего комментария. Не таблица поиска, но по сути те же результаты.

# you have to figure out where `application` is coming from
PackageApplicationFromGit = dict(
  path = "/spec/path",
  name = f"application-{application}",
  value = f"/some/applications/path/{application}",
)

AppsGitRepoBranchPatch = dict(
  kind = "GitRepository",
  path = "/spec/ref/branch",
  value = "my-branch-name",
)

patch = YamlPatch(**AppsGitRepoBranchPatch)

Вместо производных классов вы можете хранить dict изменяемые параметры и значения по умолчанию dict.

default = {'kind': 'test', 'name': 'my-name', 'namespace': 'my-namespace',
           'op': 'replace', 'path': '/test', 'value': 'hello'}

patches = {
    'patchA': ['name'],
    'patchB': ['path'],
    'patchC': ['namespace', 'value']
}


class YamlPatch:
    def __init__(self, patch, *args):
        values = default.copy()
        for i, var in enumerate(patches[patch]):
            values[var] = args[i]

        target = dict(list(values.items())[:3]),
        scalar = [dict(list(values.items())[3:])]

        self.yaml = (target, scalar)


pat = []
pat.append(YamlPatch('patchA', 'hello').yaml)
for app in ['app1', 'app2']:
    pat.append(YamlPatch('patchB', f'/{app}').yaml)
pat.append(YamlPatch('patchC', 'outerspace', 'goodbye').yaml)


print(pat)

Выход

[(({'kind': 'test', 'name': 'hello', 'namespace': 'my-namespace'},), [{'op': 'replace', 'path': '/test', 'value': 'hello'}]),
 (({'kind': 'test', 'name': 'my-name', 'namespace': 'my-namespace'},), [{'op': 'replace', 'path': '/app1', 'value': 'hello'}]),
 (({'kind': 'test', 'name': 'my-name', 'namespace': 'my-namespace'},), [{'op': 'replace', 'path': '/app2', 'value': 'hello'}]),
 (({'kind': 'test', 'name': 'my-name', 'namespace': 'outerspace'},), [{'op': 'replace', 'path': '/test', 'value': 'goodbye'}])]

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

...

patches = {
    'patchA': 'name',
    'patchB': 'path',
}


class YamlPatch:
    def __init__(self, patch, value):
        values = default.copy()
        values[patches[patch]] = value

        target = dict(list(values.items())[:3]),
        scalar = [dict(list(values.items())[3:])]

        self.yaml = (target, scalar)

...