Перейти к содержанию

Classes

Documentation: Documentation » The Python Tutorial » 9. Classes

Creating a new class creates a new type of object, allowing new instances of that type to be made. Each class instance can have attributes attached to it for maintaining its state. Class instances can also have methods (defined by its class) for modifying its state.

Python Scopes and Namespaces

Namespaces

Some definitions:

  1. A namespace is a mapping from names to objects.
  2. There is absolutely no relation between names in different namespaces.
  3. In the expression modname.funcname, modname is a module object and funcname is an attribute of it.

Namespaces are created at different moments and have different lifetimes:

  1. The namespace containing the built-in names is created when the Python interpreter starts up, and is never deleted.
  2. The global namespace for a module is created when the module definition is read in; normally, module namespaces also last until the interpreter quits.
  3. The statements executed by the top-level invocation of the interpreter, either read from a script file or interactively, are considered part of a module called main, so they have their own global namespace. (The built-in names actually also live in a module; this is called builtins.)
  4. The local namespace for a function is created when the function is called, and deleted when the function returns or raises an exception that is not handled within the function. Recursive invocations each have their own local namespace.

Summary

Namespaces: built-inglobal for a module → top-level invocation of the interpreter → local for a function.

Scopes

A scope is a textual region of a Python program where a namespace is directly accessible.

Although scopes are determined statically, they are used dynamically. At any time during execution, there are 3 or 4 nested scopes whose namespaces are directly accessible:

  1. the innermost scope, which is searched first, contains the local names of the (textually) current function;
  2. the scopes of any enclosing functions, which are searched starting with the nearest enclosing scope, contains non-local, but also non-global names;
  3. the next-to-last scope contains the current module’s global names;
  4. the outermost scope (searched last) is the namespace containing built-in names.

Summary

Scopes: local → enclosing functions non-local & non-global → current module’s globalbuilt-in.

The global statement can be used to indicate that particular variables live in the global scope and should be rebound there; the nonlocal statement indicates that particular variables live in an enclosing scope and should be rebound there.

Note

If no global or nonlocal statement is in effect – assignments to names always go into the innermost scope.

Important

Assignments do not copy data — they just bind names to objects. The same is true for deletions: the statement del x removes the binding of x from the namespace referenced by the local scope.

Example

scope_test.py
def scope_test():
    def do_local():
        spam = 'local spam'  #  local assignment (which is default)
        # didn’t change `scope_test`'s binding of `spam`

    def do_nonlocal():
        nonlocal spam  # changed `scope_test`'s binding of spam
        spam = 'nonlocal spam'

    def do_global():
        global spam  # changed the module-level binding
        spam = 'global spam'

    spam = 'test spam'  # scope_test's binding of spam

    do_local()
    print('After local assignment:', spam)

    do_nonlocal()
    print('After nonlocal assignment:', spam)

    do_global()
    print('After global assignment:', spam)

scope_test()
print("In global scope:", spam)
output
After local assignment: test spam
After nonlocal assignment: nonlocal spam
After global assignment: nonlocal spam
In global scope: global spam

A First Look at Classes

Class Definition Syntax

class ClassName:
    pass
When a class definition is entered, a new namespace is created, and used as the local scope — thus, all assignments to local variables go into this new namespace.

When a class definition is left normally (via the end), a class object is created. This is basically a wrapper around the contents of the namespace created by the class definition. The original local scope (the one in effect just before the class definition was entered) is reinstated, and the class object is bound here to the class name given in the class definition header (ClassName in the example).

Class Objects

Class objects support two kinds of operations: attribute references and instantiation.

Attribute references use the standard syntax used for all attribute references in Python: obj.name.

class MyClass(object):
   """A simple example class."""

   i: int = 12345  # Class variable shared by all instances.

   data: list[int] = []  # Mistaken use of a class variable; don't use mutable objects because
   # just a single list would be shared by all instances; use an instance variable instead.

   def __init__(self, data: list[int]):
      """Customize to a specific initial state."""
      self.data: list[int] = data  # Data attribute or instance variable unique to each instance.
      # If the same attribute name occurs in both an instance and in a class,
      # then attribute lookup prioritizes the instance.

      print(f'type(self.data): {type(self.data)}')
      print('data: ' + f'{self.data=}'.split('=')[1].split('.')[-1])

   def f(self):  # Method.
      return 'hello world'

