Несколько конструкторов: путинский путь?
У меня есть класс контейнера, который содержит данные. Когда контейнер создается, существуют разные методы передачи данных.
- Передайте файл, содержащий данные
- Передача данных напрямую через аргументы
- Не передавать данные; просто создайте пустой контейнер
В 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
.
Каково самое питоническое решение?
Ответы
Ответ 1
В 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
⚠ Никогда не имеют изменяемые типы в качестве значений по умолчанию в python. ⚠ См. здесь.
Ответ 2
У вас не может быть нескольких конструкторов, но вы можете использовать несколько методов с наименьшим количеством имен factory.
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.
Однако вы не можете легко запретить пользователю использовать конструктор. (Если это важно, в качестве меры предосторожности во время разработки, вы можете проанализировать стек вызовов в конструкторе и проверить, что вызов сделан из одного из ожидаемых методов.)
Ответ 3
Большинство Pythonic будет тем, что уже делает стандартная библиотека Python. Основной разработчик Раймонд Хеттингер (парень collections
) рассказал об этом, а также общие рекомендации по написанию классов.
Используйте отдельные функции класса для инициализации экземпляров, например, как dict.fromkeys()
не является инициализатором класса, но возвращает экземпляр dict
. Это позволяет вам быть гибкими в отношении аргументов, которые вам нужны, не меняя сигнатуры методов при изменении требований.
Ответ 4
Каковы системные цели для этого кода? С моей точки зрения, ваша критическая фраза but the user has to know which method he requires.
Какой опыт вы хотите, чтобы ваши пользователи имели с вашим кодом? Это должно привести к созданию интерфейса.
Теперь переходите к ремонтопригодности: какое решение проще всего читать и поддерживать? Опять же, я считаю, что решение C хуже. Для большинства команд, с которыми я работал, решение B предпочтительнее A: его немного легче читать и понимать, хотя оба они легко ломаются в небольшие блоки кода для лечения.
Ответ 5
Я не уверен, правильно ли я понял, но не работает ли это?
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 {}
Ответ 6
Если вы находитесь на Python 3.4+, вы можете использовать functools.singledispatch
decorator для этого (с небольшой дополнительной помощью от methoddispatch
decorator, который @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
Ответ 7
Самый pythonic способ - убедиться, что любые необязательные аргументы имеют значения по умолчанию. Поэтому включите все аргументы, которые вам известны, и назначьте им соответствующие значения по умолчанию.
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