text: Contents
  We concentrated in Chapter 1 on computational processes and on the role of functions in program design. We saw how to use primitive data (numbers) and primitive operations (arithmetic operations),.. Thanks to the explosive growth of the Internet, a vast amount of structured information about the world is freely available to us all online.
  In the beginning of this course, we distinguished between functions and data: functions performed operations and data were operated upon. When we included function values among our data, we acknowledged that data too can have behavior. Functions could be operated upon like data, but could also be called to perform computation.
  In this course, in letters and numbers.(2011, 9, 12)
  While today was constructed from primitive numbers, it behaves like a date. For instance, subtracting it from another date will give a time difference, which we can display as a line of text by calling str.
  >>> str(date(2011, 12, 2) - today) '81 days, 0:00:00'
  Objects have attributes, which are named values that are part of the object. In Python, we use dot notation to designated an attribute of an object.
  <expression> . <name>
  Above, the <expression> evaluates to an object, and <name> is the name of an attribute for that object.
  Unlike the names that we have considered so far, these attribute names are not available in the general environment. Instead, attribute names are particular to the object instance preceding the dot.
  >>> today.year 2011
  Objects also have methods, which are function-valued attributes. Metaphorically, the object "knows" how to carry out those methods. Methods compute their results from both their arguments and their object. For example, The strftime method of today takes a single argument that specifies how to display a date (e.g., %A means that the day of the week should be spelled out in full).
  >>> today.strftime('%A, %B %d') 'Monday, September 12'
  Computing the return value of strftime requires two inputs: the string that describes the format of the output and the date information bundled into today. Date-specific logic is applied within this method to yield this result. We never stated that the 12th of September, 2011,.
  Even though we haven't described precisely how objects work yet, it is time to start thinking about data as objects now, because in Python every value is an object.
  Every object in Python has a type. The type function allows us to inspect the type of an object.
  >>> type(today) <class 'datetime.date'>
  So far, the only kinds of objects we have studied are numbers, functions, Booleans, and now dates. We also briefly encountered sets and strings, course, Dive Into Python 3 gives a pragmatic overview of all Python's native data types and how to use them effectively, including numerous usage examples and practical tips. You needn't read that chapter now, but consider it a valuable reference.
  As we consider the wide set of things in the world that we would like to represent in our programs, we find that most of them have compound structure. A date has a year, a month, and a day; a geographic position has a latitude and a longitude. To represent positions, we would like our programming language to have the capacity to "glue together" a latitude and longitude to form a pair --- a compound data value --- that our programs could manipulate in a way that would be consistent with the fact that we regard a position as a single conceptual unit, which has two parts.
  The use of compound data also is a methodology that enables us to isolate how a compound data object, Pythonic --- it's not necessarily the typical way to implement our ideas in the language. What we write is instructive, however, because it demonstrates how these abstractions can be constructed! Remember that computer science isn't just about learning to use programming languages, but also learning. the following three functions:
  We are using here a powerful strategy of synthesis: wishful thinking. We haven't yet said how a rational number is represented, or how the functions numer, denom, and make_rat should be implemented. Even so, if we did have these three functions, we could then add, multiply, and test equality of rational numbers by calling them:
  >>> def add_rat(x, y): nx, dx = numer(x), denom(x) ny, dy = numer(y), denom(y) return make_rat(nx * dy + ny * dx, dx * dy)
  >>> def mul_rat(x, y): return make_rat(numer(x) * numer(y), denom(x) * denom(y))
  >>> def eq_rat(x, y): return numer(x) * denom(y) == numer(y) * denom(x)
  Now we have the operations on rational numbers defined in terms of the selector functions numer and denom, and the constructor function make_rat,, written as make_rat(n, d): return (n, d)
  >>> def numer(x): return getitem(x, 0)
  >>> def denom(x): return getitem(x, 1)
  A function for printing rational numbers completes our implementation of this abstract data type.
  >>> def str_rat(x): """Return a string 'n/d' for numerator n and denominator d.""" return '{0}/{1}'.format(numer(x), denom(x))
  Together with the arithmetic operations we defined earlier, we can manipulate rational numbers with the functions we have defined.
  >>> half = make_rat(1, 2) >>> str_rat(half) '1/2' >>> third = make_rat(1, 3) >>> str_rat(mul_rat(half, third)) '1/6' >>> str_rat(add_rat(third, third)) '6/9'
  As the final example shows, our rational-number implementation does not reduce rational numbers to lowest terms. We can remedy this by changing make_rat. make_rat
  >>> str_rat(add_rat(third, third)) '2/3'
  as desired. This modification was accomplished by changing the constructor without changing any of the functions that implement the actual arithmetic operations.
  Further reading. The str_rat make_rat_rat, mul_rat, and eq_rat. These, in turn, are implemented solely in terms of the constructor and selectors make_rat,: make_rat, make_pair and getitem_pair that fulfill this description just as well as a tuple.
  >>> def make_pair(x, y): """Return a function that behaves like a pair.""" def dispatch(m): if m == 0: return x elif m == 1: return y return dispatch
  >>> def getitem_pair(p, i): """Return the element at index i of pair p.""" return p(i)
  With this implementation, we can create and manipulate pairs.
  >>> p = make_pair(1, 2) >>> getitem_pair(p, 0) 1 >>> getitem_pair(p, 1) 2
  This use of functions corresponds to nothing like our intuitive notion of what data should be. Nevertheless, these functions suffice to represent compound data in our programs.
  The subtle point to notice is that the value returned by make student at Berkeley, or every university in the world, or every student in every university. We can list every class ever taken, every assignment ever completed, every grade ever received. develop a particular abstract data type that can implement the sequence abstraction. We then introduce built-in Python types that also implement the same abstraction..
  A standard way to visualize a pair --- in this case, the pair (1,2) --- is called box-and-pointer notation. Each value, compound or primitive, is depicted as a pointer to a box. The box for a primitive value contains a representation of that value. For example, the box for a number contains a numeral. The box for a pair is actually a double box: the left part contains (an arrow to) the first element of the pair and the right part contains the second.
  This Python expression for a nested tuple,
  >>> ((1, 2), (3, 4)) ((1, 2), (3, 4))
  would have the following structure. will explore a range of hierarchical structures in Chapter 3. For now, we consider a particularly important structure.
  We can use nested pairs to form lists of elements of arbitrary length, which will allow us to implement the sequence abstraction. The figure. We can construct this structure using a nested tuple literal:
  >>> (1, (2, (3, (4, None)))) (1, (2, (3, (4, None))))
  This nested structure corresponds to a very useful way of thinking about sequences in general, which we have seen before in the execution rules of the Python interpreter. introduce later in this chapter. A recursive list can be constructed from a first element and the rest of the list. The value None represents an empty recursive list.
  >>> empty_rlist = None >>> def make_rlist(first, rest): """Make = make_rlist(1, make_rlist(2, make_rlist(3, make.
  The series of environment diagrams below illustrate the iterative process by which getitem_rlist finds the element 2 at index 1 in the recursive list. First, the function getitem_rlist is called, creating a local frame.
  The expression in the while header evaluates to true, which causes the assignment statement in the while suite to be executed.
  In this case, the local name s now refers, completing the call).
  Mapping is itself an instance of a general pattern of computation: iterating over all elements in a sequence. To map a function over a sequence, we do not just select a particular element,. For example (pun intended), we can write:
  >>>:
  Step 1 refers to an iterable value. Sequences are iterable, and their elements are considered in their sequential order. Python does include. To ensure that our implementation of recursive lists supported these additional behaviors would require some work. which tuples, first n even. technique for creating modular programs is to introduce new kinds of data that may change state over time. In this way, a single data object can represent something that evolves independently of the rest of the program. The behavior of a changing object may be influenced by its history, just like an entity in the world. Adding state to data. In this section, we will introduce a collection of mutable data types. Mutable objects can change throughout the execution of a program.
  Our first example of a mutable object will be a function that has local state. That state will change during the course of execution of a program.
  To illustrate what we mean by having a function with local state, let us model the situation of withdrawing money from a bank account. We will do so by creating a function called withdraw, which takes as its argument an amount to be withdrawn. If there is enough money in the account to accommodate the withdrawal, then withdraw should return the balance remaining after the withdrawal. Otherwise, withdraw should return the message 'Insufficient funds'. For example, if we begin with $100 in the account, we would like to obtain the following sequence of return values by calling withdraw:
  >>> withdraw(25) 75 >>> withdraw(25) 50 >>> withdraw(60) 'Insufficient funds' >>> withdraw(15) 35
  Observe that the expression withdraw(25), evaluated twice, yields different values. This is a new kind of behavior for a user-defined function: it is non-pure. Calling the function not only returns a value, but also has the side effect of changing the function in some way, so that the next call with the same argument will return a different result. All of our user-defined functions so far have been pure functions, unless they called a non-pure built-in function. They have remained pure because they have not been allowed to make any changes outside of their local environment frame!
  For withdraw to make sense, it must be created with an initial account balance. The function make_withdraw is a higher-order function that takes a starting balance as an argument. The function withdraw is its return value.
  >>> withdraw = make_withdraw(100)
  An implementation of make_withdraw requires a new kind of statement: a nonlocal statement. When we call make_withdraw, we bind the name balance to the initial amount. We then define and return a local function, withdraw, which updates and returns the value of balance when called.
  >>> def make_withdraw(balance): """Return a withdraw function that draws down balance with each call.""" def withdraw(amount): nonlocal balance # Declare the name "balance" nonlocal if amount > balance: return 'Insufficient funds' balance = balance - amount # Re-bind the existing balance name return balance return withdraw
  The environment. The nonlocal statement indicates that the name appears somewhere in the environment other than the first (local) frame or the last (global) frame.
  We can visualize these changes with environment diagrams. The following environment diagrams illustrate the effects of each call, starting with the definition above. We abbreviate away code in the function values and expression trees that isn't central to our discussion.
  Our definition statement has the usual effect: it creates a new user-defined function and binds the name make_withdraw to that function in the global frame.
  Next, we call make_withdraw with an initial balance argument of 20.
  >>> wd = make_withdraw(20)
  This assignment statement binds the name wd to the returned function in the global frame.
  The returned function, (intrinsically) called withdraw, is associated with the local environment for the make_withdraw invocation in which it was defined. The name balance is bound in this local environment. Crucially, there will only be this single binding for the name balance throughout the rest of this example.
  Next, we evaluate an expression that calls withdraw on an amount 5.
  >>> wd(5) 15
  The name wd is bound to the withdraw function, so the body of withdraw is evaluated in a new environment that extends the environment in which withdraw was defined. Tracing the effect of evaluating withdraw illustrates the effect of a nonlocal statement in Python.
  The assignment statement in withdraw would normally create a new binding for balance in withdraw's local frame. Instead, because of the nonlocal statement, the assignment finds the first frame in which balance was already defined, and it rebinds the name in that frame. If balance had not previously been bound to a value, then the nonlocal statement would have given an error.
  By virtue of changing the binding for balance, we have changed the withdraw function as well. The next time withdraw is called, the name balance will evaluate to 15 instead of 20.
  When we call wd a second time,
  >>> wd(3) 12
  we see that the changes to the value bound to the name balance are cumulative across the two calls.
  Here, the second call to withdraw did create a second local frame, as usual. However, both withdraw frames extend the environment for make_withdraw, which contains the binding for balance. Hence, they share that particular name binding. Calling withdraw has the side effect of altering the environment that will be extended by future calls to withdraw.
  Practical guidance. By introducing nonlocal statements, we have created a dual role for assignment statements. Either they change local bindings, or they change nonlocal bindings. In fact, assignment statements already had a dual role: they either created new bindings or re-bound existing names. The many roles of Python assignment can obscure the effects of executing an assignment statement. It is up to you as a programmer to document your code clearly so that the effects of assignment can be understood by others.
  Non-local assignment is an important step on our path to viewing a program as a collection of independent and autonomous objects, which interact with each other but each manage their own internal state.
  In particular, non-local assignment has given us the ability to maintain some state that is local to a function, but evolves over successive calls to that function. The balance associated with a particular withdraw function is shared among all calls to that function. However, the binding for balance associated with an instance of withdraw is inaccessible to the rest of the program. Only withdraw is associated with the frame for make_withdraw in which it was defined. If make_withdraw is called again, then it will create a separate frame with a separate binding for balance.
  We can continue our example to illustrate this point. A second call to make_withdraw returns a second withdraw function that is associated with yet another environment.
  >>> wd2 = make_withdraw(7)
  This second withdraw function is bound to the name wd2 in the global frame. We've abbreviated the line that represents this binding with an asterisk. Now, we see that there are in fact two bindings for the name balance. The name wd is still bound to a withdraw function with a balance of 12, while wd2 is bound to a new withdraw function with a balance of 7.
  Finally, we call the second withdraw bound to wd2:
  >>> wd2(6) 1
  This call changes the binding of its nonlocal balance name, but does not affect the first withdraw bound to the name wd in the global frame.
  In this way, each instance of withdraw is maintaining its own balance state, but that state is inaccessible to any other function in the program. Viewing this situation at a higher level, we have created an abstraction of a bank account that manages its own internals but behaves in a way that models accounts in the world: it changes over time based on its own history of withdrawal requests.
  Our environment model of computation cleanly extends to explain the effects of non-local assignment. However, non-local assignment introduces some important nuances in the way we think about names and values.
  Previously, our values did not change; only our names and bindings changed. When two names a and b were both bound to the value 4, it did not matter whether they were bound to the same 4 or different 4's. As far as we could tell, there was only one 4 object that never changed.
  However, functions with state do not behave this way. When two names wd and wd2 are both bound to a withdraw function, it does matter whether they are bound to the same function or different instances of that function. Consider the following example, which contrasts the one we just analyzed.
  >>> wd = make_withdraw(12) >>> wd2 = wd >>> wd2(1) 11 >>> wd(1) 10
  In this case, calling the function named by wd2 did change the value of the function named by wd, because both names refer to the same function. The environment diagram after these statements are executed shows this fact.
  It is not unusual for two names to co-refer to the same value in the world, and so it is in our programs. But, as values change over time, we must be very careful to understand the effect of a change on other names that might refer to those values.
  The key to correctly analyzing code with non-local assignment is to remember that only function calls can introduce new frames. Assignment statements always change bindings in existing frames. In this case, unless make_withdraw is called twice, there can be only one binding for balance.
  Sameness and change. These subtleties arise because, by introducing non-pure functions that change the non-local environment, we have changed the nature of expressions. An expression that contains only pure function calls is referentially transparent; its value does not change if we substitute one of its subexpression with the value of that subexpression.
  Re-binding operations violate the conditions of referential transparency because they do more than return a value; they change the environment. When we introduce arbitrary re-binding, we encounter a thorny epistemological issue: what it means for two values to be the same. In our environment model of computation, two separately defined functions are not the same, because changes to one may not be reflected in the other. bank accounts that happen to have the same balance, but are different objects.
  Despite the complications it introduces, non-local assignment is a powerful tool for creating modular programs. Different parts of a program, which correspond to different environment frames, can evolve separately throughout program execution. Moreover, using functions with local state, we are able to implement mutable data types. In the remainder of this section, we introduce some of the most useful built-in data types in Python, along with methods for implementing those data types using functions with non-local assignment.
  The list is Python's most useful and flexible sequence type. A list is similar to a tuple, but it is mutable. Method calls and assignment statements can change the contents of a list.
  We can introduce many list editing operations through an example that illustrates the history of playing cards (drastically simplified). Comments in the examples describe the effect of each method invocation.
  Playing cards were invented in China, perhaps around the 9th century. An early deck had three suits, which corresponded to denominations of money.
  >>> chinese_suits = ['coin', 'string', 'myriad'] # A list literal >>> suits = chinese_suits # Two names refer to the same list
  As cards migrated to Europe (perhaps through Egypt), only the suit of coins remained in Spanish decks (oro).
  >>> suits.pop() # Removes and returns the final element 'myriad' >>> suits.remove('string') # Removes the first element that equals the argument
  Three more suits were added (they evolved in name and design over time),
  >>> suits.append('cup') # Add an element to the end >>> suits.extend(['sword', 'club']) # Add all elements of a[0:2] = ['heart', 'diamond'] # Replace a slice >>> suits ['heart', 'diamond', 'spade', 'club']
  Methods also exist for inserting, sorting, and reversing lists. All of these mutation operations change the value of the list; they do not create new list objects.
  Sharing and Identity. Because we have been changing a single list rather than creating new lists, the object bound to the name chinese_suits has also changed, because it is the same list object that was bound to suits.
  >>> chinese_suits # This name co-refers with "suits" to the same list ['heart', 'diamond', 'spade', 'club']
  Lists can be copied using the list constructor function. Changes to one list do not affect another, unless they share structure.
  >>> nest = list(suits) # Bind "nest" to a second list with the same elements >>> nest[0] = suits # Create a nested list
  After this final assignment, we are left with the following environment, where lists are represented using box-and-pointer notation.
  According to this environment, changing the list referenced by suits will affect the nested list that is the first element of nest, but not the other elements.
  >>> suits.insert(2, 'Joker') # Insert an element at index 2, shifting the rest >>> nest [['heart', 'diamond', 'Joker', 'spade', 'club'], 'diamond', 'spade', 'club']
  And likewise, undoing this change in the first element of nest will change suit as well.
  >>> nest[0].pop(2) 'Joker' >>> suits ['heart', 'diamond', 'spade', 'club']
  As a result of this last invocation of the pop method, we return to the environment depicted above.
  Because two lists may have the same contents but in fact be different lists, we require a means to test whether two objects are the same. Python includes two comparison operators, called is and is not, that test whether two expressions in fact evaluate to the identical object. Two objects are identical if they are equal in their current value, and any change to one will always be reflected in the other. Identity is a stronger condition than equality.
  >>> suits is nest[0] True >>> suits is ['heart', 'diamond', 'spade', 'club'] False >>> suits == ['heart', 'diamond', 'spade', 'club'] True
  The final two comparisons illustrate the difference between is and ==. The former checks for identity, while the latter checks for the equality of contents.
  List comprehensions. A list comprehension uses an extended syntax for creating lists, analogous to the syntax of generator expressions.
  For example, the unicodedata module tracks the official names of every character in the Unicode alphabet. We can look up the characters corresponding to names, including those for card suits.
  >>> from unicodedata import lookup >>> [lookup('WHITE ' + s.upper() + ' SUIT') for s in suits] ['♡', '♢', '♤', '♧']
  List comprehensions reinforce the paradigm of data processing using the conventional interface of sequences, as list is a sequence data type.
  Further reading. Dive Into Python 3 has a chapter on comprehensions that includes examples of how to navigate a computer's file system using Python. The chapter introduces the os module, which for instance can list the contents of directories. This material is not part of the course, but recommended for anyone who wants to increase his or her Python expertise.
  Implementation. list as its local state. Lists need to have an identity, like any mutable value. In particular, we cannot use None to represent an empty mutable list, because two empty lists are not identical values (e.g., appending to one does not append to the other), but None is None. On the other hand, two different functions that each have empty = make = make)))"
  In principle, the operations push_first and pop_first suffice to make arbitrary changes to a list. We can always empty out the list entirely and then replace its old contents with the desired result.
  Message passing. Given some time, we could implement the many useful mutation operations of Python lists, such as extend and insert. We would have a choice: we could implement them all as functions, which use the existing messages pop_first and push_first to make all changes. Alternatively, we could add additional elif clauses to the body of dispatch, each checking for a message (e.g., 'extend') and applying the appropriate change to contents directly.
  This second approach, which encapsulates the logic for all operations on a data value within one function that responds to different messages, is.
  Dictionaries are Python's built-in data type for storing and manipulating correspondence relationships. A dictionary contains key-value pairs, where both the keys and values are objects. The purpose of a dictionary is to provide an abstraction for storing and retrieving values that are indexed not by consecutive integers, but by descriptive keys.
  Strings commonly serve as keys, because strings are our conventional representation for names of things. This dictionary literal gives the values of various Roman numerals.
  >>> numerals = {'I': 1.0, 'V': 5, 'X': 10}
  Looking up values by their keys uses the element selection operator that we previously applied to sequences.
  >>> numerals['X'] 10
  A dictionary can have at most one value for each key. Adding new key-value pairs and changing the existing value for a key can both be achieved with assignment statements.
  >>> numerals['I'] = 1 >>> numerals['L'] = 50 >>> numerals {'I': 1, 'X': 10, 'L': 50, 'V': 5}
  Notice that 'L' was not added to the end of the output above. Dictionaries are unordered collections of key-value pairs. When we print a dictionary, the keys and values are rendered in some order, but as users of the language we cannot predict what that order will be.
  The dictionary abstraction also supports various methods of iterating of the contents of the dictionary as a whole. The methods keys, values, and items all return iterable values.
  >>> sum(numerals.values()) 66
  A list of key-value pairs can be converted into a dictionary by calling the dict constructor function.
  >>> dict([(3, 9), (4, 16), (5, 25)]) {3: 9, 4: 16, 5: 25}
  Dictionaries do have some restrictions:
  This first restriction is tied to the underlying implementation of dictionaries in Python. The details of this implementation are not a topic of this course. Intuitively, consider that the key tells Python where to find that key-value pair in memory; if the key changes, the location of the pair may be lost.
  The second restriction is a consequence of the dictionary abstraction, which is designed to store and retrieve values for keys. We can only retrieve the value for a key if at most one such value exists in the dictionary.
  A useful method implemented by dictionaries is get, which returns either the value for a key, if the key is present, or a default value. The arguments to get are the key and the default value.
  >>> numerals.get('A', 0) 0 >>> numerals.get('V', 0) 5
  Dictionaries also have a comprehension syntax analogous to those of lists and generator expressions. Evaluating a dictionary comprehension yields a new dictionary object.
  >>> {x: x*x for x in range(3,6)} {3: 9, 4: 16, 5: 25}
  Implementation. We can implement an abstract data type that conforms to the dictionary abstraction as a list of records, each of which is a two-element list consisting of a key and the associated value.
  >>> def make_dict(): "" = make_dict() >>>.
  Mutable data allows us to simulate systems with change, but also allows us to build new kinds of abstractions. In this extended example, we combine nonlocal assignment, lists, and dictionaries to build a constraint-based system that supports computation in multiple directions. Expressing programs as constraints is a type of declarative programming, in which a programmer declares the structure of a problem to be solved, but abstracts away the details of exactly how the solution to the problem is computed.
  Computer programs are traditionally organized as one-directional computations, which perform operations on pre-specified arguments to produce desired outputs. On the other hand, we often want to model systems in terms of relations among quantities. For example, we previously considered the ideal gas law, which relates the pressure (p), volume (v), quantity (n), and temperature (t) of an ideal gas via Boltzmann's constant (k):
  p * v = n * k * t
  Such an equation is not one-directional. Given any four of the quantities, we can use this equation to compute the fifth. Yet translating the equation into a traditional computer language would force us to choose one of the quantities to be computed in terms of the other four. Thus, a function for computing the pressure could not be used to compute the temperature, even though the computations of both quantities arise from the same equation.
  In this section, we sketch the design of a general model of linear relationships. We define primitive constraints that hold between quantities, such as an adder(a, b, c) constraint that enforces the mathematical relationship a + b = c.
  We also define a means of combination, so that primitive constraints can be combined to express more complex relations. In this way, our program resembles a programming language. We combine constraints by constructing a network in which constraints are joined by connectors. A connector is an object that "holds" a value and may participate in one or more constraints.
  For example, we know that the relationship between Fahrenheit and Celsius temperatures is:
  9 * c = 5 * (f - 32)
  This equation is a complex constraint between c and f. Such a constraint can be thought of as a network consisting of primitive adder, multiplier, and constant constraints.
  In this figure, we see on the left a multiplier box with three terminals, labeled a, b, and c. These connect the multiplier to the rest of the network as follows: The a terminal is linked to a connector celsius, which will hold the Celsius temperature. The b terminal is linked to a connector w, which is also linked to a constant box that holds 9. The c terminal, which the multiplier box constrains to be the product of a and b, is linked to the c terminal of another multiplier box, whose b is connected to a constant 5 and whose a is connected to one of the terms in the sum constraint. the celsius connector to a value (say 25), the leftmost multiplier will be awakened, and it will set u to 25 * 9 = 225. Then u awakens the second multiplier, which sets v to 45, and v awakens the adder, which sets the fahrenheit connector to 77.
  Using the Constraint System. To use the constraint system to carry out the temperature computation outlined above, we first create two named connectors, celsius and fahrenheit, by calling the make_connector constructor.
  >>> celsius = make_connector('Celsius') >>> fahrenheit = make_connector('Fahrenheit')
  Then, we link these connectors into a network that mirrors the figure above. The function make_converter assembles the various connectors and constraints in the network.
  >>> def make_converter(c, f): """Connect c to f with constraints to convert from Celsius to Fahrenheit.""" u, v, w, x, y = [make_connector() for _ in range(5)] multiplier(c, w, u) multiplier(v, x, u) adder(v, y, f) constant(w, 9) constant(x, 5) constant(y, 32)
  >>> make_converter(celsius, fahrenheit)
  We will use a message passing system to coordinate constraints and connectors. Instead of using functions to answer messages, we will use dictionaries. A dispatch dictionary will have string-valued keys that denote the messages it accepts. The values associated with those keys will be the responses to those messages.
  Constraints are dictionaries that do not hold local states themselves. Their responses to messages are non-pure functions that change the connectors that they constrain.
  Connectors are dictionaries that hold a current value and respond to messages that manipulate that value. Constraints will not change the value of connectors directly, but instead will do so by sending messages, so that the connector can notify other constraints in response to the change. In this way, a connector represents a number, but also encapsulates connector behavior.
  One message we can send to a connector is to set its value. Here, we (the 'user') set the value of celsius to 25.
  >>> celsius['set_val']('user', 25) Celsius = 25 Fahrenheit = 77.0
  Not only does the value of celsius change to 25, but its value propagates through the network, and so the value of fahrenheit is changed as well. These changes are printed because we named these two connectors when we constructed them.
  Now we can try to set fahrenheit to a new value, say 212.
  >>> fahrenheit['set_val']('user', 212) Contradiction detected: 77.0 vs 212
  The connector complains that it has sensed a contradiction: Its value is 77.0, and someone is trying to set it to 212. If we really want to reuse the network with new values, we can tell celsius to forget its old value:
  >>> celsius['forget']('user') Celsius is forgotten Fahrenheit is forgotten
  The connector celsius finds that the user, who set its value originally, is now retracting that value, so celsius agrees to lose its value, and it informs the rest of the network of this fact. This information eventually propagates to fahrenheit, which now finds that it has no reason for continuing to believe that its own value is 77. Thus, it also gives up its value.
  Now that fahrenheit has no value, we are free to set it to 212:
  >>> fahrenheit['set_val']('user', 212) Fahrenheit = 212 Celsius = 100.0
  This new value, when propagated through the network, forces celsius to have a value of 100. We have used the very same network to compute celsius given fahrenheit and to compute fahrenheit given celsius. This non-directionality of computation is the distinguishing feature of constraint-based systems.
  Implementing the Constraint System. As we have seen, connectors are dictionaries that map message names to function and data values. We will implement connectors that respond to the following messages:
  Constraints are also dictionaries, which receive information from connectors by means of two messages:
  When constraints receive these messages, they propagate them appropriately to other connectors.
  The adder function constructs an adder constraint over three connectors, where the first two must add to the third: a + b = c. To support multidirectional constraint propagation, the adder must also specify that it subtracts a from c to get b and likewise subtracts b from c to get a.
  >>> from operator import add, sub >>> def adder(a, b, c): """The constraint that a + b = c.""" return make_ternary_constraint(a, b, c, add, sub, sub)
  We would like to implement a generic ternary (three-way) constraint, which uses the three connectors and three functions from adder to create a constraint that accepts new_val and forget messages. The response to messages are local functions, which are placed in a dictionary called constraint.
  >>> def
  The dictionary called constraint is a dispatch dictionary, but also the constraint object itself. It responds to the two messages that constraints receive, but is also passed as the source argument in calls to its connectors.
  The constraint's local function new_value is called whenever the constraint is informed that one of its connectors has a value. This function first checks to see if both a and b have values. If so, it tells c to set its value to the return value of function ab, which is add in the case of an adder. The constraint passes itself (constraint) as the source argument of the connector, which is the adder object. If a and b do not both have values, then the constraint checks a and c, and so on.
  If the constraint is informed that one of its connectors has forgotten its value, it requests that all of its connectors now forget their values. (Only those values that were set by this constraint are actually lost.)
  A multiplier is very similar to an adder.
  >>> from operator import mul, truediv >>> def multiplier(a, b, c): """The constraint that a * b = c.""" return make_ternary_constraint(a, b, c, mul, truediv, truediv)
  A constant is a constraint as well, but one that is never sent any messages, because it involves only a single connector that it sets on construction.
  >>> def constant(connector, value): """The constraint that connector = value.""" constraint = {} connector['set_val'](constraint, value) return constraint
  These three constraints are sufficient to implement our temperature conversion network.
  Representing connectors. A connector is represented as a dictionary that contains a value, but also has response functions with local state. The connector must track the informant that gave it its current value, and a list of constraints in which it participates.
  The constructor make_connector has local functions for setting and forgetting values, which are the responses to messages from constraints.
  >>> def make_connector(name=None): """A connector between constraints.""" informant = None
  A connector is again a dispatch dictionary for the five messages used by constraints to communicate with connectors. Four responses are functions, and the final response is the value itself.
  The local function set_value is called when there is a request to set the connector's value. If the connector does not currently have a value, it will set its value and remember as informant the source constraint that requested the value to be set. Then the connector will notify all of its participating constraints except the constraint that requested the value to be set. This is accomplished using the following iterative function.
  >>> def inform_all_except(source, message, constraints): """Inform all constraints of the message, except source.""" for c in constraints: if c != source: c[message]()
  If a connector is asked to forget its value, it calls the local function forget-value, which first checks to make sure that the request is coming from the same constraint that set the value originally. If so, the connector informs its associated constraints about the loss of the value.
  The response to the message has_val indicates whether the connector has a value. The response to the message connect adds the source constraint to the list of constraints.
  The constraint program we have designed introduces many ideas that will appear again in object-oriented programming. Constraints and connectors are both abstractions that are manipulated through messages. When the value of a connector is changed, it is changed via a message that not only changes the value, but validates it (checking the source) and propagates its effects (informing other constraints). In fact, we will use a similar architecture of dictionaries with string-valued keys and functional values to implement an object-oriented system later in this chapter. ease the task of implementing all of these useful. Our example of a constraint program began to develop this metaphor by passing messages between constraints and connectors. The Python object system extends this metaphor with new ways to express how different parts of a program relate to and communicate with each other. a class. New classes can be defined in Python, just as new functions can be defined.
  A class serves as a template for all objects whose type is that class. Every object is an instance of some particular class. The objects we have used so far all have built-in classes, but new classes can be defined similarly to how new functions can be defined.. We would like additional behavior to complete the account abstraction: a bank account should be able to return its current balance, return the name of the account holder, and accept deposits. 'Jim', the account holder's name.
  >>> a = Account('Jim')  and a base class (discussed in the section on Inheritance), then includes a suite of statements to define the attributes of the class:
  class <name>(<base not with the class itself, but with each object of that class. The class specifies the instance attributes of its objects by defining a method for initializing new objects. For instance, "init"), and is called the constructor for the class.
  >>> class Account(object):'. 'Jim'
  Identity. Each new account instance has its own balance attribute, the value of which is independent of other objects of the same class.
  >>> b = Account('Jack') >>>(object):.
  >>> tom_account = Account('Tom') >>> tom_account.deposit(100) 100 >>> tom_account.withdraw(90) 10 >>> tom_account.withdraw(90) 'Insufficient funds' >>> tom_account.holder 'Tom'
  When a method is invoked via dot notation, the object itself (bound to tom. The details of the procedure for evaluating dot notation follow in the next section. tom course,(tom_account.deposit) <class 'method'>
  These two results differ only in the fact that the first is a standard two-argument function with parameters self and amount. The second is a one-argument method, where the name self will be bound to the object named tom(tom_account, 1001) # The deposit function requires 2 arguments 1011 >>> tom.
  Practical guidance: naming conventions. Class names are conventionally written using the CapWords convention (also called CamelCase because the capital letters in the middle of a name are('Tom') >>>.
  We specify inheritance by putting the base class in parentheses after the class name. First, we give a full implementation of the Account class, which includes docstrings for the class and its methods.
  >>> class Account(object): "" course, the same way as the elements of the built-in object system. We have already seen how to implement message-passing behavior through dispatch dictionaries. To implement an object system in full, we send messages between instances, classes, and base classes, all of which are dictionaries that contain attributes.
  Unlike an instance, the get function for classes does not query its class when an attribute is not found, but instead queries its base_class. No method binding is required for classes.
  Initialization. The new function in make_class calls init_instance, which first makes a new instance, then invokes a method called __init__.
  >>> def init_instance(cls, *args): """Return a new object with type cls, initialized with args.""" instance = make_instance(cls) init = cls['get']('__init__') if init: init(instance, *args) return instance
  This final function completes our object system. We now have instances, which set locally but fall back to their classes on get. After an instance looks up a name in its class, it binds itself to function values to create methods. Finally, classes can create new instances, and they apply their __init__ constructor function immediately after instance creation.
  In this object system, the only function that should be called by the user is create_class. All other functionality is enabled through message passing. Similarly, Python's object system is invoked via the class statement, and all of its other functionality is enabled through dot expressions and calls to classes.
  We now return to use the bank account example from the previous section. Using our implemented object system, we will create an Account class, a CheckingAccount subclass, and an instance of each.
  The Account class is created through a create_account_class function, which has structure similar to a class statement in Python, but concludes with a call to make_class.
  >>> def make_account_class(): """Return the Account class, which has deposit and withdraw methods."""({'__init__': __init__, 'deposit': deposit, 'withdraw': withdraw, 'interest': 0.02})
  In this function, the names of attributes are set at the end. Unlike Python class statements, which enforce consistency between intrinsic function names and attribute names, here we must specify the correspondence between attribute names and values manually..""" def withdraw(self, amount): return Account['get']('withdraw')(self, amount + 1) return make_class({'withdraw': withdraw, 'interest': 0.01},..
  More importantly,.
  Special methods.)
  Further reading..(x, y): nx, dx = x.numer, x.denom ny, dy = y.numer, y.denom return Rational(nx * dy + ny * dx, dx * dy)
  >>> def mul_rational_and_complex = lambda r, z: mul_complex_and_rational(z, r) >>> apply.implementations = {('mul', ('com', 'com')): mul_complex, ('mul', ('com', 'rat')): mul_complex_and_rational, ('mul', ('rat', 'com')): mul_rational_and_complex, ('mul', ('rat', 'rat')): mul_rational}, ('add', 'com'): add_complex, ('add', 'rat'): add_rational}.