Movatterモバイル変換


[0]ホーム

URL:


— FREE Email Series —

🐍 Python Tricks 💌

Python Tricks Dictionary Merge

🔒 No spam. Unsubscribe any time.

Browse TopicsGuided Learning Paths
Basics Intermediate Advanced
apibest-practicescareercommunitydatabasesdata-sciencedata-structuresdata-vizdevopsdjangodockereditorsflaskfront-endgamedevguimachine-learningnumpyprojectspythontestingtoolsweb-devweb-scraping

Table of Contents

Recommended Video Course
Managing Attributes With Python's property()

Python's property(): Add Managed Attributes to Your Classes

Python's property(): Add Managed Attributes to Your Classes

byLeodanis Pozo RamosPublication date Dec 15, 2024Reading time estimate 44mintermediatebest-practicespython

Table of Contents

Remove ads

Watch Now This tutorial has a related video course created by the Real Python team. Watch it together with the written tutorial to deepen your understanding:Managing Attributes With Python's property()

The@property decorator simplifies the management of attributes in your Python classes. It allows you to control attribute access, enabling features such as data validation, lazy evaluation, and the creation of backward-compatible APIs without modifying the class’s public interface. By using@property, you can avoid the clutter of getter and setter methods, keeping your code clean and Pythonic.

By the end of this tutorial, you’ll understand that:

  • A property in Python is a tool for creatingmanaged attributes in classes.
  • The@property decorator allows you to definegetter,setter, anddeleter methods for attributes.
  • You use properties when you needcontrolled access or want toencapsulate logic without changing the API.
  • Properties are useful forvalidating data,computing attributes, and creatingread-only or read-write attributes.
  • You should avoid@property whendirect access is sufficient orperformance is critical.

First, you’ll learn how to useproperty() as a function through practical examples. These examples will demonstrate how to validate input data, compute attribute values dynamically, log your code, and more. Then you’ll explore the@property decorator, the most common syntax for working with properties. To get the most out of this tutorial, you should know the basics ofobject-oriented programming,classes, anddecorators in Python.

Get Your Code:Click here to download the free sample code that shows you how to use Python’s property() to add managed attributes to your classes.

Take the Quiz: Test your knowledge with our interactive “Python's property(): Add Managed Attributes to Your Classes” quiz. You’ll receive a score upon completion to help you track your learning progress:


Python's property(): Add Managed Attributes to Your Classes

Interactive Quiz

Python's property(): Add Managed Attributes to Your Classes

In this quiz, you'll test your understanding of Python's property(). With this knowledge, you'll be able to create managed attributes in your classes, perform lazy attribute evaluation, provide computed attributes, and more.

Managing Attributes in Your Classes

When you define aclass in anobject-oriented programming language, you’ll probably end up with some instance and classattributes. In other words, you’ll end up with variables that are accessible through the instance, class, or even both, depending on the language. Attributes represent and hold the internalstate of a given object, which you’ll often need to access and mutate.

Typically, you have at least two ways to access and mutate an attribute. Either you can access and mutate the attribute directly or you can use methods. Methods arefunctions attached to a given class. They provide the behaviors and actions that an object can perform with its internal data and attributes.

If you expose attributes to the user, then they become part of the class’s publicAPI. This means that your users will access and mutate them directly in their code. The problem comes when you need to change the internal implementation of a given attribute.

Say you’re working on aCircle class and add an attribute called.radius, making it public. You finish coding the class and ship it to your end users. They start usingCircle in their code to create a lot of awesome projects and applications. Good job!

Now suppose that you have an important user that comes to you with a new requirement. They don’t wantCircle to store the radius any longer. Instead, they want a public.diameter attribute.

At this point, removing.radius to start using.diameter could break the code of some of your other users. You need to manage this situation in a way other than removing.radius.

Programming languages such asJava andC++ encourage you to never expose your attributes to avoid this kind of problem. Instead, you should providegetter and setter methods, also known asaccessors andmutators, respectively. These methods offer a way to change the internal implementation of your attributes without changing your public API.

Note: Getter and setter methods are often considered ananti-pattern and a signal of poor object-oriented design. The main argument behind this proposition is that these methods breakencapsulation. They allow you to access and mutate the components of your objects from the outside.

These programming languages need getter and setter methods because they don’t have a suitable way to change an attribute’s internal implementation when a given requirement changes. Changing the internal implementation would require an API modification, which can break your end users’ code.

The Getter and Setter Approach in Python

Technically, there’s nothing that stops you from using getter and settermethods in Python. Here’s a quick example that shows how this approach would look:

Pythonpoint_v1.py
classPoint:def__init__(self,x,y):self._x=xself._y=ydefget_x(self):returnself._xdefset_x(self,value):self._x=valuedefget_y(self):returnself._ydefset_y(self,value):self._y=value

In this example, you create aPoint class with twonon-public attributes._x and._y to hold theCartesian coordinates of the point at hand.

Note: Python doesn’t have the notion ofaccess modifiers, such asprivate,protected, andpublic, to restrict access to attributes and methods. In Python, the distinction is betweenpublic andnon-public class members.

If you want to signal that a given attribute or method is non-public, then you have to use the well-known Pythonconvention of prefixing the name with anunderscore (_). That’s the reason behind the naming of the attributes._x and._y.

Note that this is just a convention. It doesn’t stop you and other programmers from accessing the attributes usingdot notation, as inobj._attr. However, it’s bad practice to violate this convention.

To access and mutate the value of either._x or._y, you can use the corresponding getter and setter methods. Go ahead and save the above definition ofPoint in a Pythonmodule andimport the class into aninteractive session. Then run the following code:

