Установка атрибутов по умолчанию / пусто для пользовательских классов в __init__ - PullRequest
2 голосов
/ 22 апреля 2019

У меня приличный уровень программирования, и я получаю большую пользу от сообщества здесь.Однако у меня никогда не было большого академического преподавания в программировании, и я не работал рядом с действительно опытными программистами.Следовательно, я иногда борюсь с «наилучшей практикой».

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

Вопрос:

Когда я создаю новый класс, я должен установить все атрибуты экземпляра в init , даже если они равны None, и фактически назначенные значения в классе позжеметоды?

См. Пример ниже для результатов атрибутов MyClass:

class MyClass:
    def __init__(self,df):
          self.df = df
          self.results = None

    def results(df_results):
         #Imagine some calculations here or something
         self.results = df_results

Я обнаружил, что в других проектах атрибуты класса могут быть похоронены, когда они появляются только в методах класса, и многое еще происходит.

Итак, для опытного профессионального программиста, что является стандартной практикой для этого?Определите ли вы все атрибуты экземпляра в init для удобства чтения?

И если у кого-нибудь есть ссылки на материалы о том, где я могу найти такие принципы, тогда, пожалуйста, поместите их в ответ, это будет оченьоценили.Я знаю о PEP-8 и уже несколько раз обыскивал мой вопрос выше, и не могу найти никого, кто бы касался этого.

Спасибо

Энди

Ответы [ 2 ]

1 голос
/ 23 апреля 2019

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

Посмотрите на две слегка измененные версии вашего класса с сеттером и геттером:

class MyClass1:
    def __init__(self, df):
          self.df = df
          self.results = None

    def set_results(self, df_results):
         self.results = df_results

    def get_results(self):
         return self.results

И

class MyClass2:
    def __init__(self, df):
          self.df = df

    def set_results(self, df_results):
         self.results = df_results

    def get_results(self):
         return self.results

Единственная разница между MyClass1 и MyClass2 заключается в том, что первый инициализирует results в конструкторе, а второй - в set_results. Сюда приходит пользователь вашего класса (обычно вы, но не всегда). Все знают, что вы не можете доверять пользователю (даже если это вы):

MyClass1("df").get_results()
# returns None

Или

MyClass2("df").get_results()
# Traceback (most recent call last):
# ...
# AttributeError: 'MyClass2' object has no attribute 'results'

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

Но это еще не весь ответ. Какую бы версию вы не выбрали, у вас есть проблема: объект не использовался и не должен был быть, потому что он не был полностью инициализирован. Вы можете добавить строку документации к get_results: """Always use set_results **BEFORE** this method""". К сожалению, пользователь также не читает строки документации.

У вас есть две основные причины неинициализированных полей в вашем объекте: 1. вы не знаете (пока) значение поля; 2. Вы хотите избежать экспансивных операций (вычислений, доступа к файлам, сети, ...), или «ленивой инициализации». Обе ситуации встречаются в реальном мире и сталкиваются с необходимостью использования только полностью инициализированных объектов.

К счастью, существует хорошо документированное решение этой проблемы: шаблоны проектирования, а точнее шаблоны создания . В вашем случае ответом может быть модель Factory или модель Builder. E.g.:

class MyClassBuilder:
    def __init__(self, df):
          self._df = df # df is known immediately
          # give a default value to other fields if possible

    def results(self, df_results):
         self._results = df_results
         return self # for fluent style

    ... other field initializers

    def build(self):
        return MyClass(self._df, self._results, ...)

class MyClass:
    def __init__(self, df, results, ...):
          self.df = df
          self.results = results
          ...

    def get_results(self):
         return self.results

    ... other getters

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

>>> b = MyClassBuilder("df").build()
Traceback (most recent call last):
...
AttributeError: 'MyClassBuilder' object has no attribute '_results'
>>> b = MyClassBuilder("df")
>>> b.results("r")
... other fields iniialization
>>> x = b.build()
>>> x
<__main__.MyClass object at ...>
>>> x.get_results()
'r'

