class in py, __str__, __repr__, self etc

class in py, __str__, __repr__, self etc

·

16 min read

str and repr

In Python, __str__ and __repr__ are special methods that provide a string representation of an object. These methods are defined within a class and can be overridden to customize how an object is displayed when converted to a string.

Here's a brief explanation of each method:

  1. __str__(self): This method returns a string representation of the object when it is used with the str() function or when the object is printed using print(). It is intended to provide a human-readable description of the object's state.

  2. __repr__(self): This method returns a string representation of the object that can be used to recreate the object. It is typically used for debugging or logging purposes. If the __str__ method is not defined, __repr__ is called as a fallback for the string representation of the object.

To use __str__ and __repr__ in your Python classes, you need to define these methods within your class and implement the desired behavior. Here's an example:

class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def __str__(self):
        return f"Person(name={self.name}, age={self.age})"

    def __repr__(self):
        return f"Person(name='{self.name}', age={self.age})"

In this example, we have a Person class with a name and age attribute. The __str__ method returns a string representation in the format Person(name=..., age=...), while the __repr__ method returns a string representation that can be used to recreate the object. Note that the __repr__ method includes quotes around the name attribute.

Now, when you create a Person object and print it, the __str__ method will be used:

person = Person("Alice", 25)
print(person)

Output:

Person(name=Alice, age=25)

If you call repr() on the object or output the object directly, the __repr__ method will be used:

print(repr(person))

Output:

Person(name='Alice', age=25)

By defining these methods, you can customize the string representation of your objects to provide meaningful information and facilitate debugging when necessary.

Note this when using __str__, the return statement is always required

In the example below

    def my_print(self):
        """prints a square with the character #"""
        if self.__size == 0:
            return ""
        result = ""
        for _ in range(self.__position[1]):
            result += "\n"
        for _ in range(self.__size):
            result += ' ' * self.__position[0] + '#' * self.__size + "\n"
        return result[:-1]

    def __str__(self):
        """Prints a string rep of the object"""
        self.my_print()

This implementation is missing a return statement in the __str__(), which means the __str__() method does not return anything. As a result, when you try to print an instance of the Square class or convert it to a string, you won't see any output.
You will get a TypeError instead

TypeError: __str__ returned non-string (type NoneType)

To fix this issue, you need to modify the __str__() method to return the string representation generated by the my_print() method. Here's the corrected code:

def __str__(self):
    """Returns a string rep of the object"""
    return self.my_print()

By including the return statement with self.my_print(), the __str__() method will return the string representation generated by the my_print() method. This allows you to see the desired output when you print or convert an instance of the Square class to a string.

It doesn't matter if my_print() already returns the result.

Now, why we dont call with the underscores like this print(__repr__(person))

The __str__ and __repr__ methods are special methods in Python, also known as "dunder" methods (short for "double underscore"). These methods are automatically invoked by Python under specific circumstances.

When you use the print() function with an object, it internally calls the str() function on the object to obtain a string representation. Similarly, when you use the repr() function on an object, Python internally calls the repr() function on the object. If the object does not have a specific implementation of the str method, Python falls back to calling the repr method to obtain a string representation.

You don't need to manually call __str__ or __repr__ with the underscores because Python handles this for you. The underscores are part of the method names, indicating that they are special methods.

In other words, instead of calling __repr__(person) or __str__(person), you can directly use repr(person) or str(person) respectively. Python recognizes these function calls and internally invokes the appropriate special methods.

Here's an example:

class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def __str__(self):
        return f"Person(name={self.name}, age={self.age})"

    def __repr__(self):
        return f"Person(name='{self.name}', age={self.age})"

person = Person("Alice", 25)

# Using str() and print() - invokes __str__()
print(str(person))
print(person)

# Using repr() and print() - invokes __repr__()
print(repr(person))

# Output:
# Person(name=Alice, age=25)
# Person(name=Alice, age=25)
# Person(name='Alice', age=25)

As you can see, the str(person) and print(person) statements invoke the __str__ method, while the repr(person) statement invokes the __repr__ method.

Nb: calling __repr__ directly as __repr__(person) will result in a NameError. The reason is that __repr__ is a special method and should not be called directly using the __repr__() syntax.

To obtain the string representation using __repr__, you should use the repr() function instead.

repr() and str() on built-in datatypes:

Why is the following output the same?

>>> x = 10 * 3.25
>>> y = 200 * 200
>>> repr((x, y, ('spam', 'eggs')))
"(32.5, 40000, ('spam', 'eggs'))"
>>> str((x, y, ('spam', 'eggs')))
"(32.5, 40000, ('spam', 'eggs'))"

