# Up to a point, classes in Python follow the standard model # derived from Simula and established by the likes of C++ and Java. # As outlined in the introduction, classes are types (in the sense # they are sets of values), while they also provide an interface and # functionality to their instances. # The first major deviation from C++-like languages is that # «instance attributes» are «not» listed in the class definition # itself. They are instead created when an instance is initialized # in the ‹__init__› method. Like this: class Base: def __init__( self, value ): self.value = value def add( self, amount ): self.value += amount # You will immediately notice another difference: the ‹self› # argument is «explicit» in all methods. What more, we also have to # explicitly access all attributes (and methods) through ‹self›. # This might take some getting used to. Let's define a derived class # to observe a few more issues: class Derived( Base ): # Another observation: ‹self› already exists at the time # ‹__init__› is called, so it is not a ‘true’ constructor. We # will get into the woods of how objects are created and # initialized some other day. And while there is a certain # degree of magic in ‹__init__› (it does get called # automatically on new instances, after all), the amount of # magic is quite limited. More specifically, ‹__init__› methods # of superclasses are «not» automatically called before the # ‹__init__› of the one in the current class. def __init__( self, value ): self.other = 2 * value def test_classes(): # demo # To make an instance, simply ‘call’ the class itself, as if it # was a function. Parameters passed to this call are forwarded # to the ‹__init__› method above (the ‹self› parameter is added # by the internal machinery of object construction; clearly, at # the point of the call, the object does «not» exist, so we # can't pass it in explicitly even if we wanted to). base = Base( 3 ) # Python being dynamic and duck-typed, we can use a builtin # function, ‹hasattr›, to check whether an object has a # particular attribute (notice that the second argument is a # «string», not an identifier; ‹hasattr› isn't «that» magic). # Let's see: assert hasattr( base, 'value' ) assert not hasattr( base, 'other' ) assert base.value == 3 # Now for the derived class. We expect an ‹other› attribute to # be present, but ‹value› to be missing, since we did not call # ‹__init__› from ‹Base›: deriv = Derived( 2 ) assert not hasattr( deriv, 'value' ) assert hasattr( deriv, 'other' ) assert deriv.other == 4 # Let us make another derived class, to show how to call super-class # constructors: class MoreDerived( Derived ): def __init__( self, value, other ): # To call a method in the direct superclass, we can use # ‹super›, which essentially creates a version of ‹self› # that skips the current class during (method, attribute) # lookup. super().__init__( other ) # To reach indirect bases, we need to name them explicitly. # Like this (note though that «normally», the above call # would itself call ‹__init__› of ‹Base›, so we wouldn't # have to do that here): Base.__init__( self, value ) # However, ‹super()› does «not» bind the current «instance» # in a ‘normal’ way: the object that ‘falls out’ of ‹super› # does keep a reference to (our) ‹self›, but only uses it to # bind the ‹self› parameter of methods when we call them # through ‹super›. Instance attributes are nowhere to be # found: assert not hasattr( super(), 'other' ) assert hasattr( super(), 'add' ) # Last note about ‹super›: while it is ostensibly a ‘normal’ # function call, it somehow gains access to ‹self› (not by # name: it grabs the first argument, whatever it is named) # and to its class: it does so by peeking into the «call # frame» of its caller (we will talk in more detail about # these next week). Hence, it only works in methods. def test_super(): # demo deriv = MoreDerived( 1, 2 ) assert hasattr( deriv, 'value' ) assert hasattr( deriv, 'other' ) assert deriv.value == 1 assert deriv.other == 4 # One last remark before we conclude this demo: there is one other # crucial difference between C++ and Python. Since Python is # dynamically typed through and through, the object bound to ‹self› # is of the «actual, dynamic» type of that object and «not», like in # C++, the sub-object that corresponds to an instance of ‹base› # itself. Worth keeping in mind. if __name__ == '__main__': test_classes() test_super()