Why is adding attributes to an already instantiated object allowed?
Asked Answered
B

9

77

I am studying python, and although I think I get the whole concept and notion of Python, today I stumbled upon a piece of code that I did not fully understand:

Say I have a class that is supposed to define Circles but lacks a body:

class Circle():
    pass

Since I have not defined any attributes, how can I do this:

my_circle = Circle()
my_circle.radius = 12

The weird part is that Python accepts the above statement. I don't understand why Python doesn't raise an undefined name error. I do understand that via dynamic typing I just bind variables to objects whenever I want, but shouldn't an attribute radius exist in the Circle class to allow me to do this?

EDIT: Lots of wonderful information in your answers! Thank you everyone for all those fantastic answers! It's a pity I only get to mark one as an answer.

Blinny answered 24/9, 2012 at 16:20 Comment(9)
When you initialize self.radius at the __init__ aren't you doing exactly the same thing?Liquid
@Liquid yes you do, but in this case, you are explicitly defining a radius attribute for class Circle(). In my case I didn't create any attribute in the class body.Blinny
@Blinny No, you are doing the same thing because the self is just a variable like any other.Liquid
@Blinny Also, Python is not Java and a language that doesn't affect the way you think about programming, is not worth knowing - [Alan Perlis](en.wikiquote.org/wiki/Alan_Perlis)Liquid
@Blinny No you aren't. You define a function, which assigns to an attribute of its first argument. It happens that this function is referenced by the __init__ attribute of a class which happens to be invoked after object construction.Cesta
@delnan Could you elaborate a little bit more on that because it confused me? I thought __init__() is called during this statement: my_circle = Circle() not after it. radius is assigned after the __init()__ method has been called and the object is already constructed.Blinny
@Blinny Yes, __init__ is called before radius is added in your example. But it is called after the object is created, and you can replace it at any time. The syntax def __init__(self, ...) isn't a magic incantation. It simply defines a function which become an attribute of the class object and then is called after the object is created but before it is returned and stored in my_circle.Cesta
@NlightNFotis.. There are two special methods - __init__ and __new__. It is the __new__ method that is called to create a class's instance, whereas, __init__ method is used to initialize the instance after it has been created.. So, there is a difference between the two methods.. Also, __init__ method takes an argument self whereas __new__ does not. (You can think why?? Because an instance has not been created yet..)Singer
It's allowed because it's handy. Imagine having an object that represents a UI. You might want to create widgets on the fly, and keep their reference by storing them in new attributes of your "program window" class.Displacement
C
60

A leading principle is that there is no such thing as a declaration. That is, you never declare "this class has a method foo" or "instances of this class have an attribute bar", let alone making a statement about the types of objects to be stored there. You simply define a method, attribute, class, etc. and it's added. As JBernardo points out, any __init__ method does the very same thing. It wouldn't make a lot of sense to arbitrarily restrict creation of new attributes to methods with the name __init__. And it's sometimes useful to store a function as __init__ which don't actually have that name (e.g. decorators), and such a restriction would break that.

Now, this isn't universally true. Builtin types omit this capability as an optimization. Via __slots__, you can also prevent this on user-defined classes. But this is merely a space optimization (no need for a dictionary for every object), not a correctness thing.

If you want a safety net, well, too bad. Python does not offer one, and you cannot reasonably add one, and most importantly, it would be shunned by Python programmers who embrace the language (read: almost all of those you want to work with). Testing and discipline, still go a long way to ensuring correctness. Don't use the liberty to make up attributes outside of __init__ if it can be avoided, and do automated testing. I very rarely have an AttributeError or a logical error due to trickery like this, and of those that happen, almost all are caught by tests.

Cesta answered 24/9, 2012 at 16:28 Comment(1)
its nice in the way but flexibility, also raises readbility issues, when maintaining other peopls code, I constently forgot what attriute the object has at a certain point.Mosier
O
51

Just to clarify some misunderstandings in the discussions here. This code:

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

foo = Foo(5)

And this code:

class Foo(object):
    pass

foo = Foo()
foo.bar = 5

is exactly equivalent. There really is no difference. It does exactly the same thing. This difference is that in the first case it's encapsulated and it's clear that the bar attribute is a normal part of Foo-type objects. In the second case it is not clear that this is so.

In the first case you can not create a Foo object that doesn't have the bar attribute (well, you probably can, but not easily), in the second case the Foo objects will not have a bar attribute unless you set it.

So although the code is programatically equivalent, it's used in different cases.

Overview answered 24/9, 2012 at 17:2 Comment(2)
What would a use case of the second be? It sort of breaks OOP, which of course is fine... But if you aren't programming OOP why would you care about having a class anyway? These are not rhetorical questions, I'm genuinely curious!Ossian
It doesn't stop being OOP just because you aren't dogmatic about it. The second case is still OOP.Overview
U
20

No, python is flexible like that, it does not enforce what attributes you can store on user-defined classes.

There is a trick however, using the __slots__ attribute on a class definition will prevent you from creating additional attributes not defined in the __slots__ sequence:

>>> 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'
Underclassman answered 24/9, 2012 at 16:22 Comment(12)
But what happens when I want to enforce some error safety like java code does? I mean that in java if I don't have a public variable named radius, or a private one with accessor methods I can not reference it, or assign values to it. How could I do so in java? I know about the _variable convention, but it is only this: a convention.Blinny
@Blinny You don't. Python isn't Java and you shouldn't try to write Java in Python. If you want to do that, write Java.Countercharge
@NlightNFotis: You generally don't in python. You use unit tests and embrace the flexibility.Underclassman
Share the joy of programming without a safety net :)Southeastwardly
@NlightNFotis: The type safety of Java is a an illusion. It feels like it's safer and more secure and that you can trust the code more, but you really can't. The cake is a lie.Overview
@LennartRegebro Could you explain it a little bit more?Blinny
@Blinny He may refer to the facts that (1) access modifies and the like can be circumvented via reflection and (2) static typing can be circumvented using casts and (3) a type system as limited as Java's only encodes very basic guarantees, which cover only a very tiny subset of application correctness and security. (There are type systems which guarantee progressively more but become progressively harder to use and understand.)Cesta
@NlightNFotis: Python has a philosophy described as "we're all consenting adults here". It means that if you want to break with convention and change foo._variable directly - perhaps for debugging, or to avoid some bug in foo - you can. But you don't get to complain to the author of foo if it breaks something.Talc
__slots__ is there as a memory-savings enhancement; it should not be used as a way to lockdown a class.Linchpin
So if you want to lock it down, is it best to define your own __setattr__ function in your class?Shotgun
@RufusVS: Even __setattr__ can be bypassed, but yes.Underclassman
Just saved me from a slip of madness - was trying to figure out why self.attr = value was failing - the class had slots defined...Rameau
E
20

Python lets you store attributes of any name on virtually any instance (or class, for that matter). It's possible to block this either by writing the class in C, like the built-in types, or by using __slots__ which allows only certain names.

The reason it works is that most instances store their attributes in a dictionary. Yes, a regular Python dictionary like you'd define with {}. The dictionary is stored in an instance attribute called __dict__. In fact, some people say "classes are just syntactic sugar for dictionaries." That is, you can do everything you can do with a class with a dictionary; classes just make it easier.

You're used to static languages where you must define all attributes at compile time. In Python, class definitions are executed, not compiled; classes are objects just like any other; and adding attributes is as easy as adding an item to a dictionary. This is why Python is considered a dynamic language.

Ecdysis answered 24/9, 2012 at 16:27 Comment(2)
Hi, so are you saying that in Python, the purpose of a class is not to bundle data and behaviors (OOP), but rather just to make identify the purpose of a given dictionary by dressing it up with some human-readable syntax?Ossian
You can use them to bundle data and behaviors and the syntax encourages that, and it works basically like you'd expect for OOP (though not a strong version of OOP—encapsulation is pretty weak in Python since there are no private attributes). But, underneath the class syntax, classes are basically dictionaries with some extra (very useful) behavior on top. If you were writing a compiler you'd likely use a dictionary (hash) to keep track of the members of a class during definition; Python just does that at runtime.Ecdysis
E
7

It creates a radius data member of my_circle.

If you had asked it for my_circle.radius it would have thrown an exception:

>>> print my_circle.radius # AttributeError

Interestingly, this does not change the class; just that one instance. So:

>>> my_circle = Circle()
>>> my_circle.radius = 5
>>> my_other_circle = Circle()
>>> print my_other_circle.radius # AttributeError
Erasmoerasmus answered 24/9, 2012 at 16:24 Comment(1)
although you could do Circle.xyz = 5 and change the class rather than the instance...Early
S
4

There are two types of attributes in Python - Class Data Attributes and Instance Data Attributes.

Python gives you flexibility of creating Data Attributes on the fly.

Since an instance data attribute is related to an instance, you can also do that in __init__ method or you can do it after you have created your instance..

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  

So, you see that we have created two instance attributes, one inside __init__ and one outside, after instance is created..

But a difference is that, the instance attribute created inside __init__ will be set for all the instances, while if created outside, you can have different instance attributes for different isntances..

This is unlike Java, where each Instance of a Class have same set of Instance Variables..

  • NOTE: - While you can access a class attribute through an instance, you cannot delete it.. Also, if you try to modify a class attribute through an instance, you actually create an instance attribute which shadows the class attribute..
Singer answered 24/9, 2012 at 16:34 Comment(2)
No, you don't declare class attributes either. You define them. Those definitions are executable statements and perfectly ordinary, just instead of manipulating some function's scope, they manipulate the class's attributes. And the class attributes aren't set in stone either: It's trivial to add, replace and remove class attributes.Cesta
I still don't see why you distinguish between class and instance attributes in the beginning. Both are explicitly defined, in both cases at runtime, and in both cases these definitions and re-definitions can happen at any time.Cesta
C
3

To add to Conchylicultor's answer, Python 3.10 added a new parameter to dataclass.

The slots parameter will create the __slots__ attribute in the class, preventing creation of new attributes outside of __init__, but allowing assignments to existing attributes.

If slots=True, assigning to an attribute that was not defined will throw an AttributeError.

Here is an example with slots and with frozen:

from dataclasses import dataclass

@dataclass
class Data:
    x:float=0
    y:float=0

@dataclass(frozen=True)
class DataFrozen:
    x:float=0
    y:float=0

@dataclass(slots=True)
class DataSlots:
    x:float=0
    y:float=0

p = Data(1,2)
p.x = 5 # ok
p.z = 8 # ok

p = DataFrozen(1,2)
p.x = 5 # FrozenInstanceError
p.z = 8 # FrozenInstanceError

p = DataSlots(1,2)
p.x = 5 # ok
p.z = 8 # AttributeError
Crotchety answered 10/2, 2022 at 16:25 Comment(0)
M
2

How to prevent new attributes creation ?

Using class

To control the creation of new attributes, you can overwrite the __setattr__ method. It will be called every time my_obj.x = 123 is called.

See the documentation:

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

Note that users can still bypass the checking if they call directly object.__setattr__(a, 'attr_name', attr_value).

Using dataclass

With dataclasses, you can forbid the creation of new attributes with frozen=True. It will also prevent existing attributes to be updated.

@dataclasses.dataclass(frozen=True)
class A:
  x: int


a = A(x=123)
a.y = 123  # Raise FrozenInstanceError
a.x = 123  # Raise FrozenInstanceError

Note: dataclasses.FrozenInstanceError is a subclass of AttributeError

Mothering answered 13/1, 2020 at 6:59 Comment(0)
F
1

As delnan said, you can obtain this behavior with the __slots__ attribute. But the fact that it is a way to save memory space and access type does not discard the fact that it is (also) a/the mean to disable dynamic attributes.

Disabling dynamic attributes is a reasonable thing to do, if only to prevent subtle bugs due to spelling mistakes. "Testing and discipline" is fine but relying on automated validation is certainly not wrong either – and not necessarily unpythonic either.

Also, since the attrs library reached version 16 in 2016 (obviously way after the original question and answers), creating a closed class with slots has never been easier.

>>> 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'
Frau answered 7/3, 2018 at 13:7 Comment(1)
Another mechanism to disable dynamic attributes that doesn't use slots and therefore doesn't break inheritance: from pystrict import strict \n @strict \n class Circle: ...Annabel

© 2022 - 2024 — McMap. All rights reserved.