On The Topic of Extending Type by Subclassing

I used to think you misunderstood it, but now I think you do understand it, but are not expressing it in the right terms, so we had miscommunication.

Exactly! self is the instance of the class, it’s not just a regular argument to a regular function. It can access everything the class has, like append, __repr__, etc.

Exactly! I think you get it.

But to clarify the terminology, self (a variable) does not refer to the same thing as list (a builtin object). So self is list is false. But self is an instance of the list type, or in short, self is a list. Just like

class A:
    def __init__(self):
        print(isinstance(self, A)) # True: self is an instance of the class A
        print(self is A) # False: self is not "literally" A

a = A() # `a` behaves exactly like `self`
print(isinstance(a, A)) # True: a is an instance of the class A
print(a is A) # False: a is not "literally" A

I think your confusion is between the two expressions self is list (False) vs “self is a list” (True). They mean different things. You get it, right?

Edit: I misspelled isinstance.

1 Like

Thank you a bunch @FelixFourcolor for your time and for your sincere efforts. I believe that I understand this now (knock on wood as they say). Because we are using a built-in as the inherited class, self represents the list built-in object. This special feature does not apply to any other class, ONLY to a built-in keyword class, such as list. I know, I experimented by trying it with an arbitrary class, and it did not work (self.append(some_value). This is when I had my eureka moment.

Append is a feature of a list object. An arbitrary class would not have it unless explicitly added.
Now it makes sense.

Again, thank you very much for helping me understand this concept.

:100: :grinning:

No, it has nothing to do with the fact that list is a builtin. This feature applies to any class that you inherit from.

No, that’s because your base class doesn’t implement an append method.

Here is a simple example to show how it works, using all user-defined classes:

class base:
    def base_method(self):
        print("calling the base method of", self)

class derived(base):
    def derived_method(self):
        print("calling the derived method of", self, ", and then...")
        self.base_method()

an_object = derived()
an_object.derived_method()

Notice that we can use base_method from within derived_method, because everything that is an instance of derived is automatically an instance of base, too. Therefore, it has all the methods that base does. an_object doesn’t have append, because neither base nor derived defines that. But it does have both base_method and derived_method.

Correct. (Of course, other already-existing classes may have such a named method as well. In particular, among builtins, bytearray also has it.)

2 Likes

Thank you @kknechtel for helping to clarify my still apparent confusion. Can you please provide a sample super class such that if I execute the following command:

print(self)   # Print the list

… it will print the contents; in this case the instantiation argument ([4,5,6,7,8]), as an example. Maybe I need to see the code explicitly to understand why it is exceptional.

Do you have the list class internals?

Here you go:

class List: # not inheriting the builtin list
    def __init__(self, data):
        self.data = data

    # In order to customize what print() looks like, we must implement __repr__.
    # The reason we don't need to if the class inherits `list` is that `list` already implements it.
    # The way inheritance works is that all subclasses have access to the parent,
    # but if we don't inherit anything, we must write it ourselves.
    def __repr__(self):
        # this is buggy, don't use it for real, but it roughly shows what implenting __repr__ looks like
        output = "[" # open bracket
        for element in self.data:
            output += str(element) # add the element to the string
            output += "," # comma
        output += "]" # close bracket
        return output

class CustomList(List): # inherit List
    # we don't implement __init__ and __repr__
    # meaning we reuse exactly what List has

    def show(self):
        print(self)

s = CustomList([1, 1, 2, 3])
s.show() # [1,1,2,3,]

It doesn’t look exactly like the result of print([1, 1, 2, 3]) because that’s how I wrote the __repr__. I could’ve taken care to format it properly, but I simplified the code for the sake of explanation. In the case of list, that logic is written in C and you get it for free by inheriting it

This is nothing special with __repr__ by the way, all methods work the same: either you inherit a parent class to reuse the its code, or write it yourself. As I’ve said in a earlier reply, you should read the source of collections.UserList (at the time of writing it’s on line 1213) to learn how to get list-like behavior without inheriting list.

1 Like

Note that Felix has omitted a small detail.

Technically, print() calls str(obj) for each obj you ask it to
print, which calls obj.__str__(). But he only defined a __repr__
method.

So what happens?

Neither CustomList nor List define a __str__ method, so it goes
back to List’s parent class, which is object (the root class of
all classes in Python). So print() effectively runs:

 object.__str__(obj)

and if we look at what object.__str__ does, here:

it says “The default implementation defined by the built-in type object
calls object.repr().” Which actually means it calls
obj.__repr__(), which in turn is found… in the List class.

Cheers,
Cameron Simpson cs@cskk.id.au

1 Like

Thanks for clarifying. I did not know that print calls __str__ instead of __repr__. I otherwise would’ve implemented List.__str__ and make the situation a lot less confusing.

Loosely speaking, str() is for things you’d print for a user (even yourself) and repr() is what you might like for debugging (more fiddly details shown, sometimes even a Python usable expression to make the objects you’re repr()ing).

For example, str() of a string is itself, which is what you’d print, whereas repr() of a string is a valid Python string expression, eg 'abc' including the quote marks.

Thank you for hanging in there @FelixFourcolor. I reviewed the collection.UserList class code that you provided via the link. After testing a few different combinations and observing the results, I see that the list class code supplies its own __repr__ method. __repr__ is an overloading method that automatically gets called when an instance (say print(x)) of the object gets printed. However, because an instance of a class implicitly represent self, this means that the __repr__ overloading method must also apply to a self object (since both represent the same object).

Here is the test code that I used (note that I implemented the same constructor from the class found in the link that you provided to make it a bit practical which is perhaps a bit closer to what the actual list class has in its code albeit in the C language - as you had stated).

class UserList:
        
    """A more or less complete user-defined wrapper around list objects."""

    def __init__(self, initlist = None):
        
        self.value = []
        
        if initlist is not None:
            
            # XXX should this accept an arbitrary sequence?
            if type(initlist) == type(self.value):
                self.value[:] = initlist
                
            elif isinstance(initlist, UserList):
                self.value[:] = initlist.value[:]
                
            else:
                self.value = list(initlist)  
                
    # Overloading method automatically called when instance is printed
    # - includes self (intance name is synonym for `self`)
    def __repr__(self):
        
        return repr(self.value)                
    
class ArbClass(UserList):
    
    def __init__(self, value = None):
        
        if value == None:
            value = []
            
        self.value = value
        
    def print_me(self):
        
        print("\nPrinting 'self' (internal to class):")
        print(self)

        
instance_object = ArbClass([55,77,99])  # class instance object
print("\nPrinting 'instance': (external to class)") 
print(instance_object)     # Prints instance
instance_object.print_me() # Prints `self` which instance name represents implicitly
                           # inside class method definition

# Using `list` derivative
print("\nPrinting via 'list':")
x = UserList([44, 66, 88])
print(x) 

As shown in the code, both the instance object which is external to the class and the self which is internal to it, cause the __repr__ overloading method to be called when a print operation is being applied to either of them. This makes sense … a bit more explicit now.

Per a previous post, the confusion was in my point of reference to that of dealing with attributes - by qualification (object.attr_x). But I had to make the connection that we could also print the self just as we could with the instance object name.

Again, thank you for your many efforts and nudging me in the right direction with both text and code example. :raised_hands: :handshake:

1 Like

All has been forgiven. :wink:

But in all seriousness, thank you as well for adding a little bit more background. It definitely helps in the understanding.

Much appreciated.

The problem is that “the contents” is not well defined. Felix gave you an example that implements __repr__ by operating on self.data, assuming this is a list, and emulates what the list’s own __repr__ does.

The Set class in the OP, similarly, already implements its own __repr__:

As you can see, it delegates to the base class (i.e. list) version of __repr__, and adds a tag to the start. This works because this Set example is a subtype of list. Just like how append is available from the base class, __repr__ as well - but we must explicitly say we want the list class version of the method, to avoid recursion. By writing list.__repr__(self), we look the method up directly in the list class (so we need to pass self explicitly, because it won’t get provided by a descriptor). Another way to do this is super().__repr__() (this does use the method-lookup magic, and pass self automatically).

However, keep in mind that print will use str, not repr. Calling str will fall back on __repr__ when __str__ is not available (thanks to logic implemented by the base-of-everything, object - it does not come from str’s own logic), but this matters if you define both so that they do different things.

Builtin types, in the reference Python implementation (called CPython; this is the one that you get from python.org) are implemented in C. You can see all the code in the github repository. Generally, the Objects folder contains the .c files (headers are elsewhere) for built-in types; lists are implemented by Objects/listobject.c; the Python-level __repr__ corresponds to list_repr, which wraps list_repr_impl, in C.

As a FYI, collections.UserList is a wrapper for lists that is rarely actually useful for anything. (A very long time ago, it wasn’t possible to subclass list directly, and this filled in the gap.) So it doesn’t actually show you the mechanics of how a list’s string representation is calculated - it just kicks back to repr, which will in turn use the C-implemented __repr__ of the underlying list.

Make sure you understand that instance_object and self are not separate objects; they are different names for the same object, in different contexts.

2 Likes

Thank you for this insightful nugget. It aids in the understanding of the script.

I take it that it was only provided as a reference as to what the actual list code might look like (albeit in C ) and as a test Super class to implement for testing purposes.

Yes, I have understood this part. It is just when it came to the printing that got me off balance.

Thank you for additional background on the subject.

Cheers!

No; like I said, originally it was created so that you could actually inherit from it, because class Example(list): didn’t work. It has worked for a long time, but UserList is maintained - in part for backwards compatibility, and in part because overriding methods of a base class (rather than just adding new ones) can be tricky.

From @FelixFourcolor, its to: learn how to get list-like behavior without inhering list.

So, for this particular thread, it was used as a reference (to get the general idea).

Ah, ok. That was Felix’s purpose, not the standard library’s :slight_smile:

I think we’ve resolved everything, then.

1 Like