Skip to content

Instantly share code, notes, and snippets.

@ccritchfield
Last active July 16, 2021 16:50
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ccritchfield/a1f0de906a1ff8d6c503e48d1925cffa to your computer and use it in GitHub Desktop.
Save ccritchfield/a1f0de906a1ff8d6c503e48d1925cffa to your computer and use it in GitHub Desktop.
Python - Object-Oriented Programming Demo
--------------------------------------------
Python Object-Oriented Programming Walk-Thru
--------------------------------------------
.py files that demonstrate Object-Oriented programming
in Python in step-by-step fashion.
Python class in college was mix of undergrads and grads,
because Python was turned into the Intro to Programming
class for undergrads (due to push towards data sci), but
grad students that had intro to prog using Java still
had to go through Python 101 as a prereq to get into
Data Sci and Big Data classes. (College was making a big
push for Python for analytics.)
And, the "undergrad + grad" Python class was taught by
the grad-level Data Sci instructor. So, he blew through
things at an insane rate using Data Sci examples that
left the undergrads struggling.
So, no surprise, he blew through Python OO in one short evening.
There was no book for the class, just prof's slides, which were
very lacking. (EG: just barebones code examples
with hardly any comments).
Undergrads were completely lost, because for most of them it
was their first time learning to program (much less, OO program).
So, I went home, googled up how to do OO in Python, and hammered
out a massive demonstration of it one night to help them out.
These .py files are what I ended up with.
Since I had already gone through Java during my INSY undergrad,
I tried to relate the Python examples to Java examples they'd
see once they got to "Programming II" (which would be their
intro to Java).
Problem is, Java is OO-primary, so it's pretty strict about what
you can do OO-wise in it. Meanwhile, Python seems to have had
OO tacked on, and it sort of over-complicates it a bit and
also lets you make new vars inside an object on-the-fly while
also directly accessing supposedly "hidden" variables.
I tried to let students know that this was pretty unusual
( 07 - Oddities & Weirdness ).
######################################
"""
Object-Oriented Programming
Best, short vid explanation I could find...
https://www.youtube.com/watch?v=pTB0EiLXUC8
--------------------------------------------
A bit more on Polymorphism & Abstraction...
https://www.youtube.com/watch?v=L1-zCdrx8Lk
(the vid may just confuse you more w/o a code example, but...)
The power of OO programming comes into play
when dealing with large systems and complex
stuff happening behind the scenes.
Let's say we're making a data reporting system.
It has 2 parts...
1) GUI that shows data
2) middle-ware that pulls data from various databases to pass to GUI
The data we're pulling can come from
SQL Server, MS Access, Oracle, Dbase, etc databases.
Each of those require different
ways to connect to them.
When the end-user fires up the
GUI, they have a dropdown to
select the data source. Then
they click "execute" and it
connects to the data source.
We could... code the GUI
front-end to have a massive
if/else to determine what
connection type we're doing
and how to execute it.
... OR ...
we can create a "connection"
object as a super-class,
and it has an empty method in
it called "makeConnection()"
All this means is that any
sub-class we make using the
connection blue-print must
have a method in it called
"makeConnection()" .. and
each sub-class can implement
it in their own way.
So... we start making sub-classes...
connection.sqlserverconn.makeConnection(){however it handles it}
connection.msaccessconn.makeConnection(){however it handles it}
connection.oracleconn.makeConnection(){however it handles it}
connection.dbaseconn.makeConnection(){however it handles it}
So, the super-class gives
the template sub-classes
have to stick to, but the
sub-classes actually implement
the code logic of how to
make and handle the connection
to the specific data source.
When these objects are kicked
off, we can still refer to them
as "connection' objects, b/c they
inherit and are encapsulated by the
super-class 'connection'.
So, the developer working on
the GUI front-end can do something like...
//---------------------------------
// create generic connection object
// but use it to encapsulate specific
// sub-connection type user chose
//---------------------------------
Connection conn = Connection(dropdownbox.selection)
// kick off the connection.. we don't
// care about the specifics, b/c the
// sub-class handles that stuff
conn.makeConnection()
//---------------------------------
The back-end developer
will be the one coding all
the connection objects,
and they can make more
without it impacting the
front-end developer,
b/c the connection object
acts as an abstraction bridge
between the front-end and
back-end.
The front-end guy doesn't
care how the connection
sub-classes actually
do the connection.
And, he probably doesn't
care how many different
sub-connection types there are.
EG:
When the GUI starts up,
he'll simply have some
code iterate over all
the sub-connection types
to create a dropdown
that an end-user can pick
from.. so the front-end
code never has to get
modified if the back-end
guy adds more connection
sub-classes (eg: adds
a new one to handle Oracle
databases.)
And.. let's say a new version
of SQL Server rolls out, and it
totally changes how the connection
needs to be handled.
The back-end developer simply
updates the SQL Server connection
sub-object code, and there is
no need to update the
front-end code. B/c the code
logic is stored in the bakc-end
object instead of in the front-end.
So, it creates modular code
that's easier to maintain
without having to go "oops,
I think we need to update
fifteen systems if we make
this change in this one system."
And, it can create a generic
"bridge" between systems to
let multiple developers work
on various parts all at once
without having to know the
exact details of what's going
on in another area.
The GUI guy can code his GUI
to use connection object
without needing to know the
details of what's going on
in the back-end.. as long
as all the connection sub-objects
follow the connection super-class
template the back-end guy and
front-end guy agreed upon and
are coding their respective
pieces to use as the bridge.
You'll get into more on
this in Java b/c it's
more OO. And (if you go
into masters INSY developer
path) you'll have Adv Systems
Design class that really
looks at leveraging abstraction,
SOLID principles, etc in
design patterns developed
by Gang of Four to turn complex
systems into simple, modular things
that are easy to modify.
"""
######################################
######################################
"""
---------------------------------
Objects have ...
* properties (attributes, fields ... containers that can hold data relevant to object)
* methods (actions, functions ... things that can do stuff, usually with the object's stuff)
---------------------------------
The properties of the object are data that would be unique
to the object to do it's job.
---------------------------------
Methods are whatever functions would help the object do it's job.
Basic methods most objects have are...
constructors .. used to kick off and make new instances of object
setters (mutators) .. used to set (mutate / change) the properties / attributes of the object
getters (accessors) .. used to get (access / return) the value of props / attrs of the object
"stringify" .. used to show a summary of the object.. what props it has and what they're set to
in Java it's "object.toString". In Python it let's you do "print(object)".
---------------------------------
We'll talk about this stuff more, but for now let's kick off
an example...
UTA Java classes use Customer and Address a lot, so
let's create those in Python....
"""
########################################
########################################
# US Postal Address object
########################################
class AddressTest01:
# constructor ... default & main
def __init__( self, street = "", apt_po = "", city = "", state = "", zipcode = "" ):
self.street = street
self.apt_po = apt_po
self.city = city
self.state = state
self.zipcode = zipcode
########################################
print("-" * 50)
print("AddressTest01 Test...")
#-------------------------------
# create new address instance using default values
#-------------------------------
a1 = AddressTest01()
# ... THEN set the address instance properties
a1.street = "1313 13th str."
a1.apt_po = "13"
a1.city = "portland"
a1.state = "OR"
a1.zipcode = "12345"
msg = ""
msg += "street = " + a1.street + "\n"
msg += "apt_po = " + a1.apt_po + "\n"
msg += "city = " + a1.city + "\n"
msg += "state = " + a1.state + "\n"
msg += "zipcode = " + a1.zipcode
print("-" * 50)
print("a1 ...\n" + msg)
#-------------------------------
# now we'll do by first creating vars to store inputs
# (eg: these could represent inputs from a user that
# we snag first before making the object).
#-------------------------------
ad = "100 n. lane"
po = "po box 6578"
ct = "dallas"
st = "tx"
zp = "75123"
# ... now create new address object by passing in our arguments
a2 = AddressTest01( ad, po, ct, st, zp)
msg = ""
msg += "street = " + a2.street + "\n"
msg += "apt_po = " + a2.apt_po + "\n"
msg += "city = " + a2.city + "\n"
msg += "state = " + a2.state + "\n"
msg += "zipcode = " + a2.zipcode
print("-" * 50)
print("a2 ...\n" + msg)
##################################
"""
In Java, there's usually 2 constructors:
1) default / empty .. let's you create a new instance
of the object using default values built into
the object. This is useful for when you want to load
an object into memory, and worry about populating
it's values later.
2) main ... pass values directly as arguments when
you create the object.
There's pro's and con's to doing it either way, and it
depends on what you need to do in your application.
(eg: in advanced design principles, you can create a default
object in memory, and then do shallow or deep copies of it
to "stamp out parts / clone" more objects from it... perhaps
tying back to the original's properties, so if the original's
properties are change the cloned objects reflect it, too,
or to where the clones have their own properties. EG: if
you had enemies in a game all with the same graphic model,
you cuold create an enemy object in memory with the graphic
assigned, then clone enemies off it, but have them track
their own damage. This wuold help reduce memory load,
since common properties are tracked by the original
object and the clones only track what's needed on their own.)
Maybe you need to gather a bunch of info from user,
and do a bunch of stuff with it before making an
object that stores that info.
Maybe you want to cut down on memory usage, so you
want to create the object first, since it essentially
contains variables to hold the data you want, and you
can then just populate the data directly instead of
creating middle-man variables to catch the data then
pass it to the object's properties.
It depends on what you need to do in your applications.
Also, in Java, you have properties outside of the constructor
methods.
////////////////////////////////
public class Account
{
// private / internal / hidden vars
private String name;
private double balance;
//----------------------------------------
// constructors (overloading method)
//----------------------------------------
// overloading just means we use the same
// function name multiple times, and the compiler
// knows which to use when called based on
// the arguments passed inside the function call.
//
// Python doesn't have overloading, but most C-style
// languages do.
//----------------------------------------
// so if we call "Account a = Account()" it has
// no arguments passed, it knows to use default/empty here
public Account() // if no vals are set
{
name = "(none)";
balance = 0.0;
}
// if we call "Account a = Account("bob", 123.45)" it has
// arguments passed, it knows to use main here
public Account(String n, double b) // if users pass values
{
name = n;
balance = b;
}
}
////////////////////////////////
----------------------------------------
In Python, __init__ acts as both a constructor method,
and can contain the properties that the object has.
(But, we'll change that here in a moment.)
Plus, since you can set the parameters of __init__
to default values, you can use __init__ as both
a main and default/empty constructor.
----------------------------------------
"""
########################################
########################################
"""
It's a pain to have to create a special
output variable to string together all the
object properties whenever we want to get
a summary of them.
So, OO languages often have a "stringify"
object method ... which just lets you
dump a summary .. what vars it has,
what they're set to.. maybe any extra
info a user (usually a programmer)
would want to know when using the
variable.
in Java you create toString() method.
In Python you have __str__
(note that it's 2 double-scores before
and after. There's a __str___
that has 2 double-scores before
and 3 after, but that returns
the object's memory address.)
If you try to print(object)
without having the __str__
made, then you'll just
get the object memory address
spit out at you.
So, it's good to setup the __str__
method so you can dictate what
the object shows when printed.
"""
###################################
class AddressTest02:
# constructor ... default & main
def __init__( self, street = "", apt_po = "", city = "", state = "", zipcode = "" ):
self.street = street
self.apt_po = apt_po
self.city = city
self.state = state
self.zipcode = zipcode
# print(object) ("stringify" object summary)
def __str__( self ):
msg = ""
msg += "street = " + self.street + "\n"
msg += "apt_po = " + self.apt_po + "\n"
msg += "city = " + self.city + "\n"
msg += "state = " + self.state + "\n"
msg += "zipcode = " + self.zipcode
return msg
######################################
print("-" * 50)
print("AddressTest02 Test...")
# create new address instance using default values
a1 = AddressTest02()
# set the address instance properties
a1.street = "1313 13th str."
a1.apt_po = "13"
a1.city = "portland"
a1.state = "OR"
a1.zipcode = "12345"
# now we can get rid of all of this stuff...
"""
msg = ""
msg += "street = " + a1.street + "\n"
msg += "apt_po = " + a1.apt_po + "\n"
msg += "city = " + a1.city + "\n"
msg += "state = " + a1.state + "\n"
msg += "zipcode = " + a1.zipcode
"""
# and replace it with this...
print("-" * 50)
print("a1 ...")
print(a1)
# create vars to store inputs we'll pass
# in creating new Address object
# these can get changed to user inputs,
# or could be values passed in from another program
ad = "100 n. lane"
po = "po box 6578"
ct = "dallas"
st = "tx"
zp = "75123"
# create new address object by passing in our arguments
a2 = AddressTest02( ad, po, ct, st, zp)
# ditto
"""
msg = ""
msg += "street = " + a2.street + "\n"
msg += "apt_po = " + a2.apt_po + "\n"
msg += "city = " + a2.city + "\n"
msg += "state = " + a2.state + "\n"
msg += "zipcode = " + a2.zipcode
"""
# since our object is not a string,
# if we want to concat it with another
# string we have to convert it to a string
print("-" * 50)
print("a2 ...\n" + str(a2))
######################################
"""
Our address object as-is is "open by default",
which is bad.. it means anyone external can
directly access the internal attributes of
the object. So, someone can do the following...
print("-" * 50)
a1.state = "Who Cares"
print("a1.state = " + a1.state)
print("oops... we don't want folks setting that as a state")
----------------------------------------
We want to avoid people directly accessing our object
attributes, b/c WE want to control what this object
can accept for properties, and perhaps hide some
special hidden properties within the object for
internal use that nothing else should see.
This is where setters and getters come in
handy, b/c they provide gateways to the
object, but lets the object developer
control the flow of access.
So, we need to make these properties private,
and create setters and getters to access them.
In Java, attributes you create inside
a class are considered "closed by default"
and automatically hidden. You create setters
to filter inputs to make sure they're ok
for what the object's property is needing,
and getters to show folks the value if it's
called. Once you make setters, you often
go back to your constructor and force
it to use the setters instead of allowing
the constructor to set values directly
when called....
eg:
//////////////////////////////////
public class Account
{
// vars
private String name;
private double balance;
//----------------------------------------
// constructors (overloading method)
//----------------------------------------
// with setters created, we now use
// them in the constructor to ensure
// data passed in goes through any
// validation / scrubbing necessary
// in the setters.
// this object has no scrubbing/validation,
// but is still setup just in case we add it later.
//----------------------------------------
public Account() // if no vals are set
{
setName("(none)");
setBalance(0.0);
}
public Account(String n, double b) // if users pass values
{
setName(n);
setBalance(b);
}
//----------------------------------------
// setters
//----------------------------------------
public void setName(String name) {this.name = name;}
public void setBalance(double balance) {this.balance = balance;}
//----------------------------------------
// getters
//----------------------------------------
public String getName() {return name;}
public double getBalance() {return balance;}
//----------------------------------------
// other
//----------------------------------------
public String toString() {return("Name " + name + ", Balance " + balance);}
}
//////////////////////////////////
But, in Python, they have a "everyone's
an adult here" philosophy that means
it's assumed everyone working in the
Python code knows what they're doing
and not trying to screw it up on purpose.
But, we can still privatize the attributes
and create getters and setters for them,
so we have more control over what can be
seen and what can be done with them.
The problem in doing so is that people
may have previous code that went...
a1.street = "blah"
If we change our object to now
force people to access the properties
via getters / setters, it will break
all code that was coded to access
them directly...
b/c...
a.street = "blah" # setting street directly
...will fail, and now have to get recoded as...
a.set_street("blah") # using a function to set street
So, Python uses an alternate method that
abstracts (hides) the setter/getter
functionality by making it seem
just like property manipulation directly
while still using functions...
You can read more about this here...
https://www.programiz.com/python-programming/property
But, for our Address example it's done like so...
"""
################################
class AddressTest03:
#------------------------------------
# constructor
#------------------------------------
# we use the self.property same as before,
# but since we defined @property.setter
# functions further below, what this is
# doing now is calling those setter functions
# to pass them the values we're trying to set
# properties to, and is now using hidden
# __property variables to store things in
# that only this object can see internally.
def __init__( self, street = "", apt_po = "", city = "", state = "", zipcode = "" ):
self.street = street # same as self.street(street)
self.apt_po = apt_po # same as self.apt_no(apt_po)
self.city = city # etc
self.state = state
self.zipcode = zipcode
#------------------------------------
# getters
#------------------------------------
# for each property, we create getters that
# are used for returning the property's value
# when called in AddressTest03.street fashion.
# this lets us control what properties outside
# parties (code, developers, etc) can see
# and how the properties are returned.
# (eg: we may store a date as a string, but
# convert it to a date when it's called)
#------------------------------------
@property
def street(self):
return self.__street
@property
def apt_po(self):
return self.__apt_po
@property
def city(self):
return self.__city
@property
def state(self):
return self.__state
@property
def zipcode(self):
return self.__zipcode
#------------------------------------
# setters
#------------------------------------
# for each property, we create a setter
# to allow us to control what values are
# allowed and how they are transformed
# for storage. But, they are referred
# to using the normal property name
# (eg: object.street). What they're
# really doing is creating and using
# hidden properties that only the
# object can see internally (eg:
# self.street(value) is setting
# the value to self.__street property
# behind the scenese now, b/c
# self.street is now a function
# instead of a property.
#------------------------------------
@street.setter
def street( self, street ):
self.__street = street
@apt_po.setter
def apt_po( self, apt_po ):
self.__apt_po = apt_po
@city.setter
def city( self, city ):
self.__city = city
@state.setter
def state( self, state ):
self.__state = state
@zipcode.setter
def zipcode( self, zipcode ):
self.__zipcode = zipcode
#------------------------------------
# stringify / toString / print(object)
#------------------------------------
def __str__( self ):
msg = ""
msg += "street = " + self.street + "\n"
msg += "apt_po = " + self.apt_po + "\n"
msg += "city = " + self.city + "\n"
msg += "state = " + self.state + "\n"
msg += "zipcode = " + self.zipcode
return msg
######################################
"""
what we did was turn the normal property names
(street, city, etc) into setter function calls,
and the real properties now have double-underscore
in front of them and hidden inside the object
only accessible from setters and getters.
but, anyone using our object doesn't know that.
.. that extra middle layer of setting and getting
is abstraction, and hides it. And, nobody is
the wiser, because a previous call of...
a1.street = "blah"
...still works.. it's just this time instead
of setting a property "street" directly,
it's passing the value through the setter.
same for the getter when someone does...
print(a1.street)
.. works just the same as before, but is
now passing through the getter middle layer.
"""
###############################################
#---------------------------------------------
# let's test it out
#---------------------------------------------
# same code as before, but using AddressTest03 instead of AddressTest02
print("-" * 50)
print("AddressTest03 Test...")
# create new address instance using default values
a1 = AddressTest03()
# set the address instance properties
a1.street = "1313 13th str."
a1.apt_po = "13"
a1.city = "portland"
a1.state = "OR"
a1.zipcode = "12345"
# check object summary
print("-" * 50)
print("a1 ...")
print(a1)
# create vars to store inputs we'll pass
# in creating new Address object
# these can get changed to user inputs,
# or could be values passed in from another program
ad = "100 n. lane"
po = "po box 6578"
ct = "dallas"
st = "tx"
zp = "75123"
# create new address object by passing in our arguments
a2 = AddressTest03( ad, po, ct, st, zp)
# check object summary
print("-" * 50)
print("a2 ...")
print(a2)
print("-" * 50)
print("everything works the same, even though we're using setters / getters now")
################################
"""
Now that we have the template of setters / getters
setup, we can leverage them.
EG: We can create a hidden / private / internal-only
attribute that contains US states, and use
that in the state setter to double-check its
validity before acceptance.
"""
################################
class AddressTest04:
#------------------------------------
# internal constants
#------------------------------------
# setup state validation as tuple (immutable / unchageable)
__STATE_VALIDATION = ('AL','AK','AZ','AR','CA','CO','CT',
'DE','FL','GA','HI','ID','IL','IN',
'IA','KS','KY','LA','ME','MD','MA',
'MI','MN','MS','MO','MT','NE','NV',
'NH','NJ','NM','NY','NC','ND','OH',
'OK','OR','PA','RI','SC','SD','TN',
'TX','UT','VT','VA','WA','WV','WI',
'WY','') # we added '' since '' is our valid default value
#------------------------------------
# constructor
#------------------------------------
# self.property is calling setters below,
# so the values passed in as arguments, or the default
# values set in __init__ get passed into setters for
# validation, manipulation, etc before getting set
# to hidden properties behind the scenes.
def __init__( self, street = "", apt_po = "", city = "", state = "", zipcode = "" ):
self.street = street
self.apt_po = apt_po
self.city = city
self.state = state
self.zipcode = zipcode
#------------------------------------
# getters
#------------------------------------
@property
def street(self):
return self.__street
@property
def apt_po(self):
return self.__apt_po
@property
def city(self):
return self.__city
@property
def state(self):
return self.__state
@property
def zipcode(self):
return self.__zipcode
#------------------------------------
# setters
#------------------------------------
# we're going to standardize inputs
# to all uppercase, so we run each through
# str.upper() to convert them in their setters
# before storing them.
#
# While user can enter inputs in any way
# (upper, lower, mix), often data systems
# will store them in upper in order to
# try to speed up comparisons... it's
# faster when comparing uppers only
# eg: DOG = DOG
# instead of having to do mixed comparison
# eg: DOG can be Dog, dog, dOg, etc, etc.
#
# This was mostly used back in the old
# days to try to speed up data searches.
# some systems still use it as a hold-over
# and I'm just using it here to show
# how we can now use the setters to
# manipulate the inputs before storing
# them in our __property vars
@street.setter
def street( self, value ):
self.__street = str.upper(value)
@apt_po.setter
def apt_po( self, value ):
self.__apt_po = str.upper(value)
@city.setter
def city( self, value ):
self.__city = str.upper(value)
# we're also validating state
# input before accepting it.
# since our __STATE_VALIDATION list
# stores the state abbr's in uppercase
# we have to uppercase input before
# validating that, too.
@state.setter
def state( self, value ):
st = str.upper(value)
# if input is in validation list
# (we're calling the hidden validation
# list directly instead of using a getter)...
if st in self.__STATE_VALIDATION:
# accept it
self.__state = st
else:
# print statement just gives us feedback for debug.
# in a real-world scenario we may return an exception
# to the calling code or something to let them
# know they borked up and they need to correct
# it on their end and try again, or maybe
# we make our object code more robust to
# switch to a better default (eg: by looking
# up zip code and determing state from that).
print("-" * 50)
print("invalid state ... reverting to default of ''")
self.__state = "" # set to default value
# we don't care about uppercase'ing zip
# codes since they're numbers, but we store
# them as strings since they could be
# 00000-0000 format.
#
# later, we could hook this object
# into a zipcode validation system
# that would double-check to see
# if the zip is valid, and possibly
# auto-populate city / state from
# that system as well.
#
# if we did that, then we would only
# allow things to view (get) the state
# and city, but not set them, b/c
# we'd only allow city/state to get
# change by setting a new zip.
# thus we'd shut off the setters
# for the city/state then.
#
# But, for now, still bare-bones.
@zipcode.setter
def zipcode( self, value ):
self.__zipcode = value
#------------------------------------
# stringify / toString / print(object)
#------------------------------------
# since we control the stringify output
# we can prevent others from seeing our
# __STATE_VALIDATION list ...
# or, we can let them see it if we want...
# depends on how much we want them to see
def __str__( self ):
# each of the self.property calls below
# will call the @property function def
# for the respective property, returning
# whatever is in that function (which,
# for all of them currently, is just their
# double-underscore private property value).
msg = ""
msg += "street = " + self.street + "\n"
msg += "apt_po = " + self.apt_po + "\n"
msg += "city = " + self.city + "\n"
msg += "state = " + self.state + "\n"
msg += "zipcode = " + self.zipcode
return msg
######################################
# again, everything we coded previously
# should work as usual...just using this new object
print("-" * 50)
print("AddressTest04 Test...")
# create new address instance using default values
a1 = AddressTest04()
# set the address instance properties
a1.street = "1313 13th str."
a1.apt_po = "13"
a1.city = "portland"
a1.state = "OR"
a1.zipcode = "12345"
# check object summary
print("-" * 50)
print("a1 ...")
print(a1)
# create vars to store inputs we'll pass
# in creating new Address object
# these can get changed to user inputs,
# or could be values passed in from another program
ad = "100 n. lane"
po = "po box 6578"
ct = "dallas"
st = "tx"
zp = "75123"
# create new address object by passing in our arguments
a2 = AddressTest04( ad, po, ct, st, zp)
# check object summary
print("-" * 50)
print("a2 ...")
print(a2)
#-----------------------------------
# now let's try inputting invalid state
#-----------------------------------
ad = "14409 Hampton Dr."
po = "" # we're going to treat this like a home addy, not an apt/po
ct = "Detroit"
st = "NeverNeverLand" # invalid state input
zp = "12345-6789"
# create new address object by passing in our arguments
a3 = AddressTest04( ad, po, ct, st, zp)
# check object summary
print("-" * 50)
print("a3 ...")
print(a3)
#########################################
"""
We've got a pretty good Address object designed...
So, we stripped out a lot of the comments bloat,
and created a separate import file out of it...
"""
#########################################
# we can import the Address object via...
#
# 1) importing the entire object file:
#
#import object_example_address
#
# 2) import specific stuff from it:
#
from object_example_address import Address
# we could also import the whole file by
# sending it * as a wild card (* = all)
# from object_example_address import * # everything
###########################
"""
We can use objects within objects...
So, we can use the Address object within a Customer object...
"""
###########################
class Customer01:
#------------------------------------
# constructor
#------------------------------------
# property = default
def __init__( self, id = "",
fname = "",
lname = "",
address = Address()):
#--------------------------------
self.id = id
self.fname = fname
self.lname = lname
self.address = address
#--------------------------------
#------------------------------------
# getters
#------------------------------------
@property
def id(self):
return self.__id
@property
def fname(self):
return self.__fname
@property
def lname(self):
return self.__lname
@property
def address(self):
return self.__address
#------------------------------------
# setters
#------------------------------------
# modification ... string inputs to uppercase
# (address already u-cases itself internally)
@id.setter
def id( self, value ):
self.__id = value # int
@fname.setter
def fname( self, value ):
self.__fname = str.upper(value)
@lname.setter
def lname( self, value ):
self.__lname = str.upper(value)
@address.setter
def address( self, value ):
self.__address = value # object
#------------------------------------
# stringify / toString / print(object)
#------------------------------------
def __str__( self ):
msg = ""
msg += "id = " + str(self.id) + "\n"
msg += "fname = " + self.fname + "\n"
msg += "lname = " + self.lname + "\n"
# address object already has it's own stringify
# so all we have to do is ...
msg += str(self.address)
return msg
###########################
"""
So.. we can create a customer in a few ways...
"""
###########################
#--------------------------------
# 1 ) create an address first, then use it to create a customer
#--------------------------------
addy = Address("1001 UTA dr.", "465", "Arlington", "TX", "78960")
# check object summary
print("-" * 50)
print("addy ...")
print(addy)
# I was watching Fast and Furious earlier today...
cust1 = Customer01(12345, "Paul", "Walker", addy)
# check object summary
print("-" * 50)
print("cust1 ...")
print(cust1)
#--------------------------------
# 2 ) create blank customer and fill in values after
#--------------------------------
# make default
cust2 = Customer01()
cust2.id = 65656
cust2.fname = "Dom"
cust2.lname = "Torretto"
# when having to drill down into something to use
# it for a while, it's good to create a var
# directly referencing it...
# EG: we could have...
"""
cust2.address.street = "1111 UTA"
cust2.address.apt_po = "234"
cust2.address.city = "Arlington"
cust2.address.state = "TX"
cust2.address.zipcode = "78901"
"""
# .. or we can do ..
ca = cust2.address
ca.street = "1111 UTA"
ca.apt_po = "234"
ca.city = "Arlington"
ca.state = "TX"
ca.zipcode = "78901"
# check object summary
print("-" * 50)
print("cust2 ...")
print(cust2)
#--------------------------------
# 3 ) pass everything in arguments all at once on creation
#--------------------------------
# since address is an object, we create an Address object
# as an argument in the Customer constructor call and
# pass the Address the arguments
cust3 = Customer01( 34566, "John", "Smith",
Address("6565 Happy Dr.", "", "Seattle", "WA", "45345")
)
# check object summary
print("-" * 50)
print("cust3 ...")
print(cust3)
#--------------------------------
# 4 ) some combination there-of
#--------------------------------
# (not going to do an example, you get the idea)
###########################
"""
So we've got these objects using objects,
(yo dawg, I heard you like objects ...)
but we can make objects also have
extracurricular functions in them.
Let's say the Customer is an object
in a store interface, and they've
racked up charges. Now we need
to total those charges.
"""
###########################
# import the address object
from object_example_address import Address
###########################
class Customer:
#------------------------------------
# constructor
#------------------------------------
# property = default
def __init__( self, id = 0,
fname = "",
lname = "",
address = Address(),
charges = None):
#--------------------------------
self.id = id
self.fname = fname
self.lname = lname
self.address = address
self.charges = charges # this will be a list
#--------------------------------
#------------------------------------
# getters
#------------------------------------
@property
def id(self):
return self.__id
@property
def fname(self):
return self.__fname
@property
def lname(self):
return self.__lname
@property
def address(self):
return self.__address
@property
def charges(self):
return self.__charges
#------------------------------------
# setters
#------------------------------------
# modification ... string inputs to uppercase
# (address already u-cases itself internally)
@id.setter
def id( self, value ):
self.__id = value # int
@fname.setter
def fname( self, value ):
self.__fname = str.upper(value)
@lname.setter
def lname( self, value ):
self.__lname = str.upper(value)
@address.setter
def address( self, value ):
self.__address = value # object
@charges.setter
def charges( self, value ):
self.__charges = value # list
#------------------------------------
# other functions
#------------------------------------
# Total Charges (sums list of charges)
# very simply function.. just sums charges
# we don't want to print or anything else,
# we'll let other functions (like the __str__)
# handle what they want to do with the float
# sum we return
def totalCharges( self ):
return sum(self.charges)
#------------------------------------
# stringify / toString / print(object)
#------------------------------------
def __str__( self ):
msg = ""
msg += "id = " + str(self.id) + "\n"
msg += "fname = " + self.fname + "\n"
msg += "lname = " + self.lname + "\n"
# address object already has it's own stringify
# so all we have to do is ...
msg += str(self.address) + "\n"
msg += "charges = " + str(self.charges) + "\n"
msg += "tot chgs = " + str(self.totalCharges())
return msg
#############################
# UTA student... look at those hundred dollar problems.
# must be buying books at the UTA bookstore.
chgs = [ 150.12, 211.00, 350.95 ]
addy = Address("1001 UTA dr.", "465", "Arlington", "TX", "78960")
cust = Customer(6578, "Vin", "Diesel", addy, chgs)
# check object summary
print("-" * 50)
print("cust ...")
print(cust)
###########################
# import the address object
from object_example_address import Address
# import the customer object
from object_example_customer import Customer
"""
if we didn't need to work with Address objects directly,
all we'd need to do is import the Customer object,
b/c it already imports the Address object.
In fact, if you comment out the Address object import,
the code below all still runs. But, the Spyder IDE
will give you a warning on the addy variable
saying Address is undefined (doesn't know what
it is). But, when you run the code, it seems
to figure it out by looking at the customer
object import.
However, it's usually safer to just import the
objects and things you need diretly in your code
if you use them instead of relying on other
objects chaining together imports. As something
gets update, a developer could change the
customer object to no longer need address
object import, so just importing customer
object would then break the addy variable
trying to make an Address object instantiation.
So, better safe then sorry.
"""
###########################
# prep from last time
chgs = [ 150.12, 211.00, 350.95 ]
addy = Address("1001 UTA dr.", "465", "Arlington", "TX", "78960")
cust = Customer(6578, "Vin", "Diesel", addy, chgs)
# check object summary
print("-" * 50)
print("cust ...")
print(cust)
#############################
"""
So... some weird Python behaviour to note...
"""
#############################
"""
we can look at individual cust properties,
b/c we created setters and getters for that.
"""
print("-" * 50)
print("cust.fname = " + cust.fname)
"""
but, in the Spyder IDE, you can see
the hidden / private / internal properties
in the Customer object model...
eg: you type cust. and it shows
the intellisense dropdown and in
the list you see ... __fname, __lname, etc
but, when you try to access them, you get an error...
'Customer' object has no attribute '__fname'
"""
# uncomment line below to see error msg
#print("cust.__fname = " + cust.__fname)
#-----------------------------
"""
But, what if we do this...
"""
#-----------------------------
cust.__id = 1
cust.__fname = "dog"
cust.__lname = "cat"
# check object summary
print("-" * 50)
print("cust ...")
print(cust)
print("-" * 50)
print("cust shows same info as before...")
print("-" * 50)
print("But now we can print these things and get this...!!!")
print("cust.__id = " + str(cust.__id))
print("cust.__fname = " + cust.__fname)
print("cust.__lname = " + cust.__lname)
#----------------------------
"""
What the heck is going on?!?!?
We tried to do print(cust.__fname) the first
time and we got an error. Then we set it
and now we can print it.
Instantiated objects have a dictionary that
contains the property names and values.
We can print it to see what's going on...
"""
#----------------------------
print("-" * 50)
print("we can look at the insantiated object's hidden stuff" + "\n" +
"by looking at the hidden __dict__ object.. a dictionary" + "\n" +
"in the object that stores all it's properties and methods...")
print("-" * 50)
print(cust.__dict__)
print()
print("... python created new, different vars in the cust object w/ same names as private ones on-the-fly... ugh")
"""
Notice the output...
{
'_Customer__id': 6578,
'_Customer__fname': 'VIN',
'_Customer__lname': 'DIESEL',
'_Customer__address': <__main__.Address object at 0x000002CABF90D9B0>,
'_Customer__charges': [150.12, 211.0, 350.95],
'__id': 1,
'__fname': 'dog',
'__lname': 'cat'
}
anything starting with _Customer is valid... it's
valid private properties in the object.
The last 3 items...
'__id': 1,
'__fname': 'dog',
'__lname': 'cat'
... they didn't exist until we implicitly created them
just a moment ago when we did...
cust.__id = 1
cust.__fname = "dog"
cust.__lname = "cat"
If you come from other languages like Java, C++, C#, Visual Basic, etc,
most of them force you to explicitly declare variables before you
can use them.
In Python, it creates them on-the-fly... even making new
ones in the object that didn't exist in the object
template!
The Customer object template itself doesn't have those
new BS variables we created. Only the cust instantiated
object.
So, we don't have direct access to the private variables
unless we did something like ...
"""
cust._Customer__id = 1
print("-" * 50)
print(cust.__dict__)
print()
print("... we finally got access to a private property, but had to work-around the name mangling.. if someone is doing that to get around the property privacy, then you need to have a chat with them about how they're NOT supposed to do that")
"""
Note how in the __dict__ output the _Customer__id is now 1
But, we had to bend over backwards to do that.
So, the private variables are private unless you bend over
backwards to break the name mangling that Python does
to hide them.
AND...
Python will be more then happy to create new
variables in your object on-the-fly, even ones with the
same names as your private variables! Which can make
you think you have direct access to them .. but you
don't.
Honestly.. the Spyder IDE shouldn't show the private
__prop variables in the first place, b/c that's
what causes some confusion of thinking you still
have direct access to them. So, it could be an IDE
thing.
But, it's just interesting to note.
So, if you're code's not doing what you think it
should, be sure to double-check your spelling.
Because you could end up doing something like...
"""
# update list of customer charges
chgs = [10, 20, 30, 40]
# apply to customer
cust.chargs = chgs
"""
and you think you're setting the customer's internal charges
list to the chgs list you just updated.
Then you total up the customer charges and see...
"""
print("-" * 50)
print("total charges = " + str( cust.totalCharges() ) )
print()
print("... uh ... " + str(chgs) + " = " + str(sum(chgs)) + " not 712 .. wth")
"""
.. well crap
what you really did was create a NEW variable called
"chargs".. since you misspelled the word "charges".
"""
print("-" * 50)
print(cust.__dict__)
print()
print("...chgs was applied to a newly created var 'chargs' instead of 'charges'... awesome. smh")
"""
---------------------------------
{
'_Customer__id': 1,
'_Customer__fname': 'VIN',
'_Customer__lname': 'DIESEL',
'_Customer__address': <object_example_address.Address object at 0x000002CABF9C2898>,
'_Customer__charges': [150.12, 211.0, 350.95],
'__id': 1,
'__fname': 'dog',
'__lname': 'cat',
'chargs': [150.12, 211.0, 350.95]
}
... chargs ... *sigh*
---------------------------------
Most other programming languages would throw back an error
complaining that you haven't first declared the variable
"chargs"
... but not Python.
Even in Visual Basic.. where it lets you create undeclared
vars on-the-fly.. it has an "Option Explicit" flag to force
you to declare vars explicitly before you can use them and
not just make them secretly / implicitly on-the-fly
... but not Python.
So... it's important to point out some oddities and nuances
that may throw you for a loop if you encountered them
w/o realizing what was going on.
"""
################################
"""
You can get rid of an erroneously made property / attribute
by deleting it.. but, since most of our code will stop running
after it does what it does, and/or you've bug checked it and
will stop it running to go back and correct an erroneous
part of code that's kicking off new vars... it's pretty
pointless right now. But, useful to know just in case.
"""
del cust.__id
del cust.__fname
del cust.__lname
del cust.chargs
cust.charges = chgs
cust.id = 6578
print("-" * 50)
print(cust.__dict__)
print()
print("...purged bogus variables, id back to original value, and updated charges properly")
# check object summary
print("-" * 50)
print("cust ...")
print(cust)
##############################
"""
Our Address Object after we've got it sorted out.
We can import it going forward to reduce code bloat.
"""
##############################
class Address:
#------------------------------------
# internal constants
#------------------------------------
# setup private state validation as tuple (immutable / unchageable)
__STATE_VALIDATION = ('AL','AK','AZ','AR','CA','CO','CT',
'DE','FL','GA','HI','ID','IL','IN',
'IA','KS','KY','LA','ME','MD','MA',
'MI','MN','MS','MO','MT','NE','NV',
'NH','NJ','NM','NY','NC','ND','OH',
'OK','OR','PA','RI','SC','SD','TN',
'TX','UT','VT','VA','WA','WV','WI',
'WY','') # we added '' since '' is our valid default value
#------------------------------------
# constructor
#------------------------------------
def __init__( self, street = "", apt_po = "",
city = "", state = "", zipcode = "" ):
self.street = street
self.apt_po = apt_po
self.city = city
self.state = state
self.zipcode = zipcode
#------------------------------------
# getters
#------------------------------------
@property
def street(self):
return self.__street
@property
def apt_po(self):
return self.__apt_po
@property
def city(self):
return self.__city
@property
def state(self):
return self.__state
@property
def zipcode(self):
return self.__zipcode
#------------------------------------
# setters
#------------------------------------
# modification ... all inputs to uppercase
@street.setter
def street( self, value ):
self.__street = str.upper(value)
@apt_po.setter
def apt_po( self, value ):
self.__apt_po = str.upper(value)
@city.setter
def city( self, value ):
self.__city = str.upper(value)
# validate state input
@state.setter
def state( self, value ):
st = str.upper(value)
if st in self.__STATE_VALIDATION:
# accept it
self.__state = st
else:
self.__state = "" # set to default value
@zipcode.setter
def zipcode( self, value ):
self.__zipcode = value
#------------------------------------
# stringify / toString / print(object)
#------------------------------------
def __str__( self ):
msg = ""
msg += "street = " + self.street + "\n"
msg += "apt_po = " + self.apt_po + "\n"
msg += "city = " + self.city + "\n"
msg += "state = " + self.state + "\n"
msg += "zipcode = " + self.zipcode
return msg
######################################
# setup up some debug / testing code
# that will only run if this file
# is active as "main"
######################################
if __name__ == "__main__":
BORDER_DASAH = "-" * 75
print(BORDER_DASAH)
addy = Address()
print("address obj created ( default ):\n" + str(addy))
print(BORDER_DASAH)
addy = Address( "123 45th St.", "Apt. 678", "Dallas", "TX", "75064")
print("address obj created ( param'd ):\n" + str(addy))
print(BORDER_DASAH)
# show that we can make another STATE_VALIDATION
# variable, but Address objects won't use it
# for state validation, b/c their own internal
# one is used/called via self-STATE_VALIDATION
STATE_VALIDATION = "blah"
print("external STATE_VALIDATION var = " + STATE_VALIDATION)
# error, can't access property normally
# print(addy.__STATE_VALIDATION)
print(BORDER_DASAH)
# can access property by breaking name mangling
# (which things shouldn't do to bypass private)
print(addy._Address__STATE_VALIDATION)
##################################
"""
Our Customer object after we got it sorted out.
Can just import it going forward to reduce code bloat.
"""
##################################
# import the address object, b/c Customer uses it
from object_example_address import Address
##################################
class Customer:
#------------------------------------
# constructor
#------------------------------------
# property = default
def __init__( self, id = 0,
fname = "",
lname = "",
address = Address(),
charges = []):
#--------------------------------
self.id = id
self.fname = fname
self.lname = lname
self.address = address
self.charges = charges # this will be a list
#--------------------------------
#------------------------------------
# getters
#------------------------------------
@property
def id(self):
return self.__id
@property
def fname(self):
return self.__fname
@property
def lname(self):
return self.__lname
@property
def address(self):
return self.__address
@property
def charges(self):
return self.__charges
#------------------------------------
# setters
#------------------------------------
# modification ... string inputs to uppercase
# (address already u-cases itself internally)
@id.setter
def id( self, value ):
self.__id = value # int
@fname.setter
def fname( self, value ):
self.__fname = str.upper(value)
@lname.setter
def lname( self, value ):
self.__lname = str.upper(value)
@address.setter
def address( self, value ):
self.__address = value # object
@charges.setter
def charges( self, value ):
self.__charges = value # list
#------------------------------------
# other functions
#------------------------------------
# Total Charges (sums list of charges)
# very simply function.. just sums charges
# we don't want to print or anything else,
# we'll let other functions (like the __str__)
# handle what they want to do with the float
# sum we return
def totalCharges( self ):
return sum(self.charges)
#------------------------------------
# stringify / toString / print(object)
#------------------------------------
def __str__( self ):
msg = ""
msg += "id = " + str(self.id) + "\n"
msg += "fname = " + self.fname + "\n"
msg += "lname = " + self.lname + "\n"
# address object already has it's own stringify
# so all we have to do is ...
msg += str(self.address) + "\n"
msg += "charges = " + str(self.charges) + "\n"
msg += "tot chgs = " + str(self.totalCharges())
return msg
######################################
# setup up some debug / testing code
# that will only run if this file
# is active as "main"
######################################
if __name__ == "__main__":
BORDER_DASAH = "-" * 75
print(BORDER_DASAH)
cust1 = Customer()
print("customer obj created ( default ):\n" + str(cust1))
print(BORDER_DASAH)
addy = Address( "123 45th St.", "Apt. 678", "Dallas", "TX", "75064" )
chgs = [123.45, 987.65, 555.55]
cust2 = Customer( 1234, "Barry", "White", addy, chgs )
print("customer obj created ( param'd ):\n" + str(cust2))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment