Python编程小技巧(3)

Py的魔法

派生内置不可变类并修改实例化行为的魔法


一个类的实例化方法是__init__ ?

不是呢, 是__new__, __init__只是初始化方法.

魔法原料:

_new_:创建对象时调用,会返回当前对象的一个实例

_init_:创建完对象后调用,对当前对象的一些实例初始化,无返回值

魔法咒语:

1.如果__new__和__init__同时存在,会优先调用__new__.

2.__new__方法会返回所构造的对象,__init__则不会 (__init__无返回值).

3.如果__new__返回一个对象的实例,会隐式调用__init__.

4.如果__new__不返回一个对象的实例,__init__不会被调用.

5.在对象的实例创建完成后调用。参数被传给类的构造函数.

6.如果基类有__init__方法,子类必须显示调用基类的__init__.

如果说现在我想写个继承于tuple的子类.这个类能够自动过滤掉非数字的元素,(就叫它NumTuple类吧).

这个部分Pyhton3Python2差别还是很大的, Py2中叫做类型(type), Py3中叫做类(class)

这里使用的环境是Python3,所以函数签名已经改变了, Python2的函数签名就像是公交车.

从上面的魔法咒语中就能明白, 实现这个功能的关键就在__new__方法了.通过重载__new__方法,可以操作要返回的对象.那么,为了调用其父类的__init__方法, 我们使用super.

super是Python中的类, 用来做基类和子类的对象代理, 什么是对象代理呢?

可以简单的理解成Java中的多态? 简单的说就是调用同辈或者父类的函数, 比如这样的小栗子:

class Handler:
  def login(self, token):
    write2DB(token)
    render()

class Auth(Handler):
  def login(self, token):
    if validate(token):
      super().login(token) # 等同于super(Auth, self)
    else:
      exit()

这样子类就可以非常便利的调用父类的方法了.

现在尝试实现一下NumTuple:

class NumTuple(tuple):
  def __new__(self, iterable):
    g = ( x for x in iterable if isinstance(x, int) or isinstance(x, float))
    return super().__new__(self, g)
  def __init__(self, iterable):
    self.proto = iterable
    super().__init__()

节省内存的魔法


实例化对象是一件消耗内存的操作. 想想这么一种场景, 一个网络游戏服务要给每一个玩家实例化一个角色类, 这数百万的实例会消耗掉许多内存,在Python中节省内存是很简单的.

class Person1(object):
  def __init__(self, uid, name, status=0, level=1);
    self.uid = uid
    self.name = name
    self.status = status
    self.level = level
class Person2(object):
  __slots__ = ['uid', 'name', 'status', 'level']
  def __init__(self, uid, name, status=0, level=1);
    self.uid = uid
    self.name = name
    self.status = status
    self.level = level

这两个类基本一样, 唯一的不同就在第二个类多了一个__slots__, 这个属性表示该类的实例有且仅有这几个属性, 且不能再添加.

实例化看一下效果:

from XX import Person1, Person2

p1 = Person1("0001", "P1")
p2 = Person2("0002", "P2")

print(set(dir(p1)) - set(dir(p2))) # 看看两者之间究竟有什么差别!

最后打印出来的结果是:

{'__weakref__', '__dict__'}

其中,最占用内存的就是__dict__了, 这个属性允许对象动态的进行属性的增加.

你可以试试为p1增加一个属性, 接着查看一下p1.__dict__, 你会发现这个对象消耗了很多资源.

查看大小的方法可以使用sys模块提供的getsizeof方法.

import sys
sys.getsizeof(p1.__dict__)
# 132

Python3中,这个已得到优化, 占用132个字节, 而在Python2中, 仅仅为一个对象增加两个属性就会消耗掉1048个字节!

上下文管理的魔法


Python提供了with关键字提供了便捷的上下文管理, 比如最经典的用于IO流的使用:

with open("sample.txt", "r") as f:
  cont = f.read()
# f.close() 不再需要了

如果说我想让我自己的类也实现这样的魔法呢?

事实上, 只要实现两个方法就可以实现上下文的管理: __enter____exit__.

现在我们实现一个支持上下文管理的类.

class Query(object):
  def __init__(self, name):
    self.name = name
  def __enter__(self):
    print("Enter!")
    return self
  def __exit__(self, exc_type, exc_value, traceback):
    if exc_type:
      print('Error!')
    else:
      print("Exit!")
  def query(self):
    print('Query info about %s...' % self.name)

现在我们就可以使用with关键字来进行自动的上下文管理了.

with QUery('name') as q:
  q.query()
# print(q)

这样写仍然有一点麻烦, 好在Python提供了装饰器使得代码得到了简化.

可以这样写: ( 从标准库contextlib中导入contextmanager )

from contextlib import contextmanager
class Query(object):
  def __init__(self, name):
    self.name = name
  def query(self):
    print("Query info about %s" % self.name)