Преимущества очевидны:

  1. Легче обнаружить и исправить ошибку создания, чем ошибку позднего использования;
  2. Вы не выпускаете неинициализированную (и, следовательно, потенциально разрушающую) версию вашего объекта в дикой природе.

Наличие неинициализированных полей в построителе не является противоречием: эти поля неинициализированы проектом, поскольку роль Строителя заключается в их инициализации. (На самом деле, эти поля являются некими передовыми полями для Builder.) Это тот случай, о котором я говорил в своем вступлении. На мой взгляд, их следует установить на значение по умолчанию (если оно существует) или оставить неинициализированным, чтобы вызвать исключение, если вы попытаетесь создать незавершенный объект.

Вторая часть моего ответа: используйте шаблон Creational для обеспечения правильной инициализации объекта .

Примечание: я очень подозрительно отношусь к классу с сеттерами и . Мое эмпирическое правило таково: всегда пытайтесь отделить их, потому что когда они встречаются, объекты становятся нестабильными.

0 голосов
/ 23 апреля 2019

Чтобы понять важность (или нет) инициализации атрибутов в __init__, давайте возьмем модифицированную версию вашего класса MyClass в качестве примера. Целью занятия является вычисление оценки по предмету с учетом имени ученика и балла. Вы можете следовать за ним в интерпретаторе Python.

>>> class MyClass:
...     def __init__(self,name,score):
...         self.name = name
...         self.score = score
...         self.grade = None
...
...     def results(self, subject=None):
...         if self.score >= 70:
...             self.grade = 'A'
...         elif 50 <= self.score < 70:
...             self.grade = 'B'
...         else:
...             self.grade = 'C'
...         return self.grade

Этот класс требует двух позиционных аргументов name и score. Эти аргументы должны быть предоставлены для инициализации экземпляра класса. Без них объект класса x не может быть создан, и TypeError будет вызван:

>>> x = MyClass()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: __init__() missing 2 required positional arguments: 'name' and 'score'

В этот момент мы понимаем, что мы должны предоставить name студента и score для предмета как минимум, но grade сейчас не важен, потому что это будет вычислено позже, в методе results. Итак, мы просто используем self.grade = None и не определяем его как позиционный аргумент. Давайте инициализируем экземпляр класса (объект):

>>> x = MyClass(name='John', score=70)
>>> x
<__main__.MyClass object at 0x000002491F0AE898>

<__main__.MyClass object at 0x000002491F0AE898> подтверждает, что объект класса x был успешно создан в данной ячейке памяти. Теперь Python предоставляет несколько полезных встроенных методов для просмотра атрибутов созданного объекта класса. Одним из методов является __dict__. Подробнее об этом можно прочитать здесь :

>>> x.__dict__
{'name': 'John', 'score': 70, 'grade': None}

Это ясно дает dict представление обо всех начальных атрибутах и ​​их значениях. Обратите внимание, что grade имеет значение None, назначенное в __init__.

Давайте на минутку поймем, что делает __init__. Существует множество ответов и онлайн-ресурсов, объясняющих, что делает этот метод, но я подведу итог:

Как и __init__, в Python есть еще один встроенный метод, который называется __new__(). Когда вы создаете объект класса, подобный этому x = MyClass(name='John', score=70), Python сначала вызывает __new__(), чтобы сначала создать новый экземпляр класса MyClass, а затем вызывает __init__, чтобы инициализировать атрибуты name и score. Конечно, в этих внутренних вызовах, когда Python не находит значения для требуемых позиционных аргументов, он вызывает ошибку, как мы видели выше. Другими словами, __init__ инициализирует атрибуты. Вы можете назначить новые начальные значения для name и score следующим образом:

>>> x.__init__(name='Tim', score=50)
>>> x.__dict__
{'name': 'Tim', 'score': 50, 'grade': None}

Также возможно получить доступ к отдельным атрибутам, как показано ниже. grade ничего не дает, потому что это None.

>>> x.name
'Tim'
>>> x.score
50
>>> x.grade
>>>

