As a professional C++ programmer since C++03, the C++ way object-oriented thinking has been deeply embedded in my mind, and it helped me a lot when I picked up a new language such as C# and Java. However, the benefit was not apparent when I encountered Python the first time. Python is also an object-oriented programming language but significantly differs from other object-oriented programming languages like C++ and Java. Therefore, this series tries to point out some noteworthy Pythonic programming that may surprise C++ programmers, and it is not a Python tutorial. I hope my experience could help people who know C++ to pick up Python even simpler.
The first article of the Python vs. C++ Series starts from one basic object-oriented programming concept – encapsulation and access functions.
Note that the Python code in the series assumes Python 3.7 or newer.
Brief Review of Encapsulation
Encapsulation is an object-oriented programming concept which encloses the implementation detail of a class. Why not disclose the implementation detail? One good programming practice is that a client code should only access class’ public interfaces. As long as the interface remains the same, the client code does not need to change if the implementation changes. Encapsulation also reduces the complexity and simplifies debugging processes. Besides, an encapsulated class helps protect the data and prevent misuse because a client code cannot access the protected portion of a class.
What should be protected? How to access if necessary?
In addition to the implementation details, data members, in general, are also the details that should be protected. However, it may be appropriate to provide a public interface allowing a client code to access the data members in the class’s context. This type of interface is usually called an access function. Access functions typically come in two flavors: getter and setter. A getter is a method that is called when we access a data member for reading. In contrast to getter, a setter is a method called to modify a data member.
Benefits of Access Functions
Besides providing the accessibility of data members, access functions offer other benefits. For instance, a setter can perform some operations before return, and a getter can return value in a friendly format. We can add checks logic for getters to verify that the input value is valid before updating the data member.
Access Functions in C++
The access restriction to C++ class members is labeled by the access specifiers – public, private, and protected – within the class body. A class member in the public section is accessible publicly. Only the class self can access the members inside the private section. A protected member can be used by the class self and its derived class.
The following example demonstrates the basic idea of getters and setters in C++.
class MyClass
{
public:
int getMyData() {
return myData;
}
void setMyData(int value) {
myData = value;
}
private:
int myData = 0;
};
Access Control and Property in Python
Python does not use access specifiers to restrict access to a class. In fact, Python does not have a mechanism to prevent a client code from accessing private members. Everything is accessible in Python; everything is public. Note that the term private is usually not used in Python programming since no attribute is really private in Python. The term internal is used instead to indicate an attribute is supported to be used internally.
Although Python does not restrict any access, it does not mean encapsulation is no longer critical to Python. It just means Python has a different approach to support data encapsulation.
Naming Convention
Python programming relies on naming conventions to establish a contract between the code owner and users. If a class member is internal, its name starts with a single underscore; otherwise, it’s public. For example,
class MyClass:
def __init__(self) -> None:
# this variable starts with an underscore (_),
# it indicates it is supposed to be used internally only
self._my_data: int = 0
def get_my_data(self) -> int:
return self._my_data
def set_my_data(self, value: int) -> None:
self._my_data = value
# A method starts with an underscore (_), also means private.
def _private_method(self) -> None:
pass
When a Python programmer sees a class member named with a leading underscore, they will know it is intended to be used internally and not accessed by external code.
However, a client code can still access the internal member if the client really wants to do so. The following code is totally valid.
my_class = MyClass()
my_class._my_data = 10
(The sample code is available at python_example.py)
Name Mangling
In addition to prefix with a single underscore, double leading underscores also means private. However, their behavior is slightly different. The difference is name mangling – when a class member is named with a double leading underscore (e.g., __my_member), name mangling is invoked, and its name is replaced with a name that includes an underscore and the class name before the actual name, like _ClassName__my_member.
Name mangling makes it harder to access an internal member. However, a client code can still access an internal member with double leading underscores via its mangling name. See the example below.
class MyClass:
def __init__(self) -> None:
# Name mangling
self.__my_member = 0
if __name__ == "__main__":
my_class._MyClass__my_member = 10
Although nothing really prevents access to an internal attribute, the naming convention, at least, tells the cline that the attributes are internally used; if you really want to access them, please be careful.
More details about the Python naming convention can be found at PEP8 – Naming Conventions.
Single Leading Underscore or Double Leading Underscore?
Regarding using single leading underscore or double leading underscores to name an internal attribute, we should always prefer a single leading underscore. Using double-leading underscore is discouraging. According to PEP8, “Use one leading underscore only for non-public methods and instance variables. To avoid name clashes with subclasses, use two leading underscores to invoke Python’s name mangling rules.” In addition, a member named with a double underscore prefix also reduces the readability, which may confuse the client with Python Special Method.
Pythonic way to do getters and setters: Property
Because everything is public in Python, providing access functions such as getter and setter in the same way as C++ does not make sense. But sometimes, we need getters and setters for our internal attributes, not to protect them but to perform some operations or checks before returning the value or update the internal members. The Pythonic way is to use @property – a built-in decorator provided by Python language.
The following code demonstrates how to use @property decorator to implement a getter and a setter.
class Contact:
def __init__(self, first_name: str, last_name: str) -> None:
self._first_name = first_name
self._last_name = last_name
self._email: Optional[str] = None
@property
def name(self) -> str:
# The @property decorator turns the name() method into a getter
# for a read-only attribute with the same name, so a client can
# access it by doing c.name if c is an instance of Contact.
return f"{self._first_name} {self._last_name}"
@property
def email(self) -> str:
# The @property docorator turns the email() method into a getter.
if self._email:
return self._email
else:
return "No associated email"
@email.setter
def email(self, email_address) -> None:
# A property object also has a setter method used as a decorator that
# create a copy of the property with the corresponding accessor
# function set to the decorated function. Therefore, the setter
# decorator for email is @email.setter. And a client can access it
# by doing c.email = email@email.com if c is an instance of Contact.
if re.fullmatch(r"^\S+@\S+$", email_address):
self._email = email_address
else:
raise ValueError(f"{email_address} is invalid.")
(The sample code is available at python_property.py)
The @property decorator provides a nice way to implement getters and setters, and we can apply some operations or checks on them. The @property decorator also increases the readability of the code of getters and setters for the code developer and the client. In the example above, a client code can access the attributes like the following.
contact = Contact(first_name="John", last_name="Wick")
contact.email = "john.wick@email.com"
print(f"Name: {contact.name}")
print(f"Email: {contact.email}")
The output would look like the following:
Name: John Wick
Email: john.wick@email.com
Conclusion
Although @property does not prevent a client from accessing the underline members (_first_name, _last_name, and _email in this article’s example), it offers a Pythonic way to define access functions (i.e., getters and setters). Any experience Python programmer would know that they should access the @property members, not the internal members.