Как мне избежать «self.x = x; self.y = y; шаблон self.z = z ”в __init__?

170

Я вижу такие шаблоны, как

def __init__(self, x, y, z):
    ...
    self.x = x
    self.y = y
    self.z = z
    ...

довольно часто, часто с гораздо большим количеством параметров. Есть ли хороший способ избежать такого утомительного повторения? Должен ли класс наследовать от namedtuple?

MaxB
источник
31
Не вся восприимчивость плохая. Имейте в виду, что модель классов Python не содержит явного определения атрибутов экземпляра, поэтому эти назначения являются самодокументируемыми эквивалентами.
Чепнер
4
@chepner: Ну, не требует явного определения. Вы можете использовать __slots__для этой цели, хотя ; это слегка не пифонично (более многословно, чтобы сэкономить память), но мне нравится это во многом, чтобы избежать риска самовосстановления целого нового атрибута, если я опишу имя.
ShadowRanger
2
У любого хорошего редактора будут шаблоны. Вы печатаете, ini <shortcut> x, y, z): <shortcut>и вы сделали.
Геренюк
3
Именованные кортежи великолепны, если вы хотите неизменный объект значения. Если вам нужен обычный, изменчивый класс, вы не можете его использовать.
RemcoGerlich
4
«Не» - хорошая опция, любая доступная опция уничтожит сигнатуру метода (и, следовательно, потенциально весь интерфейс). Кроме того, если ваши классы имеют невыносимое количество полей для инициализации, вы можете рассмотреть возможность их разделения.
Kroltan

Ответы:

87

Изменить: Если у вас есть 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__))
Siphor
источник
2
хороший ответ, но не будет работать с python2.7: нетsignature
MaxB
3
@alexis, decorator.decorator, декоратор автоматически оборачивает функцию
Siphor
4
Я очень волнуюсь, любить это или ненавидеть. Я ценю сохранение подписи.
Кайл Стрэнд,
14
«... Явное лучше, чем неявное. Простое лучше, чем сложное ...» - Дзен из Python
Джек Стаут,
9
-1 Откровенно говоря, это ужасно. Я понятия не имею, что этот код делает с первого взгляда, и это буквально в десять раз больше кода. Быть умным - это круто, но это неправильное использование твоего очевидного ума.
Ян Ньюсон
108

Отказ от ответственности: Кажется, что несколько человек обеспокоены представлением этого решения, поэтому я предоставлю очень четкую оговорку. Вы не должны использовать это решение. Я предоставляю это только в качестве информации, чтобы вы знали, что язык способен на это. Остальная часть ответа просто показывает языковые возможности, а не одобряет их использование таким образом.


Нет ничего плохого в том, чтобы явно копировать параметры в атрибуты. Если в 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
gruszczy
источник
16
хороший ответ +1 ... хотя self.__dict__.update(kwargs)может быть немного более питоническим
Джоран Бизли
44
Проблема с этим подходом состоит в том, что нет записи о том, что на A.__init__самом деле ожидают аргументы , и нет проверки ошибок для имен аргументов с ошибками.
MaxB
7
@JoranBeasley Обновление словаря экземпляров вслепую, kwargsоставляя вас открытым для эквивалента атаки SQL-инъекцией. Если у вашего объекта есть метод с именем my_methodи вы передаете аргумент с именем my_methodв конструктор, то update()в словаре вы просто переписали метод.
Педро
3
Как говорили другие, предложение действительно плохой стиль программирования. Это скрывает важную информацию. Вы можете показать это, но вы должны явно отговорить ОП использовать его.
Геренюк
3
@Pedro Есть ли смысловая разница между синтаксисом gruzczy и JoranBeasley?
Gerrit
29