# MyClass.i (int) and MyClass.f (function object) are valid attribute references.
print(f'MyClass.i: {MyClass.i}')
print(f'MyClass.f: {MyClass.f}')

# Class attributes can also be assigned to.
MyClass.i = 123
print(f'MyClass.i: {MyClass.i}')

# __doc__ is also a valid attribute, returning the docstring belonging to the class.
print(f'MyClass.__doc__: {MyClass.__doc__}')
output
MyClass.i: 12345
MyClass.f: <function MyClass.f at 0x7f7c4c97a320>
MyClass.i: 123
MyClass.__doc__: A simple example class.

Class instantiation uses function notation. Just pretend that the class object is a parameterless function that returns a new instance of the class.

# Class instantiation.
x = MyClass(data=[1, 2, 3, 4])

output
type(self.data): <class 'list'>
data: [1, 2, 3, 4]

Instance Objects

Note

The only operations understood by instance objects are attribute references.

There are two kinds of valid instance attribute names: data attributes and methods.

Data attributes need not be declared; like local variables, they spring into existence when they are first assigned to.

x.counter = 1  # Data attribute.
print(x.counter)
del x.counter  # Will print 1.
Method. A method is a function that «belongs to» an object.

Note

x.fmethod object; MyClass.ffunction object.

Method Objects

# Calling a class method:
print(f'x.f(): {x.f()}')

# Is exactly equivalent to:
print(f'MyClass.f(x): {MyClass.f(x)}')

# Method object can be stored away and called at a later time.
xf = x.f
print(f'xf(): {xf()}')
output
x.f(): hello world
MyClass.f(x): hello world
xf(): hello world

Class and Instance Variables

Generally speaking:

  1. Instance variables are for data unique to each instance.
  2. Class variables are for attributes and methods shared by all instances of the class.
class Dog:

    kind = 'canine'         # class variable shared by all instances

    def __init__(self, name):
        self.name = name    # instance variable unique to each instance

Important

Don't use mutable objects as class variables. Use an instance variable instead.

Random Remarks

This is valid, but don't do it in such a way as not to confuse the reader of the code:

# Function defined outside the class
def f1(self, x, y):
    return min(x, x+y)

class C:
    f = f1

Each value is an object, and therefore has a class (also called its type).

print(x.__class__)  # <class '__main__.MyClass'

Inheritance

Info

Inheritance is a way of creating a new class for using details of an existing class without modifying it. The newly formed class is a derived class (or child class). Similarly, the existing class is a base class (or parent class).

class DerivedClassName(BaseClassName):
    pass

class DerivedClassName(modname.BaseClassName):
   '''When the base class is defined in another module'''
    pass

Resolving attribute references. If a requested attribute is not found in the class, the search proceeds to look in the base class. This rule is applied recursively if the base class itself is derived from some other class.

Resolving method references. The corresponding class attribute is searched, descending down the chain of base classes if necessary, and the method reference is valid if this yields a function object.

Derived classes may override methods of their base classes. An overriding method in a derived class may in fact want to extend rather than simply replace the base class method of the same name. There is a simple way to call the base class method directly: just call BaseClassName.methodname(self, arguments).

Python has two built-in functions that work with inheritance:

  1. Use isinstance() to check an instance’s type: isinstance(obj, int) will be True only if obj.__class__ is int or some class derived from int.
  2. Use issubclass() to check class inheritance: issubclass(bool, int) is True since bool is a subclass of int. However, issubclass(float, int) is False since float is not a subclass of int.

Multiple Inheritance

# Python supports a form of multiple inheritance as well.
class DerivedClassName(Base1, Base2, Base3):
    pass
For most purposes, in the simplest cases, you can think of the search for attributes inherited from a parent class as depth-first, left-to-right, not searching twice in the same class where there is an overlap in the hierarchy.

