python-class

Class

Python class is more like C++ class, it supports inheritance, function overridden (not like C++, if you create an instance of child class, you never see parent's method if child defines method with same name even different signature(argument list)), operator overload, static method etc.


Overloading vs Overriding
Overloading occurs when two or more methods in one class have the same method name but different parameters.

Overriding occurs when two methods have the same method name and parameters. One of the methods is in the parent class, and the other is in the child class. Overriding allows a child class to provide the specific implementation of a method that is already present in its parent class


  • basic rules

    • "_" is used to indicate that it should be used in a private context, but still can be accessed outside of class
    • prefix __show two underscores for private user defined, NO accessable outside of class, exception happens when accesses it.
    • prefix __len__ for built-in attr, but accessable outside of class
    • no prefix for public attribute
    • def __init__(self) is not need if nothing in it
    • child must call super().__init__() only if parent needs input
    • self can be other name, but it has to be the first parameter of any function in the class
    • del instance
  • parameter can be class type or other built-in type as well

1
2
3
4
5
6
7
8
9
def fun_parm_types(fun_n, class_n):
fun_n()
t1 = class_n('show test', 12) # create an instance of class_n
t1.show()

def factory(n):
return n("hello")

print(factory(str)) # pass str type!!!
  • Always use composition not inheritance for python class
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class CA:
def __init__(self, company, group):
self._company = company # plan to be private, but still acceable outside
self.__group = group # private, not acceable outside

def _should_show(self):
print(self._company)

def __show(self):
print(self.__group)

# __xx__ is for built-in, internal function
def __say__(self):
print('hello')

c1 = CA("google", 'CJ')
print(c1._company)
c1.__say__()
c1._should_show()

try:
c1.__show()
except AttributeError as e:
print(str(e))

try:
print(c1.__group)
except AttributeError as e:
print(str(e))
google
hello
google
'CA' object has no attribute '__show'
'CA' object has no attribute '__group'

dynamic attribute

There is one special feature for python that is different from C++, you can add attribute dynamically that means you can add attribute anytime you want to a class or an instance, but it’s not a good habit as others do not know that attribute at all.

by default you can add any attribute to an instance, but you can restrict it by __slots__, but restriction only applies on instance not class.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class CA:
# attribute allowed to added for instance
__slots__ = ('new_attr_c','new_attr_b')
def __init__(self):
pass
c1 = CA()
CA.new_attr = 12 # you can see we can add attr for class even not in __slots__
# this must before print, but can after creating an instance, after this, all instances now have new_attr
print(c1.new_attr)

c1.new_attr_c = 13
print(c1.new_attr_c)

try:
c1.new_attr_d = 12
except AttributeError:
print("add new attr not in __slots__, exception happened")

12
13
add new attr not in __slots__, exception happened

function overloading/overriding

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
class person:
'''
this part will be person.__doc__
'''
def __init__(self, name):
# constructor!!!
self.__name = name

def eat(self):
print("I eat from person")
self.after()
# if self is child class, self.after() will call child's after(), if not found, call person.after()

def after(self):
print("after eat from person")


class student(person): # inheritance this way
def __init__(self, name, sid):
# child must call parent init
super().__init__(name)
self.__sid = sid

def after(self):# overriding
print("after eat from student")

class teacher(person):
def __init__(self, name, tid):
super().__init__(name) # call parent's constructor
self.__tid = tid

def eat(self, msg): # overloading
print("i eat %s from teacher" % msg)

s1 = student("s1", 1)
t1 = teacher("t1", 1)
s1.eat()

# t1.eat() called with no parameter
# error as parent function is hiden even with different signatures

t1.eat("apple")
I eat from person
after eat from student
i eat apple from teacher

operator overloading

In some case, you may want to access user defined class like other built-in type, like len(user_class_instance), user_class_instance[0] what you need to do is to implement special functions, here are a list for such special functions.

built-in function(descriptor)

1
2
3
4
5
6
7
- len           __len__
- str __str__
- [] __getitem__
- ins.attr __getattr__
- CLASS() __call__
- hash __hash__
- with/as __enter__/__exit__

operator

p1 + p2 __add__
p1 - p2 __sub__
*,/,// __xx__
p1 % p2 __mod__
<<,>> __xx__
p1 < p2 __lt__
>, <=, >, >= __le__, __gt__, __ge__
1
2
3
4
5
6
class Phone:      
def __str__(self):
return 'Apple'

def __len__(self):
return len('Apple')
1
2
3
import sys
ph1 = Phone()
str(ph1)
'Apple'
1
print(len(ph1))
5

setattr/getattr/hasattr/delattr with class

Most of time, class attribute(include function) is defined when defines a class, so we know the name of it, hence we can access it by class_instance.name or class_instance.get_name(), but there are times when you might not know the name of a attribute until runtime as some of them defined by setattr at runtime!!!, that’s where hasattr and getattr (as well as delattr and setattr) come into play.

1
2
3
4
5
6
7
8
9
10
11
12
13
# must quote attribute
# getattr(class_name, 'attribute')
# getattr(instance, 'attribute')

class Apple:
pass

# without default value
val = getattr(Apple, 'name')
var = 'name'

# with default value
val = getattr(Apple, var, 'default value')
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Foo:
def __init__(self, name):
self.name = name

def get_name(self):
return self.name

f1 = Foo('jason')
print("all attributes(not include function) for instance are stored at instance._dict__=", f1.__dict__)

if not hasattr(f1, 'id'):
print("class instance doesn't have id, use default value ", getattr(f1, 'id', '123'))

print("f1.get_name return ", getattr(f1, 'get_name')())
all attributes(not include function) for instance are stored at instance._dict__= {'name': 'jason'}
class instance doesn't have id, use default value  123
f1.get_name return  jason

staticmethod and classmethod

staticmethod

staticmethod is used to group functions which have some logical connection within a class. it knows nothing about the class or instance it was called on, It just gets the arguments that were passed, no self argument, just group functions in a class. but it can be called by class or instance as well.

1
2
3
Class.staticmethod() # class directly
Or even
Class().staticmethod() # instance calls it

It is basically useless in Python you can just use a module function instead of a staticmethod

classmethod

A classmethod is a method that is bound to a class rather than its object. It doesn't require creation of a class instance, much like staticmethod.

The differences between a static method and a class method are:

  • Static method knows nothing about the class and just deals with the parameters
  • Class method works with the class since its first parameter is always the class type.

The class method can be called both by the class and its object, both pass class type as first parameter.

1
2
3
Class.classmethod() # class directly
Or even
Class().classmethod() # instance

But no matter what, the class method is always attached to a class with first argument as the class itself cls.

1
2
# cls is class type!!!
def classMethod(cls, args...)

This is useful when you want a method to be a factory for the class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class Student:
COUNT = 0 # COUNT here means it blongs to CLASS, can access it by class name or instance

# can use COUNT as without class prefix as parameter
# but inside function, must add prefix Student.count inside function
def __init__(self, ct=COUNT):
self.count_inc_class() # even COUNT is increase here,

@classmethod
def count_inc_class(cls): # pass CLASS, cls is CLASS type, not self!
cls.COUNT += 1 # access class COUNT by CLASS name

@staticmethod
def get_count_static():
return Student.COUNT # access class COUNT by instance

@classmethod
def get_count_class(cls): # pass CLASS as firt argument, not self
print(cls)
s3 = cls()
return cls.COUNT


s1 = Student()
s2 = Student()

print(s1.get_count_static(), Student.get_count_static())
print(s1.get_count_class(), Student.get_count_class())
#s1.get_count_class passed CLASS implicitly not self to classmethod

print(s1)
print(Student)
2 2
<class '__main__.Student'>
<class '__main__.Student'>
3 4
<__main__.Student object at 0x7f5e5d055df0>
<class '__main__.Student'>

setter/getter

setter/getter provide a way that you can access/modify attribute with obj.name but the implementation behind it is a function, this is very useful in some case, let’s say your old application writing code with obj.name, but after several years, you want to /need to return value depends on more logic, but without changing user code, getter comes into play.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Student:
# two name functions with different decorators
@property
def name(self):
print('getting name')
return self._name

@name.setter #@name must be same with attribute
def name(self, value):
print('setting name %s' % value)
self._name = value


st1 = Student()
# behind it, a function is called!!!
st1.name = "jason"
print(st1.name)
setting name jason
getting name
jason

enum class

enum is used to group literal together, so that you can use them from class, more specific for its purpose

1
2
3
4
from enum import Enum
# Month is the ID of the enum
mth = Enum('Month', ("Jan", "Feb"))
print(mth.Jan) # the resut is a number but can use name like c enum
Month.Jan