Как избежать шаблона "self.x = x; self.y = y; self.z = z" в __init__?
Я вижу такие шаблоны, как
def __init__(self, x, y, z):
...
self.x = x
self.y = y
self.z = z
...
довольно часто, часто с гораздо большим количеством параметров. Есть ли хороший способ избежать такой утомительной повторяемости? Если класс наследует от namedtuple
вместо?
Ответы
Ответ 1
Изменение: Если у вас есть Python 3. 7+ просто используйте классы данных
Решение для декоратора, которое сохраняет подпись:
import decorator
import inspect
import sys
@decorator.decorator
def simple_init(func, self, *args, **kws):
"""
@simple_init
def __init__(self,a,b,...,z)
dosomething()
behaves like
def __init__(self,a,b,...,z)
self.a = a
self.b = b
...
self.z = z
dosomething()
"""
#init_argumentnames_without_self = ['a','b',...,'z']
if sys.version_info.major == 2:
init_argumentnames_without_self = inspect.getargspec(func).args[1:]
else:
init_argumentnames_without_self = tuple(inspect.signature(func).parameters.keys())[1:]
positional_values = args
keyword_values_in_correct_order = tuple(kws[key] for key in init_argumentnames_without_self if key in kws)
attribute_values = positional_values + keyword_values_in_correct_order
for attribute_name,attribute_value in zip(init_argumentnames_without_self,attribute_values):
setattr(self,attribute_name,attribute_value)
# call the original __init__
func(self, *args, **kws)
class Test():
@simple_init
def __init__(self,a,b,c,d=4):
print(self.a,self.b,self.c,self.d)
#prints 1 3 2 4
t = Test(1,c=2,b=3)
#keeps signature
#prints ['self', 'a', 'b', 'c', 'd']
if sys.version_info.major == 2:
print(inspect.getargspec(Test.__init__).args)
else:
print(inspect.signature(Test.__init__))
Ответ 2
Отказ от ответственности: Кажется, что несколько человек обеспокоены представлением этого решения, поэтому я предоставлю очень четкую оговорку. Вы не должны использовать это решение. Я предоставляю это только в качестве информации, так что вы знаете, что язык способен на это. Остальная часть ответа просто показывает языковые возможности, а не одобряет их использование таким образом.
Нет ничего плохого в том, чтобы явно копировать параметры в атрибуты. Если в ctor слишком много параметров, это иногда считается запахом кода, и, возможно, вам следует сгруппировать эти параметры в меньшее количество объектов. В других случаях это необходимо, и в этом нет ничего плохого. В любом случае, делать это явно - путь.
Однако, поскольку вы спрашиваете, КАК это можно сделать (а не нужно ли это делать), то одно из решений заключается в следующем:
class A:
def __init__(self, **kwargs):
for key in kwargs:
setattr(self, key, kwargs[key])
a = A(l=1, d=2)
a.l # will return 1
a.d # will return 2
Ответ 3
явный лучше, чем неявный...
так что вы можете сделать его более кратким:
def __init__(self,a,b,c):
for k,v in locals().items():
if k != "self":
setattr(self,k,v)
Чем лучше вы спрашиваете?
... если вы хотите именованный кортеж, я бы рекомендовал использовать namedtuple (помните, что кортежи имеют определенные условия, связанные с ними)... возможно, вам нужен упорядоченный диск или даже просто dict...
Ответ 4
Как уже упоминалось, повторение не так уж плохо, но в некоторых случаях именованный элемент может отлично подходить для такого типа проблем. Это позволяет избежать использования locals() или kwargs, которые обычно являются плохими идеями.
from collections import namedtuple
# declare a new object type with three properties; x y z
# the first arg of namedtuple is a typename
# the second arg is comma-separated or space-separated property names
XYZ = namedtuple("XYZ", "x, y, z")
# create an object of type XYZ. properties are in order
abc = XYZ("one", "two", 3)
print abc.x
print abc.y
print abc.z
Я нашел для него ограниченное использование, но вы можете наследовать namedtuple как и любой другой объект (пример продолжен):
class MySuperXYZ(XYZ):
""" I add a helper function which returns the original properties """
def properties(self):
return self.x, self.y, self.z
abc2 = MySuperXYZ(4, "five", "six")
print abc2.x
print abc2.y
print abc2.z
print abc2.properties()
Ответ 5
Чтобы расширить ответ на gruszczy
, я использовал шаблон, например:
class X:
x = None
y = None
z = None
def __init__(self, **kwargs):
for (k, v) in kwargs.items():
if hasattr(self, k):
setattr(self, k, v)
else:
raise TypeError('Unknown keyword argument: {:s}'.format(k))
Мне нравится этот метод, потому что он:
- избегает повторения
- устойчив к опечаткам при конструировании объекта
- хорошо работает с подклассом (может просто
super().__init(...)
)
- позволяет документацию атрибутов на уровне класса (где они принадлежат), а не в
X.__init__
До Python 3.6 это не дает контроля над порядком, в котором установлены атрибуты, что может быть проблемой, если некоторые атрибуты являются свойствами с сеттерами, которые получают доступ к другим атрибутам.
Возможно, это немного улучшилось, но я единственный пользователь своего собственного кода, поэтому меня не беспокоит какая-либо форма входной санитарии. Возможно, более подходящим будет AttributeError
.
Ответ 6
Вы также можете сделать:
locs = locals()
for arg in inspect.getargspec(self.__init__)[0][1:]:
setattr(self, arg, locs[arg])
Конечно, вам нужно будет импортировать модуль inspect
.
Ответ 7
Это решение без каких-либо дополнительных импортов.
Вспомогательная функция
Небольшая вспомогательная функция делает ее более удобной и повторно используемой:
def auto_init(local_name_space):
"""Set instance attributes from arguments.
"""
self = local_name_space.pop('self')
for name, value in local_name_space.items():
setattr(self, name, value)
Применение
Вам нужно позвонить ему с помощью locals()
:
class A:
def __init__(self, x, y, z):
auto_init(locals())
Test
a = A(1, 2, 3)
print(a.__dict__)
Вывод:
{'y': 2, 'z': 3, 'x': 1}
Без изменения locals()
Если вы не хотите менять locals()
, используйте эту версию:
def auto_init(local_name_space):
"""Set instance attributes from arguments.
"""
for name, value in local_name_space.items():
if name != 'self':
setattr(local_name_space['self'], name, value)
Ответ 8
Интересная библиотека, которая обрабатывает это (и избегает многих других шаблонов), является attrs. Ваш пример, например, может быть сведен к этому (предположим, что класс называется MyClass
):
import attr
@attr.s
class MyClass:
x = attr.ib()
y = attr.ib()
z = attr.ib()
Вам даже больше не нужен метод __init__
, если только он не выполняет другие функции. Здесь хорошее введение от Глифа Лефковица.
Ответ 9
Мои 0.02 $. Это очень близко к ответу Джорана Бисли, но более элегантно:
def __init__(self, a, b, c, d, e, f):
vars(self).update((k, v) for k, v in locals().items() if v is not self)
Кроме того, ответ Майка Мюллера (лучший на мой вкус) можно уменьшить с помощью этой техники:
def auto_init(ns):
self = ns.pop('self')
vars(self).update(ns)
И как раз вызов auto_init(locals())
из вашего __init__
Ответ 10
Это естественный способ делать вещи в Python. Не пытайтесь изобретать что-то более умное, это приведет к чрезмерно умному коду, который никто в вашей команде не поймет. Если вы хотите стать игроком команды, а затем продолжать писать так.
Ответ 11
Python 3.7 года
В Python 3.7 вы можете (ab) использовать декоратор dataclass
, доступный из модуля dataclasses
. Из документации:
Этот модуль предоставляет декоратор и функции для автоматического добавления сгенерированных специальных методов, таких как __init__()
и __repr__()
в пользовательские классы. Первоначально он был описан в PEP 557.
Переменные-члены, используемые в этих сгенерированных методах, определяются с помощью аннотаций типа PEP 526. Например этот код:
@dataclass
class InventoryItem:
'''Class for keeping track of an item in inventory.'''
name: str
unit_price: float
quantity_on_hand: int = 0
def total_cost(self) -> float:
return self.unit_price * self.quantity_on_hand
Помимо прочего, добавим __init__()
который выглядит следующим образом:
def __init__(self, name: str, unit_price: float, quantity_on_hand: int=0):
self.name = name
self.unit_price = unit_price
self.quantity_on_hand = quantity_on_hand
Обратите внимание, что этот метод автоматически добавляется в класс: он не указан напрямую в приведенном выше определении InventoryItem.
Если ваш класс большой и сложный, может быть нецелесообразно использовать dataclass
. Я пишу это в день выпуска Python 3.7.0, поэтому шаблоны использования еще не установлены.