如何让类中的方法不需要提供self参数

前言

在我初学Python的时候,对方法/函数2种叫法如何区分产生或疑惑。所谓函数,是一段代码,通过名字来进行调用。它能将一些数据(参数)传递进去进行处理,然后返回一些数据(当然也可能并不需要返回)。而方法是一种定义在类里面的函数,它的特殊之处是和对象相关,必须有一个额外的第一个参数名称,但是在调用这个方法的时候开发者并不需要为这个参数赋值,Python会自动提供这个值。这个特别的变量指对象本身,按照惯例它的名称是self。我们先通过一个例子感受下Python是如何自动给这个参数赋值的:

1
2
3
4
5
6
7
8
9
10
11

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试试:

1
2
3
4
5
6
7
8

class Person(object):
def sayHi():
print 'Hi!'


p = Person()
p.sayHi()

执行一下:

1
2
3
4
5

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:

1
2
3
4

class Person(object):
def sayHi():
print 'Hi!'

实现的步骤是:

  1. 首先给每个类中的方法的参数都加上self参数。
  2. 在方法内的命名空间中加上对应的赋值,比如存在self.x, 那么方法内就要可以直接使用x,这个x的值就是self.x…
  3. 给旧的方法注入代码之后,基于它创建同名新的方法。
  4. 实现一个元类,应用上述对类中方法的处理。
    我们分步骤实现:

    1. 加self参数

    为了易于演示,假设方法后面的参数都放在一行,原理很简单,就是找左括号,然后在对应位置加上self:
1
2
3
4

def insert_self_in_header(header):
return header[0:header.find("(") + 1] + "self, " + \
header[header.find("(") + 1:]

2. 方法命名空间内赋值

这个思路就是借用sys._getframe找到对应方法内部的self,然后通过inspect.getmembers找到self的全部属性,在其中找到符合的属性然后赋值:

1
2
3
4
5
6
7
8

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参数的意义:

  1. sys._getframe(0) 当前函数
  2. sys._getframe(1) 调用该函数的函数
    需要注意var not in f_locals这句,如果本地变量中已经有了xxx, 就不能执行xxx = self.xxx来污染了。
    需要注意sys._getframe返回的是调用栈的对象,所以需要在运行期间使用。另外没事翻翻标准库源码,inspect.getmembers也不是什么黑科技,其实就是dir一下,然后对每个属性getattr获取对应的属性值,不过大家以后有这种需求的时候可以直接使用这个方法而不是自己造啦:
1
2
3
4
5
6
7
8
9
10
11
12
13
14

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方法的代码应该是这样:

1
2
3
4

def sayHi(self):
exec(magic())
print 'Hi!'

由于Python语法对代码是要求缩进的,首先我们要处理缩进问题,缩进问题分2步:

  1. 去掉方法相对于行首的空格都去掉,sayHi并让它不缩进,从:
1
2
3
4
5

class Person(object):
def sayHi(self):
exec(magic())
print 'Hi!'

抽取处理后成为:

1
2
3
4

def sayHi(self):
exec(magic())
print 'Hi!'

代码这样写:

1
2
3
4
5
6
7
8

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]
  1. 获取缩进的字符串,因为注入的exec(magic())前面也要正确的缩进:
1
2
3
4
5
6
7
8

def get_indent_string(srcline):
indent = ''
for ch in srcline:
if not ch.isspace():
break
indent += ch
return indent

把上述工作串起来:

1
2
3
4
5
6
7
8
9
10
11
12

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. 实现元类

也就是在创建类的时候动态的改变类的代码:

1
2
3
4
5
6
7
8
9
10
11

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包装一下。

验证一下

到了检验的时候了:

1
2
3
4
5
6
7
8
9
10
11
12
13

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')

执行一下:

1
2
3
4

❯ python demo_without_self.py
Hi World!
Hi Python!

初步实现了。

这篇文章并不是真的让你不写self

虽然我们可以通过指定使用上面这个元类的方式不再需要指定self,但事实上只是这个元类帮你去指定罢了。而且这个例子并没有考虑到property等场景,就算实现了这样的元类也不应该在生产环境中使用它。其实在很久之前,有人就提交过一个[在Python
3中去掉这个self的草案](https://mail.python.org/pipermail/python-
dev/2006-January/059446.html),不过被核心开发者拒绝了,有兴趣的可以去深入的看看。Python之禅里面说过:

Explicit is better than implicit.
我在知乎回答「为什么Python里类中方法self是显式的,而C++中this是隐式的?」中也说过,Python不希望基于规则而是希望把它明确出来,显式的写self的这种方式就很好。
那这篇文章的意义是什么呢?其实就是给大家展示一下Python动态修改源代码的能力,希望读者同学们把这样的玩法应用到有实际意义的地方。
PS:本文全部代码可以在微信公众号文章代码库项目中找到。

参考资料

[MAKING SELF IMPLICIT IN
OBJECTS](http://code.activestate.com/recipes/362305-making-self-implicit-in-
objects/)
Draft proposal: Implicit self in Python
3.0

版权声明:本文由 董伟明 原创,未经作者授权禁止任何微信公众号和向掘金(juejin.im)转载,技术博客转载采用 保留署名-非商业性使用-禁止演绎 4.0-国际许可协议
python