Thus, if an attribute is not found in DerivedClassName, it is searched for in Base1, then (recursively) in the base classes of Base1, and if it was not found there, it was searched for in Base2, and so on.

Private Variables

Note

«Private» instance variables that cannot be accessed except from inside an object don’t exist in Python.

Non-public. There is a convention that is followed by most Python code: a name prefixed with an underscore (e.g. _spam) should be treated as a non-public part of the API (whether it is a function, a method or a data member). It should be considered an implementation detail and subject to change without notice.

Name mangling: any identifier of the form __spam (at least two leading underscores, at most one trailing underscore) is textually replaced with _classname__spam, where classname is the current class name with leading underscore(s) stripped.

Name mangling is helpful for letting subclasses override methods without breaking intraclass method calls. For example:

class Mapping:
    def __init__(self, iterable):
        self.items_list = []
        self.__update(iterable)

    def update(self, iterable):
        for item in iterable:
            self.items_list.append(item)

    __update = update   # private copy of original update() method

class MappingSubclass(Mapping):

    def update(self, keys, values):
        # provides new signature for update()
        # but does not break __init__()
        for item in zip(keys, values):
            self.items_list.append(item)

Note

The mangling rules are designed mostly to avoid accidents; it still is possible to access or modify a variable that is considered private.

Iterators

It is easy to add iterator behavior to your classes. Define an __iter__() method which returns an object with a __next__() method.

Note

If the class defines __next__(), then __iter__() can just return self.

class Reverse:
    """Iterator for looping over a sequence backwards."""
    def __init__(self, data):
        self.data = data
        self.index = len(data)

    def __iter__(self):
        return self

    def __next__(self):
        if self.index == 0:
            raise StopIteration  # To signal that there are no further items produced by the iterator.
        self.index = self.index - 1
        return self.data[self.index]
>>> rev = Reverse('spam')
>>> iter(rev)
<__main__.Reverse object at 0x00A1DB50>
>>> for char in rev:
...     print(char)
...
m
a
p
s

Generators

Generators are a simple and powerful tool for creating iterators. They are written like regular functions but use the yield statement whenever they want to return data. Each time next() is called on it, the generator resumes where it left off (it remembers all the data values and which statement was last executed).

def reverse(data):
    for index in range(len(data)-1, -1, -1):
        yield data[index]
>>> for char in reverse('golf'):
...     print(char)
...
f
l
o
g
Anything that can be done with generators can also be done with class-based iterators. What makes generators so compact is that the __iter__() and __next__() methods are created automatically.

Note

Another key feature is that the local variables and execution state are automatically saved between calls. This made the function easier to write and much more clear than an approach using instance variables like self.index and self.data.

In addition to automatic method creation and saving program state, when generators terminate, they automatically raise StopIteration.

Methods, classmethods and staticmethods

Documentation:

  1. method;
  2. classmethod;
  3. staticmethod.

Let's look at the difference with an example:

# below x will be used by static method
# if we do not define it, the staticmethod will generate error.
x: int = 10

class NumberAdder(object):
    x: int = 5  # class variable

    def __init__(self, x: int):
        self.x: int = x  # instance variable

    def AddMethod(self, y: int):
        print("method:", self.x + y)

    @classmethod
    # as convention, `cls` must be used for classmethod, instead of `self`
    def AddClassMethod(cls, y: int):
        print("classmethod:", cls.x + y)

    @staticmethod
    def AddStaticMethod(y: int):
        print("staticmethod:", x + y)

def main():
    # 1. method
    m = NumberAdder(x=1)
    m.AddMethod(y=2)  # method: 3

    # 2. classmethod
    cm = NumberAdder(x=2)
    # for class method, class variable x = 5, will be used for addition
    # instead of an instance variable
    cm.AddClassMethod(y=10)  # classmethod: 15

    # 3. for static method, x=10 (at the top of file), will be used for addition
    sm = NumberAdder(x=1)
    sm.AddStaticMethod(y=2)  # staticmethod: 12

if __name__ == '__main__':
    main()