Як я уникаю “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
"Не" - хороший варіант, будь-яка доступна опція знищить підпис методу (і, таким чином, потенційно весь інтерфейс). Крім того, якщо у ваших класах є нестерпна кількість полів для ініціалізації, ви, можливо, захочете їх розділити.
Кролтан

Відповіді:


87

Редагувати: Якщо у вас пітон 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
Jack Stout

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's та JoranBeasley?
Герріт

29

Як уже згадували інші, повторення непогано, але в деяких випадках названий набір може бути чудовим пристосуванням для такого типу питань. Це дозволяє уникнути використання місцевих жителів () або карворгів, що зазвичай є поганою ідеєю.

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

Я знайшов обмежене використання для нього, але ви можете успадкувати найменування nametuple, як і будь-який інший об'єкт (приклад продовжується):

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
Крім того, для вашого рядка декларування найменування не потрібні коми після імен полів, він XYZ = namedtuple("XYZ", "x y z")працює так само добре.
PaulMcG

Дякую @PaulMcGuire Я намагався придумати дійсно просту надбудову, щоб показати спадщину та вид між ними. Ви на 100% праві, і це чудова стенограма з іншими успадкованими об'єктами! Я згадую, що назви полів можуть бути розділені комою або пробілом - я віддаю перевагу CSV за звичкою
Маленький сценарій оболонки

1
Я часто використовую namedtuples для цієї точної мети, особливо в математичному коді, де функція може бути сильно параметризована і мати купу коефіцієнтів, які мають сенс разом.
detly

Проблема namedtupleполягає в тому, що вони доступні лише для читання. Ти не можеш робити abc.x += 1нічого подібного.
хом'як

29

явне краще, ніж неявне ... тож переконайтеся, що ви можете зробити це більш стислим:

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

Питання краще, чи не варто?

... що сказав, що ви хочете назвати кортеж, я б рекомендував використовувати nametuple (пам’ятайте, кортежі мають до них певні умови) ... можливо, ви хочете впорядкований вирок або навіть просто наказ ...


Тоді об’єкт потребує циклічного збирання сміття, оскільки він є атрибутом
Джон Ла Руй

3
@bernie (чи це bemie?), інколи важко зайнятись
кіт

4
Для трохи ефективніших тестів if k != "self":може бути змінено на if v is not self:дешевий тест ідентичності, а не порівняння рядків. Я припускаю, що технічно це __init__можна назвати вдруге після будівництва і передавати його selfяк наступний аргумент, але я дійсно не хочу думати, який би монстр це зробив би. :-)
ShadowRanger

Це може бути зроблено в функцію , яка приймає значення , що повертається locals: set_fields_from_locals(locals()). Тоді це не більше, ніж більш магічні рішення на основі декораторів.
Лій

20

Для розширення gruszczyвідповіді s я використав такий зразок:

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 Я додав версію, яка не змінює місцевих жителів ().
Майк Мюллер

7

Цікава бібліотека, яка обробляє це (і уникає безлічі інших котельних плит) - attrs . Наприклад, ваш приклад можна звести до цього (припустимо, клас називається MyClass):

import attr

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

Вам навіть __init__метод більше не потрібен , якщо тільки це не робить і інші речі. Ось приємний вступ Гліфа Лефковіца .


Наскільки функціонально attrскладене зайве dataclasses?
Герріт

1
@gerrit Це обговорюється в документації пакета attrs . Tbh, відмінності вже не здаються такими великими.
Іво Мерч'є

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.