17. Methods, I¶
We have used operators and functions so far in our programming. Here we introduce another particular style of functionality that exists in Python: methods. Each type within Python will have its own set of these, and here we investigate their syntax and how to use them.
17.1. Object methods: motivation¶
Up until this point, we have used standalone functions that take 0, 1 or more parameters to do something, such as:
y = input()
abs(-100)
print("hello")
np.arange(-5, 10, 2)
np.arange(-5, 10, num=31)
x = int(905.26)
etc. That is, to create new values or display something, we have functions in a style of:
FUNCTION(VAL0, VAL1, VAL2 = SOMETHING, ...)
which comes under the branch of procedural programming: a function has been written to do some process or procedure, and we provide it any number of allowed inputs to work with as args and kwargs. All the inputs are put inside the paretheses after the function's name, just like in mathematics. We have seen how we can use built-in functions or import them from modules (and we will soon look at writing our own). The general role of the function is to take inputs and do something, often producing an output value or display. Mathematically, we tend to think of functions as mapping the inputs to some kind of output quantity.
Quite often, a function can have inputs of different types and still
work. For instance, the absolute value function abs
function will
work with bool, int, float or complex input; there is an appropriate
mapping in each case. In that sense, many functions are fairly
general and flexible across type.
However, there is also some functionality that is specifically
associated with a given type. For example, if we have a string, it
might be useful to make all letters uppercase, all letters lowercase,
or just capitalize the first---this would be very "string-specific"
behavior, and it wouldn't make sense to try to capitalize a float.
The name of the tool to make a string entirely uppercase is upper
;
but it is not used like a normal function would be (the following
gives an error):
upper("a string")
Instead, it is used as follows:
"a string".upper()
... though it behaves as if "a string"
were some kind an
input, producing:
'A STRING'
This change of syntax occurs because upper
is not a
function---it is a method. A method behaves a lot like a
function, just one that has been designed to focus on an input of a
particular type (the one that appears to its left). As we start using
methods, most of the other aspects of using them will actually look
similar to using functions. We can supply various args (optional and
required) and kwargs in the parentheses, and a more general usage
might look like:
VAL0.METHOD(VAL1, VAL2 = SOMETHING, ...)
We wrote the base quantity VAL0
in the same style with the
parameters in the parenthesis, because we will often think of it as
another input, just placed in front.
This style of programming that uses methods is known as
object-oriented programming. It focuses on the object type (or
class, as it is often called in object-oriented discussions) as
the basis of functionality. Each object of a particular type has
attributes beyond just its value: these are bits of information
that are stored in it and inherent to any member of that class. A
method can make use of these attributes without specifying each one
separately as input parameters, because any method is made
specifically to work with a particular type. For example, an object
with complex type/class has real and imaginary components that are
internally known attributes (beyond the overall value); a NumPy array
has element dtype, a number of dimensions, the total number of array
elements, and more. That might be one reason why VAL0
is written
out front: it emphasizes that the method has its roots in the specific
base object's properties (type/class and attributes).
If the exact differences between functions and methods are not clear, that is OK---there is a lot of similarity in usage and overlap in operations. But in the end knowing how to use existing methods in the Python will be quite beneficial. We will explore them a bit here and keep using them throughout the rest of our time working with Python. We won't write our own, but it is useful to know a bit of the terminology (class, method, attribute, etc.) because it can come up in either error messages or help documentation.
Note
From here onwards, we will use type and class interchangeably, often reflecting the context. For example, we might use the latter more when discussing methods and attributes of an object (its "object-oriented" nature).
17.2. Using methods¶
We have seen several examples of types so far in Python: int, float, bool, str, np.ndarray, ... (and we will meet others). From a background theory point of view, we can summarize the brief discussion of object-oriented aspects of Python from above: each type is created as a class and has associated methods that provide specific functionality for that class. The methods are specific to a given class, because they typically make use of underlying, class-specific properties that each object has, called attributes.
From a practical usage standpoint, we should know that: for each type
we meet in Python, there are many useful methods for us to have in our
programming toolbox and to use essentially like functions in our
work. How do we find out what methods are available and how to use
them? We can look at the help docstring for each type with
help(TYPE_NAME)
.
But before we do that, let's look a specific quirk of how the help
descriptions of many methods are written, using the above case of the
string class method upper
. Its help blurb looks like this:
| upper(self, /)
| Return a copy of the string converted to uppercase.
First, recall that the /
does not represent any input, but instead
symbolizes a divide in the inputs: to its left are required ones, to
its right are optional ones. Then, it looks like this method should
take one argument, called self
. However, that is not actually
the case. The self
is a keyword, referring to the base from which
we must call the method---in our example above, that base is "a
string"
, and generally the thing to the left of the method, joined
by a dot: base.method()
. If I ruled the world, I might suggest
that Line 1 of the help be written as:
self.upper( / )
... because self
really represents the quantity used there.
But instead we must learn the following rule of method syntax:
pretend that self
doesn't appear in the argument list, but
instead is written out front. (Perhaps picture in your mind the help
written as just above in the "dream" example.) Thus, if a method's
help were given as:
| mmm(self, name, low=0, high=100)
... then when we use it, we should think of it like:
self.mmm(name, low=0, high=0)
... and put one value out front (the self
or base value), one
argument by position (for name
), and then possibly any of the
optional the kwargs (low
and high
). So, as the initial help
looks, we need to provide two values; it is just that one goes out
front and one goes within the parentheses.
Methods can operate either in-place to change the base ("self")
object or out-of-place to produce new output that could be
assigned to a variable. When we discuss the "list" type, we will see
a method for sorting a list in-place---the list itself is rearranged
without creating a new output to use. (Probably more methods operate
out-of-place, but we always need to read the help file to be sure.)
For example, the string method upper()
takes the string to which
it is applied as input and creates a new string with all the letters
in uppercase. As we see from this example, this method works
out-of-place, leaving the original string unchanged, while creating a
new one:
name1 = "Kwame Nkrumah"
name2 = name1.upper() # 'out-of-place' method, creates new obj
print("name1 :", name1)
print("name2 :", name2)
... produces:
name1 : Kwame Nkrumah
name2 : KWAME NKRUMAH
17.3. Examples: float type (class)¶
OK, let's look at some more method examples, perhaps for the float
type. The top of the help(float)
description (with a couple other
pieces included) looks like:
1Help on class float in module builtins:
2
3class float(object)
4 | float(x=0, /)
5 |
6 | Convert a string or number to a floating point number, if possible.
7 |
8 | Methods defined here:
9 |
10 | __abs__(self, /)
11 | abs(self)
12 |
13 | __add__(self, value, /)
14 | Return self+value.
15 |
16 | __bool__(self, /)
17 | self != 0
18 |
19 | __divmod__(self, value, /)
20 | Return divmod(self, value).
21 |
22 | __eq__(self, value, /)
23 | Return self==value.
24...
25
26 | as_integer_ratio(self, /)
27 | Return integer ratio.
28 |
29 | Return a pair of integers, whose ratio is exactly equal to the original float
30 | and with a positive denominator.
31 |
32 | Raise OverflowError on infinities and a ValueError on NaNs.
33 |
34 | >>> (10.0).as_integer_ratio()
35 | (10, 1)
36 | >>> (0.0).as_integer_ratio()
37 | (0, 1)
38 | >>> (-.25).as_integer_ratio()
39 | (-1, 4)
40...
41
42 | is_integer(self, /)
43 | Return True if the float is an integer.
44 |
45...
We can use our new terminology above to translate:
Lines 1-3: what we think of as type
float
is synonymous with a class "float". This float class was "built-in" to Python itself (what we might consider the "main module" that is installed), and we don't have to import it from a separate module.Lines 4-6: we can use
float()
as a function for (explicit) type conversion. Actually, we see that this function has a default argument: when called with no input, it returns0.0
.Line 8: there are a lot of methods (AKA functionalities) that we can use with any object of this type. These were written by whomever defined the float class and created its properties and attributes.
Lines 10 and following: a list of the class's methods, such as
__abs__()
,__add__()
,is_integer()
, etc. Each has a brief description or equivalent operation below it (sometimes almost too brief); some even have explicit examples cases (Lines 27-40). Note that many of these method names start and end with a double-underscore---it looks funny, but is allowed as a name.
Q: Take the example of the method __abs__(self, /)
,
above. How many values do we need to provide to use it? Where is
each placed when we call it?
Q: Take the example of the method __eq__(self, value, /)
,
above. How many values do we need to provide to use it? Where is
each placed when we call it?
Consider the is_integer
method. The following are all valid ways
to call it:
x = 1.0
x.is_integer() # outputs: True
(2.0).is_integer() # outputs: True
(3.1).is_integer() # outputs: False
4.0.is_integer() # outputs: True
5..is_integer() # outputs: True
float(6).is_integer() # outputs: True
Some of the above look clearer than others. When not operating on a variable, it seems nice to have the parentheses present. That is often a style choice, but not always. Consider the following:
-1.0.__abs__() # outputs: -1.0
(-1.0).__abs__() # outputs: 1.0
... and how the order of evaluations changes, with important considerations for the final value.
If we try to use this method like a function with one input (after all, that is how we would read the help, if we didn't know it was actually a method):
is_integer(5.0)
... we would get an error:
---------------------------------------------------------------------------
NameError Traceback (most recent call last)
<ipython-input-28-3e2569272ec5> in <module>
----> 1 is_integer(5.0)
NameError: name 'is_integer' is not defined
Again, is_integer
is a float method, so it always needs to be
called from a base float.
Let's try applying some of those float methods to get a better feel
for using them. Again, we can read the method definitions like
standard function docstrings, with the extra rule of pretending like
the self
parameter is not present. Here are a few use cases:
aaa = 10.5
bbb = -4.8
ccc = 200.
aaa_methodized = aaa.__add__(10) # Return self+value.
bbb_methodized = bbb.__abs__() # abs(self)
print(aaa_methodized)
print(bbb_methodized)
print((-12345.678).__abs__())
print(aaa.is_integer())
print(ccc.is_integer())
print(aaa.as_integer_ratio())
... which produce the following values (comments added separately):
20.5 # added 10 to 10.5
4.8 # abs val of (-4.8)
12345.678 # another abs val, applied directly to the value; note () here
False # is 10.5 an integer-valued float?
True # is 200. an integer-valued float?
(21, 2) # pair of ints, whose ratio gives float value
Hopefully we can connect the way each method takes the self and any
possible arguments and produces outputs from the help descriptions.
Could we have calculated some of these values differently? Sure! We
could have calculated aaa + 10
instead of aaa.__add__(10)
, and
maybe the former is simpler. As is often the case in programming,
there are multiple ways to perform similar tasks. For some of the
more complicated functionality, such as is_integer
or
as_integer_ratio
, though, the methods are likely more convenient.
Note
The particular method as_integer_ratio()
might give some
unexpected results. For example, we might expect that the
integer ratio of 123.45
would be (2469, 20)
or at
most (12345, 100)
. However, it turns out to be
(8687021468732621, 70368744177664)
. Wow! This goes
back to an earlier point that we made (and is a way for the reader to verify):
while humans might work in base-10 systems, computers don't.
The decimals we see get swapped out for binary
representations (and sometimes, as here, binary
approximations), and this is the direct evidence of it. If
you want, you can verify our earlier claim that 0.1 cannot
be exactly represented in binary by using
(0.1).as_integer_ratio()
to see how it is approximated.
17.4. Examples: np.ndarray type (class)¶
We have already introduced arrays and emphasized their particular benefits in representing mathematical things such as vectors and sequences. In the same section, we investigated some of the NumPy module functions that we can use to create and operate on arrays. Now, let's take a look at the methods and attributes associated with this type (= class).
You can view the entire class help with help(np.ndarray)
,
recalling that np.ndarray
is the type name, whereas np.array
is a function to create an array. Because there is quite a lot of
text, we just show the top plus some highlights to work with here:
1Help on class ndarray in module numpy:
2
3class ndarray(builtins.object)
4 | ndarray(shape, dtype=float, buffer=None, offset=0,
5 | strides=None, order=None)
6 |
7 | An array object represents a multidimensional, homogeneous array
8 | of fixed-size items. An associated data-type object describes the
9 | format of each element in the array (its byte-order, how many bytes it
10 | occupies in memory, whether it is an integer, a floating point number,
11 | or something else, etc.)
12...
13
14 | Attributes
15 | ----------
16 | T : ndarray
17 | Transpose of the array.
18 | data : buffer
19 | The array's elements, in memory.
20 | dtype : dtype object
21 | Describes the format of the elements in the array.
22 | flags : dict
23 | Dictionary containing information related to memory use, e.g.,
24 | 'C_CONTIGUOUS', 'OWNDATA', 'WRITEABLE', etc.
25 | flat : numpy.flatiter object
26 | Flattened version of the array as an iterator. The iterator
27 | allows assignments, e.g., ``x.flat = 3`` (See `ndarray.flat` for
28 | assignment examples; TODO).
29 | imag : ndarray
30 | Imaginary part of the array.
31 | real : ndarray
32 | Real part of the array.
33 | size : int
34 | Number of elements in the array.
35 | itemsize : int
36 | The memory use of each array element in bytes.
37 | nbytes : int
38 | The total number of bytes required to store the array data,
39 | i.e., ``itemsize * size``.
40 | ndim : int
41 | The array's number of dimensions.
42...
Here we see some attributes of the class listed -- that is, internal features inherent in any object we have of this type. We can get the values of these attributes and use them, too, if we like. (NB: some of these will be more familiar than others at this point. That is fine, we just want to get a sense of available ones. Some will also be more useful when we start working with multidimensional arrays.) Observe that attributes are not "function-y" like methods: there no parentheses used with these nor any parameters. These are more like internal variables within an object (like "sub-variables").
For example, let's make the following arrays:
arr = np.arange(-3, 3, 0.2)
brr = np.array([10, -5, 1, 6, 9, -3, 0, 1])
We could find out how many elements there are in the array with
len(arr)
, and what the element datatype is of each with
np.dtype(arr[0])
. Or, from looking at the attribute list, we
could display the following attributes, instead:
print(arr.size)
print(arr.dtype)
... which should produce 30
and float64
, respectively. We
can also see how many bytes are used in each element, and how many
dimensions the array has:
print(arr.itemsize)
print(arr.ndim)
... which outputs 8
(we have 8-byte elements) and 1
(our
vector-like arrays, at present), respectively. These attributes are
all created internally when the object is made, so all np.ndarray
objects have a size
, dtype
, etc. attribute from birth. They
get updated as the properties of the object change; and we can get
their value just like we would from any other variable.
Note
There are also properties in a class called descriptors. We will just consider these a kind of attribute, and we won't explore any separate aspects of them. So, if you see a list of descriptors in a help file, consider using them like attributes.
Taking a look further down in the help(np.ndarray)
docstring, we
see a lot of methods listed, such as:
1 | Methods defined here:
2 |
3 | __abs__(self, /)
4 | abs(self)
5 |
6 | __add__(self, value, /)
7 | Return self+value.
8 |
9...
10
11 | max(...)
12 | a.max(axis=None, out=None, keepdims=False, initial=<no value>, where=True)
13 |
14 | Return the maximum along a given axis.
15 |
16 | Refer to `numpy.amax` for full documentation.
17 |
18 | See Also
19 | --------
20 | numpy.amax : equivalent function
21 |
22 | mean(...)
23 | a.mean(axis=None, dtype=None, out=None, keepdims=False)
24 |
25 | Returns the average of the array elements along given axis.
26 |
27 | Refer to `numpy.mean` for full documentation.
28 |
29 | See Also
30 | --------
31 | numpy.mean : equivalent function
32...
33
34 | sort(...)
35 | a.sort(axis=-1, kind=None, order=None)
36 |
37 | Sort an array in-place. Refer to `numpy.sort` for full documentation.
38...
The names of some of these are pretty self-explanatory, and some are basically duplicates of functions in the NumPy module (even explicitly referring the reader to those functions for more detailed help descriptions).
Thus, to find the max or mean of an array, we could use the following:
arr_max = arr.max()
arr_mean = arr.mean()
... which evaluate to 2.800000000000005
and
-0.09999999999999748
, respectively (note the presence of
roundoff error).
We see also in the list here a method that works "in-place": the
sort()
method. Since the object itself will be changed by this
operation, we have to decide if we want to make a copy to operate on.
We will discuss in a later section that some
care has to be made in copying arrays (and other Python collections);
np.copy
is OK for 1D arrays, but not higher dimensional ones. For
the moment, we can make a copy of the 1D array this way and then sort
elements as follows:
crr = np.copy(brr) # copy array (only for 1D array)
crr.sort() # sort new array in-place
print(brr)
print(crr)
... which produces:
[10 -5 1 6 9 -3 0 1]
[-5 -3 0 1 1 6 9 10]
Q: Look at help(np.ndarray)
. What is the difference
between the argmax
and the max
methods? What would be the
output for the following 1D array?
x = np.array([4.0, -5.1, 2.8, 9.1])
print(x.argmax())
print(x.max())
Q: Earlier, we checked the element dtype of an array by using
the type
function on an element, e.g., type(x[0])
. Above,
we have seen the dtype
attribute within the np.ndarray class
can provide similar information.
Let y = np.array([1.0])
. Check those approaches for getting
the element datatype are similar and different in terms of printing
the result, as well as of checking for equality with the float
and np.float64
types. Does the function- or attribute-based
approach seem more useful?
17.5. Final note¶
As emphasized above, there is a lot of overlap between what methods can do and what functions can do. The main difference between them might be a bit more of a philosophical one. A mathematical function like:
P = f(x, y, z)
... provides a rule to map the three inputs x, y and z to P. In some sense the inputs have equal standing; each might be a coordinate in a separate component. We use the values of each and get some output.
With a method, we always start by focusing on a single object. It has several internal attributes that are inherently part of its class or type. For example, within a string object there are the values of each element, but also information on whether a given character is alphabetic or numeric; uppercase or lowercase; etc. These are examples of the attributes within its class. The actual set of properties that an object has across all attributes at any given instant defines its state. For example, in the following string, each character is alphabetic and each is lowercase:
x = "example"
Methods focus on that single object and its current state, and they
typically either: query the current state (e.g. isalpha
and
islower
check if the string is entirely alphabetic or lowercase,
respectively); or change the current state (e.g., upper
and
capitalize
each affects the upper/lowercase state of various
chars). But at their core, methods are very state-focused, which is
why each is tied to a particular type: each relies on having that
class's attributes known.
And again, there is no strict separation between what functions and methods can do. Many operations can be performed equally by methods or functions; we saw that above for float type objects, for calculating the absolute value of an object or its the Boolean equivalent, etc. Furthermore, both functions and methods can take many additional parameters to support functionality. In the end it is useful to know about each and to use them efficiently.
17.6. Practice¶
Let
a = np.linspace(0,3,10)
. What class isa
an object of? Find a method to round the element values of this array, and use it to make a new arrayb
, which has each of the same values asa
rounded to 2 decimal places.
more coming soon