Function attributes are a relatively little-known corner of the Python language. Ultimately, this is because most of the problems they may solve are arguably better solved by a different approach (as will be shown). However, understanding why they exist may highlight some interesting cornerstones of Python's design to beginners or even intermediate users.
Prior to beginning, I'd like to make it clear that my purpose is not to proselytize, merely inform. Function attributes appears to be a topic that has not been blogged-to-death so I thought they might be worthwhile to discuss at some length.
Function Attributes 101
Attributes in general should be most familiar in the context of custom classes.1 For the sake of introduction and comparison, consider this example:
class Foo: some_attribute = 'spam' another_attribute = 'eggs' foo_instance = Foo() Foo.yet_another_attribute = 'a dynamic attribute' print(foo_instance.some_attribute) # Output: spam print(foo_instance.yet_another_attribute) # Output: a dynamic attribute
In the example above, some_attribute
and another_attribute
are attributes of the class Foo
. They are class attributes, as opposed to instance attributes, though understanding the distinction is not particularly important for the purpose of this article (IRL is another matter). In line 6, a third class attribute named yet_another_attribute
is added; it is called a dynamic attibute because it is added at runtime, i.e. after the class definition.
A Minimal Example
Back to the topic at hand, it is also possible in Python to add attributes to function objects in the same way(s) they can be added to custom classes, such as in the Foo
example above. Consider the function shout
:
def shout(s): print(str(s).upper() + '!') shout('albatross')
Output:
ALBATROSS!
Attributes can be added to this function both within the function definition and at runtime, as shown below:
def shout(s): # Ignore (for now) the potential pitfalls of the function referring # to itself by name. shout.some_attribute = 'Function attribute' print(str(s).upper() + '!') shout('burma') shout.another_attribute = "Another function attribute" print(shout.some_attribute) print(shout.another_attribute)
Output:
BURMA! Function attribute Another function attribute
Note: The potential problems related to functions referring to themselves by name is discussed in Function Names Are Not Magic elsewhere on this blog.
As the example shows, attributes can be assigned to shout
, a function object, just as they were to the custom type Foo
in the earlier example, and are accessed in the same object.attribute
syntax. This is because class and function attributes are fundamentally the same - the topic of the next section. Additionally, the function of course continues to be callable and work as intended.
Under the Hood
This section delves into what's happening under the hood with regard to function attributes. This information mostly comes from the Standard Type Hierarchy section of the Data Model portion of the docs, which I suggest reading if you want to know more. If magic methods and __dict__
are old news to you, I suggest skipping ahead to the next section.
First-class objects
In Python, class and function attributes are fundamentally the same because classes and functions are fundamentally the same - they are both objects. A basic cirriculum in programming typically makes object synonymous with "instance of a class." However, to delve into the innards of the language, it is crucial to understand that everything is an object in modern Python (i.e. 3.x, and new-style classes in 2.1+).2
Practically speaking, everything being an object means everything is an instance of a class that inherits from object
. Integers, booleans, etc. are not primitives as they are in languages such as C, C#, and Java - they are all instances of classes whose top-level parent is object
. A function is no exception; calling type
on a typical function results in <class 'function'>
.
Beyond being objects, essentially everything in Python is a first-class object, which means anything may generally be assigned to, passed as an argument, and returned from a function. For example, even a module in Python may be passed to a function. Though not typically included in the definition, the setting and getting of attributes on objects can be thought of as an extension of this concept in Python, as by default anything that inherits from object
has these capabilities, though they can be overridden.
For more about this topic, see this helpful StackOverflow question and its answers.
__dict__ and __setattr__
As stated previously, functions are first-class objects in Python, and implement a lot of the same functionality via so-called "magic methods" as e.g. user-created classes. An illustration:
class C: pass def f(): pass print('Intersection of class and function attributes') for attr in set(dir(f)) & set(dir(C)): print(attr)
Output:
Intersection of class and function attributes __le__ __reduce__ __init__ __sizeof__ __class__ __ge__ __new__ __gt__ __format__ __doc__ __eq__ __repr__ __reduce_ex__ __lt__ __dir__ __dict__ __delattr__ __str__ __setattr__ __module__ __hash__ __getattribute__ __ne__ __subclasshook__
As per the docs, note that dir()
is not guaranteed to return every possible attribute on an object. However, the above list serves as an important demonstration for this discussion because relevant attributes are present.
The most relevant attributes to this discussion in the intersection list above are __dict__
and __setattr__
. These are the underlying reason that runtime attributes are possible in both functions and custom classes. In code such as...
class Foo: pass s = Foo() s.bar = 5
...under the hood, the final line results in a call to __setattr__
with eggs
and 5
as the arguments; __setattr__
adds the attribute name eggs
to the instance's __dict__
attribute, and maps it to the value 5
. This is explicitly illustrated in the following example:
class Foo: def __setattr__(self, *args): print('setattr got:', args) super().__setattr__(*args) f = Foo() print(f.__dict__) f.bar = 5 print(f.__dict__)
Output:
{} setattr got: ('bar', 5) {'bar': 5}
The __dict__
of a custom class instance contains the current instance attribute dictionary; class attributes would be held in the class object's __dict__
. It is important to note that local variables do not appear in an object's __dict__
. This, of course, falls in line with proper Python scoping as such variables should not be visible externally.
Functions work in the same manner:
def foo(): local_variable = True print(foo.__dict__) foo.bar = 5 print(foo.__dict__)
Output:
{} {'bar': 5}
The same process occurred in the function's case: at the line foo.bar = 5
, foo.__setattr__
was called which added 'bar'
to the function's __dict__
with a value of 5.
Note that local_variable
never appears in foo.__dict__
for the scope reasons cited previously. A function's __dict__
will typically be empty; it is only used to hold function attributes. In fact, functions did not have a __dict__
attribute before the introduction of function attributes in PEP 232.3
Conclusion
There are inherently two questions being asked when asking "why" function attributes exist in Python: 1) why they exist from a language design perspective, and 2) why they exist from a pragmatic, use-case perspective. This article has attempted to elucidate much of the answer to the first question. In short, functions, as first-class objects in Python, implement much of the same functionality as other user-defined objects, including the dynamic setting of attributes. Given Python's propensity to empower rather than restrict, it's reasonable that functions, as objects, should have this capability. The second question is a subject of some debate, and is the topic of the future articles in this series.
Part II of this series will focus on providing a more practical example of the use of a function attribute, and introduce alternative means to similar functionality.
Notes
-
The specificity of custom classes is necessary. Dynamic attributes cannot be added to built-in types and functions without wrapping them. ↩
-
Python 1.x and old-style classes will be largely ignored in this series, and Python 3.x assumed unless noted. ↩
-
Note that some objects (e.g. ints) have no
__dict__
, and some (e.g. modules) have a read-only__dict__
. ↩