如何让类中的方法不需要提供self参数
/ / / 阅读数:5466前言
在我初学 Python 的时候,对方法 / 函数 2 种叫法如何区分产生或疑惑。所谓函数,是一段代码,通过名字来进行调用。它能将一些数据(参数)传递进去进行处理,然后返回一些数据(当然也可能并不需要返回)。而方法是一种定义在类里面的函数,它的特殊之处是和对象相关,必须有一个额外的第一个参数名称,但是在调用这个方法的时候开发者并不需要为这个参数赋值,Python 会自动提供这个值。这个特别的变量指对象本身,按照惯例它的名称是 self。我们先通过一个例子感受下 Python 是如何自动给这个参数赋值的:
class Person(object): def sayHi(self): print 'Hi!' p = Person() p.sayHi() p = Person() Person.sayHi(p) |
这 2 种方式都是正确的。注意第二种,sayHi 中传递了一个 Person 对象 p 进去,相当于我们「人工」来赋值。而第一种(也是我们日常用的这种)是由 Python 隐式的这样转换的罢了。再想一个更复杂的例子,假如你有一个类称为 MyClass 和这个类的一个实例 MyObject。当你调用这个对象的方法 MyObject.method (arg1, arg2) 的时候,这会由 Python 自动转为 MyClass.method (MyObject, arg1, arg2) - 这就是 self 的原理了。
假如我们不传递这个 self 试试:
class Person(object): def sayHi(): print 'Hi!' p = Person() p.sayHi() |
执行一下:
Traceback (most recent call last): File "self_demo.py", line 19, in <module> p.sayHi() TypeError: sayHi() takes no arguments (1 given) |
可见 Python 解释器要求我们必须额外的给类的方法的参数中的第一位加一个 self。那么有什么办法就是不加呢?也是可以的,不过要周折一些。
先讲一下思路
如上例,我们希望写代码的时候不再写 self:
class Person(object): def sayHi(): print 'Hi!' |
实现的步骤是:
- 首先给每个类中的方法的参数都加上 self 参数。
- 在方法内的命名空间中加上对应的赋值,比如存在 self.x, 那么方法内就要可以直接使用 x,这个 x 的值就是 self.x...
- 给旧的方法注入代码之后,基于它创建同名新的方法。
- 实现一个元类,应用上述对类中方法的处理。
我们分步骤实现:
1. 加 self 参数
为了易于演示,假设方法后面的参数都放在一行,原理很简单,就是找左括号,然后在对应位置加上 self:
def insert_self_in_header(header): return header[0:header.find("(") + 1] + "self, " + \ header[header.find("(") + 1:] |
2. 方法命名空间内赋值
这个思路就是借用 sys._getframe 找到对应方法内部的 self,然后通过 inspect.getmembers 找到 self 的全部属性,在其中找到符合的属性然后赋值:
def magic(): s = "" for var, value in inspect.getmembers(sys._getframe(1).f_locals["self"]): if not (var.startswith("__") and var.endswith("__")) \ and var not in f_locals: s += var + " = self." + var + "\n" return s |
我介绍下 sys._getframe 参数的意义:
sys._getframe (0)
当前函数sys._getframe (1)
调用该函数的函数
需要注意var not in f_locals
这句,如果本地变量中已经有了 xxx, 就不能执行xxx = self.xxx
来污染了。
需要注意 sys._getframe 返回的是调用栈的对象,所以需要在运行期间使用。另外没事翻翻标准库源码,inspect.getmembers 也不是什么黑科技,其实就是 dir 一下,然后对每个属性 getattr 获取对应的属性值,不过大家以后有这种需求的时候可以直接使用这个方法而不是自己造啦:
def getmembers(object, predicate=None): """Return all members of an object as (name, value) pairs sorted by name. Optionally, only return members that satisfy a given predicate.""" results = [] for key in dir(object): try: value = getattr(object, key) except AttributeError: continue if not predicate or predicate(value): results.append((key, value)) results.sort() return results |
3. 更新方法内容
上例中的生成 sayHi 方法的代码应该是这样:
def sayHi(self): exec(magic()) print 'Hi!' |
由于 Python 语法对代码是要求缩进的,首先我们要处理缩进问题,缩进问题分 2 步:
- 去掉方法相对于行首的空格都去掉,sayHi 并让它不缩进,从:
class Person(object): def sayHi(self): exec(magic()) print 'Hi!' |
抽取处理后成为:
def sayHi(self): exec(magic()) print 'Hi!' |
代码这样写:
def outdent_lines(lines): outer_ws_count = 0 for ch in lines[0]: if not ch.isspace(): break outer_ws_count += 1 return [line[outer_ws_count:] for line in lines] |
- 获取缩进的字符串,因为注入的
exec (magic ())
前面也要正确的缩进:
def get_indent_string(srcline): indent = '' for ch in srcline: if not ch.isspace(): break indent += ch return indent |
把上述工作串起来:
def rework(func): srclines, line_num = inspect.getsourcelines(func) srclines = outdent_lines(srclines) dst = insert_self_in_header(srclines[0]) if len(srclines) > 1: dst += get_indent_string(srclines[1]) + 'exec(magic())\n' for line in srclines[1:]: dst += line dst += 'new_func = eval(func.__name__)\n' exec(dst) return new_func |
其中 inspect.getsourcelines 非常有用,它可以动态获取源代码。通过它,在 IPython 中能通过??
获得对应函数 / 方法源代码。
另外new_func = eval(func.__name__)
最后会被 exec,函数本地变量中就包含了基于原来 func 生成的 new_func 了。
4. 实现元类
也就是在创建类的时候动态的改变类的代码:
class WithoutSelf(type): def __init__(self, name, bases, attrs): super(WithoutSelf, self).__init__(name, bases, attrs) try: for attr, value in attrs.items(): if isinstance(value, FunctionType): setattr(self, attr, rework(value)) except IOError: print "Couldn't read source code - it wont work." sys.exit() |
如果是一个 FunctionType 类型的属性,就用 rework 包装一下。
验证一下
到了检验的时候了:
class Person(object): __metaclass__ = WithoutSelf def __init__(name): self.name = name def sayHi(name=None): print 'Hi {}!'.format(name or self.name) p = Person('World') p.sayHi() p.sayHi('Python') |
执行一下:
❯ python demo_without_self.py Hi World! Hi Python! |
初步实现了。
这篇文章并不是真的让你不写 self
虽然我们可以通过指定使用上面这个元类的方式不再需要指定 self,但事实上只是这个元类帮你去指定罢了。而且这个例子并没有考虑到 property 等场景,就算实现了这样的元类也不应该在生产环境中使用它。其实在很久之前,有人就提交过一个 在 Python 3 中去掉这个 self 的草案 ,不过被核心开发者拒绝了,有兴趣的可以去深入的看看。Python 之禅里面说过:
Explicit is better than implicit.
我在知乎回答「为什么 Python 里类中方法 self 是显式的,而 C++ 中 this 是隐式的?」中也说过,Python 不希望基于规则而是希望把它明确出来,显式的写 self 的这种方式就很好。
那这篇文章的意义是什么呢?其实就是给大家展示一下 Python 动态修改源代码的能力,希望读者同学们把这样的玩法应用到有实际意义的地方。
PS:本文全部代码可以在 微信公众号文章代码库项目 中找到。
参考资料
MAKING SELF IMPLICIT IN OBJECTS Draft proposal: Implicit self in Python 3.0