< Notes > Python - 08 Python Object Oriented Advanced Features

Keywords: Attribute Python Lambda

08 Python Object-Oriented Advanced Features

By Kevin Song

  • 08-01 __slots__
  • 08-02 @property
  • 08-03 multiple inheritance
  • 08-04 custom class
  • 08-05 enumeration class
  • 08-06 meta class

08-01 __slots__

Binding attributes and methods to objects

class Student(object):
    pass

Binding properties to objects

>>> s = Student()
>>> s.name = 'Kevin' # Dynamic binding of an attribute to an instance
>>> print(s.name)
Kevin

Object Binding Method

>>> def set_age(self, age): # Define a function as an instance method
...     self.age = age
...
>>> from types import MethodType
>>> s.set_age = MethodType(set_age, s) # Binding a method to an instance
>>> s.set_age(25) # Call instance method
>>> s.age # test result
25

Binding an instance does not work for another instance

>>> s2 = Student() # Create a new instance
>>> s2.set_age(25) # Try calling method
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'Student' object has no attribute 'set_age'

Binding methods to class es, all instances are available

>>> def set_score(self, score):
...     self.score = score
...
>>> Student.set_score = set_score
>>> s.set_score(100)
>>> s.score
100
>>> s2.set_score(99)
>>> s2.score
99

Restrict binding properties and methods to objects

When defining a class, define a special u slots_ variable to restrict the attributes that the class instance can add.

class Student(object):
    __slots__ = ('name', 'age') # Define attribute names that allow binding with tuple

Only name and age can be bound

>>> s = Student() # Create a new instance
>>> s.name = 'Michael' # Binding property'name'
>>> s.age = 25 # Binding property'age'
>>> s.score = 99 # Binding property'score'
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'Student' object has no attribute 'score'

Note: u slots_u Defined attributes only work for the current class instance and not for inherited subclasses

>>> class GraduateStudent(Student):
...     pass
...
>>> g = GraduateStudent()
>>> g.score = 9999

08-02 @property

It is not logical to modify attributes directly because they can be modified directly.

s = Student()
s.score = 9999

In order to limit the scope of score, you can use set_score() method to set the score, and get_score() to get the score.

class Student(object):

    def get_score(self):
         return self._score

    def set_score(self, value):
        if not isinstance(value, int):
            raise ValueError('score must be an integer!')
        if value < 0 or value > 100:
            raise ValueError('score must between 0 ~ 100!')
        self._score = value
>>> s = Student()
>>> s.set_score(60) # ok!
>>> s.get_score()
60
>>> s.set_score(9999)
Traceback (most recent call last):
  ...
ValueError: score must between 0 ~ 100!

The above invocation method is slightly more complex than using attributes directly, so use the @property decorator to change the method into attributes

class Student(object):

    @property
    def score(self):
        return self._score

    @score.setter
    def score(self, value):
        if not isinstance(value, int):
            raise ValueError('score must be an integer!')
        if value < 0 or value > 100:
            raise ValueError('score must between 0 ~ 100!')
        self._score = value
  • property: Turn a getter method into an attribute
  • @score.setter: Turn a setter method into an attribute
>>> s = Student()
>>> s.score = 60 # OK, actually converted to s.set_score(60)
>>> s.score # OK, actually converted to s.get_score()
60
>>> s.score = 9999
Traceback (most recent call last):
  ...
ValueError: score must between 0 ~ 100!

You can also define a read-only property, a getter method only, and a read-only property if you don't define a setter method:

class Student(object):

    @property
    def birth(self):
        return self._birth

    @birth.setter
    def birth(self, value):
        self._birth = value

    @property
    def age(self):
        return 2015 - self._birth

08-03 multiple inheritance

class Animal(object):
    pass

# Major categories:
class Mammal(Animal):
    pass

class Bird(Animal):
    pass

# Various animals:
class Dog(Mammal):
    pass

class Bat(Mammal):
    pass

class Parrot(Bird):
    pass

class Ostrich(Bird):
    pass

Define Runnable and Flyable classes

class Runnable(object):
    def run(self):
        print('Running...')

class Flyable(object):
    def fly(self):
        print('Flying...')

Add Runnable to Dog and Flyable to Bat

class Dog(Mammal, Runnable):
    pass
class Bat(Mammal, Flyable):
    pass

MixIn

To better see the inheritance relationship: Runnable and Flyable changed to Runnable MixIn and Flyable MixIn

class Dog(Mammal, RunnableMixIn, CarnivorousMixIn):
    pass

08-04 custom class

Output custom information: u str_ and u repr_()

Print custom information: u str_u

Define Student classes, print instances

>>> class Student(object):
...     def __init__(self, name):
...         self.name = name
...
>>> print(Student('Michael'))
<__main__.Student object at 0x109afb190>

Print custom instance information with u str_u

>>> class Student(object):
...     def __init__(self, name):
...         self.name = name
...     def __str__(self):
...         return 'Student object (name: %s)' % self.name
...
>>> print(Student('Michael'))
Student object (name: Michael)

Direct display of variable customization information: u repr_()

>>> class Student(object):
...     def __init__(self, name):
...         self.name = name
...     def __str__(self):
...         return 'Student object (name: %s)' % self.name
...     __repr__ = __str__
...
>>> s = Student('Michael')
>>> s
Student object (name: Michael)

__iter__

If a class wants to be used for... An in loop, like a list or tuple, must implement an iter() method that returns an iterated object