Python
>>>frompoint_v1importPoint>>>point=Point(12,5)>>>point.get_x()12>>>point.get_y()5>>>point.set_x(42)>>>point.get_x()42>>># Non-public attributes are still accessible>>>point._x42>>>point._y5

With.get_x() and.get_y(), you can access the current values of._x and._y. You can use the setter method to store a new value in the corresponding managed attribute. From the two final examples, you can confirm that Python doesn’t restrict access to non-public attributes. Whether or not you access them directly is up to you.

The Pythonic Approach

Even though the example you just saw uses the Python coding style, it isn’t Pythonic. In the example, the getter and setter methods don’t perform any further processing with the values of._x and._y, so you could just have plain attributes instead of methods.

You can rewritePoint in a more concise and Pythonic way:

Python
>>>classPoint:...def__init__(self,x,y):...self.x=x...self.y=y...>>>point=Point(12,5)>>>point.x12>>>point.y5>>>point.x=42>>>point.x42

This code uncovers a fundamental Python principle:exposing attributes to the end user is normal and common. This is cool because you don’t need to clutter your classes with getter and setter methods all the time.

The question is, how do you handle requirement changes that would imply modifying the implementation of attributes without changing your APIs? For example, say that you need to add validation functionality on top of a given attribute. How would you do that if your attribute doesn’t have setter and getter methods where you can put that functionality?

Unlike Java and C++, Python provides handy tools that allow you to change the underlying implementation of your attributes without changing your public API. The most popular approach is to turn your attributes intoproperties.

Note: Another common approach to providing managed attributes is to use descriptors. In this tutorial, however, you’ll learn about properties only. If you want to dig into descriptors, check out thePython Descriptors: An Introduction tutorial.

Properties represent an intermediate functionality between a plain attribute, or field, and a method. In other words, they allow you to create methods that behave like attributes. With properties, you can change how you compute the target attribute whenever you need to.

For example, you can turn both.x and.y into properties. With this change, you can continue to access them as attributes while having them perform actions when your users access and mutate them.

Note: Properties aren’t exclusive to Python. Languages such asJavaScript,C#,Kotlin, and others also provide tools and techniques for creating properties as class members.

Python properties allow you to expose attributes as part of your classes’ public APIs. If you ever need to change an attribute’s underlying implementation, then you can conveniently turn it into a property at any time. In the following sections, you’ll learn how to create properties in Python.

Getting Started With Python’sproperty()

Using Python’sproperty() is the Pythonic way to avoid getter and setter methods in your classes. Thisbuilt-in function allows you to turnclass attributes intoproperties ormanaged attributes. Becauseproperty() is a built-in function, you can use it without importing anything. Additionally,property() isimplemented in C, which ensures optimized performance.

Note: It’s common to refer toproperty() as a built-in function. However,property is a class with a function-like name. That’s why most Python developers call it a function.

In this tutorial, you’ll follow the common practice of callingproperty() a function rather than a class. However, in some sections, you’ll see it called a class to facilitate the explanation.

Withproperty(), you can attach implicit getter and setter methods to given class attributes. You can also specify a way to handle attribute deletion and provide an appropriatedocstring for your properties.

Here’s the full signature ofproperty():

Python Syntax
property([fget=None,fset=None,fdel=None,doc=None])

The first two arguments take function objects that will play the role of getter (fget) and setter (fset) methods. Python automatically calls these function objects when you access or mutate the attribute at hand.

Here’s a summary of what each argument does:

ArgumentDescription
fgetA function object that returns the value of the managed attribute
fsetA function object that allows you to set the value of the managed attribute
fdelA function object that defines how the managed attribute handles deletion
docA string representing the property’sdocstring

Thereturn value ofproperty() is the managed attribute itself. If you access the managed attribute with something likeobj.attr, then Python automatically callsfget(). If you assign a new value to the attribute with something likeobj.attr = value, then Python callsfset() using the inputvalue as an argument. Finally, if you run adel obj.attr statement, then Python automatically callsfdel().

Note: The first three arguments toproperty() take function objects. You can think of a function object as the function name without the calling pair of parentheses.

You can usedoc to provide an appropriate docstring for your properties. You and your fellow programmers will be able to read that docstring using Python’shelp(). Thedoc argument is also useful when you’re working withcode editors and IDEs that support docstring access.

You can useproperty() either as afunction ordecorator to build your properties. In the following two sections, you’ll learn how to use both approaches. However, the decorator approach is more popular in the Python community.

Creating Attributes Withproperty()

You can create a property by callingproperty() with an appropriate set of arguments and assigning its return value to a class attribute. All the arguments toproperty() are optional. However, you typically provide at least agetter function.

The following example shows how to create aCircle class with a property that manages its radius:

Pythoncircle_v1.py
classCircle:def__init__(self,radius):self._radius=radiusdef_get_radius(self):print("Get radius")returnself._radiusdef_set_radius(self,value):print("Set radius")self._radius=valuedef_del_radius(self):print("Delete radius")delself._radiusradius=property(fget=_get_radius,fset=_set_radius,fdel=_del_radius,doc="The radius property.")

In this code snippet, you createCircle. The class initializer,.__init__(), takesradius as an argument and stores it in a non-public attribute called._radius. Then, you define three non-public methods:

  1. ._get_radius() returns the current value of._radius
  2. ._set_radius() takesvalue as an argument and assigns it to._radius
  3. ._del_radius() deletes the instance attribute._radius

Once you have these three methods in place, you create a class attribute called.radius to store the property object. To initialize the property, you pass the three methods as arguments toproperty(). You also pass a suitable docstring for your property.

In this example, you usekeyword arguments to improve readability and prevent confusion. That way, you know exactly which method goes into each argument.

To giveCircle a try, run the following code in your PythonREPL:

Python
>>>fromcircle_v1importCircle>>>circle=Circle(42.0)>>>circle.radiusGet radius42.0>>>circle.radius=100.0Set radius>>>circle.radiusGet radius100.0>>>delcircle.radiusDelete radius>>>circle.radiusGet radiusTraceback (most recent call last):...AttributeError:'Circle' object has no attribute '_radius'>>>help(circle)Help on Circle in module __main__ object:class Circle(builtins.object)    ... |  radius |      The radius property.

The.radius property hides the non-public instance attribute._radius, which is now your managed attribute in this example. You can access and assign.radius directly. Internally, Python automatically calls._get_radius() and._set_radius() when needed. When you executedel circle.radius, Python calls._del_radius(), which deletes the underlying._radius.

Besides using regular named functions to provide getter methods in your properties, you can also uselambda functions.

Here’s a version ofCircle in which the.radius property uses alambda function as its getter method:

Python
>>>classCircle:...def__init__(self,radius):...self._radius=radius...radius=property(lambdaself:self._radius)...>>>circle=Circle(42.0)>>>circle.radius42.0

If your getter method’s functionality is limited to returning the current value of the managed attribute, then using alambda function can be the solution.

Properties areclass attributes that manageinstance attributes. You can think of a property as a collection of methods bundled together. If you inspect.radius carefully, then you’ll find the raw methods you provided as thefget,fset, andfdel arguments:

Python
>>>Circle.radius.fget<function Circle._get_radius at 0x7fba7e1d7d30>>>>Circle.radius.fset<function Circle._set_radius at 0x7fba7e1d78b0>>>>Circle.radius.fdel<function Circle._del_radius at 0x7fba7e1d7040>>>>dir(Circle.radius)[..., '__get__', ..., '__set__', ...]

You can access the getter, setter, and deleter methods in a given property through the corresponding.fget,.fset, and.fdel attributes.

Properties are alsodescriptors. If you usedir() to check the internal members of a given property, then you’ll find.__set__() and.__get__() in the list. These methods provide a default implementation of thedescriptor protocol.

Note: If you want to better understand the internal implementation ofproperty as a class, then check out thepure PythonProperty class described in the documentation.

The default implementation of.__set__(), for example, runs when you don’t provide a custom setter method. This implementation gives you anAttributeError when you try to set the attribute.

Usingproperty() as a Decorator

Decorators are frequently used in Python. They’re typically functions that take another function as an argument and return a new function with added functionality. With a decorator, you can attach pre- and post-processing operations to an existing function.

Note: In Python, you can define decorators using either a function or a class. So, you can have both function-based and class-based decorators.

The decorator syntax consists of placing the name of the decorator function with a leading@ symbol right before the definition of the function you want to decorate:

Python Syntax
@decoratordeffunction():...

In this code,@decorator can be a function or class intended to decoratefunction(). This syntax is equivalent to the following:

Python Syntax
deffunction():...function=decorator(function)

The final line of code reassigns the namefunction to hold the result of callingdecorator(function). Note that this is the same syntax you used to create a property in the previous section.

Python’sproperty() can work as a decorator, so you can use the@property syntax to create your properties quickly:

Pythoncircle_v2.py
 1classCircle: 2def__init__(self,radius): 3self._radius=radius 4 5@property 6defradius(self): 7"""The radius property.""" 8print("Get radius") 9returnself._radius1011@radius.setter12defradius(self,value):13print("Set radius")14self._radius=value1516@radius.deleter17defradius(self):18print("Delete radius")19delself._radius

Circle now is more Pythonic and clean. You don’t need to use method names such as._get_radius(),._set_radius(), and._del_radius() anymore. Now you have three methods with the same descriptive attribute-like name. How’s that possible?

The decorator approach for creating properties requires defining a first method using the public name for the underlying managed attribute, which is.radius in this example. This method should implement the getter logic. In the above example, lines 5 to 9 implement that method.

Lines 11 to 14 define the setter method for.radius. The syntax is different. Instead of using@property again, you use@radius.setter. Why do you need to do that? Take a look at thedir() output:

Python
>>>fromcircle_v2importCircle>>>dir(Circle.radius)[..., 'deleter', ..., 'getter', 'setter']

Besides.fget,.fset,.fdel, and a bunch of other special attributes and methods,property also provides.deleter(),.getter(), and.setter(). These three methods each return a new property.

When you decorate the second.radius() method with@radius.setter on line 11, you create a new property and reassign the class-level name.radius from line 6 to hold it. This new property contains the same set of methods of the initial property on line 6 with the addition of the new setter method provided on line 12. Finally, the decorator syntax reassigns the new property to the.radius class-level name.

The mechanism to define the deleter method is similar. This time, you need to use the@radius.deleter decorator. At the end of the process, you get a full-fledged property with the getter, setter, and deleter methods.

Now, how can you provide suitable docstrings for your properties when you use the decorator approach? If you checkCircle again, you’ll notice that you already did so by adding a docstring to the getter method on line 7.

The newCircle implementation works the same as the example in the section above:

Python
>>>fromcircle_v2importCircle>>>circle=Circle(42.0)>>>circle.radiusGet radius42.0>>>circle.radius=100.0Set radius>>>circle.radiusGet radius100.0>>>delcircle.radiusDelete radius>>>circle.radiusGet radiusTraceback (most recent call last):...AttributeError:'Circle' object has no attribute '_radius'>>>help(circle)Help on Circle in module __main__ object:class Circle(builtins.object)    ... |  radius |      The radius property.

First, note that you don’t need to use a pair of parentheses for calling.radius() as a method. Instead, you can access.radius as you would access a regular attribute, which is the primary purpose of properties. They allow you to treat methods as attributes.

Here’s a recap of some important points to remember when you’re creating properties with the decorator approach:

  • The@property decorator must decorate thegetter method.
  • The docstring must go in thegetter method.
  • Thesetter and deleter methods must be decorated with the name of the getter method plus.setter and.deleter, respectively.