In the given example, repr() and str() both produce the same output because the default implementation of repr() for tuples, floats, integers, and strings returns a string representation of the object that is equivalent to the output produced by str().

Here's an explanation of the repr() and str() functions:

  • repr(): This function returns a string that represents a printable version of an object. The representation should be a valid Python expression that can be used to recreate the object. The goal of repr() is to provide a detailed and unambiguous string representation of the object.

  • str(): This function returns a string that represents a human-readable version of an object. It is intended to be used for displaying the object to end-users in a readable form.

For many built-in data types, including tuples, floats, integers, and strings, the default implementation of repr() and str() returns the same output, which is a string representation of the object's contents. This behavior is designed to be convenient and consistent in most cases.

In the given example, the tuple ((x, y, ('spam', 'eggs'))) is composed of a float (x), an integer (y), and another tuple. Both repr() and str() produce the same output because their default implementations for tuples, floats, integers, and strings return a string representation of the object's contents.

Therefore, repr((x, y, ('spam', 'eggs'))) and str((x, y, ('spam', 'eggs'))), both result in "(32.5, 40000, ('spam', 'eggs'))" as the output.

Keep in mind that for other objects or custom classes, repr() and str() can produce different outputs if their implementations are overridden to provide distinct representations.

Self

You must be wondering how Python gives the value for self and why you don't need to give a value for it. An example will make this clear. Say you have a class called MyClass and an instance of this class called myobject. When you call a method of this object as myobject.method(arg1, arg2), this is automatically converted by Python into MyClass.method(myobject, arg1, arg2) - this is all the special self is about.

This also means that if you have a method which takes no arguments, then you still have to have one argument - the self.

@staticmethod

Anytime you create a function that is not directly related to the real-world object you are trying to model, It should be static,
Eg: a method that counts how many instances of the class has been created

pass

pass is a way to type a class that does nothing
when defining a class or method, you need atleast one line, so you can key in pass when you are not ready to write the code for the class, function etc

Underscore as a placeholder in for loop

for _ in range(self.__position[1]):

In Python, the underscore _ is often used as a convention to indicate a variable that is not actually being used or accessed within a loop or iteration. It is a way to communicate to other developers that the value assigned to _ is not significant or relevant to the logic of the loop.

assert in __init__

    def __init__(self, data, next_node):
        assert type(data) == int and type (next_node) == int
        self.data = data
        self.next_node = next_node

In the code snippet, the assert statement is used to perform a runtime check on the types of the data and next_node arguments passed to the __init__ method.

The assert statement checks if a given condition is True. If the condition is False, it raises an AssertionError exception, indicating that an assumption made in the code is incorrect.

It's worth noting that assert statements are typically used for debugging and development purposes. They can be disabled by running Python code with the -O (optimize) command-line option or by setting the PYTHONOPTIMIZE environment variable to a non-empty string. When disabled, the assert statements are completely ignored, and their conditions are not evaluated.

Using this code snippet from task 6(adv) to clear a point

class Node:
    """Node class"""
    def __init__(self, data, next_node=None):
        """Initializes instances of the class"""
        self.data = data
        self.next_node = next_node

    @property
    def data(self):
        """getter for data"""
        return self.__data

    @data.setter
    def data(self, value):
        """setter for data"""
        if not isinstance(value, int):
            raise TypeError("data must be an integer")
        self.__data = value

    @property
    def next_node(self):
        """getter for next_node"""
        return self.__next_node

    @next_node.setter
    def next_node(self, value):
        """getter for next_node"""
        if value is not None and not isinstance(value, Node):
            raise TypeError("next_node must be a Node object")
        self.__next_node = value


class SinglyLinkedList:
    """SinglyLinkedList"""
    def __init__(self):
        """Initializes instances of the class"""
        self.head = None

    def sorted_insert(self, value):
        """inserts a new Node into the correct sorted position in the list"""
        new_node = Node(value)
        if self.head is None or value < self.head.data:
            new_node.next_node = self.head
            self.head = new_node
        else:
            current = self.head
            while (current.next_node is not None and
                    value >= current.next_node.data):
                current = current.next_node
            new_node.next_node = current.next_node
            current.next_node = new_node

    def __str__(self):
        """String representation of the objects"""
        current = self.head
        result = ""
        while current is not None:
            result += str(current.data) + "\n"
            current = current.next_node
        return result[:-1]

Getters and setters

