理解Ruby中的类

发布时间:2019-09-11 11:39:39编辑:auto阅读(39)

    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.classClass, 也就是说, 在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 的元编程就像普通编程一样, 没有任何神秘复杂的语法. 这里真的只是冰山一角.

关键字