Up to this point, you’ve learned how to create managed attributes usingproperty() as a function and a decorator. It’s time to think about when you should use properties.

Deciding When to Use Properties

If you check the implementation of yourCircle class so far, then you’ll note that its getter and setter methods don’t add extra functionality on top of your attributes.

In general, you should avoid using properties for attributes that don’t require extra functionality or processing. If you do use properties this way, then you’ll make your code:

  • Unnecessarily verbose
  • Confusing to other developers
  • Slower than code based on regular attributes

Unless you need something more than bare attribute access and mutation, don’t use properties. They’ll waste yourCPU time and, more importantly,your time.

Finally, you should avoid writing explicit getter and setter methods and then wrapping them in a property. Instead, use the@property decorator. That’s currently the Pythonic way to go.

Providing Read-Only Attributes

Probably the most elementary use case ofproperty() is to provideread-only attributes in your classes. Say you need animmutablePoint class that doesn’t allow the user to mutate the original value of its coordinates,x andy. To achieve this goal, you can createPoint like in the following example:

Pythonpoint_v2.py
classPoint:def__init__(self,x,y):self._x=xself._y=y@propertydefx(self):returnself._x@propertydefy(self):returnself._y

Here, you store the input arguments in the attributes._x and._y. As you already learned, using the leading underscore (_) in names tells other developers that they’re non-public attributes and shouldn’t be accessed using dot notation, such as inpoint._x. Finally, you define two getter methods and decorate them with@property.

Now you have two read-only properties,.x and.y, as your coordinates:

Python
>>>frompoint_v2importPoint>>>point=Point(12,5)>>># Read coordinates>>>point.x12>>>point.y5>>>point.x=42Traceback (most recent call last):...AttributeError:can't set attribute

Here,.x and.y are read-only properties because you can’t assign new values to them. Their behavior relies on the underlying descriptor thatproperty provides. The default.__set__() implementation on this descriptor raises anAttributeError when you don’t define a setter method.

If you need custom behavior on a read-only property, then you can provide an explicit setter method that raises a custom exception with more elaborate and specific messages:

Pythonpoint_v3.py
classWriteCoordinateError(Exception):passclassPoint:def__init__(self,x,y):self._x=xself._y=y@propertydefx(self):returnself._x@x.setterdefx(self,value):raiseWriteCoordinateError("x coordinate is read-only")@propertydefy(self):returnself._y@y.setterdefy(self,value):raiseWriteCoordinateError("y coordinate is read-only")

In this example, you define a custom exception calledWriteCoordinateError. This exception allows you to customize the way you implement your immutablePoint class. Now, both setter methods raise your custom exception with a more explicit message. Go ahead and give your improvedPoint a try!

Creating Read-Write Attributes

You can also useproperty() to provide managed attributes withread-write capabilities. In practice, you just need to provide the appropriate getter (“read”) and setter (“write”) methods to your properties in order to create read-write managed attributes.

For example, say you want yourCircle class to have a.diameter attribute. Taking the radius and the diameter in the class initializer seems unnecessary because you can compute the one using the other.

Here’s aCircle that manages.radius and.diameter as read-write attributes but only takes the radius at instance creation time:

Pythoncircle_v3.py
classCircle:def__init__(self,radius):self.radius=radius@propertydefradius(self):returnself._radius@radius.setterdefradius(self,value):self._radius=float(value)@propertydefdiameter(self):returnself.radius*2@diameter.setterdefdiameter(self,value):self.radius=value/2

Here, you create aCircle class with a read-write.radius property. The getter method just returns the radius value. The setter method converts the radius to a floating-point number and assigns it to the non-public._radius attribute, which is the variable you use to store the final data.

This new implementation ofCircle has a subtle detail that you should note. In this case, the class initializer assigns the input value to the.radius property directly instead of storing it in a dedicated non-public attribute, such as._radius. Why? Because you must ensure that every radius value—including the initial one—goes through the setter method and gets converted to a floating-point number.

Circle also implements a.diameter attribute as a property. The getter method computes the diameter using the radius. The setter method calculates the radius and stores the result in.radius instead of storing the input diameter in a dedicated attribute. This way of dealing with the diameter makes your class more memory-efficient because you’re only storing the radius.

Here’s how yourCircle works:

Python
>>>fromcircle_v3importCircle>>>circle=Circle(42)>>>circle.radius42.0>>>circle.diameter84.0>>>circle.diameter=100>>>circle.diameter100.0>>>circle.radius50.0

In this example, both.radius and.diameter work as normal attributes, providing a clean and Pythonic public API for yourCircle class.

Providing Write-Only Attributes

You can also createwrite-only attributes by tweaking the getter method of properties. For example, you can make your getter method raise an exception every time a user accesses the underlying attribute.

Here’s a hypothetical example of handling passwords with a write-only property:

Pythonusers.py
importhashlibimportosclassUser:def__init__(self,name,password):self.name=nameself.password=password@propertydefpassword(self):raiseAttributeError("Password is write-only")@password.setterdefpassword(self,plaintext):salt=os.urandom(32)self._hashed_password=hashlib.pbkdf2_hmac("sha256",plaintext.encode("utf-8"),salt,100_000)

The initializer ofUser takes the username and password as arguments and stores them in.name and.password, respectively.

Note: The above example is only for educational purposes. It’s not a recipe for securely handling passwords in your code.

You use a property to manage how your class processes the input password. The getter method raises anAttributeError whenever a user tries to retrieve the current password. This turns.password into a write-only attribute:

Python
>>>fromusersimportUser>>>john=User("John","secret")>>>john._hashed_passwordb'b\xc7^ai\x9f3\xd2g ... \x89^-\x92\xbe\xe6'>>>john.passwordTraceback (most recent call last):...AttributeError:Password is write-only>>>john.password="supersecret">>>john._hashed_passwordb'\xe9l$\x9f\xaf\x9d ... b\xe8\xc8\xfcaU\r_'

In this example, you createjohn as aUser instance with an initial password. The setter method hashes the password and stores it in._hashed_password. Note that when you try to access.password directly, you get anAttributeError. Finally, assigning a new value to.password triggers the setter method and creates a new hashed password.

In the setter method of.password, you useos.urandom() to generate a 32-byte randomstring as your hashing function’ssalt. To generate the hashed password, you usehashlib.pbkdf2_hmac(). Then you store the resulting hashed password in the non-public attribute._hashed_password. Doing so ensures that you never save the plaintext password in any retrievable attribute.

Putting Python’sproperty() Into Action

So far, you’ve learned how to use Python’sproperty() to create managed attributes in your classes. You’ve usedproperty() as a function and as a decorator and learned about the differences between these two approaches. You also learned how to create read-only, read-write, and write-only attributes.

In the following sections, you’ll code a few examples that will help you get a better practical understanding of common use cases ofproperty().

Validating Input Values

Validating input is one of the most common use cases ofproperty() and managed attributes.Data validation is a common requirement in code that takes input from users or other sources that you could consider untrusted. Python’sproperty() provides a quick and reliable tool for dealing with input validation.

For example, getting back to thePoint class, you may require the values of.x and.y to be validnumbers. Since your users are free to enter any type of data, you need to make sure that your points only accept numbers.

Here’s an implementation ofPoint that manages this requirement:

Pythonpoint_v4.py
classPoint:def__init__(self,x,y):self.x=xself.y=y@propertydefx(self):returnself._x@x.setterdefx(self,value):try:self._x=float(value)print("Validated!")exceptValueError:raiseValueError('"x" must be a number')fromNone@propertydefy(self):returnself._y@y.setterdefy(self,value):try:self._y=float(value)print("Validated!")exceptValueError:raiseValueError('"y" must be a number')fromNone

The setter methods of.x and.y usetryexcept blocks that validate input data using the PythonEAFP (easier to ask forgiveness than permission) style. If the call tofloat() succeeds, then the input data is valid, and you getValidated! on your screen. Iffloat() raises aValueError, then the user gets aValueError with a more specific message.

Note: In the example above, you use the syntaxraisefrom None to hide internal details related to the context in which you’re raising the exception. From the end user’s viewpoint, these details can be confusing and make your class look unpolished.

Check out the section on theraise statement in the documentation for more information about this topic. You can also learn more aboutraise inPython’sraise: Effectively Raising Exceptions in Your Code.

It’s important to note that assigning the.x and.y properties directly in.__init__() ensures that the validation also occurs during object initialization. Not doing so can lead to issues when usingproperty() for data validation.

Here’s how yourPoint class works now:

Python
>>>frompoint_v4importPoint>>>point=Point(12,5)Validated!Validated!>>>point.x12.0>>>point.y5.0>>>point.x=42Validated!>>>point.x42.0>>>point.y=100.0Validated!>>>point.y100.0>>>point.x="one"Traceback (most recent call last):...ValueError:"x" must be a number>>>point.y="1o"Traceback (most recent call last):...ValueError:"y" must be a number

If you assign.x and.y values thatfloat() can turn into floating-point numbers, then the validation is successful, and the value is accepted. Otherwise, you get aValueError.

This implementation ofPoint uncovers a fundamental weakness ofproperty(). Did you spot it? That’s it! You have repetitive code that follows specific patterns. This repetition breaks theDRY (Don’t Repeat Yourself) principle, so you would want torefactor this code to avoid it. To do so, you can abstract out the repetitive logic using adescriptor that you can callCoordinate:

Pythonpoint_v5.py
classCoordinate:def__set_name__(self,owner,name):self._name=namedef__get__(self,instance,owner):returninstance.__dict__[self._name]def__set__(self,instance,value):try:instance.__dict__[self._name]=float(value)print("Validated!")exceptValueError:raiseValueError(f'"{self._name}" must be a number')fromNoneclassPoint:x=Coordinate()y=Coordinate()def__init__(self,x,y):self.x=xself.y=y

Now your code is a bit shorter and way less repetitive. You definedCoordinate as a descriptor to manage the data validation in a single place. Then, you create.x and.y as class attributes holding instances of the target descriptor. The code works just like its earlier implementation. Go ahead and give it a try!

In general, if you find yourself copying and pasting property definitions throughout your code or if you spot repetitive code, like in the example above, then you should consider using descriptors.

Providing Computed Attributes

If you need an attribute that builds its value dynamically whenever you access it, then using a property can be a great choice. These kinds of attributes are commonly known ascomputed attributes. They’re handy when you need something that works like aneager attribute, but you want it to belazy.

The main reason for creating lazy attributes is to postpone their computation until the attributes are needed, which can make your code more efficient.

Here’s an example of how to useproperty() to create a computed attribute called.area in aRectangle class:

Pythonrectangle.py
classRectangle:def__init__(self,width,height):self.width=widthself.height=height@propertydefarea(self):returnself.width*self.height

In this example, theRectangle initializer takeswidth andheight as arguments and stores them in the corresponding instance attributes. The read-only property,.area, computes and returns the area of the current rectangle every time you access it.

Another cool use case of properties is to provide aformatted value for a given attribute:

Pythonproduct.py
classProduct:def__init__(self,name,price):self._name=nameself._price=float(price)@propertydefprice(self):returnf"${self._price:,.2f}"

In this example,.price is a property that formats and returns the price of a particular product. To provide a currency-like format, you use anf-string with an appropriateformat specifier.

