I’ve been thinking that a good way to introduce program design might involve objects of type Triangle, i.e. instances of the Triangle class.
mytri = Triangle(3,4,5) mytri.area 6.0 mytri.C 90.0
Now isn’t it true that 3 sides uniquely determine a triangle *except* some triangles have handedness, and if they're not allowed to flip on the griddle, you can have two of the same edge lengths (e.g. 3-4-5) that can't be made to have the same orientation? We might learn about such asymmetries by playing Tetris. The "L" pieces come in two varieties. Anyway, here's a design question: do we want to compute the angles and store them as properties at the time of construction, i.e. in the init? Or do we want to make getAngles() a method? Compromise: call _getAngles from the init. The advantage of always running a method is you always use the a,b and c of the moment, so if these change e.g. mytri.a = 7, then getAngles() will take that change into account. If angles A,B and C are computed from a, b, c in the constructor and saved as instance variables, then a subsequent change to b must force a change to A,B and C, as well as in area and perimeter. We could do this in more than one way. Which way is best? I think we might go for a class that lets users modify a,b and c as properties, but doesn't encourage direct access to A,B and C. Angles are a result of the sides, but we don't go in the other direction. Trying to set mytri.A would raise an exception. However, even with sides, we need to patrol for violations of triangular inequality. No two lengths are allowed to sum to less than the third length, and if they exactly equal it, then two of the angles are 0, one is 180, and area is 0. Here's a version that uses the getAngles() and getArea() approach. A,B,C aren't saved as instance variables at all, but as references in a dictionary that gets returned, but is not saved in the object. It still needs an underunder repr. class Triangle: def __init__(self, a,b,c): "Construct a triangle" if (c > a + b) or (a > b + c) or (b > a + c): raise Exception("Illegal edges") self.a = a self.b = b self.c = c def getArea(self): "Heron's Forumula" s = 0.5 * (self.a + self.b + self.c) return math.sqrt( s * (s - self.a) * (s - self.b) * (s - self.c) ) def getAngles(self): a,b,c = self.a, self.b, self.c # for brevity angles = {} # empty dictionary A = math.acos((-a**2 + b**2 + c**2)/(2.0*b*c) ) B = math.acos(( a**2 - b**2 + c**2)/(2.0*a*c) ) C = math.acos(( a**2 + b**2 - c**2)/(2.0*a*b) ) angles['A'] = round(math.degrees(A),5) angles['B'] = round(math.degrees(B),5) angles['C'] = round(math.degrees(C),5) return angles Kirby -- No virus found in this outgoing message. Checked by AVG Anti-Virus. Version: 7.0.338 / Virus Database: 267.10.13/78 - Release Date: 8/19/2005
-----Original Message----- From: edu-sig-bounces@python.org [mailto:edu-sig-bounces@python.org] On Behalf Of Kirby Urner Sent: Friday, August 19, 2005 8:30 PM To: edu-sig@python.org Subject: [Edu-sig] Design patterns
Anyway, here's a design question: do we want to compute the angles and store them as properties at the time of construction, i.e. in the init?
PyGeo emphasizes dynamism. So on the one hand I am anxious - for performance reasons - to push as much processing as I can into __init__. IOW, start-up time for a construction is much less a concern than the responsiveness when there is change, i.e. when a point of the construction is picked and moved and all the implications of that move needs be determined and all elements affected by it redrawn. And exactly for the same reason, there are clear constraints as to what I can in fact put in __init__. Everything intended to be dynamic needs to be in methods calculated on the fly. What I end up with to get decent performance is a patterns of registration. Part of __init__ for any element is a process with registers it on a chain of dependencies, so that when a point is in fact picked and moved the point knows what other elements of the construction depend - directly and indirectly - on its position and only those elements are recalced and redrawn. The moral of the story is, I think, it depends on what in more precise terms you are trying to do. The great fun of building PyGeo has been that I always had a pretty clear idea of what I was trying to do. Given that, it seemed a process of simply discovering the way to do it. That is as opposed to inventing the way to do it. The way to do it was pretty much dictated by the intent of the project, and was more and less sitting there. I have just needed to find it. Art
Kirby Urner wrote:
I’ve been thinking that a good way to introduce program design might involve objects of type Triangle, i.e. instances of the Triangle class.
Anyway, here's a design question: do we want to compute the angles and store them as properties at the time of construction, i.e. in the init? Or do we want to make getAngles() a method? Compromise: call _getAngles from the init.
Here's how to do the angles in 2.4 w/ properties: class Triangle(object): def __init__(self, a,b,c): "Construct a triangle" if (c >= a + b) or (a >= b + c) or (b >= a + c): raise ValueError("Illegal edges: %s/%s/%s" % (a, b, c)) self.a = a self.b = b self.c = c def area(self): "Heron's Formula" s = 0.5 * (self.a + self.b + self.c) return math.sqrt( s * (s - self.a) * (s - self.b) * (s - self.c)) @property def A(self): return math.acos((-self.a**2 + self.b**2 + self.c**2) / (2.0 * self.b * self.c)) @property def B(self): return math.acos((self.a**2 - self.b**2 + self.c**2) / (2.0 * self.a * self.c)) @property def C(self): return math.acos((self.a**2 + self.b**2 - self.c**2) / (2.0 * self.a * self.b)) Now you can do: t = Triangle(3,4,5) t.c, math.degrees(t.C) t.a/math.sin(t.A), t.b/math.sin(t.B), t.c/math.sin(t.C) t.b = t.c = 3 t.c, math.degrees(t.C) t.a/math.sin(t.A), t.b/math.sin(t.B), t.c/math.sin(t.C) This takes advantage of the fact that property with a single parameter defines only the "get value" accessor, and makes setting or deleting the property illegal. -- Scott David Daniels Scott.Daniels@Acm.Org
Now you can do:
t = Triangle(3,4,5) t.c, math.degrees(t.C) t.a/math.sin(t.A), t.b/math.sin(t.B), t.c/math.sin(t.C) t.b = t.c = 3 t.c, math.degrees(t.C) t.a/math.sin(t.A), t.b/math.sin(t.B), t.c/math.sin(t.C)
This takes advantage of the fact that property with a single parameter defines only the "get value" accessor, and makes setting or deleting the property illegal.
-- Scott David Daniels Scott.Daniels@Acm.Org
Totally excellent Scott. Thank you. Kirby
-----Original Message----- From: edu-sig-bounces@python.org [mailto:edu-sig-bounces@python.org] On Behalf Of Scott David Daniels
Here's how to do the angles in 2.4 w/ properties:
<snip>
@property def A(self): return math.acos((-self.a**2 + self.b**2 + self.c**2) / (2.0 * self.b * self.c))
Hmmm... We seem to be saying that @property works out-of-the box as a decorator in 2.4 - at least where we want read only properties. I can only assume that this is more of a side-effect of the introduction of decorators, than a feature fully designed into the language. Which is not to say that it is not a fortunate side effect. But it is unfortunate combo for me - since I never fully understood the purpose of properties, and don't fully get the mechanics of decorators. My most important points of confusion: What beyond sugar for leaving off a "()" when trying to retrieve a value from a method are we accomplishing by using properties? I have tended to look at properties mostly an accommodation to those coming from other languages which have something similar, but never as something that was core to Python or a Pythonic approach to things. Am I missing something fundamental? and Am I correct that the use of property as a "built-in" decorator is not designed into the language - at least in the same way that @classmethod has been? What would it take to create a @property decorator that allows one to set as well as get? Would we want to? Art
-----Original Message----- From: edu-sig-bounces@python.org [mailto:edu-sig-bounces@python.org] On Behalf Of Arthur
What beyond sugar for leaving off a "()" when trying to retrieve a value from a method are we accomplishing by using properties? I have tended to look at properties mostly an accommodation to those coming from other languages which have something similar, but never as something that was core to Python or a Pythonic approach to things. Am I missing something fundamental?
The searching I do on this point only confirms to me that my own confusion is well shared - perhaps indicating this to be an area not totally OT for this forum. Ray Hettinger's "How-To Guide for Descriptors" http://users.rcn.com/python/download/Descriptor.htm covers properties, but the use case given is extremely unsatisfying - essentially offering a situation where a fundamental design change has been made to a program in mid-stream, and since properties were used in the initial design, the change can be made with little refactoring. Somehow I don't expect a programming language to accommodate the possibility of a midstream change in a programs design/intentions of the kind cited here. New design, and the expectation would be to need to recode to the new design. All within bounds, of course. OOP, I think, does try to address this issue to a reasonable degree. But the properties use case in this article is, to my intuition, a stretch beyond those reasonable bounds. I can't believe that a language or coding style intended to accommodate the range of possibilities for the kind of fundamental midstream redesign referenced in this use case could lead anywhere else but to some kind of madness. The fact is that I do use properties to a limited degree in PyGeo - the actual implementation of properties being trivial enough. Much less trivial is the why and when. The brain dead use of properties in PyGeo being definitely on my list for refactoring. Just not sure whether I should be consistent in avoiding them, or consistently using them, or when is which and which is when. Art
Hi Arthur -- Yes, decorator syntax isn't designed to integrate with properties quite the same way. All a decorator does is run the subsequently defined function f2 through the f1 in @f1, rebinding f2 to the returned output of f1. E.g. 'f2 = f1(f2)' and '@f1; def f2(): pass' amount to the same thing. The non-decorator version of Triangle, with properties, might be:
import math
class Triangle(object):
def __init__(self, a,b,c): "Construct a triangle" if (c >= a + b) or (a >= b + c) or (b >= a + c): raise ValueError("Illegal edges: %s/%s/%s" % (a, b, c)) self.a = a self.b = b self.c = c def getArea(self): "Heron's Formula" s = 0.5 * (self.a + self.b + self.c) return math.sqrt( s * (s - self.a) * (s - self.b) * (s - self.c)) def getA(self): return math.acos((-self.a**2 + self.b**2 + self.c**2) / (2.0 * self.b * self.c)) def getB(self): return math.acos((self.a**2 - self.b**2 + self.c**2) / (2.0 * self.a * self.c)) def getC(self): return math.acos((self.a**2 + self.b**2 - self.c**2) / (2.0 * self.a * self.b)) A = property(getA, doc='Angle A in radians') B = property(getB, doc='Angle B in radians') C = property(getC, doc='Angle C in radians') area = property(getArea, doc="Triangle's area")
mytri = Triangle(3,4,5) math.degrees(mytri.C) 90.0 math.degrees(mytri.A) 36.86989764584402 mytri.area = 6
Traceback (most recent call last): File "<pyshell#28>", line 1, in -toplevel- mytri.area = 6 AttributeError: can't set attribute Alex Martelli's example in 'Python in a Nutshell' is similar: a rectangle's area is computed on the fly (pg. 85). Note that defining attributes in this way doesn't mean we only find out an attribute is really a property much further along in the code (everything is expressed in one line) -- which was the problem decorators addressed i.e. a long function def might obscure the fact that we're defining a classmethod (the older f1 = classmethod(f1) had to come at the end).
help(Triangle) # now get us this useful section in the documentation:
| ---------------------------------------------------------------------- | Properties defined here: | | A | Angle A in radians | | <get> = getA(self) | | B | Angle B in radians | | <get> = getB(self) | | C | Angle C in radians | | <get> = getC(self) | | area | Triangle's area | | <get> = getArea(self) | Heron's Formula | | ---------------------------------------------------------------------- I think it's fine to call it "syntactic sugar" when we make object attributes call their associated accessors and mutators behind the scenes. Conceptually, we're giving the user of the class a simpler API while giving ourselves as programmers a lot of control over what triggers what. It's easy to think of angles and area as attributes, even if we don't allow them to be set except through changes to edges. Why should we force a user to remember what's a method and what's not, given edges, angles and area (could add perimeter too). A weakness in the above design: we only check for violations of triangle inequality in the constructor, yet allow changes to a,b,c through the API. Kirby
Arthur wrote:
What beyond sugar for leaving off a "()" when trying to retrieve a value from a method are we accomplishing by using properties? I have tended to look at properties mostly an accommodation to those coming from other languages which have something similar, but never as something that was core to Python or a Pythonic approach to things. Am I missing something fundamental?
The searching I do on this point only confirms to me that my own confusion is well shared - perhaps indicating this to be an area not totally OT for this forum.
Ray Hettinger's "How-To Guide for Descriptors"
http://users.rcn.com/python/download/Descriptor.htm
covers properties, but the use case given is extremely unsatisfying - essentially offering a situation where a fundamental design change has been made to a program in mid-stream, and since properties were used in the initial design, the change can be made with little refactoring.
I'd propose two reasons why properties are so successful. The first explanation comes from eXtreme Programming. One big goal of XP is to stop wasting time doing "big design up front." I've spent enough time in companies where long design meetings (months) produced huge design documents. The documents themselves had a tendency to look like everything was nailed down, while not really answering a lot of questions that had to be solved when writing the code. The length of time spent producing the document, and the general unavailability of the group that wrote it (they typically moved into other committees to write other design documents), led to an increasingly rigid design that reflected neither discoveries or innovations from the coders nor changes in requirements (or at least our understanding of those requirements). XP can be seen as a reaction to that problem. The second explanation is much lower level. In O-O code, there is a distinction between interface and implementation. Essentially, the interface to an object is how the object behaves from a user of that object's point of view. The implementation is how that object accomplishes its behavior. When you separate these concerns, you can more likely keep a programs complexity (in the sense of debugging/ extending) from growing exponentially with the number of lines of code. Properties let you to hide the trade-off between accessing a stored value and computing it on the fly. Without properties, the interface to classes that want to reserve the access/calculate tradeoff must use the Java-like "getVar" functions to fetch any values that might be calculated.
What would it take to create a @property decorator that allows one to set as well as get? Would we want to?
def writeprop(viewvar): '''decorator makes a property from access and a write method''' def view(self): return getattr(self, viewvar) def buildprop(writemethod): return property(view, writemethod) return buildprop Triangle as an example: import math class BaseTriangle(object): @classmethod def check(klass, a, b, c): '''Check three lengths for suitability as triangle sides''' if a >= b + c: raise ValueError, 'Too long: %s >= %s + %s' % (a, b, c) if a <= abs(b - c): raise ValueError, 'Too short: %s <= abs(%s - %s)' % (a, b,c) def __init__(self, a, b, c): self.check(a, b, c) self._a = a self._b = b self._c = c self._reset() def __repr__(self): return '%s(%s, %s, %s)' % (self.__class__.__name__, self.a, self.b, self.c) def _reset(self): '''Called whenever the sides of the triangle change''' pass @writeprop('_a') def a(self, v): self.check(v, self.b + self.c) self._a = v self._reset() @writeprop('_b') def b(self, v): self.check(v, self.a, self.c) self._b = v self._reset() @writeprop('_c') def c(self, v): self.check(v, self.a, self.b) self._c = v self._reset() # One kind of triangle with angles: class Triangle(BaseTriangle): @property def perimeter(self): return self.a + self.b + self.c @property def area(self): "Heron's Formula" s = 0.5 * self.perimeter return math.sqrt(s * (s - self.a) * (s - self.b) * (s - self.c)) @property def A(self): return math.acos((-self.a**2 + self.b**2 + self.c**2) / (2.0 * self.b * self.c)) @property def B(self): return math.acos((self.a**2 - self.b**2 + self.c**2) / (2.0 * self.a * self.c)) @property def C(self): return math.acos((self.a**2 + self.b**2 - self.c**2) / (2.0 * self.a * self.b)) # Another kind of triangle with angles: class Triangle2(BaseTriangle): def _reset(self): self.perimeter = self.a + self.b + self.c s = 0.5 * self.perimeter self.area = math.sqrt(s * (s - self.a) *(s - self.b) * (s - self.c)) self.A = math.acos((-self.a**2 + self.b**2 + self.c**2) / (2.0 * self.b * self.c)) self.B = math.acos((self.a**2 - self.b**2 + self.c**2) / (2.0 * self.a * self.c)) self.C = math.acos((self.a**2 + self.b**2 - self.c**2) / (2.0 * self.a * self.b)) --Scott David Daniels Scott.Daniels@Acm.Org
Scott:
def writeprop(viewvar): '''decorator makes a property from access and a write method''' def view(self): return getattr(self, viewvar) def buildprop(writemethod): return property(view, writemethod) return buildprop
Pretty fancy Scott and again, highly worthy of study. An element in this design pattern is using a function (writeprop) to build the decorator function (buildprop) that'll transform the following def (e.g. def c). Scott used the same technique when he enhanced my hypertoon program (a VPython cartoon generator) with decorator syntax. @f1(args) def f2: pass uses f1(args) to build a function which then takes f2 as its argument, rebinding the result to f2, i.e. f2 = (f1(args))(f2), where f1(args) by itself returns a callable. In this case, buildprop returns a curried property function (curried in that the 'view' method is already bound, defined in terms of getattr). The decorator returns a function the effect of which is equivalent to 'c = property(view, c)' i.e. c-the-function goes in as buildprop's writemethod and is rebound to make c a property.
@writeprop('_c') def c(self, v): self.check(v, self.a, self.b) self._c = v self._reset()
Here's a similar base triangle class without the decorators: class BaseTriangle2(object): @classmethod def check(klass, a, b, c): '''Check three lengths for suitability as triangle sides''' if a >= b + c: raise ValueError, 'Too long: %s >= %s + %s' % (a, b, c) if a <= abs(b - c): raise ValueError, 'Too short: %s <= abs(%s - %s)' % (a, b,c) def __init__(self, a, b, c): self.check(a, b, c) self._a = a self._b = b self._c = c self._reset() def __repr__(self): return '%s(%s, %s, %s)' % (self.__class__.__name__, self.a, self.b, self.c) def _reset(self): '''Called whenever the sides of the triangle change''' pass def _seta(self, v): self.check(v, self.b, self.c) # fixed typo in original self._a = v self._reset() def _setb(self, v): self.check(v, self.a, self.c) self._b = v self._reset() def _setc(self, v): self.check(v, self.a, self.b) self._c = v self._reset() def _geta(self): return self._a def _getb(self): return self._b def _getc(self): return self._c a = property(_geta, _seta) b = property(_getb, _setb) c = property(_getc, _setc) Kirby
-----Original Message----- From: edu-sig-bounces@python.org [mailto:edu-sig-bounces@python.org] On Behalf Of Scott David Daniels
I'd propose two reasons why properties are so successful.
The first explanation comes from eXtreme Programming. One big goal of XP is to stop wasting time doing "big design up front."
Then my situation is a bit ironic. What I don't seem to have realized - at least to my own satisfaction - is some rational scheme for the use of properties. They are used - somewhere in the range from haphazardly to intuitively. So I am left to refactor working code to give some scheme to my use of properties. And since this refactoring is coming at a fairly late stage of design, I don't feel the same need to leave my XP options open to the extent that I might had I thought more thoroughly about the use of properties earlier in the process. So where I come out on the use of properties now might not be the same as where I would have come out at some different point. Why do I find this unsatisfying? As an API matter, I find myself liking the clue that "()" provides as to whether one is accessing something designed be determined dynamically. So I find myself leaning towards the option of making the use of properties go away in PyGeo. Art
Arthur wrote:
As an API matter, I find myself liking the clue that "()" provides as to whether one is accessing something designed be determined dynamically.
In general I agree with that sentiment.
I find myself leaning towards the option of making the use of properties go away in PyGeo.
If you want to keep anything as properties, I'd keep the ones those properties with the least dynamic nature. --Scott David Daniels Scott.Daniels@Acm.Org
Why do I find this unsatisfying?
I think part of the picture is you're coding for yourself i.e. your APIs are used internally by your own code. Refactoring *everything* is always a possibility. Suppose you'd already published a Triangle class API and then discovered you needed more dynamism. The property feature lets you sneak in some methods without changing the already-published API and breaking client code. A lot of these lessons about robust software development come from group or community efforts. Some aspects of Python maybe don't much excite you because you're primarily a solo coder (as am I much of the time). Kirby
participants (3)
-
Arthur -
Kirby Urner -
Scott David Daniels