В методе results вы заметите, что «переменная» subject определяется как None, позиционный аргумент. Область действия этой переменной находится только внутри этого метода. В целях демонстрации я явно определяю subject внутри этого метода, но это также можно было бы инициализировать в __init__. Но что, если я попытаюсь получить к нему доступ с помощью своего объекта:

>>> x.subject
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'MyClass' object has no attribute 'subject'

Python вызывает AttributeError, когда он не может найти атрибут в пространстве имен класса. Если вы не инициализируете атрибуты в __init__, существует вероятность возникновения этой ошибки при доступе к неопределенному атрибуту, который может быть локальным только для метода класса. В этом примере определение subject внутри __init__ позволило бы избежать путаницы и было бы совершенно нормальным, поскольку это не требуется ни для каких вычислений.

Теперь, давайте позвоним results и посмотрим, что мы получим:

>>> x.results()
'B'
>>> x.__dict__
{'name': 'Tim', 'score': 50, 'grade': 'B'}

Это печатает оценку для оценки и уведомление, когда мы просматриваем атрибуты, grade также был обновлен. С самого начала у нас было четкое представление о начальных атрибутах и ​​о том, как их значения изменились.

А как же subject? Если я хочу узнать, сколько очков набрал Тим по математике и какую оценку получил, я могу легко получить доступ к score и grade, как мы видели ранее, но как мне узнать предмет? Поскольку переменная subject является локальной для области действия метода results, мы могли бы просто return значение subject. Измените оператор return в методе results:

def results(self, subject=None):
    #<---code--->
    return self.grade, subject

Давайте снова позвоним results(). Мы получаем кортеж с оценкой и предметом, как и ожидалось.

>>> x.results(subject='Math')
('B', 'Math')

Чтобы получить доступ к значениям в кортеже, давайте присвоим их переменным. В Python можно присваивать значения из коллекции нескольким переменным в одном и том же выражении при условии, что количество переменных равно длине коллекции. Здесь длина равна двум, поэтому мы можем иметь две переменные слева от выражения:

>>> grade, subject = x.results(subject='Math')
>>> subject
'Math'

Итак, у нас это есть, хотя для получения subject потребовалось несколько дополнительных строк кода. Было бы более интуитивно понятно получить доступ ко всем сразу, используя только оператор точки для доступа к атрибутам с x.<attribute>, но это всего лишь пример, и вы можете попробовать это с subject, инициализированным в __init__.

Далее, рассмотрим, что есть много студентов (скажем, 3), и мы хотим, чтобы имена, оценки, оценки по математике. За исключением предмета, все остальные должны быть своего рода типом данных коллекции, таким как list, который может хранить все имена, оценки и оценки. Мы могли бы просто инициализировать так:

>>> x = MyClass(name=['John', 'Tom', 'Sean'], score=[70, 55, 40])
>>> x.name
['John', 'Tom', 'Sean']
>>> x.score
[70, 55, 40]

На первый взгляд, это нормально, но когда вы по-другому (или какому-то другому программисту) смотрите на инициализацию name, score и grade в __init__, невозможно сказать, что им нужен тип данных коллекции. Переменные также названы в единственном числе, что делает более очевидным, что они могут быть просто случайными переменными, которым может понадобиться только одно значение. Задача программистов должна состоять в том, чтобы сделать намерение как можно более ясным с помощью описательных имен переменных, объявлений типов, комментариев к коду и так далее. Имея это в виду, давайте изменим объявления атрибутов в __init__. Прежде чем мы согласимся на хорошо себя ведущие , четко определенные декларации, мы должны позаботиться о том, как мы объявляем аргументы по умолчанию.


Редактировать : Проблемы с изменяемыми аргументами по умолчанию:

Теперь есть некоторые «ошибки», о которых мы должны знать при объявлении аргументов по умолчанию. Рассмотрим следующее объявление, которое инициализирует names и добавляет случайное имя при создании объекта. Напомним, что списки являются изменяемыми объектами в Python.

