Chapter 6: Object-Oriented Programming in Python
6.3 Python Special Functions
Let's dive into the special functions in Python, also known as "magic" or "dunder" methods. These methods provide a simple way to make your classes act like built-in types. This means you can use type-specific functions (like len
or +
) with your objects. You've already seen these in use with the __init__
method for classes. Let's explore more:
1. __str__
and __repr__
Methods
The __str__
and __repr__
methods in Python represent the class objects as a string – they are methods for string representation of a class. The __str__
method in Python represents the class objects as a human-readable string, while the __repr__
method is meant to be an unambiguous representation of the object, and should ideally contain more detail than __str__
. If __repr__
is defined, and __str__
is not, the objects will behave as though __str__=__repr__
.
class Employee:
def __init__(self, name, age):
self.name = name
self.age = age
def __str__(self):
return f'Employee[name={self.name}, age={self.age}]'
def __repr__(self):
return f'{self.__class__.__name__}({self.name!r}, {self.age!r})'
emp = Employee('John Doe', 30)
print(str(emp)) # Employee[name=John Doe, age=30]
print(repr(emp)) # Employee('John Doe', 30)
2. __add__
and __sub__
Methods
These methods are used to overload the +
and -
operator.
class Complex:
def __init__(self, real, imag):
self.real = real
self.imag = imag
def __add__(self, other):
return Complex(self.real + other.real, self.imag + other.imag)
def __sub__(self, other):
return Complex(self.real - other.real, self.imag - other.imag)
def __str__(self):
return f'{self.real} + {self.imag}i'
c1 = Complex(1, 2)
c2 = Complex(2, 3)
c3 = c1 + c2
c4 = c1 - c2
print(c3) # 3 + 5i
print(c4) # -1 - 1i
3. __len__
Method
The __len__
method returns the length (the number of items) of an object. The method should only be implemented for classes that are collections.
class Stack:
def __init__(self):
self.items = []
def push(self, item):
self.items.append(item)
def __len__(self):
return len(self.items)
s = Stack()
s.push('Hello')
s.push('World')
print(len(s)) # 2
4. __getitem__
and __setitem__
Methods
The __getitem__
method is used to implement self[key] for access. Similarly, __setitem__
is used for assignment to self[key].
class CustomDict:
def __init__(self, items):
self.items = items
def __getitem__(self, key):
return self.items[key]
def __setitem__(self, key, value):
self.items[key] = value
custom_dict = CustomDict({'one': 1, 'two': 2})
print(custom_dict['one']) # 1
custom_dict['three'] = 3
print(custom_dict['three']) # 3
5. __eq__
and __ne__
Methods
These methods are used to overload the (==) and (!=) operators respectively.
class Employee:
def __init__(self, name, id):
self.name = name
self.id = id
def __eq__(self, other):
return self.id == other.id
def __ne__(self, other):
return self.id != other.id
emp1 = Employee('John', 'E101')
emp2 = Employee('Jane', 'E102')
emp3 = Employee('David', 'E101')
print(emp1 == emp2) # False
print(emp1 == emp3) # True
print(emp1 != emp3) # False
6. __del__
Method
The __del__
method is a known as a destructor method in Python. It is called when all references to the object have been deleted i.e when an object is garbage collected.
class Test:
def __init__(self):
print('Constructor Executed')
def __del__(self):
print('Destructor Executed')
t1 = Test() # Constructor Executed
t1 = None # Destructor Executed
As you can see, magic methods are the key to Python's effective use of the object-oriented programming paradigm, allowing you to define behaviors for custom classes that are intuitive to understand and easy to use.
Decorators in Python
Indeed, there is one more Python concept that might be interesting to discuss in this chapter: Decorators in Python, which can be quite handy when you want to change the behavior of a method without changing its source code.
A decorator in Python is a powerful tool that helps developers modify the behavior of a function, method, or class definition without having to rewrite the entire code. It is a higher-order function that takes in another function as an argument and returns a modified version of it.
The decorator modifies the original object, which is passed to it as an argument, and returns an updated version that is bound to the name used in the definition. Decorators are widely used in the Python community and are a key feature of the language that enables developers to write more concise and elegant code.
They are particularly useful when working with large codebases, as they allow developers to make changes to a function's behavior without having to modify its implementation. In addition, decorators can be used to add new functionality to a function, such as logging, caching, or authentication, without having to modify its source code.
Overall, decorators are a powerful tool that can help developers write more efficient and maintainable code in Python.
Example:
Here is a basic example of a Python decorator:
def my_decorator(func):
def wrapper():
print("Something is happening before the function is called.")
func()
print("Something is happening after the function is called.")
return wrapper
@my_decorator
def say_hello():
print("Hello!")
say_hello()
When you run this code, you'll see:
Something is happening before the function is called.
Hello!
Something is happening after the function is called.
In the example above, @my_decorator
is a decorator. Functions that take other functions as arguments are also called higher-order functions. In this case, my_decorator
is a higher-order function.
The @
symbol is just syntactic sugar that allows us to easily apply a decorator to a function. The line @my_decorator
is equivalent to say_hello = my_decorator(say_hello)
.
This might be a lot to take in if you're new to decorators. That's okay. Decorators are a powerful tool in Python, but they can be a bit tricky to understand at first. Just take your time with this concept, play around with a few examples, and you'll get the hang of it.
The concept of decorators opens a whole new world of possibilities in Python. They can be used for logging, enforcing access control and authentication, rate limiting, caching, and much more.
Decorator factories can be used when you want to use a decorator, but need to supply it with arguments. A decorator factory is a function that returns a decorator. Here's how you can create one:
def repeat(num_times):
def decorator_repeat(func):
def wrapper(*args, **kwargs):
for _ in range(num_times):
result = func(*args, **kwargs)
return result
return wrapper
return decorator_repeat
@repeat(num_times=3)
def greet(name):
print(f"Hello {name}")
greet("World")
In this example, repeat(num_times=3)
returns a decorator that will repeat the decorated function three times. This is called a decorator factory.
When you run this code, you'll see:
Hello World
Hello World
Hello World
As you can see, the greet function was called three times.
This is a more advanced use of decorators, but once you understand them, they can be incredibly powerful and help make your code more readable and maintainable. The ability to modify the behavior of a function in such a clean and readable way is one of the things that makes Python such a great language to work with.
6.3 Python Special Functions
Let's dive into the special functions in Python, also known as "magic" or "dunder" methods. These methods provide a simple way to make your classes act like built-in types. This means you can use type-specific functions (like len
or +
) with your objects. You've already seen these in use with the __init__
method for classes. Let's explore more:
1. __str__
and __repr__
Methods
The __str__
and __repr__
methods in Python represent the class objects as a string – they are methods for string representation of a class. The __str__
method in Python represents the class objects as a human-readable string, while the __repr__
method is meant to be an unambiguous representation of the object, and should ideally contain more detail than __str__
. If __repr__
is defined, and __str__
is not, the objects will behave as though __str__=__repr__
.
class Employee:
def __init__(self, name, age):
self.name = name
self.age = age
def __str__(self):
return f'Employee[name={self.name}, age={self.age}]'
def __repr__(self):
return f'{self.__class__.__name__}({self.name!r}, {self.age!r})'
emp = Employee('John Doe', 30)
print(str(emp)) # Employee[name=John Doe, age=30]
print(repr(emp)) # Employee('John Doe', 30)
2. __add__
and __sub__
Methods
These methods are used to overload the +
and -
operator.
class Complex:
def __init__(self, real, imag):
self.real = real
self.imag = imag
def __add__(self, other):
return Complex(self.real + other.real, self.imag + other.imag)
def __sub__(self, other):
return Complex(self.real - other.real, self.imag - other.imag)
def __str__(self):
return f'{self.real} + {self.imag}i'
c1 = Complex(1, 2)
c2 = Complex(2, 3)
c3 = c1 + c2
c4 = c1 - c2
print(c3) # 3 + 5i
print(c4) # -1 - 1i
3. __len__
Method
The __len__
method returns the length (the number of items) of an object. The method should only be implemented for classes that are collections.
class Stack:
def __init__(self):
self.items = []
def push(self, item):
self.items.append(item)
def __len__(self):
return len(self.items)
s = Stack()
s.push('Hello')
s.push('World')
print(len(s)) # 2
4. __getitem__
and __setitem__
Methods
The __getitem__
method is used to implement self[key] for access. Similarly, __setitem__
is used for assignment to self[key].
class CustomDict:
def __init__(self, items):
self.items = items
def __getitem__(self, key):
return self.items[key]
def __setitem__(self, key, value):
self.items[key] = value
custom_dict = CustomDict({'one': 1, 'two': 2})
print(custom_dict['one']) # 1
custom_dict['three'] = 3
print(custom_dict['three']) # 3
5. __eq__
and __ne__
Methods
These methods are used to overload the (==) and (!=) operators respectively.
class Employee:
def __init__(self, name, id):
self.name = name
self.id = id
def __eq__(self, other):
return self.id == other.id
def __ne__(self, other):
return self.id != other.id
emp1 = Employee('John', 'E101')
emp2 = Employee('Jane', 'E102')
emp3 = Employee('David', 'E101')
print(emp1 == emp2) # False
print(emp1 == emp3) # True
print(emp1 != emp3) # False
6. __del__
Method
The __del__
method is a known as a destructor method in Python. It is called when all references to the object have been deleted i.e when an object is garbage collected.
class Test:
def __init__(self):
print('Constructor Executed')
def __del__(self):
print('Destructor Executed')
t1 = Test() # Constructor Executed
t1 = None # Destructor Executed
As you can see, magic methods are the key to Python's effective use of the object-oriented programming paradigm, allowing you to define behaviors for custom classes that are intuitive to understand and easy to use.
Decorators in Python
Indeed, there is one more Python concept that might be interesting to discuss in this chapter: Decorators in Python, which can be quite handy when you want to change the behavior of a method without changing its source code.
A decorator in Python is a powerful tool that helps developers modify the behavior of a function, method, or class definition without having to rewrite the entire code. It is a higher-order function that takes in another function as an argument and returns a modified version of it.
The decorator modifies the original object, which is passed to it as an argument, and returns an updated version that is bound to the name used in the definition. Decorators are widely used in the Python community and are a key feature of the language that enables developers to write more concise and elegant code.
They are particularly useful when working with large codebases, as they allow developers to make changes to a function's behavior without having to modify its implementation. In addition, decorators can be used to add new functionality to a function, such as logging, caching, or authentication, without having to modify its source code.
Overall, decorators are a powerful tool that can help developers write more efficient and maintainable code in Python.
Example:
Here is a basic example of a Python decorator:
def my_decorator(func):
def wrapper():
print("Something is happening before the function is called.")
func()
print("Something is happening after the function is called.")
return wrapper
@my_decorator
def say_hello():
print("Hello!")
say_hello()
When you run this code, you'll see:
Something is happening before the function is called.
Hello!
Something is happening after the function is called.
In the example above, @my_decorator
is a decorator. Functions that take other functions as arguments are also called higher-order functions. In this case, my_decorator
is a higher-order function.
The @
symbol is just syntactic sugar that allows us to easily apply a decorator to a function. The line @my_decorator
is equivalent to say_hello = my_decorator(say_hello)
.
This might be a lot to take in if you're new to decorators. That's okay. Decorators are a powerful tool in Python, but they can be a bit tricky to understand at first. Just take your time with this concept, play around with a few examples, and you'll get the hang of it.
The concept of decorators opens a whole new world of possibilities in Python. They can be used for logging, enforcing access control and authentication, rate limiting, caching, and much more.
Decorator factories can be used when you want to use a decorator, but need to supply it with arguments. A decorator factory is a function that returns a decorator. Here's how you can create one:
def repeat(num_times):
def decorator_repeat(func):
def wrapper(*args, **kwargs):
for _ in range(num_times):
result = func(*args, **kwargs)
return result
return wrapper
return decorator_repeat
@repeat(num_times=3)
def greet(name):
print(f"Hello {name}")
greet("World")
In this example, repeat(num_times=3)
returns a decorator that will repeat the decorated function three times. This is called a decorator factory.
When you run this code, you'll see:
Hello World
Hello World
Hello World
As you can see, the greet function was called three times.
This is a more advanced use of decorators, but once you understand them, they can be incredibly powerful and help make your code more readable and maintainable. The ability to modify the behavior of a function in such a clean and readable way is one of the things that makes Python such a great language to work with.
6.3 Python Special Functions
Let's dive into the special functions in Python, also known as "magic" or "dunder" methods. These methods provide a simple way to make your classes act like built-in types. This means you can use type-specific functions (like len
or +
) with your objects. You've already seen these in use with the __init__
method for classes. Let's explore more:
1. __str__
and __repr__
Methods
The __str__
and __repr__
methods in Python represent the class objects as a string – they are methods for string representation of a class. The __str__
method in Python represents the class objects as a human-readable string, while the __repr__
method is meant to be an unambiguous representation of the object, and should ideally contain more detail than __str__
. If __repr__
is defined, and __str__
is not, the objects will behave as though __str__=__repr__
.
class Employee:
def __init__(self, name, age):
self.name = name
self.age = age
def __str__(self):
return f'Employee[name={self.name}, age={self.age}]'
def __repr__(self):
return f'{self.__class__.__name__}({self.name!r}, {self.age!r})'
emp = Employee('John Doe', 30)
print(str(emp)) # Employee[name=John Doe, age=30]
print(repr(emp)) # Employee('John Doe', 30)
2. __add__
and __sub__
Methods
These methods are used to overload the +
and -
operator.
class Complex:
def __init__(self, real, imag):
self.real = real
self.imag = imag
def __add__(self, other):
return Complex(self.real + other.real, self.imag + other.imag)
def __sub__(self, other):
return Complex(self.real - other.real, self.imag - other.imag)
def __str__(self):
return f'{self.real} + {self.imag}i'
c1 = Complex(1, 2)
c2 = Complex(2, 3)
c3 = c1 + c2
c4 = c1 - c2
print(c3) # 3 + 5i
print(c4) # -1 - 1i
3. __len__
Method
The __len__
method returns the length (the number of items) of an object. The method should only be implemented for classes that are collections.
class Stack:
def __init__(self):
self.items = []
def push(self, item):
self.items.append(item)
def __len__(self):
return len(self.items)
s = Stack()
s.push('Hello')
s.push('World')
print(len(s)) # 2
4. __getitem__
and __setitem__
Methods
The __getitem__
method is used to implement self[key] for access. Similarly, __setitem__
is used for assignment to self[key].
class CustomDict:
def __init__(self, items):
self.items = items
def __getitem__(self, key):
return self.items[key]
def __setitem__(self, key, value):
self.items[key] = value
custom_dict = CustomDict({'one': 1, 'two': 2})
print(custom_dict['one']) # 1
custom_dict['three'] = 3
print(custom_dict['three']) # 3
5. __eq__
and __ne__
Methods
These methods are used to overload the (==) and (!=) operators respectively.
class Employee:
def __init__(self, name, id):
self.name = name
self.id = id
def __eq__(self, other):
return self.id == other.id
def __ne__(self, other):
return self.id != other.id
emp1 = Employee('John', 'E101')
emp2 = Employee('Jane', 'E102')
emp3 = Employee('David', 'E101')
print(emp1 == emp2) # False
print(emp1 == emp3) # True
print(emp1 != emp3) # False
6. __del__
Method
The __del__
method is a known as a destructor method in Python. It is called when all references to the object have been deleted i.e when an object is garbage collected.
class Test:
def __init__(self):
print('Constructor Executed')
def __del__(self):
print('Destructor Executed')
t1 = Test() # Constructor Executed
t1 = None # Destructor Executed
As you can see, magic methods are the key to Python's effective use of the object-oriented programming paradigm, allowing you to define behaviors for custom classes that are intuitive to understand and easy to use.
Decorators in Python
Indeed, there is one more Python concept that might be interesting to discuss in this chapter: Decorators in Python, which can be quite handy when you want to change the behavior of a method without changing its source code.
A decorator in Python is a powerful tool that helps developers modify the behavior of a function, method, or class definition without having to rewrite the entire code. It is a higher-order function that takes in another function as an argument and returns a modified version of it.
The decorator modifies the original object, which is passed to it as an argument, and returns an updated version that is bound to the name used in the definition. Decorators are widely used in the Python community and are a key feature of the language that enables developers to write more concise and elegant code.
They are particularly useful when working with large codebases, as they allow developers to make changes to a function's behavior without having to modify its implementation. In addition, decorators can be used to add new functionality to a function, such as logging, caching, or authentication, without having to modify its source code.
Overall, decorators are a powerful tool that can help developers write more efficient and maintainable code in Python.
Example:
Here is a basic example of a Python decorator:
def my_decorator(func):
def wrapper():
print("Something is happening before the function is called.")
func()
print("Something is happening after the function is called.")
return wrapper
@my_decorator
def say_hello():
print("Hello!")
say_hello()
When you run this code, you'll see:
Something is happening before the function is called.
Hello!
Something is happening after the function is called.
In the example above, @my_decorator
is a decorator. Functions that take other functions as arguments are also called higher-order functions. In this case, my_decorator
is a higher-order function.
The @
symbol is just syntactic sugar that allows us to easily apply a decorator to a function. The line @my_decorator
is equivalent to say_hello = my_decorator(say_hello)
.
This might be a lot to take in if you're new to decorators. That's okay. Decorators are a powerful tool in Python, but they can be a bit tricky to understand at first. Just take your time with this concept, play around with a few examples, and you'll get the hang of it.
The concept of decorators opens a whole new world of possibilities in Python. They can be used for logging, enforcing access control and authentication, rate limiting, caching, and much more.
Decorator factories can be used when you want to use a decorator, but need to supply it with arguments. A decorator factory is a function that returns a decorator. Here's how you can create one:
def repeat(num_times):
def decorator_repeat(func):
def wrapper(*args, **kwargs):
for _ in range(num_times):
result = func(*args, **kwargs)
return result
return wrapper
return decorator_repeat
@repeat(num_times=3)
def greet(name):
print(f"Hello {name}")
greet("World")
In this example, repeat(num_times=3)
returns a decorator that will repeat the decorated function three times. This is called a decorator factory.
When you run this code, you'll see:
Hello World
Hello World
Hello World
As you can see, the greet function was called three times.
This is a more advanced use of decorators, but once you understand them, they can be incredibly powerful and help make your code more readable and maintainable. The ability to modify the behavior of a function in such a clean and readable way is one of the things that makes Python such a great language to work with.
6.3 Python Special Functions
Let's dive into the special functions in Python, also known as "magic" or "dunder" methods. These methods provide a simple way to make your classes act like built-in types. This means you can use type-specific functions (like len
or +
) with your objects. You've already seen these in use with the __init__
method for classes. Let's explore more:
1. __str__
and __repr__
Methods
The __str__
and __repr__
methods in Python represent the class objects as a string – they are methods for string representation of a class. The __str__
method in Python represents the class objects as a human-readable string, while the __repr__
method is meant to be an unambiguous representation of the object, and should ideally contain more detail than __str__
. If __repr__
is defined, and __str__
is not, the objects will behave as though __str__=__repr__
.
class Employee:
def __init__(self, name, age):
self.name = name
self.age = age
def __str__(self):
return f'Employee[name={self.name}, age={self.age}]'
def __repr__(self):
return f'{self.__class__.__name__}({self.name!r}, {self.age!r})'
emp = Employee('John Doe', 30)
print(str(emp)) # Employee[name=John Doe, age=30]
print(repr(emp)) # Employee('John Doe', 30)
2. __add__
and __sub__
Methods
These methods are used to overload the +
and -
operator.
class Complex:
def __init__(self, real, imag):
self.real = real
self.imag = imag
def __add__(self, other):
return Complex(self.real + other.real, self.imag + other.imag)
def __sub__(self, other):
return Complex(self.real - other.real, self.imag - other.imag)
def __str__(self):
return f'{self.real} + {self.imag}i'
c1 = Complex(1, 2)
c2 = Complex(2, 3)
c3 = c1 + c2
c4 = c1 - c2
print(c3) # 3 + 5i
print(c4) # -1 - 1i
3. __len__
Method
The __len__
method returns the length (the number of items) of an object. The method should only be implemented for classes that are collections.
class Stack:
def __init__(self):
self.items = []
def push(self, item):
self.items.append(item)
def __len__(self):
return len(self.items)
s = Stack()
s.push('Hello')
s.push('World')
print(len(s)) # 2
4. __getitem__
and __setitem__
Methods
The __getitem__
method is used to implement self[key] for access. Similarly, __setitem__
is used for assignment to self[key].
class CustomDict:
def __init__(self, items):
self.items = items
def __getitem__(self, key):
return self.items[key]
def __setitem__(self, key, value):
self.items[key] = value
custom_dict = CustomDict({'one': 1, 'two': 2})
print(custom_dict['one']) # 1
custom_dict['three'] = 3
print(custom_dict['three']) # 3
5. __eq__
and __ne__
Methods
These methods are used to overload the (==) and (!=) operators respectively.
class Employee:
def __init__(self, name, id):
self.name = name
self.id = id
def __eq__(self, other):
return self.id == other.id
def __ne__(self, other):
return self.id != other.id
emp1 = Employee('John', 'E101')
emp2 = Employee('Jane', 'E102')
emp3 = Employee('David', 'E101')
print(emp1 == emp2) # False
print(emp1 == emp3) # True
print(emp1 != emp3) # False
6. __del__
Method
The __del__
method is a known as a destructor method in Python. It is called when all references to the object have been deleted i.e when an object is garbage collected.
class Test:
def __init__(self):
print('Constructor Executed')
def __del__(self):
print('Destructor Executed')
t1 = Test() # Constructor Executed
t1 = None # Destructor Executed
As you can see, magic methods are the key to Python's effective use of the object-oriented programming paradigm, allowing you to define behaviors for custom classes that are intuitive to understand and easy to use.
Decorators in Python
Indeed, there is one more Python concept that might be interesting to discuss in this chapter: Decorators in Python, which can be quite handy when you want to change the behavior of a method without changing its source code.
A decorator in Python is a powerful tool that helps developers modify the behavior of a function, method, or class definition without having to rewrite the entire code. It is a higher-order function that takes in another function as an argument and returns a modified version of it.
The decorator modifies the original object, which is passed to it as an argument, and returns an updated version that is bound to the name used in the definition. Decorators are widely used in the Python community and are a key feature of the language that enables developers to write more concise and elegant code.
They are particularly useful when working with large codebases, as they allow developers to make changes to a function's behavior without having to modify its implementation. In addition, decorators can be used to add new functionality to a function, such as logging, caching, or authentication, without having to modify its source code.
Overall, decorators are a powerful tool that can help developers write more efficient and maintainable code in Python.
Example:
Here is a basic example of a Python decorator:
def my_decorator(func):
def wrapper():
print("Something is happening before the function is called.")
func()
print("Something is happening after the function is called.")
return wrapper
@my_decorator
def say_hello():
print("Hello!")
say_hello()
When you run this code, you'll see:
Something is happening before the function is called.
Hello!
Something is happening after the function is called.
In the example above, @my_decorator
is a decorator. Functions that take other functions as arguments are also called higher-order functions. In this case, my_decorator
is a higher-order function.
The @
symbol is just syntactic sugar that allows us to easily apply a decorator to a function. The line @my_decorator
is equivalent to say_hello = my_decorator(say_hello)
.
This might be a lot to take in if you're new to decorators. That's okay. Decorators are a powerful tool in Python, but they can be a bit tricky to understand at first. Just take your time with this concept, play around with a few examples, and you'll get the hang of it.
The concept of decorators opens a whole new world of possibilities in Python. They can be used for logging, enforcing access control and authentication, rate limiting, caching, and much more.
Decorator factories can be used when you want to use a decorator, but need to supply it with arguments. A decorator factory is a function that returns a decorator. Here's how you can create one:
def repeat(num_times):
def decorator_repeat(func):
def wrapper(*args, **kwargs):
for _ in range(num_times):
result = func(*args, **kwargs)
return result
return wrapper
return decorator_repeat
@repeat(num_times=3)
def greet(name):
print(f"Hello {name}")
greet("World")
In this example, repeat(num_times=3)
returns a decorator that will repeat the decorated function three times. This is called a decorator factory.
When you run this code, you'll see:
Hello World
Hello World
Hello World
As you can see, the greet function was called three times.
This is a more advanced use of decorators, but once you understand them, they can be incredibly powerful and help make your code more readable and maintainable. The ability to modify the behavior of a function in such a clean and readable way is one of the things that makes Python such a great language to work with.