Хобрук: Ваш путь к мастерству в программировании

Почему разрешено добавление атрибутов к уже созданному объекту?

Я изучаю Python, и хотя я думаю, что понял всю концепцию и понятие Python, сегодня я наткнулся на фрагмент кода, который я не полностью понял:

Скажем, у меня есть класс, который должен определять круги, но не имеет тела:

class Circle():
    pass

Поскольку я не определил никаких атрибутов, как я могу это сделать:

my_circle = Circle()
my_circle.radius = 12

Странная часть заключается в том, что Python принимает приведенное выше утверждение. Я не понимаю, почему Python не вызывает undefined name error. Я понимаю, что с помощью динамической типизации я просто привязываю переменные к объектам, когда захочу, но разве в классе Circle не должен существовать атрибут radius, позволяющий мне это делать?

EDIT: в ваших ответах много полезной информации! Спасибо всем за все эти замечательные ответы! Жаль, что я могу отметить только один как ответ.


  • Когда вы инициализируете self.radius в __init__, разве вы не делаете то же самое? 24.09.2012
  • @JBernardo да, вы делаете, но в этом случае вы явно определяете атрибут radius для класса Circle(). В моем случае я не создавал никаких атрибутов в теле класса. 24.09.2012
  • @NlightNFotis Нет, вы делаете то же самое, потому что self - это такая же переменная, как и любая другая. 24.09.2012
  • @NlightNFotis Кроме того, Python — это не Java и язык, не влияет на то, как вы думаете о программировании, не стоит знать - [Алан Перлис](en.wikiquote.org/wiki/Alan_Perlis) 24.09.2012
  • @NlightNFotis Нет, это не так. Вы определяете функцию, которая присваивает значение атрибуту своего первого аргумента. Бывает, что на эту функцию ссылается атрибут __init__ класса, который вызывается после создания объекта. 24.09.2012
  • @delnan Не могли бы вы рассказать об этом немного подробнее, потому что это меня смутило? Я думал, что __init__() вызывается во время этого оператора, а my_circle = Circle() не после него. radius назначается после вызова метода __init()__, когда объект уже построен. 24.09.2012
  • @NlightNFotis Да, __init__ вызывается до добавления radius в вашем примере. Но вызывается он после создания объекта, и заменить его можно в любой момент. Синтаксис def __init__(self, ...) не является магическим заклинанием. Он просто определяет функцию, которая становится атрибутом объекта класса, а затем вызывается после создания объекта, но до того, как он будет возвращен и сохранен в my_circle. 24.09.2012
  • @NlightNFotis.. Есть два специальных метода - __init__ и __new__. Это метод __new__, который вызывается для создания экземпляра класса, тогда как метод __init__ используется для инициализации экземпляра после его создания. Таким образом, между двумя методами есть разница. Кроме того, метод __init__ принимает аргумент self, тогда как __new__ нет. (Вы можете подумать, почему?? Потому что экземпляр еще не создан..) 24.09.2012
  • Это разрешено, потому что это удобно. Представьте, что у вас есть объект, представляющий пользовательский интерфейс. Вы можете захотеть создавать виджеты на лету и сохранять их ссылку, сохраняя их в новых атрибутах класса окон вашей программы. 08.11.2019

Ответы:


1

Главный принцип заключается в том, что декларации не существует. То есть вы никогда не объявляете «этот класс имеет метод foo» или «экземпляры этого класса имеют панель атрибутов», не говоря уже о том, чтобы делать заявления о типах объектов, которые должны там храниться. Вы просто определяете метод, атрибут, класс и т. д., и они добавляются. Как указывает Дж. Бернардо, любой метод __init__ делает то же самое. Было бы бессмысленно произвольно ограничивать создание новых атрибутов методами с именем __init__. И иногда полезно хранить функцию как __init__, которая на самом деле не имеет этого имени (например, декораторы), и такое ограничение нарушит это.

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