In the Node class, the private instance attributes data and next_node have corresponding getters and setters to enforce certain constraints and ensure data integrity.

  1. Getter and Setter for data:

    • The getter method data is defined as @property decorator, which allows accessing the data attribute as if it were a regular attribute.

    • The setter method data is defined with the @data.setter decorator, which is called whenever the data attribute is assigned a new value.

    • When you access node.data, the getter method is called, and it returns the value of node.__data (the actual data attribute).

    • When you assign a value to node.data, the setter method is called, and it performs the following checks:

      • It verifies that the assigned value is an integer. If it's not, a TypeError exception is raised with the message "data must be an integer".

      • If the value is an integer, it assigns it to node.__data (the actual data attribute).

  2. Getter and Setter for next_node:

    • The getter method next_node and the setter method next_node are defined similarly to the data attribute.

    • The getter method next_node returns the value of node.__next_node.

    • The setter method next_node performs the following checks:

      • It verifies that the assigned value is either None or an instance of the Node class. If it's not, a TypeError exception is raised with the message "next_node must be a Node object".

      • If the value is None or a Node object, it assigns it to node.__next_node.

In the SinglyLinkedList class, the private instance attribute head does not have explicit getter and setter methods. It can only be accessed and modified directly within the class. This is why there are no getter and setter methods defined for head.

The purpose of using getters and setters is to encapsulate the attributes and provide controlled access to them. They allow you to enforce constraints and perform necessary checks whenever the attributes are accessed or modified. In the given code, the getters and setters ensure that the data attribute of a Node is always an integer and that the next_node attribute is either None or a valid Node object.

getters and setters are automatically invoked when you access or assign values to the corresponding attributes. This behavior is achieved through the use of the @property decorator for the getter methods and the @<attribute_name>.setter decorator for the setter methods.

When you access the attribute using dot notation (node.data), the getter method is automatically called, and it returns the value of the attribute.

When you assign a value to the attribute using dot notation (node.data = 10), the setter method is automatically called, and it performs any necessary checks or transformations before assigning the value to the attribute.

The @property and @<attribute_name>.setter decorators are used to define the getter and setter methods, respectively, and they enable this automatic behavior.

By using these decorators, you can treat the attributes like regular attributes, and the appropriate getter or setter method is invoked transparently, allowing you to enforce constraints and perform additional logic as needed.

underscores in init, getters and setters and when to use

In the code above, the underscore is not used in the __init__ methods of the classes Node and SinglyLinkedList because those methods are not defining private variables. The purpose of using an underscore before a variable name is to indicate that the variable is intended to be private, meaning it should not be accessed directly from outside the class.

In the Node class, the data and next_node attributes are defined as public attributes because they are accessed directly using the getter and setter methods.

More on the underscores

...Since you have getters and setters implemented, you can omit the double underscores from the init, and use it in the getters and setters like in the code sample above. The getters and setters makes sure the data are validated and can't be overridden i.e when you try to access __data for example from outside the class, that will raise an AttributeError Exception...

Public, protected and private variables

  1. Naming Conventions:

    • Public attributes: These are attributes that can be freely used inside or outside the class definition, and they don't have any special naming convention. They are typically named in lowercase or lowercase with underscores (e.g., name, age, my_variable).

    • Protected attributes: These attributes are indicated by prefixing an underscore (_) to the attribute name (e.g., _name, _age). By convention, it suggests that the attribute is intended for internal use within the class or its subclasses, and it should not be accessed directly from outside the class. However, Python doesn't enforce strict encapsulation, and these attributes can still be accessed and modified from outside the class.

    • Private attributes: These attributes are indicated by prefixing a double underscore (__) to the attribute name (e.g., __name, __age). The double underscore triggers name mangling, which modifies the attribute name to include the class name as a prefix. This makes the attribute less accessible from outside the class and its subclasses, as the mangled name discourages direct access. However, it's important to note that the attributes can still be accessed using the mangled name (e.g., _ClassName__name, _ClassName__age). It's considered non-standard practice to access these attributes directly.

  2. Accessibility of attributes:

    • Public attributes can be accessed and modified freely from inside or outside the class.

    • Protected attributes can be accessed and modified from outside the class, but it's a convention that they should only be used within the class or its subclasses.

    • Private attributes can be accessed and modified within the class itself. From outside the class, the mangled names are used to access these attributes, although it's discouraged.

When you try to access a private attribute from outside a class, it's going to raise an AttributeError.
Here's an example to demonstrate how name mangling works with double underscores:

class Square:
    def __init__(self, size):
        self.__size = size

square = Square(5)
print(square.__size)  # This will raise an AttributeError
print(square._Square__size)  # This will access the attribute correctly but is not allowed

It's worth noting that Python relies more on conventions than strict access control mechanisms. The naming conventions provide a way to communicate the intended usage and accessibility of attributes, but they can still be overridden or accessed directly if desired.

How to use a protected variable in subclass

Here's an example that demonstrates the use of a protected attribute (_name) in a subclass:

