Core Python / Classes and Objects#
What is a class in Python, and what is its purpose?
A class in Python is a blueprint or a template for creating objects. It defines a set of properties (attributes) and methods (functions) that the objects created from that class will have. The purpose of a class is to encapsulate data and behavior into a single entity, promoting code reusability, modularity, and organization. Classes are the fundamental building blocks of object-oriented programming in Python.
How do you define a class in Python?
To define a class in Python, you use the class keyword followed by the name of the class, and then a colon (:) to start the class block.
class ClassName: # Class attributes and methods go here pass
Within the class block, you can define attributes (variables) and methods (functions) that belong to the class.
class Dog: # Class attribute species = "Canis familiaris" # Constructor method def __init__(self, name, breed): self.name = name self.breed = breed # Instance method def bark(self): print("{} says: Woof!".format(self.name))
In this example,
Dog
is the name of the class. It has a class attributespecies
, a constructor method__init__
to initialize object attributes, and an instance methodbark
that prints a message.What is an object in Python, and how is it related to a class? Explain how an object has state, behavior, and an identity.
In Python, an object is an instance of a class. It is a concrete realization of the blueprint defined by the class. The class defines the structure and behavior, while the object represents a specific instance or entity within that structure.
An object has the following three characteristics:
State: The state of an object refers to its attributes or properties, which are defined in the class and hold data or values. These attributes represent the current state or condition of the object. For example, in a BankAccount class, the account_holder, account_number, and balance would be attributes representing the state of a particular bank account object.
Behavior: The behavior of an object is determined by its methods, which are functions defined within the class. These methods define how the object can interact with its data or perform specific operations. In the BankAccount class, deposit(), withdraw(), and check_balance() are methods that define the behavior of a bank account object.
Identity: Every object in Python has a unique identity, which is represented by its memory address. Even if two objects have the same state (attribute values), they are still distinct objects with different identities. The id() function in Python can be used to get the unique identity of an object. The object may posses attributes that provide a unique identifier as well.
class BankAccount: def __init__(self, account_holder, account_number, balance=0): self.account_holder = account_holder # State self.account_number = account_number # State self.balance = balance # State def deposit(self, amount): # Behavior self.balance += amount print(f"Deposit of ${amount} successful. New balance: ${self.balance}") def withdraw(self, amount): # Behavior if self.balance >= amount: self.balance -= amount print(f"Withdrawal of ${amount} successful. Remaining balance: ${self.balance}") else: print(f"Insufficient funds. Your balance is ${self.balance}") # Creating two objects account1 = BankAccount("John Doe", "12345", 1000) account2 = BankAccount("Jane Smith", "67890", 500) # Accessing state and behavior print(account1.account_holder) # State account1.deposit(500) # Behavior print(id(account1), id(account2)) # Identity
How do you create an instance of a class (an object)?
Call the object’s
__init___
method by using the class name as demonstrated in the code example of the previous question.What is the purpose of the
__init__
method in a Python class?In Python, constructing an object actually consists of two special methods:
__new__
and__init__
.__new__
:The
__new__
method is a static method that is called before the object is created.It is responsible for creating and returning the new instance of the class.
The
__new__
method is typically used for custom object creation, such as creating immutable objects or implementing custom memory allocation strategies.If the
__new__
method is not overridden, Python’s default__new__
implementation is used, which creates and returns a new instance of the class.
__init__
:The
__init__
method is an instance method that is called after the object is created by__new__
.It is responsible for initializing the attributes of the newly created object.
The
__init__
method is where you typically set the initial state of the object by assigning values to its attributes.If the
__init__
method is not defined in a class, the object will be created without any initial attribute values.
In most cases, you don’t need to override the
__new__
method unless you have specific requirements for custom object creation or memory management. However, defining the__init__
method is a common practice to initialize the object’s attributes and set up its initial state.How do you define instance attributes in a class?
In Python, instance attributes are defined within the
__init__
method of a class. These attributes are specific to each instance (object) of the class and can have different values for different instances. There are two ways to define instance attributes:Inside the
__init__
methodInstance attributes are typically initialized inside the
__init__
method. Theself
parameter is used to refer to the instance being created (technically, it is the position that matters,self
is the expected convention used.), and instance attributes are assigned values using the dot notation (self.attribute_name = value
).class Circle: def __init__(self, radius): self.radius = radius # Instance attribute self.area = 3.14 * self.radius ** 2 # Instance attribute circle1 = Circle(5) print(circle1.radius) # Output: 5 print(circle1.area) # Output: 78.5
Outside the
__init__
methodInstance attributes can also be defined directly inside the class body, outside of any method. These attributes will be shared among all instances of the class, and their values can be overridden within the init method or other instance methods.
class Rectangle: color = "blue" # Class attribute def __init__(self, length, width): self.length = length # Instance attribute self.width = width # Instance attribute rect1 = Rectangle(5, 3) print(rect1.length) # Output: 5 print(rect1.width) # Output: 3 print(rect1.color) # Output: blue. Note, accessing the class attribute via the object rect2 = Rectangle(2, 4) print(rect2.color) # Output: blue, Note, accessing the class attribute via the object rect2.color = "red" # Assigning a specific instance attribute print(rect2.color) # Output: red print(rect1.color) # Output: blue print(Rectangle.color) # Output: blue
In this example,
color
became an instance attribute withrect2.color = "red"
. Generally, it is preferred to establish instance attributes in the initializer to avoid any confusion.What is the difference between instance attributes and class attributes?
The main difference between instance attributes and class attributes in Python lies in how they are associated with objects and classes.
Instance Attributes
Instance attributes are specific to each instance (object) of a class.
Defined within the
__init__
method or other instance methods.Since Python is dynamic, can also be defined outside of the overall class definition. However, this should be avoided due to understandability concerns.
Each instance of the class has its own copy of the instance attributes, and their values can be different for different instances.
Instance attributes are accessed using the instance itself or the
self
reference within instance methods.
Example:
self.name
,self.age
, etc.Class Attributes
Class attributes are associated with the class itself, rather than any specific instance.
Defined directly within the class body, outside any method.
All instances of the class share the same copy of the class attribute.
Class attributes are usually used to store data or behavior that is common to all instances of the class.
Class attributes are accessed using the class name or an instance of the class.
Example:
ClassName.class_attribute
orinstance.class_attribute
Class attributes are useful for storing constants or default values that should be shared among all instances of the class. Instance attributes, on the other hand, are used to store data that is specific to each object.
Instance attributes take precedence over class attributes when accessed through an instance. If an instance attribute has the same name as a class attribute, the instance attribute will be used.
How do you define methods in a Python class? What is the purpose of the
self
parameter in Python class methods?To define methods in a Python class, you use the
def
keyword as with a normal function, but you include theself
parameter as the first argument of each method. The self parameter represents the instance of the class itself, allowing the method to access and manipulate the attributes (variables) and other methods of that specific instance. (Note: technically the parameter does not need to be calledself
. However, this is the standard convention in Python and using any other name will confuse other developers - and, most likely you.)The
self parameter
-allows methods to access and modify the instance attributes. For example,
self.balance
refers to thebalance
attribute of that particular instance of aBankAccount
.enables methods to call other methods of the same class by using
self.other_method()
. This way, methods can interact with each other within the same instance.distinguishes instance attributes from local variables inside methods. Without
self
, Python would treat attributes as local variables within the method scope.
Here’s an example using a BankAccount class:
class BankAccount: def __init__(self, name, initial_balance): self.name = name self.balance = initial_balance def deposit(self, amount): self.balance += amount print(f"Deposited {amount} into
How do you access and modify instance attributes from within a class method?
You need to use the
self
parameter to prefix the attribute. See the answer to the previous question for more details.What are class methods in Python, and how do they differ from instance methods?
Class methods in Python are methods that are bound to the class itself, rather than any specific instance of the class. They are defined using the @classmethod decorator and take the class (
cls
) as the first argument instead of the instance (self
). Class methods can access and modify class attributes, but they cannot access or modify instance attributes directly.The main differences between class methods and instance methods are:
Binding: Instance methods are bound to instances of the class, while class methods are bound to the class itself.
First argument: Instance methods take
self
as the first argument, which represents the instance object. Class methods takecls
as the first argument, which represents the class itself. (Note: As with theself
name,cls
is used by convention.)Access to attributes: Instance methods can access and modify both instance attributes and class attributes. Class methods can access and modify class attributes directly, but they cannot access instance attributes directly (they can access them indirectly through an instance).
Use cases: Instance methods are used for operations that involve instance-specific data or behavior. Class methods are typically used for operations that involve the class itself, such as creating
alternative constructors (factory methods) or modifying class-level data.
Here’s an example using a BankAccount class to illustrate the use of class methods:
class BankAccount: MIN_BALANCE = 1000 # Class attribute def __init__(self, name, balance): self.name = name self.balance = balance @classmethod def create_account(cls, name, initial_deposit): if initial_deposit < cls.MIN_BALANCE: print("Minimum deposit of {} is required to open an account.".format(cls.MIN_BALANCE)) return None else: return cls(name, initial_deposit) def deposit(self, amount): self.balance += amount print("Deposited {} into {}'s account. New balance: {}".format(amount, self.name, self.balance)) def withdraw(self, amount): if self.balance >= amount: self.balance -= amount print("Withdrew {} from {}'s account. Remaining balance: {}".format(amount, self.name, self.balance)) else: print("Insufficient funds in {}'s account.".format(self.name)) # Creating an account using the class method account1 = BankAccount.create_account("Alice", 2000) account2 = BankAccount.create_account("Bob", 500) # Minimum deposit requirement not met # Using instance methods if account1: account1.deposit(500) # Deposited 500 into Alice's account. New balance: 2500 account1.withdraw(1000) # Withdrew 1000 from Alice's account. Remaining balance: 1500
What are static methods in Python, and when would you use them?
Static methods in Python are methods that are bound to the class itself, rather than any instance of the class. They are defined within a class but do not take the instance (
self
) or the class (cls
) as the first argument. Static methods are used when you need a utility function that is logically related to the class but does not require access to the instance or class attributes.You would use static methods in the following scenarios:
Utility Functions: When you have a function that performs a specific operation related to the class but does not require access to the instance or class data. Static methods are useful for grouping related utility functions within a class for better code organization and readability.
Independent Operations: If you have a function that performs an operation independent of the instance or class state, it can be defined as a static method. For example, a function that performs mathematical calculations or data conversions.
Single Implementation: Static methods are useful when you want to ensure that a particular implementation of a method cannot be overridden by subclasses. This is because static methods are resolved at the class level, not the instance level.
Code Organization: Static methods can help organize code by grouping related utility functions within a class, even if they do not directly operate on the class or its instances. This can improve code readability and maintainability.
Here’s an example to illustrate the use of a static method:
import datetime as dt class Bond: def __init__(self, face_value, coupon_rate, maturity_date): self.face_value = face_value self.coupon_rate = coupon_rate self.maturity_date = maturity_date @staticmethod def present_value(future_value, discount_rate, years): return future_value / ((1 + discount_rate) ** years) @staticmethod def yield_to_maturity(bond_price, face_value, coupon_rate, years_to_maturity): # Implement yield to maturity calculation bond = Bond(1000, 0.05, dt.date(2025, 1, 1)) pv = Bond.present_value(1000, 0.06, 5) # Calculate present value ytm = Bond.yield_to_maturity(950, 1000, 0.05, 5) # Calculate yield to maturity
present_value
andyield_to_maturity
are called directly on the Bond class, without the need to create an instance of the class. They perform financial calculations that are independent of any specific bond instance.What is encapsulation in object-oriented programming, and how is it achieved in Python?
Encapsulation is a fundamental principle of object-oriented programming (OOP) that involves bundling data (attributes) and methods (functions) that operate on that data within a single unit, called a class. The main goals of encapsulation are:
Data Hiding: Encapsulation hides the internal implementation details of an object from the outside world, preventing direct access to the object’s data. This protects the data from accidental modification and ensures data integrity.
Access Control: Encapsulation allows you to control access to the object’s data and methods by defining access modifiers (public, protected, and private) that determine the level of accessibility.
However, Python does not implement these explicit access modifiers. Instead, the language relies upon naming conventions to indicate the intended accessibility:
Public Members: By default, all attributes and methods in a class are public, meaning they can be accessed from anywhere. There is no special syntax for public members.
Protected Members: A single leading underscore (
_
) before the name of an attribute or method indicates that it is protected. Protected members should be treated as private and accessed only from within the class and its subclasses.Private Members: A double leading underscore (
__
) before the name of an attribute or method indicates that it is private. Private members should only be accessible only within the class itself and not directly accessed from outside the class or its subclasses. Example:
class BankAccount: def __init__(self, balance): self._balance = balance # Protected attribute def deposit(self, amount): self._balance += amount # Accessing protected attribute within the class def withdraw(self, amount): if self._balance >= amount: self._balance -= amount else: print("Insufficient funds") def get_balance(self): return self._balance # Providing controlled access to the protected attribute # Creating an instance of the BankAccount class account = BankAccount(1000) account.deposit(500) # Accessing public method print(account.get_balance()) # Output: 1500 # accessing the protected attribute directly (not recommended) print(account._balance) # Output: 1500
In this example, the
_balance
attribute is protected, and thedeposit
,withdraw
, andget_balance
methods provide controlled access to modify and retrieve the balance.How do you define and use getters and setters in Python classes? Focus on the appropriate decorators.
Python also supports the use of getter and setter methods to provide controlled access to attributes. These methods allow you to encapsulate the logic for getting and setting attribute values, respectively. While you can simplt use method names such as
get_radius
and `set_radius as below, the pythonic way is to use decorators.class Circle: def __init__(self, radius): self._radius = radius # Protected attribute @property def radius(self): return self._radius @radius.setter def radius(self, new_radius): if new_radius >= 0: self._radius = new_radius else: raise ValueError("Radius cannot be a negative number") # Creating an instance of the Circle class circle = Circle(5) print(circle.radius) # Output: 5 circle.radius = 10 print(circle.radius) # Output: 10 circle.radius = -10 # Raises ValueException
Explain how it is not possible in Python to prevent code outside the class definition from directly accessing attributes. How do programmers signify to avoid doing this, though?
In Python, it is not possible to completely prevent code outside the class definition from directly accessing attributes. Python does not have true private attributes like some other object-oriented programming languages. However, Python uses naming conventions and name mangling to signify that certain attributes should be treated as private and not accessed directly from outside the class.
What name pattern is used in Python to override various operators such as
==
and<
? What are these methods called?The methods used to override operators are called “special methods”, “dunder (double underscore) methods” or “magic methods”. These methods follow a specific naming pattern that begins and ends with double underscores (
__
).To override comparison operators such as
==
and<
, you need to define the following special methods in your class:__eq__(self, other)
- To override the==
operator for equality comparison.__lt__(self, other)
- To override the<
operator for less than comparison.__le__(self, other)
- To override the<=
operator for less than or equal to comparison.__gt__(self, other)
- To override the>
operator for greater than comparison.__ge__(self, other)
- To override the>=
operator for greater than or equal to comparison.__ne__(self, other)
- To override the!=
operator for inequality comparison.
Here’s an example that overrides the
<
and==
operators for aPerson
class:class Person: def __init__(self, name, age): self.name = name self.age = age def __eq__(self, other): return self.age == other.age def __lt__(self, other): return self.age < other.age person1 = Person("Alice", 25) person2 = Person("Bob", 30) print(person1 < person2) # Output: True print(person1 == person2) # Output: False
What will be the output of the following Python code:
class test: def __init__(self,a="Hello World"): self.a=a def display(): print(self.a) obj=test() obj.display()
The program contains an error and a stack trace appears - the
self
argument needs to be defined in the parameter list fordisplay()
.What does built-in function
type*()
do in the context of classes?Note: This is an advanced topic and is not required for the program.
In Python, the type() function is a built-in function that returns the type of an object. However, when used in the context of classes, it has a special behavior and serves as a way to create new class objects dynamically. When you call type(name, bases, dict), it returns a new class.
name (string): The name of the class.
bases (tuple): A tuple of base classes that the new class should inherit from.
dict (dictionary): A dictionary containing the class attributes, methods, and other definitions.
Imagine you have a financial institution that offers different types of accounts, such as Savings, Checking, and Investment accounts. Instead of hardcoding the classes for each account type, you can dynamically create these classes using
type()
.# Creating a Savings Account class dynamically SavingsAccount = type('SavingsAccount', (), { 'interest_rate': 0.03, 'deposit': lambda self, amount: setattr(self, 'balance', amount), 'withdraw': lambda self, amount: setattr(self, 'balance', getattr(self, 'balance', 0) - amount) }) # Creating a Checking Account class dynamically CheckingAccount = type('CheckingAccount', (), { 'transaction_fee': 5.00, 'deposit': lambda self, amount: setattr(self, 'balance', amount), 'withdraw': lambda self, amount: setattr(self, 'balance', getattr(self, 'balance', 0) - amount - self.transaction_fee) }) # Creating instances of the dynamically created classes savings_account = SavingsAccount() checking_account = CheckingAccount() # Using the instances savings_account.deposit(1000) print(savings_account.balance) # Output: 1000 checking_account.deposit(500) checking_account.withdraw(100) print(checking_account.balance) # Output: 395.0 (after deducting the transaction fee)
In this example, we dynamically create two classes:
SavingsAccount
andCheckingAccount
. Each class has its own attributes (interest_rate
andtransaction_fee
, respectively) and methods (deposit
andwithdraw
). Note: Lambda functions will covered in the next section of this guide.The
deposit
method for both classes sets thebalance
attribute of the instance to the deposited amount. Thewithdraw
method forSavingsAccount
simply deducts the withdrawn amount from the balance, while thewithdraw
method forCheckingAccount
also deducts a transaction fee.We then create instances of these dynamically created classes and use them like regular class instances.
Using
type()
this way allows us to create classes on-the-fly based on certain conditions or configurations, without having to hardcode the class definitions. This can be useful in scenarios where the class structures need to be dynamic and flexible, such as in financial systems that offer different account types or features based on customer preferences or account configurations.How do you create an empty class in Python? Why is creating an empty class useful?
In Python, you can create an empty class by defining a class without any attributes or methods. Here’s the syntax:
class EmptyClass: pass
The
pass
statement is used as a placeholder because Python requires at least one statement in the class definition.Creating an empty class can be useful in several situations:
Placeholder for Future Implementation: Sometimes, you might want to define an empty class as a placeholder or a stub, with the intention of adding attributes and methods later. This can be helpful when you’re planning the structure of your code or working with a framework that requires certain class definitions.
Base Class for Inheritance: An empty class can serve as a base class for other classes to inherit from. This can be useful when you want to create a hierarchy of classes and provide a common base for shared behavior or attributes. Inheritance is introduced in the next notebook.
Namespace or Container: An empty class can be used as a namespace or a container to group related functions or variables together. This can make your code more organized and easier to maintain.
Mixin Classes: In object-oriented programming, mixin classes are classes that provide additional methods or functionality to other classes through inheritance. Empty classes can be used as mixin classes to extend the functionality of existing classes.
Testing and Experimentation: When learning or experimenting with object-oriented programming concepts, creating empty classes can be a good starting point for understanding class definitions and inheritance mechanisms.
class BaseClass: pass class DerivedClass(BaseClass): def __init__(self, value): self.value = value def print_value(self): print(self.value) # Using the empty class as a namespace class Utils: pass Utils.double = lambda x: x * 2 Utils.square = lambda x: x ** 2 # Using the DerivedClass obj = DerivedClass(10) obj.print_value() # Output: 10 # Using the Utils namespace print(Utils.double(5)) # Output: 10 print(Utils.square(3)) # Output: 9
In this example,
BaseClass
is an empty class that serves as a base class forDerivedClass
. TheUtils
class is an empty class used as a namespace to group utility functions (double
andsquare
).While empty classes may not be as common as classes with attributes and methods, they can be useful in certain scenarios, especially when working with object-oriented programming concepts or frameworks that require specific class structures.
What is the output of the following code?
class demo(): def __repr__(self): return '__repr__ built-in function called' def __str__(self): return '__str__ built-in function called' s=demo() print(s)
str built-in function called
What is the
__del__
method used for in Python? Explain when it is invoked. Can programmers rely upon it being invoked?The
__del__
method in Python is a special method used for object destruction and resource cleanup. The garbage collecter calls this method on an object when it is about to be destroyed and its memory is reclaimed. However, programmers should not rely on__del__
for resource cleanup, as its invocation is not guaranteed and can be delayed indefinitely by Python’s garbage collector. Further, it is possible for the interpreter to exit before__del__
is called.It may be worth using
__del__
as a fallback mechanism to release any resources that may have been missed or leaked due to programming errors or exceptional circumstances. However, it should not be the primary mechanism for resource cleanup, as its invocation is not guaranteed and can lead to resource leaks if not handled properly. Even here, thewith
statement (with the appropriate contet manager) is the preferred and correct solution.