СУХОЙ принцип в методе Python __init__ - PullRequest
0 голосов
/ 03 апреля 2019

В этом определении класса каждый параметр встречается трижды, что, кажется, нарушает принцип СУХОЙ (не повторяйся):

class Foo:
    def __init__(self, a=1, b=2.0, c=(3, 4, 5)):
        self.a = int(a)
        self.b = float(b)
        self.c = list(c)

СУХОЙ может применяться следующим образом (Python 3):

class Foo:
    def __init__(self, **kwargs):
        defaults = dict(a=1, b=2.0, c=[3, 4, 5])
        for k, v in defaults.items():
            setattr(self, k, type(v)(kwargs[k]) if k in kwargs else v)
        # ...detect illegal keywords here...

Однако это нарушает автозаполнение IDE (пробовал Spyder и Elpy), и Pylint будет жаловаться, если я попытаюсь получить доступ к атрибутам позже.

Есть ли чистый способ справиться с этим?

Редактировать: в примере есть три параметра, но я сталкиваюсь с этим, когда есть 15 параметров, где мне редко нужно переопределять значения по умолчанию;часто с более сложными типами, где мне нужно было бы сделать

if not isinstance(kwargs['x'], SomeClass):
    raise TypeError('x: must be SomeClass')
self.x = kwargs['x']

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

Ответы [ 2 ]

2 голосов
/ 03 апреля 2019

Такие принципы, как DRY, важны, но важно иметь в виду обоснование такого принципа, прежде чем применять его вслепую - возможно, самое большое преимущество кода DRY заключается в том, что вы повышаете удобство сопровождения кода, просто изменяя его.в одном месте и не нужно рисковать тонкими ошибками, которые могут возникнуть с кодом, измененным в одном месте, а не в другом.DRY может противоречить другим общим принципам, таким как YAGNI и KISS, и выбор правильного баланса для вашего приложения важен.

В частности, DRY часто применяется к значениям по умолчанию, логике приложения и другим вещам, которые могут вызвать ошибкиесли поменял в одном месте, а не в другом.Имена переменных IMO не подходят одинаково, так как рефакторинг кода для изменения каждого экземпляра переменной экземпляра Foo, равной a, на самом деле ничего не сломает, также не изменив имя в инициализаторе.

Имея это в виду, у нас есть простой тест для вашего кода.Могут ли эти переменные изменяться вместе, или инициализатор для Foo представляет собой уровень абстракции, который позволяет проводить рефакторинг входных данных независимо от переменных экземпляра класса?

Change Together: Мне скорее нравитсяответ @ chepner, и я бы сделал еще один шаг вперед.Если ваш класс представляет собой нечто большее, чем объект передачи данных, вы можете использовать решение @ chepner как способ логически сгруппировать связанные фрагменты данных (что, по общему признанию, может оказаться ненужным в вашей ситуации, и без некоторого контекста сложно выбрать оптимальный способ представлениятакая идея), например

from dataclasses import dataclass, field

@dataclass
class MyData:
    a: int
    b: float
    c: list

class Foo:
    def __init__(self, my_data):
        self.wrapped = my_data

Изменить отдельно: Тогда просто оставьте это в покое, или ПОЦЕЛУЙ, как говорится.

2 голосов
/ 03 апреля 2019

В качестве предисловия ваш код

class Foo:
    def __init__(self, a=1, b=2.0, c=(3, 4, 5)):
        self.a = int(a)
        self.b = float(b)
        self.c = list(c)

, как уже упоминалось в нескольких комментариях, хорош, как есть.Код читается гораздо больше, чем написано, и, кроме того, что нужно быть осторожным, чтобы избежать опечаток в именах при первом определении этого, цель совершенно ясна.(Хотя см. Конец ответа относительно значения по умолчанию c.)


Если вы используете Python 3.7, вы можете использовать класс данных, чтобы уменьшить количество ссылок на каждую ссылкупеременная.

from dataclasses import dataclass, field
from typing import List

@dataclass
class Foo:
    a: int = 1
    b: float = 2.0
    c: List[int] = field(default_factory=lambda: [3,4,5])

Это не мешает вам нарушать подсказки типов (Foo("1") с радостью установит a = "1" вместо a = 1 или вызовет ошибку), но обычно это ответственностьвызывающая сторона должна предоставить аргументы правильного типа.) Если вы действительно хотите применить это во время выполнения, вы можете добавить __post_init__ метод:

def __post_init__(self):
    self.a = int(self.a)
    self.b = float(self.b)
    self.c = list(self.c)

Но если вы сделаете это, вы также можетевернитесь к исходному методу __init__, закодированному вручную.


Кроме того, стандартная идиома для изменяемых аргументов по умолчанию:

def __init__(self, a=1, b=2.0, c=None):
    ...
    if c is None:
        c = [3, 4, 5]

У вашего подхода есть две проблемы:

  1. Требуется, чтобы list запускался для каждого экземпляра, а не позволял жесткий код компилятора [3,4,5].
  2. Если вы указали аргументы типа для__init__, ваше значение по умолчанию не совпадает сУхоженный тип.Вам нужно написать что-то вроде

    def init (a: int = 1, b: float = 2.0, c: Union [List [Int], Tuple [Int, Int,Int]] = (3,4,5))

Значение по умолчанию None автоматически вызывает «продвижение» типа к соответствующему необязательному типу.Следующие значения эквивалентны:

def __init__(a: int = 1, b: float = 2.0, c : List[Int] = None):
def __init__(a: int = 1, b: float = 2.0, c : Optional[List[Int]] = None):
...