Advanced Method Constructs in Python
![]()
A method definition in Python is as easy as in any other language. Functional block starts with a def keyword, followed by a function name and parentheses.
So, how does a method work in Python? We will first look into the instance methods. Let’s start with a familiar example.
Here, we have a class called Animal with two methods __init__ and animal_name, where former is a special method and later is a plain pythonic instance method declaration. It can be seen that we have passed ‘self’ keyword in both these methods. What does this mean? Let us try to understand it with an example.
The keyword ‘self’ is just a naming convention (other words can also be used). By providing ‘self’ as the first argument we are specifying that the methods within the class Animal are bound to the class instance, i.e, the first argument passed is itself the instance of a class. The below example will make it clearer.
In this case, we create an instance of class Animal which is passed as the first argument to the method ‘animal_name’ or more precisely speaking Bound method which is same as doing:
Bound and Unbound methods
In simple terms, a bound method is the one which is dependent on the instance of the class as the first argument. That’s why they are called instance methods and in our example, animal_name is an instance method. Staticmethods are examples of unbound methods. Speaking in terms of our previous example we can find an analogy, any animal_name is first bound to Animal first and then supposed to display its characteristics.
Staticmethod
In simpler terms, staticmethods are those methods which are not bound to the class instance and deal only with the arguments it takes. To declare a staticmethod we just have to add ‘@staticmethod’ decorator over the method definition as shown in the next example.
We can see in the example that we have not added ‘self’ keyword in the message which suggests that it is not related to the class or its instance. Since it is not related to class anyway, it is not mandatory to call staticmethods with instance of class. Following example will give a clear idea.
Hence, we see that staticmethods are not bound to the class. Also, the analogy with the example is that the message (“Love animals”) is more of a generic type and not related to the class, so staticmethods are the ones which do not relate to class anyway. By using staticmethods, we are essentially trying to demonstrate that the method depends on arguments and does not touch the instance (i.e. ‘self’). We can see the implementation in datetime module of CPython repository.
Classmethod
Much like staticmethods, classmethods also are independent of the instance of class but are dependent on the class itself, i.e., bound to the class. Thus, quite obviously it takes class as the first parameter. Classmethods can be created with ‘@classmethod’ decorator as described in the next example.
In the above example, we are passing ‘cls’ as the class reference. Let’s see how this works.
The example makes it clear that classmethods are bound to the class itself and thus, is powerful enough to change the class properties; in this case, the class is variable. Also, it is notable that much like staticmethods, these do not need to be called on class objects. Hence, classmethods are also called factory methods or alternate constructors. An example of alternate constructor is shown below:
An implemented example of classmethod can be seen here. Apart from classmethods and staticmethods, abstract methods are of great usage. You can read about abstractmethods and abstract base classes here. Happy Python!
Еще немного о дескрипторах в Python
Не так давно на Хабре уже был перевод статьи Раймонда Хеттингера Руководство к дескрипторам. В этой статье я постараюсь рассмотреть вопросы, которые возникли у меня после прочтения. Будет немного примеров кода, собственно вопросов и ответов к ним. Для понимания того, о чем речь, вам нужно знать, что такое дескрипторы и зачем они.
Когда вызываются дескрипторы?
Рассмотрим следующий код:
>>> class M(type):
. def __new__(cls, name, bases, dct):
. dct[‘test’] = lambda self, x: x*2
. return type.__new__(cls, name, bases, dct)
.
>>> class A(object):
. def __init__(self):
. self.__dict__[‘test2’] = lambda self, x: x*4
. __metaclass__ = M
.
>>> A().test(2)
4
>>> A().test2(2)
Traceback (most recent call last):
File " < stdin > ", line 1, in < module >
TypeError: < lambda > () takes exactly 2 arguments (1 given)
* This source code was highlighted with Source Code Highlighter .
Что не так? Вроде добавляем функцию одинаково, используя словарь объекта. Почему не вызывается дескриптор для функции «test2»? Дело в том, что функция «test» определяется для словаря класса, а функция «test2» — для словаря объекта:
* This source code was highlighted with Source Code Highlighter .
Отсюда — первый ответ: функция "__get__" вызывается только для дескрипторов, которые являются свойствами класса, а не свойствами объектов этого класса.
Что является дескриптором?
Предыдущий ответ сразу вызывает вопрос — что значит «только для дескрипторов»? Я ведь не создавал никаких дескрипторов, я создал только функцию!
Ответ ожидаемий — функции в Питоне являются дескрипторами(если быть точнее — дескрипторами не данных):
* This source code was highlighted with Source Code Highlighter .
Bound/Unbound методы
И напоследок самое вкусное. Задача:
Есть объект некоего класса, к которому необходимо динамически добавить метод, которому, конечно, должен передаваться «self» параметр. Не представляется возможным добавлять этот метод в словарь класса (мы не хотим повлиять на другие объекты).
Решить «в лоб» не выходит:
>>> class A(object):
. def __init__(self):
. self.x = 3
.
>>> def add_x(self, add):
. self.x += add
. print ‘Modified value: %s’ % (self.x,)
.
>>> a = A()
>>> a.add_x = add_x
>>> a.x
3
>>> a.add_x(3)
Traceback (most recent call last):
File " < stdin > ", line 1, in < module >
TypeError: add_x() takes exactly 2 arguments (1 given)
* This source code was highlighted with Source Code Highlighter .
Получаем ожидаемую ошибку, которая подтверждает первый ответ — при обращении к свойству-дескриптору объекта не используется "__get__". Что же делать?
Поиграем немного с методом "__get__" функции, которую мы только что создали:
>>> add_x.__get__
< method-wrapper ‘__get__’ of function object at 0x800f2caa0 >
>>> add_x.__get__(a)
< bound method ?. add_x of &# 60 ; __main__ . A object at 0x800f2dc50 >>
>>> add_x.__get__(a, A)
< bound method A . add_x of &# 60 ; __main__ . A object at 0x800f2dc50 >>
>>> add_x.__get__(None, A)
< unbound method A . add_x >
* This source code was highlighted with Source Code Highlighter .
О, кажется это то, что нам надо! В предпоследней строчке мы получили «bound» метод — именно так смотрелся бы вызов «A().add_x», если в классе «A» был бы определен метод «add_x». А в последней строчке видим «ожидаемый» результат вызова «A.add_x». Теперь мы знаем, как добавить метод к объекту:
* This source code was highlighted with Source Code Highlighter .
Бинго!
И так, именно метод "__get__" для функций-дескрипторов создает «bound/unbound» методы классов и объектов. И нет здесь никакой магии 🙂
Литература
Что еще почитать? На самом деле — немного. Есть несколько глав, в которых рассказывается о дескрипторах, в Python Data Model (implementing-descriptors), ну и можно снова вспомнить действительно хорошую статью Хеттингера (оригинал).
Descriptor HowTo Guide¶
Descriptors let objects customize attribute lookup, storage, and deletion.
This guide has four major sections:
The “primer” gives a basic overview, moving gently from simple examples, adding one feature at a time. Start here if you’re new to descriptors.
The second section shows a complete, practical descriptor example. If you already know the basics, start there.
The third section provides a more technical tutorial that goes into the detailed mechanics of how descriptors work. Most people don’t need this level of detail.
The last section has pure Python equivalents for built-in descriptors that are written in C. Read this if you’re curious about how functions turn into bound methods or about the implementation of common tools like classmethod() , staticmethod() , property() , and __slots__ .
Primer¶
In this primer, we start with the most basic possible example and then we’ll add new capabilities one by one.
Simple example: A descriptor that returns a constant¶
The Ten class is a descriptor whose __get__() method always returns the constant 10 :
To use the descriptor, it must be stored as a class variable in another class:
An interactive session shows the difference between normal attribute lookup and descriptor lookup:
In the a.x attribute lookup, the dot operator finds ‘x’: 5 in the class dictionary. In the a.y lookup, the dot operator finds a descriptor instance, recognized by its __get__ method. Calling that method returns 10 .
Note that the value 10 is not stored in either the class dictionary or the instance dictionary. Instead, the value 10 is computed on demand.
This example shows how a simple descriptor works, but it isn’t very useful. For retrieving constants, normal attribute lookup would be better.
In the next section, we’ll create something more useful, a dynamic lookup.
Dynamic lookups¶
Interesting descriptors typically run computations instead of returning constants:
An interactive session shows that the lookup is dynamic — it computes different, updated answers each time:
Besides showing how descriptors can run computations, this example also reveals the purpose of the parameters to __get__() . The self parameter is size, an instance of DirectorySize. The obj parameter is either g or s, an instance of Directory. It is the obj parameter that lets the __get__() method learn the target directory. The objtype parameter is the class Directory.
Managed attributes¶
A popular use for descriptors is managing access to instance data. The descriptor is assigned to a public attribute in the class dictionary while the actual data is stored as a private attribute in the instance dictionary. The descriptor’s __get__() and __set__() methods are triggered when the public attribute is accessed.
In the following example, age is the public attribute and _age is the private attribute. When the public attribute is accessed, the descriptor logs the lookup or update:
An interactive session shows that all access to the managed attribute age is logged, but that the regular attribute name is not logged:
One major issue with this example is that the private name _age is hardwired in the LoggedAgeAccess class. That means that each instance can only have one logged attribute and that its name is unchangeable. In the next example, we’ll fix that problem.
Customized names¶
When a class uses descriptors, it can inform each descriptor about which variable name was used.
In this example, the Person class has two descriptor instances, name and age. When the Person class is defined, it makes a callback to __set_name__() in LoggedAccess so that the field names can be recorded, giving each descriptor its own public_name and private_name:
An interactive session shows that the Person class has called __set_name__() so that the field names would be recorded. Here we call vars() to look up the descriptor without triggering it:
The new class now logs access to both name and age:
The two Person instances contain only the private names:
Closing thoughts¶
A descriptor is what we call any object that defines __get__() , __set__() , or __delete__() .
Optionally, descriptors can have a __set_name__() method. This is only used in cases where a descriptor needs to know either the class where it was created or the name of class variable it was assigned to. (This method, if present, is called even if the class is not a descriptor.)
Descriptors get invoked by the dot operator during attribute lookup. If a descriptor is accessed indirectly with vars(some_class)[descriptor_name] , the descriptor instance is returned without invoking it.
Descriptors only work when used as class variables. When put in instances, they have no effect.
The main motivation for descriptors is to provide a hook allowing objects stored in class variables to control what happens during attribute lookup.
Traditionally, the calling class controls what happens during lookup. Descriptors invert that relationship and allow the data being looked-up to have a say in the matter.
Descriptors are used throughout the language. It is how functions turn into bound methods. Common tools like classmethod() , staticmethod() , property() , and functools.cached_property() are all implemented as descriptors.
Complete Practical Example¶
In this example, we create a practical and powerful tool for locating notoriously hard to find data corruption bugs.
Validator class¶
A validator is a descriptor for managed attribute access. Prior to storing any data, it verifies that the new value meets various type and range restrictions. If those restrictions aren’t met, it raises an exception to prevent data corruption at its source.
This Validator class is both an abstract base class and a managed attribute descriptor:
Custom validators need to inherit from Validator and must supply a validate() method to test various restrictions as needed.
Custom validators¶
Here are three practical data validation utilities:
OneOf verifies that a value is one of a restricted set of options.
Number verifies that a value is either an int or float . Optionally, it verifies that a value is between a given minimum or maximum.
String verifies that a value is a str . Optionally, it validates a given minimum or maximum length. It can validate a user-defined predicate as well.
Practical application¶
Here’s how the data validators can be used in a real class:
The descriptors prevent invalid instances from being created:
Technical Tutorial¶
What follows is a more technical tutorial for the mechanics and details of how descriptors work.
Abstract¶
Defines descriptors, summarizes the protocol, and shows how descriptors are called. Provides an example showing how object relational mappings work.
Learning about descriptors not only provides access to a larger toolset, it creates a deeper understanding of how Python works.
Definition and introduction¶
In general, a descriptor is an attribute value that has one of the methods in the descriptor protocol. Those methods are __get__() , __set__() , and __delete__() . If any of those methods are defined for an attribute, it is said to be a descriptor .
The default behavior for attribute access is to get, set, or delete the attribute from an object’s dictionary. For instance, a.x has a lookup chain starting with a.__dict__[‘x’] , then type(a).__dict__[‘x’] , and continuing through the method resolution order of type(a) . If the looked-up value is an object defining one of the descriptor methods, then Python may override the default behavior and invoke the descriptor method instead. Where this occurs in the precedence chain depends on which descriptor methods were defined.
Descriptors are a powerful, general purpose protocol. They are the mechanism behind properties, methods, static methods, class methods, and super() . They are used throughout Python itself. Descriptors simplify the underlying C code and offer a flexible set of new tools for everyday Python programs.
Descriptor protocol¶
descr.__get__(self, obj, type=None) -> value
descr.__set__(self, obj, value) -> None
descr.__delete__(self, obj) -> None
That is all there is to it. Define any of these methods and an object is considered a descriptor and can override default behavior upon being looked up as an attribute.
If an object defines __set__() or __delete__() , it is considered a data descriptor. Descriptors that only define __get__() are called non-data descriptors (they are often used for methods but other uses are possible).
Data and non-data descriptors differ in how overrides are calculated with respect to entries in an instance’s dictionary. If an instance’s dictionary has an entry with the same name as a data descriptor, the data descriptor takes precedence. If an instance’s dictionary has an entry with the same name as a non-data descriptor, the dictionary entry takes precedence.
To make a read-only data descriptor, define both __get__() and __set__() with the __set__() raising an AttributeError when called. Defining the __set__() method with an exception raising placeholder is enough to make it a data descriptor.
Overview of descriptor invocation¶
A descriptor can be called directly with desc.__get__(obj) or desc.__get__(None, cls) .
But it is more common for a descriptor to be invoked automatically from attribute access.
The expression obj.x looks up the attribute x in the chain of namespaces for obj . If the search finds a descriptor outside of the instance __dict__ , its __get__() method is invoked according to the precedence rules listed below.
The details of invocation depend on whether obj is an object, class, or instance of super.
Invocation from an instance¶
Instance lookup scans through a chain of namespaces giving data descriptors the highest priority, followed by instance variables, then non-data descriptors, then class variables, and lastly __getattr__() if it is provided.
If a descriptor is found for a.x , then it is invoked with: desc.__get__(a, type(a)) .
The logic for a dotted lookup is in object.__getattribute__() . Here is a pure Python equivalent:
Note, there is no __getattr__() hook in the __getattribute__() code. That is why calling __getattribute__() directly or with super().__getattribute__ will bypass __getattr__() entirely.
Instead, it is the dot operator and the getattr() function that are responsible for invoking __getattr__() whenever __getattribute__() raises an AttributeError . Their logic is encapsulated in a helper function:
Invocation from a class¶
The logic for a dotted lookup such as A.x is in type.__getattribute__() . The steps are similar to those for object.__getattribute__() but the instance dictionary lookup is replaced by a search through the class’s method resolution order .
If a descriptor is found, it is invoked with desc.__get__(None, A) .
The full C implementation can be found in type_getattro() and _PyType_Lookup() in Objects/typeobject.c.
Invocation from super¶
The logic for super’s dotted lookup is in the __getattribute__() method for object returned by super() .
A dotted lookup such as super(A, obj).m searches obj.__class__.__mro__ for the base class B immediately following A and then returns B.__dict__[‘m’].__get__(obj, A) . If not a descriptor, m is returned unchanged.
The full C implementation can be found in super_getattro() in Objects/typeobject.c. A pure Python equivalent can be found in Guido’s Tutorial.
Summary of invocation logic¶
The mechanism for descriptors is embedded in the __getattribute__() methods for object , type , and super() .
The important points to remember are:
Descriptors are invoked by the __getattribute__() method.
Classes inherit this machinery from object , type , or super() .
Overriding __getattribute__() prevents automatic descriptor calls because all the descriptor logic is in that method.
object.__getattribute__() and type.__getattribute__() make different calls to __get__() . The first includes the instance and may include the class. The second puts in None for the instance and always includes the class.
Data descriptors always override instance dictionaries.
Non-data descriptors may be overridden by instance dictionaries.
Automatic name notification¶
Sometimes it is desirable for a descriptor to know what class variable name it was assigned to. When a new class is created, the type metaclass scans the dictionary of the new class. If any of the entries are descriptors and if they define __set_name__() , that method is called with two arguments. The owner is the class where the descriptor is used, and the name is the class variable the descriptor was assigned to.
The implementation details are in type_new() and set_names() in Objects/typeobject.c.
Since the update logic is in type.__new__() , notifications only take place at the time of class creation. If descriptors are added to the class afterwards, __set_name__() will need to be called manually.
ORM example¶
The following code is a simplified skeleton showing how data descriptors could be used to implement an object relational mapping.
The essential idea is that the data is stored in an external database. The Python instances only hold keys to the database’s tables. Descriptors take care of lookups or updates:
We can use the Field class to define models that describe the schema for each table in a database:
To use the models, first connect to the database:
An interactive session shows how data is retrieved from the database and how it can be updated:
Pure Python Equivalents¶
The descriptor protocol is simple and offers exciting possibilities. Several use cases are so common that they have been prepackaged into built-in tools. Properties, bound methods, static methods, class methods, and __slots__ are all based on the descriptor protocol.
Properties¶
Calling property() is a succinct way of building a data descriptor that triggers a function call upon access to an attribute. Its signature is:
The documentation shows a typical use to define a managed attribute x :
To see how property() is implemented in terms of the descriptor protocol, here is a pure Python equivalent:
The property() builtin helps whenever a user interface has granted attribute access and then subsequent changes require the intervention of a method.
For instance, a spreadsheet class may grant access to a cell value through Cell(‘b10’).value . Subsequent improvements to the program require the cell to be recalculated on every access; however, the programmer does not want to affect existing client code accessing the attribute directly. The solution is to wrap access to the value attribute in a property data descriptor:
Either the built-in property() or our Property() equivalent would work in this example.
Functions and methods¶
Python’s object oriented features are built upon a function based environment. Using non-data descriptors, the two are merged seamlessly.
Functions stored in class dictionaries get turned into methods when invoked. Methods only differ from regular functions in that the object instance is prepended to the other arguments. By convention, the instance is called self but could be called this or any other variable name.
Methods can be created manually with types.MethodType which is roughly equivalent to:
To support automatic creation of methods, functions include the __get__() method for binding methods during attribute access. This means that functions are non-data descriptors that return bound methods during dotted lookup from an instance. Here’s how it works:
Running the following class in the interpreter shows how the function descriptor works in practice:
The function has a qualified name attribute to support introspection:
Accessing the function through the class dictionary does not invoke __get__() . Instead, it just returns the underlying function object:
Dotted access from a class calls __get__() which just returns the underlying function unchanged:
The interesting behavior occurs during dotted access from an instance. The dotted lookup calls __get__() which returns a bound method object:
Internally, the bound method stores the underlying function and the bound instance:
If you have ever wondered where self comes from in regular methods or where cls comes from in class methods, this is it!
Kinds of methods¶
Non-data descriptors provide a simple mechanism for variations on the usual patterns of binding functions into methods.
To recap, functions have a __get__() method so that they can be converted to a method when accessed as attributes. The non-data descriptor transforms an obj.f(*args) call into f(obj, *args) . Calling cls.f(*args) becomes f(*args) .
This chart summarizes the binding and its two most useful variants:
|
Transformation |
Called from an object |
Called from a class |
|---|---|---|
|
function |
f(obj, *args) |
f(*args) |
|
staticmethod |
f(*args) |
f(*args) |
|
classmethod |
f(type(obj), *args) |
f(cls, *args) |
Static methods¶
Static methods return the underlying function without changes. Calling either c.f or C.f is the equivalent of a direct lookup into object.__getattribute__(c, "f") or object.__getattribute__(C, "f") . As a result, the function becomes identically accessible from either an object or a class.
Good candidates for static methods are methods that do not reference the self variable.
For instance, a statistics package may include a container class for experimental data. The class provides normal methods for computing the average, mean, median, and other descriptive statistics that depend on the data. However, there may be useful functions which are conceptually related but do not depend on the data. For instance, erf(x) is handy conversion routine that comes up in statistical work but does not directly depend on a particular dataset. It can be called either from an object or the class: s.erf(1.5) —> .9332 or Sample.erf(1.5) —> .9332 .
Since static methods return the underlying function with no changes, the example calls are unexciting:
Using the non-data descriptor protocol, a pure Python version of staticmethod() would look like this:
The functools.update_wrapper() call adds a __wrapped__ attribute that refers to the underlying function. Also it carries forward the attributes necessary to make the wrapper look like the wrapped function: __name__ , __qualname__ , __doc__ , and __annotations__ .
Class methods¶
Unlike static methods, class methods prepend the class reference to the argument list before calling the function. This format is the same for whether the caller is an object or a class:
This behavior is useful whenever the method only needs to have a class reference and does not rely on data stored in a specific instance. One use for class methods is to create alternate class constructors. For example, the classmethod dict.fromkeys() creates a new dictionary from a list of keys. The pure Python equivalent is:
Now a new dictionary of unique keys can be constructed like this:
Using the non-data descriptor protocol, a pure Python version of classmethod() would look like this:
The code path for hasattr(type(self.f), ‘__get__’) was added in Python 3.9 and makes it possible for classmethod() to support chained decorators. For example, a classmethod and property could be chained together. In Python 3.11, this functionality was deprecated.
The functools.update_wrapper() call in ClassMethod adds a __wrapped__ attribute that refers to the underlying function. Also it carries forward the attributes necessary to make the wrapper look like the wrapped function: __name__ , __qualname__ , __doc__ , and __annotations__ .
Member objects and __slots__¶
When a class defines __slots__ , it replaces instance dictionaries with a fixed-length array of slot values. From a user point of view that has several effects:
1. Provides immediate detection of bugs due to misspelled attribute assignments. Only attribute names specified in __slots__ are allowed:
2. Helps create immutable objects where descriptors manage access to private attributes stored in __slots__ :
3. Saves memory. On a 64-bit Linux build, an instance with two attributes takes 48 bytes with __slots__ and 152 bytes without. This flyweight design pattern likely only matters when a large number of instances are going to be created.
4. Improves speed. Reading instance variables is 35% faster with __slots__ (as measured with Python 3.10 on an Apple M1 processor).
5. Blocks tools like functools.cached_property() which require an instance dictionary to function correctly:
It is not possible to create an exact drop-in pure Python version of __slots__ because it requires direct access to C structures and control over object memory allocation. However, we can build a mostly faithful simulation where the actual C structure for slots is emulated by a private _slotvalues list. Reads and writes to that private structure are managed by member descriptors:
The type.__new__() method takes care of adding member objects to class variables:
The object.__new__() method takes care of creating instances that have slots instead of an instance dictionary. Here is a rough simulation in pure Python:
To use the simulation in a real class, just inherit from Object and set the metaclass to Type :
At this point, the metaclass has loaded member objects for x and y:
When instances are created, they have a slot_values list where the attributes are stored:
Class method differences in Python: bound, unbound and static
What is the difference between the following class methods?
Is it that one is static and the other is not?
13 Answers 13
In Python, there is a distinction between bound and unbound methods.
Basically, a call to a member function (like method_one ), a bound function
is translated to
i.e. a call to an unbound method. Because of that, a call to your version of method_two will fail with a TypeError
You can change the behavior of a method using a decorator
The decorator tells the built-in default metaclass type (the class of a class, cf. this question) to not create bound methods for method_two .
Now, you can invoke static method both on an instance or on the class directly:
Methods in Python are a very, very simple thing once you understood the basics of the descriptor system. Imagine the following class:
Now let’s have a look at that class in the shell:
As you can see if you access the foo attribute on the class you get back an unbound method, however inside the class storage (the dict) there is a function. Why’s that? The reason for this is that the class of your class implements a __getattribute__ that resolves descriptors. Sounds complex, but is not. C.foo is roughly equivalent to this code in that special case:
That’s because functions have a __get__ method which makes them descriptors. If you have an instance of a class it’s nearly the same, just that None is the class instance:
Now why does Python do that? Because the method object binds the first parameter of a function to the instance of the class. That’s where self comes from. Now sometimes you don’t want your class to make a function a method, that’s where staticmethod comes into play:
The staticmethod decorator wraps your class and implements a dummy __get__ that returns the wrapped function as function and not as a method:
Hope that explains it.
When you call a class member, Python automatically uses a reference to the object as the first parameter. The variable self actually means nothing, it’s just a coding convention. You could call it gargaloo if you wanted. That said, the call to method_two would raise a TypeError , because Python is automatically trying to pass a parameter (the reference to its parent object) to a method that was defined as having no parameters.
To actually make it work, you could append this to your class definition:
or you could use the @staticmethod function decorator.
![]()
method_two won’t work because you’re defining a member function but not telling it what the function is a member of. If you execute the last line you’ll get:
If you’re defining member functions for a class the first argument must always be ‘self’.
Accurate explanation from Armin Ronacher above, expanding on his answers so that beginners like me understand it well:
Difference in the methods defined in a class, whether static or instance method(there is yet another type — class method — not discussed here so skipping it), lay in the fact whether they are somehow bound to the class instance or not. For example, say whether the method receives a reference to the class instance during runtime
The __dict__ dictionary property of the class object holds the reference to all the properties and methods of a class object and thus
the method foo is accessible as above. An important point to note here is that everything in python is an object and so references in the dictionary above are themselves pointing to other objects. Let me call them Class Property Objects — or as CPO within the scope of my answer for brevity.
If a CPO is a descriptor, then python interpretor calls the __get__() method of the CPO to access the value it contains.
In order to determine if a CPO is a descriptor, python interpretor checks if it implements the descriptor protocol. To implement descriptor protocol is to implement 3 methods
- self is the CPO (it could be an instance of list, str, function etc) and is supplied by the runtime
- instance is the instance of the class where this CPO is defined (the object ‘c’ above) and needs to be explicity supplied by us
- owner is the class where this CPO is defined(the class object ‘C’ above) and needs to be supplied by us. However this is because we are calling it on the CPO. when we call it on the instance, we dont need to supply this since the runtime can supply the instance or its class(polymorphism)
- value is the intended value for the CPO and needs to be supplied by us
Not all CPO are descriptors. For example
This is because the list class doesnt implement the descriptor protocol.
Thus the argument self in c.foo(self) is required because its method signature is actually this C.__dict__[‘foo’].__get__(c, C) (as explained above, C is not needed as it can be found out or polymorphed) And this is also why you get a TypeError if you dont pass that required instance argument.
If you notice the method is still referenced via the class Object C and the binding with the class instance is achieved via passing a context in the form of the instance object into this function.
This is pretty awesome since if you chose to keep no context or no binding to the instance, all that was needed was to write a class to wrap the descriptor CPO and override its __get__() method to require no context. This new class is what we call a decorator and is applied via the keyword @staticmethod
The absence of context in the new wrapped CPO foo doesnt throw an error and can be verified as follows:
Use case of a static method is more of a namespacing and code maintainability one(taking it out of a class and making it available throughout the module etc).
It maybe better to write static methods rather than instance methods whenever possible, unless ofcourse you need to contexualise the methods(like access instance variables, class variables etc). One reason is to ease garbage collection by not keeping unwanted reference to objects.