Note: This example uses floating-point numbers to represent currencies, which is bad practice. Instead, you should usedecimal.Decimal from thestandard library.

As a final example of computed attributes, say you have aPoint class that uses.x and.y as Cartesian coordinates. You want to providepolar coordinates for your point so that you can use them in a few computations. The polar coordinate system represents each point using the distance to the origin and the angle with the horizontal coordinate axis.

Here’s a Cartesian coordinatesPoint class that also provides computed polar coordinates:

Pythonpoint_v6.py
importmathclassPoint:def__init__(self,x,y):self.x=xself.y=y@propertydefdistance(self):returnmath.dist((0,0),(self.x,self.y))@propertydefangle(self):returnmath.degrees(math.atan2(self.y,self.x))defas_cartesian(self):returnself.x,self.ydefas_polar(self):returnself.distance,self.angle

In this example, you define two properties to compute the polar coordinates—distance and angle—of a givenPoint object using its.x and.y Cartesian coordinates. You also add two instance methods that return the Cartesian and polar coordinates as tuples.

Here’s how this class works in practice:

Python
>>>frompoint_v6importPoint>>>point=Point(12,5)>>>point.x12>>>point.y5>>>point.distance13.0>>>point.angle22.619864948040426>>>point.as_cartesian()(12, 5)>>>point.as_polar()(13.0, 22.619864948040426)

Properties are handy tools for providing computed attributes. However, if you’re creating an attribute that you use frequently, then computing it every time can be costly and wasteful. A good strategy to avoid this additional cost is tocache the computed value once the computation is done. That’s what you’ll do in the following section.

Caching Computed Attributes

Sometimes you have a given computed attribute that you use frequently. Constantly running the same computation may be unnecessary and expensive. To work around this problem, you can cache the computed value for later reuse.

If you have a property that computes its value from constant input values, then the result will never change. In that case, you can compute the value just once:

Pythoncircle_v4.py
fromtimeimportsleepclassCircle:def__init__(self,radius):self.radius=radiusself._diameter=None@propertydefdiameter(self):ifself._diameterisNone:sleep(0.5)# Simulate a costly computationself._diameter=self.radius*2returnself._diameter

This implementation ofCircle caches the computed diameter using a dedicated non-public attribute. The code works, but it has the drawback that if you ever change the value of.radius, then.diameter won’t return a correct value:

Python
>>>fromcircle_v4importCircle>>>circle=Circle(42.0)>>>circle.radius42.0>>>circle.diameter# With delay84.0>>>circle.diameter# Without delay84.0>>>circle.radius=100.0>>>circle.diameter# Wrong diameter84.0

In these examples, you create a circle with a radius equal to42.0. The.diameter property computes its value only the first time you access it. That’s why you see a delay in the first execution and no delay in the second. When you change the value of the radius, the diameter stays the same, which is a problem.

If the input data for a computed attribute changes, then you need to recalculate its value:

Python
fromtimeimportsleepclassCircle:def__init__(self,radius):self.radius=radius@propertydefradius(self):returnself._radius@radius.setterdefradius(self,value):self._diameter=Noneself._radius=value@propertydefdiameter(self):ifself._diameterisNone:sleep(0.5)# Simulate a costly computationself._diameter=self._radius*2returnself._diameter

The setter method of the.radius property resets._diameter toNone every time you change the radius. With this little update,.diameter recalculates its value the first time you access it after every mutation of.radius:

Python
>>>fromcircle_v5importCircle>>>circle=Circle(42.0)>>>circle.radius42.0>>>circle.diameter# With delay84.0>>>circle.diameter# Without delay84.0>>>circle.radius=100.0>>>circle.diameter# With delay200.0>>>circle.diameter# Without delay200.0

Cool!Circle works correctly now! It computes the diameter the first time you access it and also every time you change the radius.

Another way to create cached properties is to usefunctools.cached_property() from thestandard library. This function works as a decorator and allows you to transform a method into a cached property. The property computes its value only once and caches it as a normal attribute during the lifetime of the instance:

Pythoncircle_v6.py
fromfunctoolsimportcached_propertyfromtimeimportsleepclassCircle:def__init__(self,radius):self.radius=radius@cached_propertydefdiameter(self):sleep(0.5)# Simulate a costly computationreturnself.radius*2

Here,.diameter computes and caches its value the first time you access it. This kind of implementation is suitable for input values that don’t change. Here’s how it works:

Python
>>>fromcircle_v6importCircle>>>circle=Circle(42.0)>>>circle.diameter# With delay84.0>>>circle.diameter# Without delay84.0>>>circle.radius=100>>>circle.diameter# Wrong diameter84.0>>># Allow direct assignment>>>circle.diameter=200>>>circle.diameter# Cached value200

When you access.diameter, you get its computed value. That value remains the same from this point on. However, unlikeproperty(),cached_property() doesn’t block attribute updates unless you provide a setter method. That’s why you can update the diameter to200 in the last couple of lines.

If you want to create a cached property that doesn’t allow modifications, then you can useproperty() andfunctools.cache() like in the following example:

Pythoncircle_v7.py
fromfunctoolsimportcachefromtimeimportsleepclassCircle:def__init__(self,radius):self.radius=radius@property@cachedefdiameter(self):sleep(0.5)# Simulate a costly computationreturnself.radius*2

This code stacks@property on top of@cache. The combination of both decorators builds a cached property that prevents changes:

Python
>>>fromcircle_v7importCircle>>>circle=Circle(42.0)>>>circle.diameter# With delay84.0>>>circle.diameter# Without delay84.0>>>circle.radius=100>>>circle.diameter# Wrong diameter84.0>>>circle.diameter=200Traceback (most recent call last):...AttributeError:can't set attribute

