Table of contents
- str and repr
- Note this when using __str__, the return statement is always required
- Now, why we dont call with the underscores like this print(__repr__(person))
- repr() and str() on built-in datatypes:
- Self
- @staticmethod
- pass
- Underscore as a placeholder in for loop
- assert in __init__
- Using this code snippet from task 6(adv) to clear a point
- Getters and setters
- underscores in init, getters and setters and when to use
- More on the underscores
- Public, protected and private variables
- How to use a protected variable in subclass
- Managing private attributes in inherited classes
- why issubclass(True, bool) will return True
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:
__str__(self)
: This method returns a string representation of the object when it is used with thestr()
function or when the object is printed usingprint()
. It is intended to provide a human-readable description of the object's state.__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 ofrepr()
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.
Getter and Setter for
data
:The getter method
data
is defined as@property
decorator, which allows accessing thedata
attribute as if it were a regular attribute.The setter method
data
is defined with the@data.setter
decorator, which is called whenever thedata
attribute is assigned a new value.When you access
node.data
, the getter method is called, and it returns the value ofnode.__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).
Getter and Setter for
next_node
:The getter method
next_node
and the setter methodnext_node
are defined similarly to thedata
attribute.The getter method
next_node
returns the value ofnode.__next_node
.The setter method
next_node
performs the following checks:It verifies that the assigned value is either
None
or an instance of theNode
class. If it's not, aTypeError
exception is raised with the message "next_node must be a Node object".If the value is
None
or aNode
object, it assigns it tonode.__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
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.
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.