python-oop

Python OOP

OOP 编程的三大法宝:多态、封装、和继承。 Python 也自然离不开这些概念。但作为一个动态类型的语言, Python 的 OOP 非常灵活,最强大的就是属性的动态绑定。 另外,Python 中的三大法宝在行为上和其他OOP语言(JAVA)有些差别,注意这些有毒的细节,避免踩坑。

Python OOP 中不要用 self.XXX 修改属于类的 XXX 属性

之前一直有一个误解,以为 Python 中的 func body 里面的 self.member_vars 对应的就是Java中的对象属性属于每一个对象,而在 class body 里面的成员就是 java 中的 静态成员变量属于类;在Python非类方法是无法修改静态成员的,这点在Java(this.XXX)是可行的,但是Python不能, 这个时候 Python 会在 对象的 object.__dict__ 中产生一个新的 XXX,初始值来自 类的XXX, 而且此时不再和类的XXX 产生联系。正确的做法是使用 ClassName.XXX 来访问。

class Task(object):
    count = 7
    def __init__(self, id, *args, **kwargs):
        self.id = id
        print args, kwargs

    def execute(self, *args, **kwargs):
        self.count -= 1
        print args, kwargs, self.count
        return self.count

    def execute2(self, *args, **kwargs):
        Task.count -= 1
        print args, kwargs, Task.count
        return Task.count

    @classmethod
    def do(cls, *args, **kwargs):
        cls.count -= 1
        print args, kwargs, cls.count
        return cls.count

以下是查看对象和类的词典的变化,很明显,对象通过复制一份类变量XXX的形式将其复制给了 self.XXX

from tasks import Task
t1 = Task(1)
t1.do()
print(t1.__dict__)

t1.execute()
print(Task.count)
print(t1.__dict__)

print(Task.__dict__)
Task.do()
print(Task.__dict__)

t1.execute2()
print(Task.count)
print(t1.__dict__)
print(Task.__dict__)

'''
() {} 6
{'id': 1}
() {} 5
6
{'count': 5, 'id': 1}
{'count': 6, 'do': <classmethod object at 0x10b6200c0>, '__module__': 'tasks', 'execute2': <function execute2 at 0x10b60d500>, '__dict__': <attribute '__dict__' of 'Task' objects>, '__weakref__': <attribute '__weakref__' of 'Task' objects>, 'execute': <function execute at 0x10b60d398>, '__init__': <function __init__ at 0x10b60d410>, '__doc__': None}
() {} 5
{'count': 5, 'do': <classmethod object at 0x10b6200c0>, '__module__': 'tasks', 'execute2': <function execute2 at 0x10b60d500>, '__dict__': <attribute '__dict__' of 'Task' objects>, '__weakref__': <attribute '__weakref__' of 'Task' objects>, 'execute': <function execute at 0x10b60d398>, '__init__': <function __init__ at 0x10b60d410>, '__doc__': None}
() {} 4
4
{'count': 5, 'id': 1}
{'count': 4, 'do': <classmethod object at 0x10b6200c0>, '__module__': 'tasks', 'execute2': <function execute2 at 0x10b60d500>, '__dict__': <attribute '__dict__' of 'Task' objects>, '__weakref__': <attribute '__weakref__' of 'Task' objects>, 'execute': <function execute at 0x10b60d398>, '__init__': <function __init__ at 0x10b60d410>, '__doc__': None}
'''
def t1():
    from tasks import Task
    func_name = 'do'
    invoke_func = getattr(Task, func_name, None)
    id = 0
    if invoke_func is not None and callable(invoke_func):
        while True:
            task = Task(id)
            if invoke_func(task, "count") > 0:
                id += 1
                continue
            else:
                break

    task1 = Task('1')
    task1.execute()

def t2():
    from tasks import Task
    task2 = Task('2')
    task2.execute()

    func_name = 'execute'
    func_name = 'execute2'
    invoke_func = getattr(Task, func_name, None)
    id = 0
    if invoke_func is not None:
        while True:
            task = Task('123123')
            if invoke_func(task, "count") > 0:
                id += 1
                continue
            else:
                break

