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

Я работаю с некоторыми патчами 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"

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

Просто превратите их в словари и передайте как **kwargs в класс данных, который я вам предоставил. Как было предложено в моем последнем комментарии под моим ответом.

OysterShucker 14.08.2024 15:34

Я не полностью следую @OysterShucker - вы имеете в виду просто создать стандартные переменные, не относящиеся к классу, а затем ссылаться на них? можете ли вы импортировать переменные из отдельного модуля? если да, то я думаю, что это может быть простой ответ здесь

turbonerd 14.08.2024 15:42

Я отредактировал свой ответ, приведя пример

OysterShucker 14.08.2024 15:49
Почему в Python есть оператор "pass"?
Почему в Python есть оператор "pass"?
Оператор pass в Python - это простая концепция, которую могут быстро освоить даже новички без опыта программирования.
Некоторые методы, о которых вы не знали, что они существуют в Python
Некоторые методы, о которых вы не знали, что они существуют в Python
Python - самый известный и самый простой в изучении язык в наши дни. Имея широкий спектр применения в области машинного обучения, Data Science,...
Основы Python Часть I
Основы Python Часть I
Вы когда-нибудь задумывались, почему в программах на Python вы видите приведенный ниже код?
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
Алиса и Боб имеют неориентированный граф из n узлов и трех типов ребер:
Оптимизация кода с помощью тернарного оператора Python
Оптимизация кода с помощью тернарного оператора Python
И последнее, что мы хотели бы показать вам, прежде чем двигаться дальше, это
Советы по эффективной веб-разработке с помощью Python
Советы по эффективной веб-разработке с помощью Python
Как веб-разработчик, Python может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
1
3
63
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 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)

Хорошо, мне нравится идея иметь один класс YamlPatch, но это приводит меня на территорию того фрагмента, который вы только что отредактировали... :) Посмотрим, смогу ли я объяснить немного более понятно. если у меня есть патч под названием PackageApplicationFromGit, я знаю - по его названию - что единственное значение, которое мне нужно указать, - это имя приложения, например. PackageApplicationFromGit("grafana"). Однако в вашем примере мне нужно было бы запомнить, какую часть патча обновить словом grafana... что даже сейчас, слетя с ума, я не могу

turbonerd 14.08.2024 15:16

Я надеялся, что смогу сделать что-то, где имя класса («тип» патча, который я использую) принимает всего один аргумент и помещает его в нужное место.

turbonerd 14.08.2024 15:17

@turbonrd - Ты можешь это сделать. Это просто плохая идея. Вы наверняка должны знать, что такое «графана». Я склонен подходить к вещам «реального мира». Реальный эквивалент того, что вы хотите, — это все равно, что не помнить, зовут ли меня Майкл, 49 лет, или Новый Орлеан.

OysterShucker 14.08.2024 15:22

возможно, мне следовало использовать реальные примеры, но все гораздо сложнее. Я помню слово «графана», но не могу вспомнить соответствующие ему строки path или value. Сейчас я обновлю свой ОП реальным примером, возможно, это будет иметь больше смысла.

turbonerd 14.08.2024 15:24

@turbonrd — Вы можете просто создать таблицу поиска аргументов по умолчанию для всех «сервисов», которые вы поддерживаете, и: YamlPatch(**lookup.get('this_service', {}))

OysterShucker 14.08.2024 15:29

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

turbonerd 14.08.2024 15:30

Хорошо, отлично. Думаю, это самое близкое к тому, что у меня было в голове. спасибо за ваши усилия :)

turbonerd 14.08.2024 15:53

Вместо производных классов вы можете хранить 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)

...

спасибо @Guy - хороший ответ; лаконично, но оно упирается в трудности, о которых я только что упомянул в своем ОП.

turbonerd 14.08.2024 15:41

@turbonrd - так или иначе вам придется что-то вспомнить. Вы пытаетесь изобрести систему Альцгеймера! :D

OysterShucker 14.08.2024 15:44

Я помню одну вещь, но не все остальное, что с ней связано!

turbonerd 14.08.2024 15:45

Другие вопросы по теме

Похожие вопросы