#!/usr/bin/python3 # -*- coding: utf-8 -*- from typing import List # %% Classes: static and class methods # We use the "class" statement to create a class class Human: # A class attribute. It is shared by all instances of this class species = "H. sapiens" # Basic initializer, this is called when this class is instantiated. # Note that the double leading and trailing underscores denote objects # or attributes that are used by Python but that live in user-controlled # namespaces. # Methods(or objects or attributes) like: __init__, __str__, # __repr__ etc. are special methods (or sometimes called dunder methods) # You should not invent such names on your own. def __init__(self, name: str) -> None: # Assign the argument to the instance's name attribute self.name = name # Initialize property self._age = 0 # An instance method. All methods take "self" as the first argument def say(self, msg: str) -> None: print("{name}: {message}".format(name=self.name, message=msg)) # Another instance method def sing(self) -> str: return "yo... yo... microphone check... one two... one two..." # A class method is shared among all instances # They are called with the calling class as the first argument @classmethod def get_species(cls) -> str: return cls.species # A static method is called without a class or instance reference @staticmethod def grunt() -> str: return "*grunt*" # A property is just like a getter # (a general term for function to get stuff). # It turns the method age() into an read-only attribute of the same name. # There's no need to write trivial getters and setters in Python, though. @property def age(self) -> int: return self._age # This allows the property to be set @age.setter def age(self, age: int) -> None: self._age = age # This allows the property to be deleted @age.deleter def age(self) -> None: del self._age # Instantiate a class i = Human(name="Ian") i.say("hi") # "Ian: hi" j = Human("Joel") j.say("hello") # "Joel: hello" # i and j are instances of type Human, # or in other words: they are Human objects # Call our class method i.get_species() # Change the shared attribute (If you're from the netherlands...) Human.species = "H. neanderthalensis" i.get_species() # => "Ian: H. neanderthalensis" j.get_species() # => "Joel: H. neanderthalensis" # Call the static method print(Human.grunt()) # => "*grunt*" # Cannot call static method with instance of object # because i.grunt() will put "self" (the object i) as an argument print(i.grunt()) # => TypeError: grunt() takes 0 positional arguments but 1 was given # Update the property for this instance # STEP THESE i.age = 42 # Get the property print(i.age) # => "Ian: 42" print(j.age) # => "Joel: 0" # Delete the property del i.age # i.age # => this would raise an AttributeError # %% Inheritance part 2 # Inheritance allows new child classes to be defined that inherit methods and # variables from their parent class. # Using the Human class defined above as the base or parent class, we can # define a child class, Superhero, which inherits the class variables like # "species", "name", and "age", as well as methods, like "sing" and "grunt" # from the Human class, but can also have its own unique properties. # To take advantage of modularization by file, # you could place the classes above in their own files, say, human.py # To import functions from other files use the following format # from "filename-without-extension" import "function-or-class" # from human import Human # Human is a base class, which didn't inherit anything you wrote. # It's method resolution order is simple: print(Human.__mro__) # Specify the parent class(es) as parameters to the class definition class Superhero(Human): # If the child class should inherit all of the parent's definitions without # any modifications, you can just use the "pass" keyword (and nothing else) # but in this case it is commented out to allow for a unique child class: # pass # Child classes can override their parents' attributes species = "Superhuman" # Children automatically inherit their parent class's constructor including # its arguments, but can also define additional arguments or definitions # and override its methods such as the class constructor. # This constructor inherits the "name" variable from the "Human" class and # adds the "superpower" and "movie" arguments: def __init__( self, name: str, movie: bool = False, superpowers: List[str] = ["super strength", "bulletproofing"], ) -> None: # add additional class attributes: self.fictional = True self.movie = movie # be aware of mutable default values, since defaults are shared self.superpowers = superpowers # The "super" function lets you access the parent class's methods # that are overridden by the child, in this case, the __init__ method. # This calls the parent class constructor: super().__init__(name) # override the sing method def sing(self) -> str: return "Dun, dun, DUN!" # add an additional instance method def boast(self) -> None: for power in self.superpowers: print("I wield the power of {pow}!".format(pow=power)) sup = Superhero(name="Tick") # Instance type checks if isinstance(sup, Human): print("I am human") if type(sup) is Superhero: print("I am a superhero") # Get the Method Resolution search Order used by both getattr() and super() # This attribute is dynamic and can be updated print(Superhero.__mro__) # => (, # => , ) # Calls parent method but uses its own class attribute print(sup.get_species()) # => Superhuman # Calls overridden method print(sup.sing()) # => Dun, dun, DUN! # Calls method from Human sup.say("Spoon") # => Tick: Spoon # Call method that exists only in Superhero sup.boast() # => I wield the power of super strength! # => I wield the power of bulletproofing! # Inherited class attribute sup.age = 31 print(sup.age) # => 31 # Attribute that only exists within Superhero print("Am I Oscar eligible? " + str(sup.movie)) # Multiple Inheritance # Another class definition # bat.py class Bat: species = "Baty" def __init__(self, can_fly: bool = True) -> None: self.fly = can_fly # This class also has a differently structured say method def say(self, msg: str) -> None: msg = "... ... ..." print(msg) # And its own method as well def sonar(self) -> str: return "))) ... (((" # Bat is another base class, so it's mro is also simple: print(Bat.__mro__) b = Bat() b.say("hello") print(b.fly) # And yet another class definition that inherits from Superhero and Bat # superhero.py # from superhero import Superhero # from bat import Bat # Define Batman as a child that inherits from both Superhero and Bat class Batman(Superhero, Bat): def __init__(self, *args, **kwargs) -> None: # Typically to inherit attributes you have to call super: # super(Batman, self).__init__(*args, **kwargs) # However we are dealing with multiple inheritance here, and super() # only works with the next base class in the MRO list. # So instead we explicitly call __init__ for all ancestors. # The use of *args and **kwargs is a clean way to pass arguments, # with each parent "peeling a layer of the onion". Superhero.__init__( self, "anonymous", movie=True, superpowers=["Wealthy"], *args, **kwargs ) Bat.__init__(self, *args, can_fly=False, **kwargs) # override the value for the name attribute self.name = "Sad Affleck" def sing(self) -> str: return "nan nan nan nan nan batman!" sup = Batman() # Batman's MRO is: # Get the Method Resolution search Order used by both getattr() and super() # This attribute is dynamic and can be updated print(Batman.__mro__) # => (, # => , # => , # => , ) # Note the child-first, depth-first, left-first, mro from the above definitions. # Calls parent method but uses its own class attribute print(sup.get_species()) # => Superhuman # Calls overridden method print(sup.sing()) # => nan nan nan nan nan batman! # Calls method from Human, because inheritance order matters sup.say("I agree") # => Sad Affleck: I agree # Call method that exists only in 2nd ancestor print(sup.sonar()) # => ))) ... ((( # Inherited class attribute sup.age = 100 print(sup.age) # => 100 # Inherited attribute from 2nd ancestor whose default value was overridden. print("Can I fly? " + str(sup.fly)) # => Can I fly? False