I'm learning python @property
annotation using this. My understanding is that it is a built-in property to facilitate accessing and modifying class properties. I created a class using this annotation on some properties, then tried implementing str to display everything.
Below, everything displays fine using the str()
method. However, when I substitute self._area
for self.area
, I get AttributeError: Circle object has no attribute _area
In the str()
implementation, why am I allowed to use self._diameter
but not self._area?
class Circle(object):
def __init__(self, radius):
self._radius=radius
@property
def radius(self):
return self._radius
@radius.setter
def radius(self, radius):
self._radius=radius
@property
def diameter(self):
return self._diameter
@diameter.setter
def diameter(self, diameter):
self._diameter=diameter
@diameter.deleter
def diameter(self):
del self._diameter
@property
def area(self):
self._area = self._radius**2*3.14
return self._area
def __str__(self):
return f'Circle has radius of {self._radius}, diameter of {self._diameter}, and area of {self.area}'
c = Circle(4)
c.diameter=3
print(str(c))
CodePudding user response:
This doesn't work because you (rather pointlessly) compute self._area
lazily, only when self.area
is accessed. So your __str__
implementation will work just fine if the user accesses c.area
ahead of time, but doesn't work when it hasn't been accessed.
Simple solution: Use an eagerly computed attribute or a lazily computed property, don't mix the two in confusing ways. As a rule, you should define (even if it's just with None
for lazily computed attributes) all attributes in __init__
; it's maintainer-friendly (they don't have to search the whole class to figure out the set of attributes) and modern CPython rewards you for it (by using key-sharing dictionaries for attribute storage, reducing the per-instance memory overhead by a significant amount, very roughly halving memory overhead).
As an aside, properties that provide full access to the underlying attribute are pointless in Python; if you're giving the user full access anyway, just make it a regular attribute, saving a bunch of boilerplate and making the code run faster. This is the case with both radius
and diameter
in your code. In the case of diameter
though, having it manage a separate attribute makes no sense (diameter
is definitionally twice the radius, so they should use common state, _diameter
should never exist), and area
is cheap enough to recompute that it should probably just be an uncached property.
A simplified, idiomatic version of your class would look like:
class Circle: # (object) only necessary on Py2; hope you're not developing for it
def __init__(self, radius):
self.radius = radius # Use a public attribute without bothering with
# pointless property wrapping that protects nothing
# and just slows things down
# diameter is just another "view" of the radius, so make the property
# perform converting mutations to the radius
@property
def diameter(self):
return self.radius * 2 # Computable from radius
@diameter.setter
def diameter(self, diameter):
self.radius = diameter / 2 # Convert to radius and store
@property
def area(self):
# This is cheap enough to compute that it's probably not worth
# caching (if the radius changes, you'd have to invalidate/recompute the cached value, it's just a pain)
return math.pi * self.radius ** 2 # Computable from radius
def __str__(self):
# Use the accessors; they're accurate, and duplicating the code to compute
# them is silly
return f'Circle has radius of {self.radius}, diameter of {self.diameter}, and area of {self.area}'
c = Circle(4)
c.diameter = 3
print(c) # print already stringifies; calling str on it is redundant
Side-note: You always want to implement __repr__
on a class, just so tracebacks and the like referencing it are useful; it's pretty easy to write (using type(self).__name__
makes it subclass friendly when the __init__
arguments don't change):
def __repr__(self):
return f'{type(self).__name__}({self._radius!r})' # !r meaningless for int/float, but it's good to use it as a habit for when it matters, e.g. str arguments