@contextmanager
def create_query(name):
  print("Begin")
  q = Query(name)
  yield q
  print("End")

这样再调用, create_query函数就会返回一个_GeneratorContextManager对象, 而这个对象就是我们使用with后面as出来的东西, 该对象就绑上了之前说的__enter__, __exit__.

再调用时就像是这样:

with create_query('Justin') as q:
  q.query()

简单的说, @contextmanager使得我们通过编写生成器来进行代码的简化.

另外, 只要你返回的对象具有close方法, 就可以通过另一个装饰器写出更简的代码.

这个装饰器也就是contextlib下的closing

from contextlib import closing
from urllib.request import urlopen
with closing(urlopen("https://justin13wyx.me")) as res:
  print(res)

其实这个装饰器实现起来也是特别容易的:

@contextmanager
def closing(self):
  try:
    yield self.thing
  finally:
    self.thing.close()

setter和getter的魔法


在Java中, 我们编写getter和setter函数来对对象实例的属性进行更改和读取.

而在Python中, 相信你也发现了我们可以直接进行读取和更改, 这往往是不安全的, 比如:

number = 'I'm not a number!!'
result = number * 2
print(result)
# I'm not a number!!I'm not a number!!

所以,更推荐调用函数, 因为我们可以进行处理.

Python作为动态语言拥有将属性访问"重定向"到函数的能力的.重点就在于一个装饰器property

使用property的其中一个注意点是:一定不要把public的属性用property装.否则,会无限递归.(因为函数名和变量名相同)

补充: Python中的私有变量.
Python中是不存在私有变量的, 只要你想,所谓的私有变量可以随意访问.
有两种所谓的私有变量, 一种是一条下划线的_variable, 这个你你几乎可以认定为是public的变量, 但, 作为一种规范, 他可以告诉别人我的这个变量是一个私有变量, 没事就别碰它.
另外一种, 是加了两条下划线的变量__variable.
这种变量可以认定为是Python的私有变量.因为解释器会自动把_variable变成_ClassName__variable.(这不就是自己骗自己吗!!??).

请看示例:

class Student(object):
    def __init__(self):
        self._score = 0
    @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

这样就可以:

s = Student()
s.score = 90
s.score # 90
s.score = 9999
ValueError: ....

其实,property还有另一种巧妙的用法:( 还有这种操作.jpg )

# 继续上面的Student类的定义:
@property
def age(self):
  return self._age

好了,我不去定义@age.setter,这样我们的age属性就会变成一个只读属性!

使类实现比较操作的魔法


观察下一些常用来比较的对象, 比如说数字和字符串,不难发现他们都会有一些神奇的魔术方法:

比如:__eq__.__gt__.__lt__.__le__.__ne__.__ge__.

所以, 如果实现运算符的重载, 实现这些方法就可以了.

这都没什么, 但有些复杂并且实现的函数略多.

所以, 可以使用标准库functools中的total_ordering来进行操作的简化.

只要装饰整个类, 就可以通过只实现两个方法(必须包括等于方法)来完成全部的运算符的重载.

使用的例子就像是这样:

from functools import total_ordering
@total_ordering
class Student(object):
  def __init__(self, age):
    self.age = age
  def __eq__(self, that):
    return self.age == that.age
  def __lt__(self, that):
    return self.age < that.age

Python进行类型检查的魔法


Python是一个弱类型语言, 尽管可以在方法中进行类型检查(isinstance), 但这样远没有Java的类型检查安全 Java.C.C++的类型检查都是在编译的时候进行 的.而动态语言Python, 他的解释器是不可能实现这种检查的.

所以为了能够在对变量声明时对变量进行类型声明以及对变量赋值的时候进行类型检查, Python同样提供了一些魔法:(描述符)

  • __set__
  • __get__
  • __delete__

你也许有这样的疑问, 既然之前都说了可以通过@property装饰器来使得属性的改变和获取都变成我们定义的函数,为什么需要这个东西呢?

注意:getset和property的用法完全不同!

class Attr(object):
  def __init__(self, name, type):
    self._name = name
    self._type = type
  def __get__(self, instance, cls):
    return instance.__dict__[self._name]
  def __set__(self, instance, value):
    if not isinstance(value, self._type):
      raise TypeError('expected an %s ' % self._type)
    instance.__dict__[self._name] = value

这样得到一个用来进行类型规范的辅助类.

通过创建这个辅助类的实例来当做用作真正要用到的类的属性, 这样方法才可以得到调用.

就像是这样:

class Student(object):
  name = Attr("name", str)
  age = Attr('age', int)
s1 = Student()
s1.name = "J"
s1.age = 5
# 没有问题
s1.name = 7
# TypeError: expected an 
s1.age = "5"
# TypeError: expected an 

你可能感兴趣的:(Python编程小技巧(3))