9. Classes

9.1 Class

  • A class is the basis of all data in python.

  • Everything is an object in python, and a class is how an object is defined.

class Duck:
    sound = 'Quack quack.'
    movement = 'Walks like a duck.'

    def quack(self):
        print(self.sound)

    def move(self):
        print(self.movement)
  • The first argument is self which is a reference to the object (not the class).

  • When an object is created from a class, self references that object.

  • Everything that references anything defined in the class is dereferenced off of self using a .

9.2 Constructor

class Animal:
    def __init__(self, **kwargs):
        self._type = kwargs['type']
        self._name = kwargs['name']
        self._sound = kwargs['sound']

    def type(self):
        return self._type

    def name(self):
        return self._name

    def sound(self):
        return self._sound
  • __init__ special name for a class function which operates as an initializer or constructor.

  • First argument is always self which is what makes it a method.

  • self._type is an object variable. Never initialized until after object is defined. Usually have an underscore (tradition) to discourage users from accessing these variables directly.

  • You should typically use an accessor or getter to access the variables. For example: type or name in the above.

9.3 Methods

  • A function that is associated with a class is a method.

  • Such functions always have the first argument as self. That’s what makes them a method, and not just any plain function.

  • The argument can be named something other than self as well, but traditional to name it self.

  • Consider:

class Animal:
    def __init__(self, **kwargs):
        self._type = kwargs['type'] if 'type' in kwargs else 'kitten'
        self._name = kwargs['name'] if 'name' in kwargs else 'fluffy'
        self._sound = kwargs['sound'] if 'sound' in kwargs else 'meow'

    def type(self, t = None):
        if t: self._type = t
        return self._type

    def name(self, n = None):
        if n: self._name = n
        return self._name

    def sound(self, s = None):
        if s: self._sound = s
        return self._sound

    def __str__(self):
        return f'The {self.type()} is named "{self.name()}" and says "{self.sound()}".'
  • type, name, sound are sometimes referred to as setter-getter functions.

  • For example:

a0 = Animal(type = 'kitten', name = 'fluffy', sound = 'rwar')
a1 = Animal(type = 'duck', name = 'donald', sound = 'quack')
a0.sound('bark')
  • can be used to set the sound of a0 and get it.

  • __str__ is a special method name. More info in the documentation

    • In this case, __str__ can be used to print the object as print(a0).

9.4 Data

  • Data may be associated with a class or an object.

  • In the above piece of code, the variabes self._var_name is an object variable. They don’t exist in the class, they’re bound to the object and not the class itself.

  • It’s not good practice to set the name of a0 as a0._name = 'Joe' (that’s why it starts with an underscore). Indeed, it only changes the _name variable in a0, not in a1.

  • You could also define a variable in the class (not in any method), and that would be a class variable. It only exists in the class. The object just draws it from the class.

  • If a class variable is changed in one object, it’s changed in the other object as well (defined based on the same class).

  • Class variables are not encapsulated.

  • In general, class variables shouldn’t be mutable.

9.5 Class inheritance

class Animal:
    def __init__(self, **kwargs):
        if 'type' in kwargs: self._type = kwargs['type']
        if 'name' in kwargs: self._name = kwargs['name']
        if 'sound' in kwargs: self._sound = kwargs['sound']

    def type(self, t = None):
        if t: self._type = t
        try: return self._type
        except AttributeError: return None

    def name(self, n = None):
        if n: self._name = n
        try: return self._name
        except AttributeError: return None

    def sound(self, s = None):
        if s: self._sound = s
        try: return self._sound
        except AttributeError: return None

class Kitten(Animal):
    def __init__(self, **kwargs):
        self._type = 'kitten'
        if 'type' in kwargs: del kwargs['type']
        super().__init__(**kwargs)
    <>
    def kill(self, s):
        print(f'{self.name()} will now kill all {s}!')
  • Animal is a base class. Kitten is a subclass that inherits Animal.

  • super is a function used to override __init__ in the subclass and inherit the parent class.

9.6 Iterator

class inclusive_range:
    def __init__(self, *args):
        numargs = len(args)
        self._start = 0
        self._step = 1
        
        if numargs < 1:
            raise TypeError(f'expected at least 1 argument, got {numargs}')
        elif numargs == 1:
            self._stop = args[0]
        elif numargs == 2:
            (self._start, self._stop) = args
        elif numargs == 3:
            (self._start, self._stop, self._step) = args
        else: raise TypeError(f'expected at most 3 arguments, got {numargs}')
        <>
        self._next = self._start
    <>
    def __iter__(self):
        return self
    <> 
    def __next__(self):
        if self._next > self._stop:
            raise StopIteration
        else:
            _r = self._next
            self._next += self._step
            return _r
  • A class that provides a sequence of items.

  • The constructor __init__ sets up all the variables based on the number of variables.

  • __iter__ is a special method that identifies the object as an iterator object.

  • __next__: A construct such as the for loop looks for this method to treat the class as an iterator.

  • Generator functions are a simpler way to do the above. yield was implemented after iterator functions.