live with scope
序言
源起于Python开发者'公众号转载的深刻理解Python中的元类一文, 回忆着自己看过的 Ruby元编程 一书, 参照写个相应的Ruby版.
Python和Ruby在很多方面都非常相像, 特别是Ruby的设计部分参考了Python. 但在很多方面, 它俩的很多概念并非一一对应的. 在这里的 元类 在Ruby 中并没有相应的概念, 如果理解为创建类的类, 最相近的应该是Class .
这里不会将那篇文章的内容都复制过来, 只是挑选不一样的地方写一写, 因此, 你最好已经读过那篇文章了. 读这篇时, 最好对照着读.
类也是对象
相比Python, Ruby语言有着最纯粹的面向对象编程的设计. 同样的,Ruby的类的概念也是借鉴于Smalltalk. 关于什么是类, 我更倾向于理解为, 描述一个对象的状态(实例变量)和操作(方法)的代码段.
class ObjectCreator < Object; end #=> nil
my_object = ObjectCreator.new #=>#<ObjectCreator:0x00000000b41400>
print my_object #=>nil
说明:
类默认继承自
Object, 因此< Object非必要. 原文的Python代码也是.在Ruby 中, 在不引起歧义的前提下, 函数调用的
()可以省略. 这里同原文的Python 代码虽然看起来相同, 但原理完全不同.Python2.7中,print实现为语句, 但在Python 3.x中, 实现为全局函数, 则必须加()表示调用.这里的
#=>表示输出的结果,print无输出, 即nil来表示无
同Python, Ruby的类同样也是对象. 不同于Python, Ruby中的class实际是打开一个类, 如果类不存在则创建它. 换句话说, 在Python中重复class定义同一类, 后者会覆盖前者, 而在Ruby中, 类是同一个, 后者只是给这个类添加了新的方法或变量.
Python:
class N1:
def __init__(self, name):
self.name = name
def hello(self, s):
return self.name + s
N1("lzp").hello(" is good man") #=> "lzp is good man"
class N1:
def __init__(self, name):
self.name = name
def world(self, s):
return self.name + s
N1("lzp").hello(" is good man") #=> AttributeError, 无属性
N1("lzp").world(" is good man") #=> "lzp is good man"
Ruby:
class N1
def initialize(name)
@name = name
end
def hello(s)
@name + s
end
end
N1.new("lzp").hello(" is good man") #=> "lzp is good man"
class N1
def initialize(name)
@name = name
end
def world(s) @name + s end
end
N1("lzp").hello(" is good man") #=> "lzp is good man"
N1("lzp").world(" is good man") #=> "lzp is good man"
Ruby少了无语的
self, 但多了无语的end.Ruby的函数默认返回最后一个表达式的值, 但在Python中则必须显示地
return.Ruby的方法定义可以写成一行,Python来咬我啊
很多语言都声称 _xx语言中一切都是对象_, 包括Java. 很明显, 不同语言中的对象概念应该是有区别的, 那么如何来理解对象呢. 这里我基本同意原文中所说, 可赋值, 可拷贝, 可增加属性, 可作参传递.
注意, 不要将对象和对象的引用混淆, 对象的引用往往表现为常见的各种标识符.
Rb: ObjectCreator.to_s #=> "ObjectCreator"
Py: str(ObjectCreator) #=> <class '__main__.ObjectCreator'>
由于print函数实际是调用对象转字符串后输出, 并无特殊意义. 下面的例子更好地展示了, 作参传递.
Python:
def new(o): return o()
oc1 = new(ObjectCreator) #=><__main__.ObjectCreator at 0x...>, 新的实例对象
Ruby:
def new(o) o.new end
oc1 = new(ObjectCreator) #=>#<ObjectCreator:0x...>, 新的实例对象
Python属性操作
Python 中有3个全局函数, 用于对象的属性操作.
hasattr(obj, 'attr_name')判断对象是否有此属性,getattr(obj, 'attr_name')获取对象指定属性,setattr(obj, 'attr_name', attr_value)则是设置指定属性obj.new_attr = attr_value设置属性.delattr(obj, 'attr_name')删除属性.
Python中的属性是一个宽泛的概念, 包括类变量, 实例变量, 类方法和实例方法. 这其中的区别是非常经典的, 且在不同语言中有不同的名称, 有不同的书面写法.
类变量, 通常指依附于类本身而非类的实例的变量, 表述的是类的状态
实例变量, 类的每个实例有独立的变量, 来表述实例对象的状态
类方法, 通过类名调用的方法
实例方法, 通过类的实例对象调用的方法
在Python中, 通过给self.var_name赋值创建实例变量, 类定义中方法外赋值的非self变量都是类变量. 定义方法时, 传递有self参数的是实例方法, 否则为类方法.
Python:
class N2:
class_var = 3 # 类变量, 也能通过实例对象访问
def __init__(self, name):
self.name = name #实例变量
def hello(self, s):
return "hello " + self.name + s
def world(s):
return "world " + N2.c_var + s
n2 = N2("lzp")
n2.hello(" is good man") #=> "hello lzp is good man"
N2.hello(n2, " is good man") #=> "hello lzp is good man"
n2.world(" lzp") #=> 函数只要参数, 但参数多余
N2.world(" lzp") #=> "world lzp"
Python在类的方法设计上很取巧. 就如之后所说, Python其实是没有类方法一说的, 全部都是函数. 类的方法第一个参数是self, 像在world方法定义中, 没有self, 方法内是不能引用实例变量的. 且此处是不是self也无所谓, 任意标识符都可以, 基于惯例使用self. 且在对象上调用方法, 本质上只是将对象作为接收者, 作为第一参数传递给函数. 若函数的第一参数不是self, 则在对象上调用方法会提示多余参数.
在Python中, 函数是对象, 同其他所有对象一样. 因此大一统的去理解Python的类概念就是: 类是对象, 对象有属性, 属性即变量名和其对应的对象. 若对应的对象是函数对象, 则对应的变量是函数名, 其中第一参数为self的为类实例方法.
从属性的角度重新定义N2, Python:
class N2: pass
N2.c_var = 3
def init(self, name): self.name = name
N2.__init__ = init
N2.hello = lambda self, s: "hello" + self.name + s
N2.world = lambda s: "world " + N2.c_var + s
这让我想起了USB, 支持热插拔, 即插即用, 想插就插,Python老爹真任性. 这里使用了lambda来定义匿名函数.
Ruby属性操作
Ruby没有属性一说, 但你也可以去宽泛地去理解. 相反的,Ruby的类变量, 实例变量, 类方法和实例方法是清晰地分开的, 毕竟是纯粹地面向对象. 另一个,Ruby其实没有函数一说, 所有函数都有其所属的类, 没有单独的函数, 或者说Ruby只有方法. 关于属性, 另一个其他面向对象语言中相似的概念是 _域_, 就是在类中占块地, 放变量还是函数都行.
Ruby:
class N2
@c_i_var = 1 #类的实例变量
@@c_var = 3 #类变量, 子类可继承
def initialize(name)
@name = name
end
def hello(s)
"hello" + @name + s
end
def self.world(s) #类方法
"world " + @@c_var.to_s + s
end
end
N2.new("lzp").hello(" is good man") #=> "lzp is good man"
N2.world(" is good man") #=> "world 3 is good man"
在这里, 类的实例变量可以理解为类作为对象的实例变量. 实例变量是专属于对象的. 而类变量则是属于整个类体系的, 即它的所有子类都可以访问.
回到原文, Python中的属性对应Ruby的多个概念. 因此对属性的操作也是分不同的在进行.
Ruby:
n1 = N1.new("lzp")
n1.instance_variables # 返回所有实例变量
n1.instance_variable_set("@age", 3) #=> 设置实例变量
n1.instance_variable_get("@age") #=>实例变量
n1.instance_variable_defined?("@age") #=> 判断有无
n1.class_variables # 返回所有类变量
n1.class_variable_get/set/defined? #同上
N1.instance_methods(false) # 列出所有非继承的实例方法
N1.singleton_methods # 列出所有非继承的类方法
这里的singleton_methods可以理解为类方法. 但严格地说, 它是专属于对象的方法. 若专属于类, 则成为类方法. 换句话说,Ruby没有类方法一说, 称为单件方法.
Ruby中, 一切皆对象. 因此有必要来理解下Ruby的对象模型, 详细地建议看 _Ruby元编程_一书.
对象由状态, 所属类的引用和操作构成. 状态和操作都是专属的, 只能由本对象进行.运算. 普通对象的状态即实例变量, 操作即单件方法, 类对象的状态即类的实例变量即类变量, 类对象的操作即类的单件方法即类方法, 其实本质是相同的. 每个对象都存储有对所属类的引用, 以此来知晓可调用的实例方法.
所谓所属类的引用, 很简单, 在对象上调用#class方法即可
1.class #=> Fixnum
"1".class #=> String
Fixnum.class #=> Class
String.class #=> Class
在后文会看到Python中相应的概念type.
动态地创建类
Ruby也能在函数中创建类.
def choose_class(name)
if (name=='foo')
Class.new {def hello "hello" end}
else
Class.new {def world "world" end}
end
end
MyClass = choose_class('foo')
MyClass.new.hello #=> "hello"
这里不能使用原文中相似的class, 会提示不能在def中定义类. 不得不提前使用大招Class.new.
之前写到String.class为Class, 也就是说, 在Ruby中, 所有的类都是Class的对象. 注意大小写. 自然, 创建新的类, 也就是创建Class的实例对象, 使用new操作, 同其他所有类一样. 不过创建的是匿名类, 赋值给一个首字母大写的常量名即可.
a = Class.new
a.name #=> nil
a.new.class #=> xxx
A = a
A.name #=> A
A.new.class #=> A
好了, 原文进行到了Python的所有类的type都是type. 在本质上, 一切类的生成都是通过调用type进行的.
将上述Ruby代码原样翻译过来, 对应的Python代码为:
def choose_class(name):
if name=='foo':
return type('Foo', (), {'hello': lambda self:"hello"})
else:
return type('Bar', (), {'world': lambda self:"world"})
MyClass = choose_class('foo')
MyClass().hello() #=> "hello"
解释下参数, 第一个是类名字符串, 第二个基类的元组,Python支持多继承, 可以有多个基类, 所谓的基类可以理解为超类, 父类等概念. 第三个是属性, 由前所知, 类中的一切都是属性. 如此即可定义一个新类.
但不同于Ruby, type的第一个参数即类名, 跟MyClass无关, 即赋值不会改变类名. 但Ruby是在将类对象第一次赋值给常量时生成类名的, 之后赋值也不会改变.
在Ruby中, Class.new(superclass)来表示继承类.Ruby中只支持单继承, 通过模块来添加不同的功能.
前文提到,Ruby的类有打开性质, 给类添加方法和变量是非常方便.
到底什么是元类
这里需要先普及几个常用的操作:
Python:
a = 1
a.__class__ #=> int, 对象的类
type(1) #=> int
int.__base__ #=> object, 类的基类
int.__bases__ #=> (object,), 类的基类元组
Ruby:
1.class #=> Fixnum, 对象的类
Fixnum.superclass #=> Integer, 类的超类
Fixnum.ancestors #=> [Fixnum, Integer, Numeric, Comparable, Object, Kernel, BasicObject], 类的祖先链
所谓祖先链, 即类, 类的超类, 类的超类的超类, ...一直到最初始的类, 即BasicObject. 其实, 在1.9之前, 所有类都是继承自Object, 后来又在前面加入了BasicObject, 个人猜测是为了所谓洁净室技术吧.
原文提到, 不断地调用.__class__属性, 最终会到达type类型 ,Ruby中对应的, 不断调用.class方法, 最终会到达Class类型. 原文中可以从type继承, 来创建元类. 但在Ruby中是不能创建Class的子类.
原文提到的__metaclass__属性, 我思考了很久, 基本确认Ruby中没有相似的概念. 就举的将属性名大写的例子而言, 应该是在用class定义类时, 会自动调用这个属性(所引用的函数对象). 初步看, 有种钩子方法的感觉. 就是"定义类"这个事件发生时, 会自动触发执行__metaclass__属性.
Ruby也有一些钩子方法:
included表示模块被包含时执行,extended表示模块被后扩展时执行,prepended表示模块被前扩展时执行,inherited表示类被继承时执行,method_missing表示对象调用不存在的方法时执行.
但目前没找到当定义类时被执行的钩子方法. 所以像原文的大写属性名的操作, 还真不知道如何进行. 但事实上,Ruby的对应属性的标识符有严格的规定, 不可能大写首字母. 如类变量@@var, 实例变量@var, 方法名two_method.
但如果实现不了这个, 总觉得Ruby有种被比下去的感觉, 虽然大写所有属性首字母的操作似乎没有意义.
class N
def hello; "hello"; end
instance_methods(false).each {|x| alias_method x.capitalize, x; remove_method x}
end
N.new.Hello #=> "hello"
N.new.hello #=> 方法未定义
这是大写所有实例方法名的首字母, 核心的思想是, 为原方法建立新的别名, 再删掉原方法. 同Python一样,Ruby的类是在执行代码.
class N; puts "hello"; end #=> "hello"
Ruby:
class N
def self.world; "world"; end
class << self
instance_methods(false).each {|x| alias_method x.capitalize, x; remove_method x}
end
end
N.World #=> "world"
N.world #=> 方法未定义
这是大写所有的类方法名的首字母.
class N
@name = "lzp"
instance_variables.each {|x| instance_variable_set("@"+x.to_s[/\w+/].capitalize, @name); @name = nil}
end
N.class_eval {@Name} #=> "lzp"
N.class_eval {@name} #=> nil
这是大写所有的类的实例变量.
由于Ruby的实例变量默认是不能从外部访问的, 不得不使用.class_eval来打开类的上下文.
不存在如何大写所有实例变量的代码, 因此在类实例化前, 实例对象的实例变量是不存在的.
好吧, 我承认, 这实现的很别扭. 在同一操作的表述上, 不同语言有不同的书面写法, 也自然有简单有繁杂.
函数式特性
谈点别的, 有关函数式特性, 使用map/filter/reduce.
Python:
a = ["he", "hk", "ok"]
list(map(lambda x: x*2, a)) #=>["hehe", "hkhk", "okok"]
list(filter(lambda x: x.startswith("h"), a)) #=> ["he", "hk"]
import functiontools.reduce
reduce(lambda x,y: x+":"+y, a) #=> "he:hk:ok"
用上述函数来替换原文中的语句:
dict(map(lambda i: (i[0].upper(), i[1]), filter(lambda i: not i[0].startswith("__"), future_class_attr.items())))
好吧, 我承认我的Python技术真不高, 如果真写成一行, 完全看不懂了, 原文作者那样写更清晰简洁易懂, 当然更主要的是, 用map/filter会引入新的难点, 容易偏离主题.
希望有高手能告诉我, 将一个类的所有非"__"的属性的键变为大写如何以更函数式的方式表达出来.
Ruby:
a = ["he", "hk", "ok"]
a.map {|x| x*2} #=> ["hehe", "hkhk", "okok"]
a.select {|x| x.start_with? "h"} #=> ["he", "hk"]
a.reject {|x| x.start_with? "h"} #=> ["ok"]
a.reduce {|sum, x| sum + ":" + x} #=> "he:hk:ok"
同样的, 用上述来替换原文的代码.
future_class_attr.reject {|k,v| k.start_with? "__"}.map {|k,v| k.upcase}
Python3.x删除了reduce函数, 推荐使用for循环, 也可以使用funtools.reduce. 这跟Ruby完全不同,Ruby提倡使用each, map等迭代, 而for在底层也是在调用each.
一切皆对象.
Python和Ruby都号称一切皆对象, 但很明显两个的对象概念并不完全对等.
Py: 1.__class__ #=> 语法错误
Py: a = 1; a.__class__ #=> int
Rb: 1.class #=> Fixnum
Py: 1.real #=> 语法错误
Py: b = 1; b.real #=> 1
Rb: 1.real #=> 1
Py: "lzp".upper() #=>"LZP", 但在ipython中不补全方法
Py: s = "lzp"; s.upper() #补全
Rb: "lzp".upcase #=> "LZP", 补全
以上说明, 对对象和对象的引用调用方法是有区别的, 具体什么原理以及详细的区别, 我说明不了.
def hello(name): return "hello" + name
hello.__class__ #=> function
Ruby的方法不是对象, 不能赋值, 不能为参传递.
def hello(name); "hello" + name; end
hello.class #=> 参数错误
new_hello = hello #=> 参数错误
def echo(o); o(); end
echo(hello) #=> 参数错误
你是不是觉得问题挺大的, 这几种对象的特征竟然都不满足. 但这些其实是一个错误, 前文有提到, 对于Ruby的方法调用, 在不引起歧义的情况下, ()是可以省略. 在这里, 所有出现hello的位置都默认你在调用方法, 但方法定义有参数, 你不传递参数, 所以错误是同一个, 少参数.
函数作为对象最终用处都是被调用, 因此, 只从表面来看, Ruby中通过def定义的方法不是对象. 但本质上, 在Ruby中, 出现方法名的地方全被视为对方法的调用, 也就是说, hello是方法调用, 而不是方法引用, 并不表征方法本身. 那么如何获取方法本身的对象呢?
new_hello = method :hello
new_hello.call("lzp") #=> "hellolzp"
new_hello.("lzp") #=> "hellolzp"
new_hello["lzp"] #=> "hellolzp"
new_hello.class #=> Method
new_2_hello = new_hello
注意, 在这里可以看出, 在绝大部分语言中, ()都是函数调用的标志. 但在Ruby中, ()只是在有歧义情况下, 区分哪个参数是哪个函数的. 因此, 当函数作为对象时, 不得不创建新的表示调用的标志, 在这里是.call, [], .().
函数并不是唯一的可调用对象.
hello = lambda {|name| "hello" + name}
hello = ->(name) {"hello" + name}
hello = proc {|name| "hello" + name}
hello = Proc.new {|name| "hello" + name}
后记
事实上, Class.new 属于 Ruby 元编程的一部分, 但 Ruby 的元编程就像普通编程一样, 没有任何神秘复杂的语法. 这里真的只是冰山一角.