In these examples, when you try to assign a new value to.diameter, you get anAttributeError because the setter functionality comes from the property’s internal descriptor.

Logging Attribute Access and Mutation

Sometimes, you need to keep track of what your code does and how your programs flow. A way to do that in Python is to uselogging. This module provides all the functionality you require for logging your code. It allows you to constantly watch the code and generate useful information about how it works.

If you ever need to keep track of how and when you access and mutate a given attribute, then you can take advantage ofproperty() for that, too:

Pythoncircle_v8.py
importlogginglogging.basicConfig(format="%(asctime)s:%(message)s",level=logging.INFO,datefmt="%H:%M:%S")classCircle:def__init__(self,radius):self._msg='"radius" was%s. Current value:%s'self.radius=radius@propertydefradius(self):logging.info(self._msg%("accessed",str(self._radius)))returnself._radius@radius.setterdefradius(self,value):try:self._radius=float(value)logging.info(self._msg%("mutated",str(self._radius)))exceptValueError:logging.info('validation error while mutating "radius"')

Here, you first importlogging and define a basic configuration. Then you implementCircle with a managed attribute.radius. The getter method generates log information every time you access.radius in your code. The setter method logs each mutation that you perform on.radius. It also logs those situations in which you get an error because of bad input data.

Here’s how you can useCircle in your code:

Python
>>>fromcircle_v8importCircle>>>circle=Circle(42.0)>>>circle.radius14:48:59: "radius" was accessed. Current value: 42.042.0>>>circle.radius=10014:49:15: "radius" was mutated. Current value: 100>>>circle.radius14:49:24: "radius" was accessed. Current value: 100100>>>circle.radius="value"15:04:51: validation error while mutating "radius"

Logging data from attribute access and mutation can help you debug your code. Logging can also help you identify sources of problematic data input, analyze the performance of your code, spot usage patterns, and more.

Managing Attribute Deletion

You can create properties that implement deletion functionality. This might be a rare use case ofproperty(), but having a way to delete an attribute can be handy in some situations.

Say you’re implementing your owntree data type. A tree is anabstract data type that stores elements in a hierarchy. The tree components are commonly known asnodes. Each node in a tree has a parent node, except for the root node. Nodes can have zero or more children.

Now, suppose you need to delete or clear the list of children of a given node. Here’s an example that implements a tree node that usesproperty() to provide most of its functionality, including the ability to clear the node’s list of children:

Pythontree.py
classTreeNode:def__init__(self,data):self._data=dataself._children=[]@propertydefchildren(self):returnself._children@children.setterdefchildren(self,value):ifisinstance(value,list):self._children=valueelse:delself.childrenself._children.append(value)@children.deleterdefchildren(self):self._children.clear()def__repr__(self):returnf'{self.__class__.__name__}("{self._data}")'

In this example,TreeNode represents a node in your custom tree data type. Each node stores its children in a Pythonlist. Then, you implement.children as a property to manage the underlying list of children. The deleter method calls.clear() on the list of children to remove them all.

Here’s how your class works:

Python
>>>fromtreeimportTreeNode>>>root=TreeNode("root")>>>child1=TreeNode("child 1")>>>child2=TreeNode("child 2")>>>root.children=[child1,child2]>>>root.children[TreeNode("child 1"), TreeNode("child 2")]>>>delroot.children>>>root.children[]

In this example, you first create aroot node to initialize the tree. Then, you create two new nodes and assign them to.children using a list. Thedel statement triggers the internal deleter method of.children and clears the list of nodes.

Creating Backward-Compatible Class APIs

As you already know, properties turn direct attribute lookups into method calls. This feature allows you to create clean and Pythonic APIs for your classes. You can expose your attributes publicly without using getter and setter methods.

If you ever need to modify how you compute a given public attribute, then you can turn it into a property. Properties allow you to perform extra processing, such as data validation, without having to modify your public APIs.

Suppose you’re creating an accounting application and need a base class to manage currencies. To this end, you create aCurrency class that exposes two attributes,.units and.cents:

Pythoncurrency_v1.py
classCurrency:def__init__(self,units,cents):self.units=unitsself.cents=cents# Currency implementation...

This class looks clean and Pythonic. Now, say that your requirements change, and you decide to store the total number of cents instead of the units and cents. Removing.units and.cents from your public API to use something like.total_cents could break the code of more than one user.

In this situation,property() can be an excellent option to keep your current API unchanged. Here’s how you can work around the problem and avoid breaking your users’ code:

Pythoncurrency_v2.py
CENTS_PER_UNIT=100classCurrency:def__init__(self,units,cents):self._total_cents=units*CENTS_PER_UNIT+cents@propertydefunits(self):returnself._total_cents//CENTS_PER_UNIT@units.setterdefunits(self,value):self._total_cents=self.cents+value*CENTS_PER_UNIT@propertydefcents(self):returnself._total_cents%CENTS_PER_UNIT@cents.setterdefcents(self,value):self._total_cents=self.units*CENTS_PER_UNIT+value# Currency implementation...

Now your class stores the total number of cents instead of independent units and cents. Because of the new properties, your users can still access and mutate.units and.cents in their code and get the same result as before. Go ahead and give it a try!

When you write code that others will build upon, you need to guarantee that modifications to your code’s internal implementation don’t affect how end users work with it.

Overriding Properties in Subclasses

When you create Python classes that include properties and distribute them in a package or library, you should expect your users to do unexpected things with them. One of those things could besubclassing them to customize their functionalities. In these cases, your users should be aware of a subtle gotcha. If you partially override a property, then you lose the non-overridden functionality.

For example, suppose you need anEmployee class to manage employee information. You already have another class calledPerson, and you think of subclassing it to reuse its functionalities.