Если вы хотите подстраховку, что ж, очень жаль. Python не предлагает его, и вы не можете разумно добавить его, и, что наиболее важно, его будут избегать программисты Python, которые используют этот язык (читай: почти все те, с которыми вы хотите работать). Тестирование и дисциплина по-прежнему имеют большое значение для обеспечения правильности. Не позволяйте создавать атрибуты за пределами __init__, если этого можно избежать, и проводите автоматизированное тестирование. У меня очень редко возникает AttributeError или логическая ошибка из-за подобных ухищрений, а из тех, что случаются, почти все отлавливаются тестами.

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

  • 2

    Просто чтобы прояснить некоторые недоразумения в обсуждениях здесь. Этот код:

    class Foo(object):
        def __init__(self, bar):
            self.bar = bar
    
    foo = Foo(5)
    

    И этот код:

    class Foo(object):
        pass
    
    foo = Foo()
    foo.bar = 5
    

    полностью эквивалентен. На самом деле нет никакой разницы. Он делает то же самое. Это отличие состоит в том, что в первом случае он инкапсулирован и понятно, что атрибут bar является нормальной частью объектов типа Foo. Во втором случае не понятно, что это так.

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

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

    24.09.2012
  • Каким может быть вариант использования второго? Это как бы ломает ООП, что, конечно, хорошо... Но если вы не программируете ООП, зачем вам в любом случае заботиться о наличии класса? Это не риторические вопросы, мне действительно интересно! 07.05.2021
  • Это не перестает быть ООП только потому, что вы не придерживаетесь догматизма. Второй случай все еще ООП. 10.05.2021

  • 3

    Python позволяет хранить атрибуты с любым именем практически в любом экземпляре (или классе, если уж на то пошло). Это можно заблокировать, либо написав класс на C, как встроенные типы, либо используя __slots__, который допускает только определенные имена.

    Причина, по которой это работает, заключается в том, что большинство экземпляров хранят свои атрибуты в словаре. Да, обычный словарь Python, который вы бы определили с помощью {}. Словарь хранится в атрибуте экземпляра с именем __dict__. На самом деле, некоторые люди говорят, что «классы — это просто синтаксический сахар для словарей». То есть вы можете делать все, что вы можете делать с классом со словарем; классы только облегчают.

    Вы привыкли к статическим языкам, где вы должны определять все атрибуты во время компиляции. В Python определения классов выполняются, а не компилируются; классы такие же объекты, как и любые другие; а добавление атрибутов так же просто, как добавление элемента в словарь. Вот почему Python считается динамическим языком.

    24.09.2012
  • Привет, так вы говорите, что в Python цель класса состоит не в том, чтобы связать данные и поведение (ООП), а скорее в том, чтобы определить цель данного словаря, придав ему какой-то удобочитаемый синтаксис? 07.05.2021
  • Вы можете использовать их для объединения данных и поведения, и синтаксис поощряет это, и он работает в основном так, как вы ожидаете от ООП (хотя и не сильная версия ООП — инкапсуляция в Python довольно слабая, поскольку нет закрытых атрибутов). Но под синтаксисом классов классы — это, по сути, словари с некоторым дополнительным (очень полезным) поведением сверху. Если бы вы писали компилятор, вы, вероятно, использовали бы словарь (хэш), чтобы отслеживать членов класса во время определения; Python просто делает это во время выполнения. 08.05.2021
  • Благодарю за разъяснение! 08.05.2021

  • 4

    Нет, python настолько гибок, он не определяет, какие атрибуты вы можете хранить в пользовательских классах.

    Однако есть хитрость: использование атрибута __slots__ в определении класса не позволит вам от создания дополнительных атрибутов, не определенных в последовательности __slots__:

    >>> class Foo(object):
    ...     __slots__ = ()
    ... 
    >>> f = Foo()
    >>> f.bar = 'spam'
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    AttributeError: 'Foo' object has no attribute 'bar'
    >>> class Foo(object):
    ...     __slots__ = ('bar',)
    ... 
    >>> f = Foo()
    >>> f.bar
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    AttributeError: bar
    >>> f.bar = 'spam'
    
    24.09.2012
  • Но что происходит, когда я хочу обеспечить некоторую защиту от ошибок, как это делает код Java? Я имею в виду, что в java, если у меня нет общедоступной переменной с именем radius или частной с методами доступа, я не могу ссылаться на нее или присваивать ей значения. Как я мог сделать это в java? Я знаю о соглашении _variable, но это только одно: соглашение. 24.09.2012
  • @NlightNFotis Нет. Python — это не Java, и вам не следует пытаться писать Java на Python. Если вы хотите сделать это, напишите Java. 24.09.2012
  • @NlightNFotis: Обычно вы не знаете в python. Вы используете модульные тесты и пользуетесь гибкостью. 24.09.2012
  • Разделите радость программирования без подстраховки :) 24.09.2012
  • @NlightNFotis: безопасность типов Java - это иллюзия. Это кажется более безопасным и надежным, и вы можете больше доверять коду, но на самом деле это не так. Торт ложь. 24.09.2012
  • @LennartRegebro Не могли бы вы объяснить это немного подробнее? 24.09.2012
  • @NlightNFotis Он может ссылаться на факты, что (1) изменение доступа и тому подобное можно обойти с помощью отражения, и (2) статическую типизацию можно обойти с помощью приведения и (3) система типов настолько ограничена, что Java кодирует только самые основные гарантии, которые охватывают лишь очень небольшое подмножество корректности и безопасности приложений. (Существуют системы типов, которые гарантируют все больше, но становятся все более сложными в использовании и понимании.) 24.09.2012
  • @NlightNFotis: У Python есть философия, описываемая как то, что мы все здесь взрослые по обоюдному согласию. Это означает, что если вы хотите нарушить соглашение и изменить foo._variable напрямую - возможно, для отладки или во избежание какой-либо ошибки в foo - вы можете это сделать. Но вы не можете пожаловаться автору foo, если он что-то сломает. 24.09.2012
  • __slots__ используется для экономии памяти; это не должно использоваться как способ блокировки класса. 24.09.2012
  • Итак, если вы хотите заблокировать его, лучше всего определить свою собственную функцию __setattr__ в своем классе? 09.09.2014
  • @RufusVS: Даже __setattr__ можно обойти, но да. 09.09.2014
  • Просто спас меня от сумасшествия - пытался понять, почему self.attr = value терпит неудачу - в классе были определены слоты... 10.03.2016

  • 5

    Он создает элемент данных radius из my_circle.

    Если бы вы запросили my_circle.radius, он бы выдал исключение:

    >>> print my_circle.radius # AttributeError
    

    Интересно, что это не меняет класс; только тот экземпляр. Так:

    >>> my_circle = Circle()
    >>> my_circle.radius = 5
    >>> my_other_circle = Circle()
    >>> print my_other_circle.radius # AttributeError
    
    24.09.2012
  • хотя вы могли бы сделать Circle.xyz = 5 и изменить класс, а не экземпляр... 24.09.2012

  • 6

    В Python есть два типа атрибутов — Class Data Attributes и Instance Data Attributes.

    Python дает вам возможность создавать Data Attributes на лету.

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

    class Demo(object):
        classAttr = 30
        def __init__(self):
             self.inInit = 10
    
    demo = Demo()
    demo.outInit = 20
    Demo.new_class_attr = 45; # You can also create class attribute here.
    
    print demo.classAttr  # Can access it 
    
    del demo.classAttr         # Cannot do this.. Should delete only through class
    
    demo.classAttr = 67  # creates an instance attribute for this instance.
    del demo.classAttr   # Now OK.
    print Demo.classAttr  
    

    Итак, вы видите, что мы создали два атрибута экземпляра, один внутри __init__ и один снаружи, после создания экземпляра.

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

    Это отличается от Java, где каждый экземпляр класса имеет одинаковый набор переменных экземпляра.

    • ПРИМЕЧАНИЕ. - Хотя вы можете получить доступ к атрибуту класса через экземпляр, вы не можете его удалить. Кроме того, если вы попытаетесь изменить атрибут класса через экземпляр, вы фактически создадите атрибут экземпляра, который затеняет атрибут класса.
    24.09.2012
  • Нет, вы также не объявляете атрибуты класса. Вы определяете их. Эти определения являются исполняемыми операторами и совершенно обычными, просто вместо того, чтобы манипулировать областью действия какой-либо функции, они манипулируют атрибутами класса. И атрибуты класса тоже не высечены на камне: добавлять, заменять и удалять атрибуты класса тривиально. 24.09.2012
  • Я до сих пор не понимаю, почему вы вначале различаете атрибуты класса и экземпляра. Оба определены явно, в обоих случаях во время выполнения, и в обоих случаях эти определения и переопределения могут произойти в любое время. 24.09.2012

  • 7

    Как сказал Делнан, вы можете получить такое поведение с помощью атрибута __slots__. Но тот факт, что это способ сэкономить место в памяти и тип доступа, не отменяет того факта, что это (также) средство для отключения динамических атрибутов.

    Отключение динамических атрибутов разумно, хотя бы для предотвращения мелких ошибок из-за орфографических ошибок. «Тестирование и дисциплина» — это хорошо, но полагаться на автоматизированную проверку, безусловно, тоже правильно — и не обязательно не питонически.

    Кроме того, поскольку библиотека attrs достигла версии 16 в 2016 году (очевидно, намного позже исходного вопроса и ответов), создание закрытого класса со слотами никогда не было проще.

    >>> import attr
    ...
    ... @attr.s(slots=True)
    ... class Circle:
    ...   radius = attr.ib()
    ...
    ... f = Circle(radius=2)
    ... f.color = 'red'
    AttributeError: 'Circle' object has no attribute 'color'
    
    07.03.2018
  • Другой механизм отключения динамических атрибутов, который не использует слоты и, следовательно, не нарушает наследование: from pystrict import strict \n @strict \n class Circle: ... 22.11.2019

  • 8

    Чтобы управлять созданием новых атрибутов, вы можете перезаписать метод __setattr__. Он будет вызываться каждый раз, когда вызывается my_obj.x = 123.

    См. документацию:

    class A():
      def __init__(self):
        # Call object.__setattr__ to bypass the attribute checking
        super().__setattr__('x', 123)
    
      def __setattr__(self, name, value):
        # Cannot create new attributes
        if not hasattr(self, name):
          raise AttributeError('Cannot set new attributes')
        # Can update existing attributes
        super().__setattr__(name, value)
    
    a = A()
    a.x = 123  # Allowed
    a.y = 456  # raise AttributeError
    

    Обратите внимание, что пользователи по-прежнему могут обойти проверку, если они вызывают напрямую object.__setattr__(a, 'attr_name', attr_value).

    13.01.2020
    Новые материалы

    Dall-E 2: недавние исследования показывают недостатки в искусстве, созданном искусственным интеллектом
    DALL-E 2 — это всеобщее внимание в индустрии искусственного интеллекта. Люди в списке ожидания пытаются заполучить продукт. Что это означает для развития креативной индустрии? О применении ИИ в..

    «Очень простой» эволюционный подход к обучению с подкреплением
    В прошлом семестре я посетил лекцию по обучению с подкреплением (RL) в моем университете. Честно говоря, я присоединился к нему официально, но я редко ходил на лекции, потому что в целом я нахожу..

    Освоение информационного поиска: создание интеллектуальных поисковых систем (глава 1)
    Глава 1. Поиск по ключевым словам: основы информационного поиска Справочная глава: «Оценка моделей поиска информации: подробное руководство по показателям производительности » Глава 1: «Поиск..

    Фишинг — Упаковано и зашифровано
    Будучи старшим ИТ-специалистом в небольшой фирме, я могу делать много разных вещей. Одна из этих вещей: специалист по кибербезопасности. Мне нравится это делать, потому что в настоящее время я..

    ВЫ РЕГРЕСС ЭТО?
    Чтобы понять, когда использовать регрессионный анализ, мы должны сначала понять, что именно он делает. Вот простой ответ, который появляется, когда вы используете Google: Регрессионный..

    Не зря же это называют интеллектом
    Стек — C#, Oracle Опыт — 4 года Работа — Разведывательный корпус Мне пора служить Может быть, я немного приукрашиваю себя, но там, где я живу, есть обязательная военная служба на 3..

    LeetCode Проблема 41. Первый пропущенный положительный результат
    LeetCode Проблема 41. Первый пропущенный положительный результат Учитывая несортированный массив целых чисел, найдите наименьшее пропущенное положительное целое число. Пример 1: Input:..