Несколько конструкторов: Питонический путь? [дубликат]


этот вопрос уже есть ответ здесь:

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

  1. передать файл, который содержит данные
  2. передайте данные непосредственно через аргументы
  3. не передавайте данные; просто создайте пустой контейнер

в Java я бы создал три конструктора. Вот как это выглядело бы, если бы это было возможно в Python:

class Container:

    def __init__(self):
        self.timestamp = 0
        self.data = []
        self.metadata = {}

    def __init__(self, file):
        f = file.open()
        self.timestamp = f.get_timestamp()
        self.data = f.get_data()
        self.metadata = f.get_metadata()

    def __init__(self, timestamp, data, metadata):
        self.timestamp = timestamp
        self.data = data
        self.metadata = metadata

в Python я вижу три очевидных решения, но ни одно из них не является симпатичным:

A: использование ключевых аргументов:

def __init__(self, **kwargs):
    if 'file' in kwargs:
        ...
    elif 'timestamp' in kwargs and 'data' in kwargs and 'metadata' in kwargs:
        ...
    else:
        ... create empty container

B: использование по умолчанию аргументы:

def __init__(self, file=None, timestamp=None, data=None, metadata=None):
    if file:
        ...
    elif timestamp and data and metadata:
        ...
    else:
        ... create empty container

C: только предоставить конструктор для создания пустых контейнеров. Укажите методы заполнения контейнеров данными из разных источников.

def __init__(self):
    self.timestamp = 0
    self.data = []
    self.metadata = {}

def add_data_from_file(file):
    ...

def add_data(timestamp, data, metadata):
    ...

решения A и B в основном одинаковы. Мне не нравится делать if/else, тем более, что я должен проверить, были ли предоставлены все аргументы, необходимые для этого метода. A является немного более гибким, чем B, если код когда-либо будет расширен четвертым методом для добавления данных.

Решение C вроде бы самый хороший, но пользователь должен знать, какой метод требует он. Например: он не может сделать c = Container(args) если он не знает, что args есть.

Whats наиболее подходящие для Python решение?

7 54

7 ответов:

вы не можете иметь несколько методов с одинаковыми именами в Python. Перегрузка функций - в отличие от Java - не поддерживается.

использовать параметры по умолчанию или **kwargs и *args аргументов.

вы можете сделать статические методы и методы класса с @staticmethod или @classmethod декоратор для возврата экземпляра класса или добавления других конструкторов.

советую вам сделать:

class F:

    def __init__(self, timestamp=0, data=None, metadata=None):
        self.timestamp = timestamp
        self.data = list() if data is None else data
        self.metadata = dict() if metadata is None else metadata

    @classmethod
    def from_file(cls, path):
       _file = cls.get_file(path)
       timestamp = _file.get_timestamp()
       data = _file.get_data()
       metadata = _file.get_metadata()       
       return cls(timestamp, data, metadata)

    @classmethod
    def from_metadata(cls, timestamp, data, metadata):
        return cls(timestamp, data, metadata)

    @staticmethod
    def get_file(path):
        # ...
        pass

⚠ никогда не имеют изменяемых типов по умолчанию в питон. ⚠ Смотрите здесь.

вы не можете иметь несколько конструкторов, но вы можете иметь несколько названных методов фабрики.

class Document(object):

    def __init__(self, whatever args you need):
        """Do not invoke directly. Use from_NNN methods."""
        # Implementation is likely a mix of A and B approaches. 

    @classmethod
    def from_string(cls, string):
        # Do any necessary preparations, use the `string`
        return cls(...)

    @classmethod
    def from_json_file(cls, file_object):
        # Read and interpret the file as you want
        return cls(...)

    @classmethod
    def from_docx_file(cls, file_object):
        # Read and interpret the file as you want, differently.
        return cls(...)

    # etc.

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

большинство Pythonic будет то, что стандартная библиотека Python уже делает. Основной разработчик Raymond Hettinger (the collections парень) рассказал об этом, плюс общие рекомендации по написанию классов.

используйте отдельные функции уровня класса для инициализации экземпляров, например how dict.fromkeys() не является инициализатором класса, но все равно возвращает экземпляр dict. Это позволяет быть гибким в отношении необходимых аргументов без изменения сигнатур методов в качестве требований изменение.

каковы системные цели для этого кода? С моей точки зрения, ваша критическая фраза but the user has to know which method he requires. какой опыт вы хотите, чтобы ваши пользователи имели с вашим кодом? Это должно управлять дизайном интерфейса.

теперь перейдем к ремонтопригодности: какое решение проще всего читать и поддерживать? Опять же, я чувствую, что решение C уступает. Для большинства команд, с которыми я работал, решение B предпочтительнее A: его немного легче читать и понимать, хотя оба легко ломаются на небольшие блоки кода для обработки.

Я не уверен, правильно ли я понял, но разве это не сработает?

def __init__(self, file=None, timestamp=0, data=[], metadata={}):
    if file:
        ...
    else:
        self.timestamp = timestamp
        self.data = data
        self.metadata = metadata

или вы могли бы даже сделать:

def __init__(self, file=None, timestamp=0, data=[], metadata={}):
    if file:
        # Implement get_data to return all the stuff as a tuple
        timestamp, data, metadata = f.get_data()

    self.timestamp = timestamp
    self.data = data
    self.metadata = metadata

благодаря советам Джона Кипарского есть лучший способ избежать глобальных деклараций на data и metadata Итак, это новый способ:

def __init__(self, file=None, timestamp=None, data=None, metadata=None):
    if file:
        # Implement get_data to return all the stuff as a tuple
        with open(file) as f:
            timestamp, data, metadata = f.get_data()

    self.timestamp = timestamp or 0
    self.data = data or []
    self.metadata = metadata or {}

Если вы находитесь на Python 3.4+ вы можете использовать functools.singledispatch декоратор для этого (с небольшой дополнительной помощью от methoddispatch декоратор, который @ZeroPiraeus писал ответ):

class Container:

    @methoddispatch
    def __init__(self):
        self.timestamp = 0
        self.data = []
        self.metadata = {}

    @__init__.register(File)
    def __init__(self, file):
        f = file.open()
        self.timestamp = f.get_timestamp()
        self.data = f.get_data()
        self.metadata = f.get_metadata()

    @__init__.register(Timestamp)
    def __init__(self, timestamp, data, metadata):
        self.timestamp = timestamp
        self.data = data
        self.metadata = metadata

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

def __init__(self, timestamp=None, data=[], metadata={}):
    timestamp = time.now()

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

вы можете принять еще больше дополнительных аргументов с помощью *args и **kwargs В конце ваших рассуждений список.

def __init__(self, timestamp=None, data=[], metadata={}, *args, **kwards):
    if 'something' in kwargs:
        # do something