The for loop calls the next() method of the iteration object to get the next value of the loop until it exits when a StopIteration error occurs.

class Fib(object):
    def __init__(self):
        self.a, self.b = 0, 1 # Initialize two counters a, b

    def __iter__(self):
        return self # The instance itself is an iteration object, so it returns to itself

    def __next__(self):
        self.a, self.b = self.b, self.a + self.b # Calculate the next value
        if self.a > 100000: # Conditions for exit from the loop
            raise StopIteration()
        return self.a # Returns the next value

__getattr__

When calling a method or attribute of a class, if it does not exist, an error will be reported

class Student(object):

    def __init__(self):
        self.name = 'Michael'
>>> s = Student()
>>> print(s.name)
Michael
>>> print(s.score)
Traceback (most recent call last):
  ...
AttributeError: 'Student' object has no attribute 'score'

To avoid this error, write a getattr() method that dynamically returns an attribute

class Student(object):

    def __init__(self):
        self.name = 'Michael'

    def __getattr__(self, attr):
        if attr=='score':
            return 99

When calling non-existent attributes, such as score, the Python interpreter attempts to call getattr(self,'score') to try to obtain attributes.

>>> s = Student()
>>> s.name
'Michael'
>>> s.score
99

Note: getattr is called only if no attribute is found. Existing attributes, such as name, are not found in getattr.

The return function is perfectly OK.

class Student(object):

    def __getattr__(self, attr):
        if attr=='age':
            return lambda: 25
>>> s.age()
25

__call__

Call instances directly

class Student(object):
    def __init__(self, name):
        self.name = name

    def __call__(self):
        print('My name is %s.' % self.name)
>>> s = Student('Kevin')
>>> s() # Do not pass in the self parameter
My name is Kevin.

Determine whether an object can be called (is it a Callable object)?

>>> callable(Student())
True
>>> callable(max)
True
>>> callable([1, 2, 3])
False
>>> callable(None)
False
>>> callable('str')
False

08-05 enumeration class

Constants defined in capitals are variables.

JAN = 1
FEB = 2
MAR = 3

Solution: Enum class (define a class type for such an enumeration type, and then each constant is a unique instance of class)

from enum import Enum

Month = Enum('Month', ('Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'))

Use Month.Jan directly to refer to a constant

>>> Month.Jan
<Month.Jan: 1>

Enumerate all members

>>> for name, member in Month.__members__.items():
...     print(name, '=>', member, ',', member.value)
... 
Jan => Month.Jan , 1
Feb => Month.Feb , 2
Mar => Month.Mar , 3
Apr => Month.Apr , 4
May => Month.May , 5
Jun => Month.Jun , 6
Jul => Month.Jul , 7
Aug => Month.Aug , 8
Sep => Month.Sep , 9
Oct => Month.Oct , 10
Nov => Month.Nov , 11
Dec => Month.Dec , 12
>>> 

More precise control of enumeration types can derive custom classes from Enum

from enum import Enum, unique

@unique
class Weekday(Enum):
    Sun = 0 # Sun's value is set to 0
    Mon = 1
    Tue = 2
    Wed = 3
    Thu = 4
    Fri = 5
    Sat = 6

The @unique decorator can help us check to ensure that there are no duplicate values

There are several ways to access these enumeration types

>>> day1 = Weekday.Mon
>>> print(day1)
Weekday.Mon
>>> print(Weekday.Tue)
Weekday.Tue
>>> print(Weekday['Tue'])
Weekday.Tue
>>> print(Weekday.Tue.value)
2
>>> print(day1 == Weekday.Mon)
True
>>> print(day1 == Weekday.Tue)
False
>>> print(Weekday(1))
Weekday.Mon
>>> print(day1 == Weekday(1))
True
>>> Weekday(7)
Traceback (most recent call last):
  ...
ValueError: 7 is not a valid Weekday
>>> for name, member in Weekday.__members__.items():
...     print(name, '=>', member)
...
Sun => Weekday.Sun
Mon => Weekday.Mon
Tue => Weekday.Tue
Wed => Weekday.Wed
Thu => Weekday.Thu
Fri => Weekday.Fri
Sat => Weekday.Sat

08-06 meta class

type()

Function 1: Look at the type of a type or variable

Define a Hello class

class Hello(object):
    def hello(self, name='world'):
        print('Hello, %s.' % name)
>>> from hello import Hello
>>> h = Hello()
>>> h.hello()
Hello, world.
>>> print(type(Hello))
<class 'type'>
>>> print(type(h))
<class 'hello.Hello'>
  • Hello is a class whose type is type.
  • h is an instance, and its type is class Hello

Function 2: Create a class object

  • The type() function passes in three parameters in turn
    1. The name of class
    2. A collection of inherited parent classes (Python supports multiple inheritance, and if there is only one parent class, it needs to be written in tuple's single element)
    3. class method name and function binding
>>> def fn(self, name='world'): # Define function first
...     print('Hello, %s.' % name)
...
>>> Hello = type('Hello', (object,), dict(hello=fn)) # Create Hello class
>>> h = Hello()
>>> h.hello()
Hello, world.
>>> print(type(Hello))
<class 'type'>
>>> print(type(h))
<class '__main__.Hello'>

metaclass

Definition: Classes of classes (classes as instances of metaclasses)
Role: Controls class creation behavior

Posted by chantown on Mon, 10 Dec 2018 14:18:05 -0800