#Not recommended
class MyClass:
    def __init__(self,names=[]):
        self.names = names
        self.names.append('Random_name')

Посмотрим, что произойдет, когда мы создадим объекты из этого класса:

>>> x = MyClass()
>>> x.names
['Random_name']
>>> y = MyClass()
>>> y.names
['Random_name', 'Random_name']

Список продолжает расти с каждым новым созданием объекта. Причина этого заключается в том, что значения по умолчанию всегда оцениваются при вызове __init__. Вызывая __init__ несколько раз, продолжает использовать один и тот же объект функции, добавляя к предыдущему набору значений по умолчанию. Вы можете проверить это самостоятельно, так как id остается неизменным для каждого создания объекта.

>>> id(x.names)
2513077313800
>>> id(y.names)
2513077313800

Итак, как правильно определить аргументы по умолчанию, а также указать тип данных, поддерживаемый атрибутом? Самый безопасный вариант - установить для аргументов по умолчанию значение None и инициализировать пустой список, если значения аргументов равны None. Ниже приведен рекомендуемый способ объявления аргументов по умолчанию:

#Recommended
>>> class MyClass:
...     def __init__(self,names=None):
...         self.names = names if names else []
...         self.names.append('Random_name')

Давайте рассмотрим поведение:

>>> x = MyClass()
>>> x.names
['Random_name']
>>> y = MyClass()
>>> y.names
['Random_name']

Теперь, это поведение - то, что мы ищем. Объект не «переносит» старый багаж и повторно инициализирует пустой список всякий раз, когда никакие значения не передаются в names. Если мы передадим некоторые действительные имена (в виде списка курса) в аргумент names для объекта y, Random_name будет просто добавлен в этот список. И снова, значения объекта x не будут затронуты:

>>> y = MyClass(names=['Viky','Sam'])
>>> y.names
['Viky', 'Sam', 'Random_name']
>>> x.names
['Random_name']

Пожалуй, самое простое объяснение этой концепции можно найти на сайте Effbot . Если вы хотите прочитать несколько отличных ответов: «Наименьшее удивление» и изменяемый аргумент по умолчанию .


На основании краткого обсуждения аргументов по умолчанию наши объявления классов будут изменены на:

class MyClass:
    def __init__(self,names=None, scores=None):
        self.names = names if names else []
        self.scores = scores if scores else []
        self.grades = []
#<---code------>

Это имеет больше смысла, все переменные имеют множественные имена и инициализируются пустыми списками при создании объекта. Мы получаем аналогичные результаты, как и раньше:

>>> x.names
['John', 'Tom', 'Sean']
>>> x.grades
[]

grades - пустой список, дающий понять, что оценки будут рассчитываться для нескольких учащихся при вызове results().Поэтому наш метод results также должен быть изменен.Сравнения, которые мы проводим, теперь должны проводиться между номерами баллов (70, 50 и т. Д.) И пунктами в списке self.scores, и в то же время список self.grades также должен обновляться с отдельными оценками.Измените метод results на:

def results(self, subject=None):
    #Grade calculator 
    for i in self.scores:
        if i >= 70:
            self.grades.append('A')
        elif 50 <= i < 70:
            self.grades.append('B')
        else:
            self.grades.append('C')
    return self.grades, subject

Теперь мы должны получить оценки в виде списка, когда мы вызываем results():

>>> x.results(subject='Math')
>>> x.grades
['A', 'B', 'C']
>>> x.names
['John', 'Tom', 'Sean']
>>> x.scores
[70, 55, 40]

Это выглядит хорошо, но представьте, если спискибыли большими и выяснить, кому принадлежит оценка / оценка, было бы абсолютным кошмаром.Именно здесь важно инициализировать атрибуты с правильным типом данных, который может хранить все эти элементы таким образом, чтобы они были легко доступны, а также четко отображали их взаимосвязи.Лучший выбор здесь - это словарь.

У нас может быть словарь с именами и оценками, определенными изначально, и функция results должна собрать все в новый словарь, который имеет все оценки, оценки и т. Д. Мы должнытакже комментируйте код правильно и явно определяйте аргументы в методе везде, где это возможно.Наконец, мы можем больше не требовать self.grades в __init__, потому что, как вы увидите, оценки не добавляются в список, а назначаются явно.Это полностью зависит от требований проблемы.

Окончательный код :

class MyClass:
"""A class that computes the final results for students"""

def __init__(self,names_scores=None):

    """initialize student names and scores
    :param names_scores: accepts key/value pairs of names/scores
                         E.g.: {'John': 70}"""

    self.names_scores = names_scores if names_scores else {}     

def results(self, _final_results={}, subject=None):
    """Assign grades and collect final results into a dictionary.

       :param _final_results: an internal arg that will store the final results as dict. 
                              This is just to give a meaningful variable name for the final results."""

    self._final_results = _final_results
    for key,value in self.names_scores.items():
        if value >= 70:
            self.names_scores[key] = [value,subject,'A']
        elif 50 <= value < 70:
            self.names_scores[key] = [value,subject,'B']
        else:
            self.names_scores[key] = [value,subject,'C']
    self._final_results = self.names_scores #assign the values from the updated names_scores dict to _final_results
    return self._final_results

Обратите внимание: _final_results - это просто внутренний аргумент, в котором хранится обновленный dict self.names_scores.Цель состоит в том, чтобы вернуть более значимую переменную из функции, которая четко сообщает intent ._ в начале этой переменной указывает на то, что это внутренняя переменная, в соответствии с соглашением.

Позволяет выполнить окончательный запуск:

>>> x = MyClass(names_scores={'John':70, 'Tom':50, 'Sean':40})
>>> x.results(subject='Math')  

  {'John': [70, 'Math', 'A'],
 'Tom': [50, 'Math', 'B'],
 'Sean': [40, 'Math', 'C']}

Это дает более четкое представлениерезультатов для каждого студента.Теперь легко получить доступ к оценкам / оценкам для любого учащегося:

>>> y = x.results(subject='Math')
>>> y['John']
[70, 'Math', 'A']

Заключение :

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

  • Переменные (атрибуты), которые, как ожидается, будут общими для методов класса, должны быть определены в __init__.В нашем примере names, scores и, возможно, subject требовались для results().Эти атрибуты могут быть использованы другим методом, например, скажем average, который вычисляет среднее значение баллов.
  • Атрибуты должны быть инициализированы с соответствующим типом данных .Это должно быть решено заранее, прежде чем решиться на классовую схему решения проблемы.
  • Необходимо соблюдать осторожность при объявлении атрибутов с аргументами по умолчанию.Изменяемые аргументы по умолчанию могут изменять значения атрибута, если включающий __init__ вызывает мутацию атрибута при каждом вызове.Наиболее безопасно объявить аргументы по умолчанию как None и повторно инициализировать в пустую изменяемую коллекцию позже, когда значение по умолчанию будет None.
  • Имена атрибутов должны быть однозначными, следуйте рекомендациям PEP8.
  • Некоторые переменные должны быть инициализированы только в рамках метода класса.Это могут быть, например, внутренние переменные, которые требуются для вычислений, или переменные, которые не нужно использовать совместно с другими методами.
  • Еще одна веская причина для определения переменных в __init__ - избегать возможных AttributeError s, которые могут возникнуть из-за доступа к безымянным / не относящимся к области атрибутам.Встроенный метод __dict__ предоставляет представление об инициализированных здесь атрибутах.
  • При назначении значений атрибутам (позиционным аргументам) при создании экземпляра класса имена атрибутов должны быть явно определены.Например:

    x = MyClass('John', 70)  #not explicit
    x = MyClass(name='John', score=70) #explicit
    
  • Наконец, цель должна состоять в том, чтобы сообщить намерение как можно более четко с комментариями. Класс, его методы и атрибуты должны быть хорошо прокомментированы. Для всех атрибутов краткое описание вместе с примером весьма полезно для нового программиста, который впервые сталкивается с вашим классом и его атрибутами.

...