Как мне избежать «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?


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

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

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модуль.


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__метод больше, если он не делает другие вещи, а также. Вот хорошее введение от Глифа Лефковица .


В какой степени функциональность 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__


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

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.