Как уже упоминалось, повторение неплохое, но в некоторых случаях именованный кортеж отлично подходит для такого типа проблем. Это позволяет избежать использования 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

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

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
Они являются кортежи, так что ваш propertiesметод может быть записан как раз return tuple(self), что становится более сопровождаемым , если в будущем больше полей добавляются к определению namedtuple.
PaulMcG
1
Кроме того, ваша строка объявления namedtuple не требует запятых между именами полей, XYZ = namedtuple("XYZ", "x y z")работает так же хорошо.
PaulMcG
Спасибо @PaulMcGuire. Я пытался придумать действительно простую надстройку, чтобы показать наследование и вид этого. Вы на 100% правы, и это отличный способ справиться с другими наследуемыми объектами! Я упоминаю, что имена полей могут быть разделены запятыми или пробелами - я предпочитаю CSV из привычки
сценарий небольшой оболочки
1
Я часто использую namedtuples для этой конкретной цели, особенно в математическом коде, где функция может быть сильно параметризована и иметь кучу коэффициентов, которые имеют смысл только вместе.
детально
Проблема в namedtupleтом, что они доступны только для чтения. Вы не можете сделать abc.x += 1или что-нибудь в этом роде.
хомякген
29

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

def __init__(self,a,b,c):
    for k,v in locals().items():
        if k != "self":
             setattr(self,k,v)

Лучший вопрос, вы должны?

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

Джоран Бизли
источник
Тогда объект будет нуждаться в циклической сборке мусора, поскольку он сам является атрибутом
John La Rooy
3
@bernie (или это bemie?), иногда работать тяжело
кошка
4
Для чуть более эффективных тестов if k != "self":можно было бы заменить if v is not self:дешевый тест на идентичность, а не сравнение строк. Я полагаю, что технически __init__можно было бы вызвать второй раз после строительства и передать selfв качестве последующего аргумента, но я действительно не хочу думать, какой монстр это сделает. :-)
ShadowRanger
Это может быть сделано в функцию , которая принимает значение , возвращаемое locals: set_fields_from_locals(locals()). Тогда это не более, чем более волшебные решения на основе декораторов.
Лий
20

Чтобы расширить 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было бы более уместным.

Геррит
источник
10

Вы также можете сделать:

locs = locals()
for arg in inspect.getargspec(self.__init__)[0][1:]:
    setattr(self, arg, locs[arg])

Конечно, вам придется импортировать inspectмодуль.

zondo
источник
8

Это решение без какого-либо дополнительного импорта.

Вспомогательная функция

Небольшая вспомогательная функция делает ее более удобной и многократно используемой:

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())

Тест

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)
Майк Мюллер
источник
docs.python.org/2/library/functions.html#locals locals() не следует изменять (в вашем случае это может повлиять на удаление интерпретатора selfиз области видимости вызывающей функции)
MaxB
@MaxB Из документов, которые вы цитируете: ... изменения могут не влиять на значения локальных и свободных переменных, используемых интерпретатором. selfвсе еще доступен в __init__.
Майк Мюллер
Правильно, читатель ожидает, что это повлияет на локальные переменные, но это может или не может , в зависимости от ряда обстоятельств. Дело в том, что это UB.
MaxB
Цитата: «Содержимое этого словаря не должно изменяться»
MaxB
@MaxB Я добавил версию, которая не меняет locals ().
Майк Мюллер
7

Интересная библиотека, которая обрабатывает это (и избегает многих других шаблонов), является attrs . Ваш пример, например, может быть сведен к этому (предположим, что класс вызывается MyClass):

import attr

@attr.s
class MyClass:
    x = attr.ib()
    y = attr.ib()
    z = attr.ib()

Вам даже не нужен __init__метод больше, если он не делает другие вещи, а также. Вот хорошее введение от Глифа Лефковица .

RafG
источник
В какой степени функциональность attrстановится избыточной dataclasses?
gerrit
1
@gerrit Это обсуждается в документации пакета attrs . Тбх, различия уже не кажутся такими большими.
Иво Меркиерс
5

Мой 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__

bgusach
источник
1
docs.python.org/2/library/functions.html#locals locals() не следует изменять (неопределенное поведение)
MaxB
4

Это естественный способ делать вещи в Python. Не пытайтесь изобретать что-то более умное, это приведет к чрезмерно умному коду, который никто в вашей команде не поймет. Если вы хотите быть командным игроком, а затем продолжайте писать это так.

Питер Круминьш
источник
4

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, поэтому шаблоны использования еще не установлены.

Геррит
источник