class Animal:
    def __init__(self, name):
        self._name = name

    def speak(self):
        print("Animal speaks")

class Dog(Animal):
    def __init__(self, name, breed):
        super().__init__(name)
        self.breed = breed

    def speak(self):
        print("Dog barks")

    def get_name(self):
        return self._name

dog = Dog("Buddy", "Labrador")
print(dog.get_name())  # Output: "Buddy"

In this example, the Animal class has a protected attribute _name. The Dog class is a subclass of Animal and inherits the _name attribute.

The Dog class has its own constructor __init__, which takes two parameters: name and breed. It calls the superclass constructor using super().__init__(name) to set the _name attribute inherited from the Animal class.

The Dog class also provides a method get_name(), which returns the value of the protected attribute _name. The method can access the _name attribute directly because it's a member of the subclass and inherits the protected attribute from the superclass.

In this way, the protected attribute _name is used within the subclass Dog to retrieve the name of the dog.

Managing private attributes in inherited classes

using this example for illustration

class Rectangle(Base):
    """Rectangle Class"""
    __nb_objects = 0

    def __init__(self, width, height, x=0, y=0, id=None):
        """Initializes the Rectangle class"""
        super().__init__(id)
        self.__width = width
        self.__height = height
        self.__x = x
        self.__y = y

    @property
    def width(self):
        """Getter fucntion width"""
        return self.__width

    @width.setter
    def width(self, value):
        """Setter function for width"""
        if (type(value) is not int):
            raise TypeError("width must be an integer")

        if value <= 0:
            raise ValueError("width must be > 0")
        self.__width = value

    @property
    def height(self):
        return self.__height

    @height.setter
    def height(self, value):
        """Setter function for height"""
        if (type(value) is not int):
            raise TypeError("height must be an integer")

        if value <= 0:
            raise ValueError("height must be > 0")
        self.__height = value

    @property
    def x(self):
        """Getter fucntion for x"""
        return self.__x

    @x.setter
    def x(self, value):
        """Setter function for x"""
        if (type(value) is not int):
            raise TypeError("x must be an integer")

        if value <= 0:
            raise ValueError("x must be >= 0")
        self.__x = value



class Square(Rectangle):
    """Represent a square."""

    def __init__(self, size, x=0, y=0, id=None):
        super().__init__(size, size, x, y, id)

    @property
    def size(self):
        return self.width

    @size.setter
    def size(self, value):
        self.width = value
        self.height = value

When Square inherits from Rectangle, it inherits all the attributes of the Rectangle class without the mangling(double underscores), and can be accessed by just using dot.
So when you do self.width = value or self.height = value it in return calls the setters and getters from the Rectangle class for the attributes and properly validates it.

Since the Square subclass does not have its own width attribute, accessing self.width in the size property getter actually accesses the inherited width attribute from the Rectangle class.

But when you use the underscores

class Square(Rectangle):
    """Represent a square."""

    def __init__(self, size, x=0, y=0, id=None):
        super().__init__(size, size, x, y, id)

    @property
    def size(self):
        return self.__width

    @size.setter
    def size(self, value):
        if type(value) is not int:
            raise TypeError("width must be an integer")
        if value <= 0:
            raise ValueError("width must be > 0")
        self.__width = value
        self.__height = value

This setter method updates the private attributes self.__width and self.__height within the Square class.

However, since the Square class inherits from the Rectangle class, these private attributes in the Square class (self.__width and self.__height) are separate from the attributes with the same names in the Rectangle class (self.__width and self.__height).

Therefore, updating the self.__width and self.__height attributes in the Square class will not affect the width and height attributes in the Rectangle class.

The code works because the size property in the Square subclass effectively acts as a getter and setter for the width attribute inherited from the Rectangle class.

Since width for eg is inherited from Rectangle, applying double underscores to it in Square kinda mangles it, and it makes it private to only Square, so you accessing it from outside Square won't work. So now because of the underscore, we won't be able to automatically access the getters and setters from the Rectangle class therefore we manually add our custom validation.

why issubclass(True, bool) will return True

The issubclass() function in Python can be used to check if one class is a subclass of another class. However, it is not intended to be used with instances of classes, such as True or False.

In the case of issubclass(True, bool), the behavior can be a bit misleading. It returns True, but it is not indicating that True is a subclass of bool. Instead, it is checking if the class of True (bool) is a subclass of bool, which is always True because a class is considered a subclass of itself.

To illustrate:

print(issubclass(bool, bool))  # True
print(issubclass(True, bool))  # True
print(issubclass(False, bool)) # True

In this example, issubclass(bool, bool) returns True because the class bool is indeed a subclass of itself. Similarly, issubclass(True, bool) and issubclass(False, bool) also return True for the same reason.