Person has a.name attribute implemented as a property. The current implementation of.name doesn’t work forEmployee because you want the employee’s name to be in uppercase letters. Here’s how you may end up writingEmployee using inheritance:

Pythonpersons.py
classPerson:def__init__(self,name):self._name=name@propertydefname(self):returnself._name@name.setterdefname(self,value):self._name=value# Person implementation...classEmployee(Person):@propertydefname(self):returnsuper().name.upper()# Employee implementation...

InEmployee, you override.name to make sure that when you access the attribute, you get the employee name in uppercase:

Python
>>>frompersonsimportEmployee,Person>>>person=Person("John")>>>person.name'John'>>>person.name="John Doe">>>person.name'John Doe'>>>employee=Employee("John")>>>employee.name'JOHN'

Great!Employee works as you need and returns the name in uppercase letters. However, subsequent tests uncover an unexpected issue:

Python
>>>employee.name="John Doe"Traceback (most recent call last):...AttributeError:can't set attribute

What happened? When you override an existing property from a parent class, you override the whole functionality of that property. In this example, you reimplemented the getter method only. Because of that,.name lost the rest of its inherited functionality. You don’t have a setter method any longer.

The takeaway here is that if you ever need to override a property in a subclass, then you must provide all the functionality you need in the new version of the property.

Conclusion

Aproperty is a special type of class member that provides functionality that’s somewhere in between regular attributes and methods. Properties allow you to modify the implementation of instance attributes without changing the class’s public API. Being able to keep your APIs unchanged helps you avoid breaking code that your users have written on top of older versions of your classes.

Properties are thePythonic way to createmanaged attributes in your classes. They have several use cases in real-world programming, making them a great addition to your skill set as a Python developer.

In this tutorial, you learned how to:

  • Createmanaged attributes with Python’sproperty()
  • Performlazy attribute evaluation and providecomputed attributes
  • Make your classes Pythonic using properties instead ofsetter andgetter methods
  • Createread-only andread-write properties
  • Create consistent andbackward-compatible APIs for your classes

You also wrote several practical examples that walked you through the most common use cases ofproperty(). Those examples include inputdata validation, computed attributes,logging your code, and more.

Get Your Code:Click here to download the free sample code that shows you how to use Python’s property() to add managed attributes to your classes.

Frequently Asked Questions

Now that you have some experience with Python’sproperty(), you can use the questions and answers below to check your understanding and recap what you’ve learned.

These FAQs are related to the most important concepts you’ve covered in this tutorial. Click theShow/Hide toggle beside each question to reveal the answer.

In Python, a property is a language construct that allows you to define methods in a class, which can be accessed like attributes. It provides a way to add custom behavior to attributes without changing the class’s public API.

The@property decorator turns a method into a getter for a managed attribute. You can also define setter and deleter methods using@property_name.setter and@property_name.deleter to control attribute assignment and deletion.

You should use a property when you need to add logic to attribute access, such as validation or transformation, without changing the class’s public API. Properties are useful for computed attributes and managing read-only or write-only access.

Avoid using the@property decorator when the attribute doesn’t require additional logic for access or mutation, as it can make the code unnecessarily complex and slower than using regular attributes.

To create read-only attributes, define a getter method with@property and omit the setter. For read-write attributes, define both getter and setter methods using@property and@property_name.setter.

Take the Quiz: Test your knowledge with our interactive “Python's property(): Add Managed Attributes to Your Classes” quiz. You’ll receive a score upon completion to help you track your learning progress:


Python's property(): Add Managed Attributes to Your Classes

Interactive Quiz

Python's property(): Add Managed Attributes to Your Classes

In this quiz, you'll test your understanding of Python's property(). With this knowledge, you'll be able to create managed attributes in your classes, perform lazy attribute evaluation, provide computed attributes, and more.

Watch Now This tutorial has a related video course created by the Real Python team. Watch it together with the written tutorial to deepen your understanding:Managing Attributes With Python's property()

🐍 Python Tricks 💌

Get a short & sweetPython Trick delivered to your inbox every couple of days. No spam ever. Unsubscribe any time. Curated by the Real Python team.

Python Tricks Dictionary Merge

AboutLeodanis Pozo Ramos

Leodanis is a self-taught Python developer, educator, and technical writer with over 10 years of experience.

» More about Leodanis

Each tutorial at Real Python is created by a team of developers so that it meets our high quality standards. The team members who worked on this tutorial are:

MasterReal-World Python Skills With Unlimited Access to Real Python

Locked learning resources

Join us and get access to thousands of tutorials, hands-on video courses, and a community of expert Pythonistas:

Level Up Your Python Skills »

MasterReal-World Python Skills
With Unlimited Access to Real Python

Locked learning resources

Join us and get access to thousands of tutorials, hands-on video courses, and a community of expert Pythonistas:

Level Up Your Python Skills »

What Do You Think?

Rate this article:

What’s your #1 takeaway or favorite thing you learned? How are you going to put your newfound skills to use? Leave a comment below and let us know.

Commenting Tips: The most useful comments are those written with the goal of learning from or helping out other students.Get tips for asking good questions andget answers to common questions in our support portal.


Looking for a real-time conversation? Visit theReal Python Community Chat or join the next“Office Hours” Live Q&A Session. Happy Pythoning!

Keep Learning

Related Topics:intermediatebest-practicespython

Recommended Video Course:Managing Attributes With Python's property()

Related Tutorials:

Keep reading Real Python by creating a free account or signing in:

Already have an account?Sign-In

Almost there! Complete this form and click the button below to gain instant access:

Python's property(): Add Managed Attributes to Your Classes

Python's property(): Add Managed Attributes to Your Classes (Sample Code)

🔒 No spam. We take your privacy seriously.


[8]ページ先頭

©2009-2025 Movatter.jp