t2()
t1()

Python @staticmethod 和 @classmethod

class Person(object):
    def __init__(self, age):
        self.age = age

    def get_age(self):
        return self.age
Person.get_age
<unbound method Person.get_age>

显示的是未绑定的方法, 为什么? 因为 Python 对象方法需要绑定到某一个实例才可以使用

Person.get_age()
---------------------------------------------------------------------------

TypeError                                 Traceback (most recent call last)

<ipython-input-46-2e11244cec48> in <module>()
----> 1 Person.get_age()


TypeError: unbound method get_age() must be called with Person instance as first argument (got nothing instead)
Person.get_age(Person(23))
23

这里其实就相当于 Person(23) 这个对象和我们的 Person.get_age 方法绑定了.这也是为什么实例方法都需要 self 做参数的原因了;

但是, 如果我们不记得类名字了, 就无法这样调用方法了. 因此 Python 提供了 Syntax Sugar, 直接通过和 Java 一样的方式调用绑定的实例方法即可!

Person(23).get_age()# <==> Person.get_age(Person(23))
23
Person(22).get_age
<bound method Person.get_age of <__main__.Person object at 0x7f5b24107e10>>

每一个绑定的方法,都可以获取他所绑定到的对象, 使用 方法 的 __self__ 属性即可;

m = Person(23).get_age
m.__self__
<__main__.Person at 0x7f5b24107d50>
m == m.__self__.get_age
True

In Python 3, the functions attached to a class are not considered as unbound method anymore, but as simple functions, that are bound to an object if required. So the principle stays the same, the model is just simplified.

>>> class Pizza(object):
...     def __init__(self, size):
...         self.size = size
...     def get_size(self):
...         return self.size
...
>>> Pizza.get_size
<function Pizza.get_size at 0x7f307f984dd0>

Static Methods

对于属于一个类,但是却从不使用该类实例变量的方法来说, 使用静态方法就够了.他既不和类的实例(self)绑定,也不和类本身(cls)绑定.只是意义上属于类的一个普通方法而已.

class Pizza(object):
    @staticmethod
    def mix_ingredients(x, y):
        return x + y

    def cook(self):
        return self.mix_ingredients(self.cheese, self.vegetables)
  1. Python 会将每一个实例方法和一个实例化对象绑定起来,而且绑定过的实例方法在Python 中也是对象,即使是同一个方法,每一个新对象都要生成一个新的绑定方法对象,非常耗费空间和时间,而使用 staticmethod 不会进行对象绑定;

  2. @staticmethod 支持子类重写,即多态.如果你使用一个模块内的顶层函数 mix_ingredients, 继承自 Pizza 的子类并不能改变行为如果你不重写cook方法.

print Pizza().cook is Pizza().cook
print Pizza().mix_ingredients is Pizza().mix_ingredients
print Pizza().mix_ingredients is Pizza.mix_ingredients
False
True
True

Class Methods

并不和对象绑定,但是和类绑定的方法, 需要加上 cls 参数

class Pizza(object):
    radius = 23
    @classmethod
    def get_radius(cls):
        return cls.radius
Pizza.get_radius
<bound method type.get_radius of <class '__main__.Pizza'>>
Pizza().get_radius
<bound method type.get_radius of <class '__main__.Pizza'>>
Pizza.get_radius.__self__ is Pizza().get_radius.__self__
True
Pizza.get_radius == Pizza().get_radius
True
Pizza.get_radius()
23

classmethod 的用法

  1. FactoryMethods. 如果我们需要使用一些预处理来创建实例, 使用 @staticmethod 必须硬编码类名 Pizza 在我们的函数中, 这样任何集成 Pizza 的子类就不能够使用工厂方法为自己所用了.
class Pizza(object):
    def __init__(self, ingredients):
        self.ingredients = ingredients

    @classmethod
    def from_fridge(cls, fridge):
        return cls(fridge.get_cheese() + fridge.get_vegetables())
  1. 需要调用静态方法的静态方法直接使用 classmethod. 原因还是因为在静态方法中又必须硬编码类名, 子类就无法;
class Pizza(object):
    def __init__(self, radius, height):
        self.radius = radius
        self.height = height

    @staticmethod
    def compute_area(radius):
         return math.pi * (radius ** 2)

    @classmethod
    def compute_volume(cls, height, radius):
         return height * cls.compute_area(radius)

    def get_volume(self):
        return self.compute_volume(self.height, self.radius)

Abstract Methods

定义在基类中还未被实现的方法.

class Pizza(object):
    def get_radius(self):
        raise NotImplementedError

任何继承自 Pizza 的子类都必须覆盖 get_radius 方法, 否则就会在调用的时候抛出 异常.

>>> Pizza()
<__main__.Pizza object at 0x7fb747353d90>
>>> Pizza().get_radius()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 3, in get_radius
NotImplementedError

但是以上实现方式会使得异常在调用该方法的时候才会发生, 要想提前知道方法未实现, 可以使用 abc 模块.

import abc

class BasePizza(object):
    __metaclass__  = abc.ABCMeta

    @abc.abstractmethod
    def get_radius(self):
         """Method that should do something."""
class Methods:
  def i_method(self,x):
    print(self,x)

  def s_method(x):
    print(x)

  def c_method(cls,x):
    print(cls,x)

  s_method = staticmethod(s_method)
  c_method = classmethod(c_method)

obj = Methods()

obj.i_method(1)
Methods.i_method(obj, 2)

obj.s_method(3)
Methods.s_method(4)

obj.c_method(5)
Methods.c_method(6)
(<__main__.Methods instance at 0x7f5b240c8e60>, 1)
(<__main__.Methods instance at 0x7f5b240c8e60>, 2)
3
4
(<class __main__.Methods at 0x7f5b24067db8>, 5)
(<class __main__.Methods at 0x7f5b24067db8>, 6)

以上实现和一下实现等价:

class Methods:
  def i_method(self,x):
    print(self,x)

  @staticmethod
  def s_method(x):
    print(x)

  @classmethod
  def c_method(cls,x):
    print(cls,x)

obj = Methods()

obj.i_method(1)
Methods.i_method(obj, 2)

obj.s_method(3)
Methods.s_method(4)

obj.c_method(5)
Methods.c_method(6)
(<__main__.Methods instance at 0x7f5b24039200>, 1)
(<__main__.Methods instance at 0x7f5b24039200>, 2)
3
4
(<class __main__.Methods at 0x7f5b24067e88>, 5)
(<class __main__.Methods at 0x7f5b24067e88>, 6)
class S:
   nInstances = 0
   def __init__(self):
      S.nInstances = S.nInstances + 1

   @staticmethod
   def howManyInstances():
      print('Number of instances created: ', S.nInstances)

a = S()
b = S()
c = S()
S.howManyInstances()
('Number of instances created: ', 3)

参考

- guide-python-static-class-abstract-methods

Python 继承类数据的传递性

Python 子类如果不覆盖父类的类属性的话,默认所有的子类是共享该属性的!!因此,我们最好覆盖掉父类的类属性,防止多个子类混用的时候出错!

class BaseClient:
    _default_clients = {}
    def __init__(self, region):
        self.region = region

    @classmethod
    def get_client(cls, region=None):
        print cls
        print dir(cls)
        if region not in cls._default_clients:
            cls._default_clients[region] = cls(region)
        return cls._default_clients[region]


class A(BaseClient):
#     _default_clients = {}
    def __init__(self, region):
        self.region = region

class B(BaseClient):
#     _default_clients = {}
    def __init__(self, region):
        self.region = region

a = A.get_client('yf')
a._default_clients['xh'] = 'sd'
print a
b = B.get_client('yf')
print b._default_clients['xh']
print b

'''
__main__.A
['__doc__', '__init__', '__module__', '_default_clients', 'get_client']
<__main__.A instance at 0x10b04b200>
__main__.B
['__doc__', '__init__', '__module__', '_default_clients', 'get_client']
sd
<__main__.A instance at 0x10b04b200>

'''