广告
返回顶部
首页 > 资讯 > 后端开发 > Python >浅谈Python中对象是如何被调用的
  • 156
分享到

浅谈Python中对象是如何被调用的

2024-04-02 19:04:59 156人浏览 安东尼

Python 官方文档:入门教程 => 点击学习

摘要

目录楔子从 python 的角度看对象的调用从解释器的角度看对象的调用小结楔子 我们知道对象是如何被创建的,主要有两种方式,一种是通过Python/C api,另一种是通过调用类型对

楔子

我们知道对象是如何被创建的,主要有两种方式,一种是通过Python/C api,另一种是通过调用类型对象。对于内置类型的实例对象而言,这两种方式都是支持的,比如列表,我们即可以通过[]创建,也可以通过list(),前者是Python/C API,后者是调用类型对象。

但对于自定义类的实例对象而言,我们只能通过调用类型对象的方式来创建。而一个对象如果可以被调用,那么这个对象就是callable,否则就不是callable。

而决定一个对象是不是callable,就取决于其对应的类型对象中是否定义了某个方法。如果从 Python 的角度看的话,这个方法就是 __call__,从解释器角度看的话,这个方法就是 tp_call。

从 Python 的角度看对象的调用

调用 int、str、tuple 可以创建一个整数、字符串、元组,调用自定义的类也可以创建出相应的实例对象,说明类型对象是可调用的,也就是callable。那么这些类型对象(int、str、tuple、class等等)的类型对象(type)内部一定有 call 方法。

# int可以调用
# 那么它的类型对象、也就是元类(type), 内部一定有__call__方法
print(hasattr(type, "__call__"))  # True

# 而调用一个对象,等价于调用其类型对象的 __call__ 方法
# 所以 int(3.14)实际就等价于如下
print(type.__call__(int, 3.14))  # 3

注意:这里描述的可能有一些绕,我们说 int、str、float 这些都是类型对象(简单来说就是类),而 123、"你好"、3.14 是其对应的实例对象,这些都没问题。但type是不是类型对象,显然是的,虽然我们称呼它为元类,但它也是类型对象,如果 print(type) 显示的也是一个类。

那么相对 type 而言,int、str、float 是不是又成了实例对象呢?因为它们的类型是 type。

所以 class 具有二象性:

  • 如果站在实例对象(如:123、"satori"、[]、3.14)的角度上,它是类型对象
  • 如果站在 type 的角度上,它是实例对象

同理 type 的类型是也是 type,那么 type 既是 type 的类型对象,type 也是 type 的实例对象。虽然这里描述的会有一些绕,但应该不难理解,并且为了避免后续的描述出现歧义,这里我们做一个申明:

  • 整数、浮点数、字符串等等,我们称之为实例对象
  • int、float、str、dict,以及我们自定义的类,我们称之为类型对象
  • type 虽然也是类型对象,但我们称它为元类

所以 type 的内部有 call 方法,那么说明类型对象都是可调用的,因为调用类型对象就是调用 type 的 call 方法。而实例对象能否调用就不一定了,这取决于它的类型对象中是否定义了 call 方法,因为调用一个对象,本质上是执行其类型对象内部的 call 方法。

class A:
    pass
    
a = A()
# 因为我们自定义的类 A 里面没有 __call__
# 所以 a 是不可以被调用的
try:
    a()
except Exception as e:
    # 告诉我们 A 的实例对象不可以被调用
    print(e)  # 'A' object is not callable    
    
# 如果我们给 A 设置了一个 __call__
type.__setattr__(A, "__call__", lambda self: "这是__call__")
# 发现可以调用了
print(a())  # 这是__call__

我们看到这就是动态语言的特性,即便在类创建完毕之后,依旧可以通过type进行动态设置,而这在静态语言中是不支持的。所以type是所有类的元类,它控制了我们自定义类的生成过程,type这个古老而又强大的类可以让我们玩出很多新花样。

但是对于内置的类,type是不可以对其动态增加、删除或者修改属性的,因为内置的类在底层是静态定义好的。因为从源码中我们看到,这些内置的类、包括元类,它们都是PyTypeObject对象,在底层已经被声明为全局变量了,或者说它们已经作为静态类存在了。所以type虽然是所有类型对象的元类,但是只有在面对我们自定义的类,type才具有增删改的能力。

Python 的动态性是解释器将字节码翻译成 C 代码的时候动态赋予的,因此给类动态设置属性或方法只适用于动态类,也就是在 py 文件中使用 class 关键字定义的类。

而对于静态类、或者编写扩展模块时定义的扩展类(两者是等价的),它们在编译之后已经是指向 C 一级的数据结构了,不需要再被解释器解释了,因此解释器自然也就无法在它们身上动手脚,毕竟彪悍的人生不需要解释。

try:
    type.__setattr__(dict, "__call__", lambda self: "这是__call__")
except Exception as e:
    print(e)  # can't set attributes of built-in/extension type 'dict'

我们看到抛异常了,提示我们不可以给内置/扩展类型dict设置属性,因为它们绕过了解释器解释执行这一步,所以其属性不能被动态设置。

同理其实例对象亦是如此,静态类的实例对象也不可以动态设置属性:

class Girl: 
    pass

g = Girl()
g.name = "古明地觉"
# 实例对象我们也可以手动设置属性
print(g.name)  # 古明地觉

lst = list()
try:
    lst.name = "古明地觉"
except Exception as e:
    # 但是内置类型的实例对象是不可以的
    print(e)  # 'list' object has no attribute 'name'

可能有人奇怪了,为什么列表不行呢?答案是内置类型的实例对象没有__dict__属性字典,因为相关属性或方法底层已经定义好了,不可以动态添加。如果我们自定义类的时候,设置了__slots__,那么效果和内置的类是相同的。

当然了,我们后面会介绍如何通过动态修改解释器来改变这一点,举个栗子,不是说静态类无法动态设置属性吗?下面我就来打自己脸:

import GC

try:
    type.__setattr__(list, "ping", "pong")
except TypeError as e:
    print(e)  # can't set attributes of built-in/extension type 'list'

# 我们看到无法设置,那么我们就来改变这一点
attrs = gc.get_referents(tuple.__dict__)[0]
attrs["ping"] = "pong"
print(().ping)  # pong

attrs["append"] = lambda self, item: self + (item,)
print(
    ().append(1).append(2).append(3)
)  # (1, 2, 3)

我脸肿了。好吧,其实这只是我们玩的一个小把戏,当我们介绍完整个 CPython 的时候,会来专门聊一聊如何动态修改解释器。比如:让元组变得可修改,让 Python 真正利用多核等等。

从解释器的角度看对象的调用

我们以内置类型 float 为例,我们说创建一个 PyFloatObject,可以通过3.14或者float(3.14)的方式。前者使用Python/C API创建,3.14直接被解析为 C 一级数据结构,也就是PyFloatObject实例;后者使用类型对象创建,通过对float进行一个调用、将3.14作为参数,最终也得到指向C一级数据结构PyFloatObject实例。

Python/C API的创建方式我们已经很清晰了,就是根据值来推断在底层应该对应哪一种数据结构,然后直接创建即可。我们重点看一下通过类型调用来创建实例对象的方式。

如果一个对象可以被调用,它的类型对象中一定要有tp_call(更准确的说成员tp_call的值是一个函数指针,不可以是0),而PyFloat_Type是可以调用的,这就说明PyType_Type内部的tp_call是一个函数指针,这在Python的层面上我们已经验证过了,下面我们再来通过源码看一下。

//typeobject.c
PyTypeObject PyType_Type = {
    PyVarObject_HEAD_INIT(&PyType_Type, 0)
    "type",                                     
    sizeof(PyHeapTypeObject),                   
    sizeof(PyMemberDef),                        
    (destructor)type_dealloc,                   
    //...                                       
    (ternaryfunc)type_call,                     
    //...
}

我们看到在实例化PyType_Type的时候PyTypeObject内部的成员tp_call被设置成了type_call。这是一个函数指针,当我们调用PyFloat_Type的时候,会触发这个type_call指向的函数。

因此 float(3.14) 在C的层面上等价于:

(&PyFloat_Type) -> ob_type -> tp_call(&PyFloat_Type, args, kwargs);
// 即:
(&PyType_Type) -> tp_call(&PyFloat_Type, args, kwargs);
// 而在创建 PyType_Type 的时候,给 tp_call 成员传递的是 type_call
// 因此最终相当于
type_call(&PyFloat_Type, args, kwargs)

如果用 Python 来演示这一过程的话:

# float(3.14),等价于
f1 = float.__class__.__call__(float, 3.14)
# 等价于
f2 = type.__call__(float, 3.14)

print(f1, f2)  # 3.14 3.14

这就是 float(3.14) 的秘密,相信list、dict在实例化的时候是怎么做的,你已经猜到了,做法是相同的。

# lst = list("abcd")
lst = list.__class__.__call__(list, "abcd")
print(lst)  # ['a', 'b', 'c', 'd']

# dct = dict([("name", "古明地觉"), ("age", 17)])
dct = dict.__class__.__call__(dict, [("name", "古明地觉"), ("age", 17)])
print(dct)  # {'name': '古明地觉', 'age': 17}

最后我们来围观一下 type_call 函数,我们说 type 的 call 方法,在底层对应的是 type_call 函数,它位于Object/typeobject.c中。

static PyObject *
type_call(PyTypeObject *type, PyObject *args, PyObject *kwds)
{  
    // 如果我们调用的是 float
    // 那么显然这里的 type 就是 &PyFloat_Type
    
    // 这里是声明一个PyObject *
    // 显然它是要返回的实例对象的指针
    PyObject *obj;
  
    // 这里会检测 tp_new是否为空,tp_new是什么估计有人已经猜到了
    // 我们说__call__对应底层的tp_call
    // 显然__new__对应底层的tp_new,这里是为实例对象分配空间
    if (type->tp_new == NULL) {
        // tp_new 是一个函数指针,指向具体的构造函数
        // 如果 tp_new 为空,说明它没有构造函数
        // 因此会报错,表示无法创建其实例
        PyErr_FORMat(PyExc_TypeError,
                     "cannot create '%.100s' instances",
                     type->tp_name);
        return NULL;
    }
  
    //通过tp_new分配空间
    //此时实例对象就已经创建完毕了,这里会返回其指针
    obj = type->tp_new(type, args, kwds);
    //类型检测,暂时不用管
    obj = _Py_CheckFunctionResult((PyObject*)type, obj, NULL);
    if (obj == NULL)
        return NULL;

    //我们说这里的参数type是类型对象,但也可以是元类
    //元类也是由PyTypeObject结构体实例化得到的
    //元类在调用的时候执行的依旧是type_call
    //所以这里是检测type指向的是不是PyType_Type
    //如果是的话,那么实例化得到的obj就不是实例对象了,而是类型对象
    //要单独检测一下
    if (type == &PyType_Type &&
        PyTuple_Check(args) && PyTuple_GET_SIZE(args) == 1 &&
        (kwds == NULL ||
         (PyDict_Check(kwds) && PyDict_GET_SIZE(kwds) == 0)))
        return obj;

    //tp_new应该返回相应类型对象的实例对象(的指针)
    //但如果不是,就直接将这里的obj返回
    //此处这么做可能有点难理解,我们一会细说
    if (!PyType_IsSubtype(Py_TYPE(obj), type))
        return obj;
  
    //拿到obj的类型
    type = Py_TYPE(obj);
    //执行 tp_init
    //显然这个tp_init就是__init__函数
    //这与Python中类的实例化过程是一致的。
    if (type->tp_init != NULL) {
        //将tp_new返回的对象作为self,执行 tp_init
        int res = type->tp_init(obj, args, kwds);
        if (res < 0) {
            //执行失败,将引入计数减1,然后将obj设置为NULL
            assert(PyErr_Occurred());
            Py_DECREF(obj);
            obj = NULL;
        }
        else {
            assert(!PyErr_Occurred());
        }
    }
    //返回obj
    return obj;
}

因此从上面我们可以看到关键的部分有两个:

  • 调用类型对象的 tp_new 指向的函数为实例对象申请内存
  • 调用 tp_init 指向的函数为实例对象进行初始化,也就是设置属性

所以这对应Python中的__new__和__init__,我们说__new__是为实例对象开辟一份内存,然后返回指向这片内存(对象)的指针,并且该指针会自动传递给__init__中的self。

class Girl:

    def __new__(cls, name, age):
        print("__new__方法执行啦")
        # 写法非常固定
        # 调用object.__new__(cls)就会创建Girl的实例对象
        # 因此这里的cls指的就是这里的Girl,注意:一定要返回
        # 因为__new__会将自己的返回值交给__init__中的self
        return object.__new__(cls)

    def __init__(self, name, age):
        print("__init__方法执行啦")
        self.name = name
        self.age = age


g = Girl("古明地觉", 16)
print(g.name, g.age)
"""
__new__方法执行啦
__init__方法执行啦
"""

__new__里面的参数要和__init__里面的参数保持一致,因为我们会先执行__new__,然后解释器会将__new__的返回值和我们传递的参数组合起来一起传递给__init__。因此__new__里面的参数除了cls之外,一般都会写

args和

*kwargs。

然后再回过头来看一下type_call中的这几行代码:

static PyObject *
type_call(PyTypeObject *type, PyObject *args, PyObject *kwds)
{  
    //......
    //......
    if (!PyType_IsSubtype(Py_TYPE(obj), type))
        return obj;
  
    //......
    //......
}

我们说tp_new应该返回该类型对象的实例对象,而且一般情况下我们是不写__new__的,会默认执行。但是我们一旦重写了,那么必须要手动返回object.__new__(cls)。可如果我们不返回,或者返回其它的话,会怎么样呢?

class Girl:

    def __new__(cls, *args, **kwargs):
        print("__new__方法执行啦")
        instance = object.__new__(cls)
        # 打印看看instance到底是个什么东东
        print("instance:", instance)
        print("type(instance):", type(instance))
        
        # 正确做法是将instance返回
        # 但是我们不返回, 而是返回个 123
        return 123

    def __init__(self, name, age):
        print("__init__方法执行啦")


g = Girl()
"""
__new__方法执行啦
instance: <__main__.Girl object at 0x000002C0F16FA1F0>
type(instance): <class '__main__.Girl'>
"""

这里面有很多可以说的点,首先就是 init 里面需要两个参数,但是我们没有传,却还不报错。原因就在于这个 init 压根就没有执行,因为 new 返回的不是 Girl 的实例对象。

通过打印 instance,我们知道了object.__new__(cls) 返回的就是 cls 的实例对象,而这里的cls就是Girl这个类本身。我们必须要返回instance,才会执行对应的__init__,否则__new__直接就返回了。我们在外部来打印一下创建的实例对象吧,看看结果:

class Girl:

    def __new__(cls, *args, **kwargs):
        return 123

    def __init__(self, name, age):
        print("__init__方法执行啦")


g = Girl()
print(g, type(g))  # 123 <class 'int'>

我们看到打印的是123,所以再次总结一些tp_new和tp_init之间的区别,当然也对应__new__和__init__的区别:

  • tp_new:为该类型对象的实例对象申请内存,在Python的__new__方法中通过object.__new__(cls)的方式申请,然后将其返回
  • tp_init:tp_new的返回值会自动传递给self,然后为self绑定相应的属性,也就是进行实例对象的初始化

但如果tp_new返回的不是对应类型的实例对象的指针,比如type_call中第一个参数接收的&PyFloat_Type,但是tp_new中返回的却是PyLonGobject *,所以此时就不会执行tp_init。

以上面的代码为例,我们Girl中的__new__应该返回Girl的实例对象才对,但实际上返回了整型,因此类型不一致,所以不会执行__init__。

下面我们可以做总结了,通过类型对象去创建实例对象的整体流程如下:

  • 第一步:获取类型对象的类型对象,说白了就是元类,执行元类的 tp_call 指向的函数,即 type_call
  • 第二步:type_call 会调用该类型对象的 tp_new 指向的函数,如果 tp_new 为 NULL,那么会到 tp_base 指定的父类里面去寻找 tp_new。在新式类当中,所有的类都继承自 object,因此最终会执行 object 的 __new__。然后通过访问对应类型对象中的 tp_basicsize 信息,这个信息记录着该对象的实例对象需要占用多大的内存,继而完成申请内存的操作

调用type_new 创建完对象之后,就会进行实例对象的初始化,会将指向这片空间的指针交给 tp_init,但前提是 tp_new 返回的实例对象的类型要一致。

所以都说 Python 在实例化的时候会先调用 new 方法,再调用 init 方法,相信你应该知道原因了,因为在源码中先调用 tp_new、再调用的 tp_init。

static PyObject *
type_call(PyTypeObject *type, PyObject *args, PyObject *kwds)
{  
    //调用__new__方法, 拿到其返回值
    obj = type->tp_new(type, args, kwds);

    if (type->tp_init != NULL) {
        //将__new__返回的实例obj,和args、kwds组合起来
        //一起传给 __init__
        //其中 obj 会传给 self,
        int res = type->tp_init(obj, args, kwds);
        //......
    return obj;
}

所以源码层面表现出来的,和我们在 Python 层面看到的是一样的。

小结

到此,我们就从 Python 和解释器两个层面了解了对象是如何调用的,更准确的说我们是从解释器的角度对 Python 层面的知识进行了验证,通过 tp_new 和 tp_init 的关系,来了解 newinit 的关系。

另外,对象调用远不止我们目前说的这么简单,更多的细节隐藏在了幕后,只不过现在没办法将其一次性全部挖掘出来。后续我们会循序渐进,一点点揭开它什么面纱,并且在这个过程中还会不断地学习到新的东西。比如说,实例对象在调用方法的时候会自动将实例本身作为参数传递给 self,那么它为什么传递呢?解释器在背后又做了什么工作呢?这些我们就以后慢慢说吧。

到此这篇关于浅谈Python中对象是如何被调用的的文章就介绍到这了,更多相关Python对象调用内容请搜索编程网以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程网!

--结束END--

本文标题: 浅谈Python中对象是如何被调用的

本文链接: https://www.lsjlt.com/news/117179.html(转载时请注明来源链接)

有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

本篇文章演示代码以及资料文档资料下载

下载Word文档到电脑,方便收藏和打印~

下载Word文档
猜你喜欢
  • 浅谈Python中对象是如何被调用的
    目录楔子从 Python 的角度看对象的调用从解释器的角度看对象的调用小结楔子 我们知道对象是如何被创建的,主要有两种方式,一种是通过Python/C API,另一种是通过调用类型对...
    99+
    2022-11-10
  • 浅谈Python中的常用内置对象
    目录一、常用的python内置对象二、变量的定义与使用三、整数四、浮点数五、布尔类型六、字符串类型七、注释一、常用的python内置对象 ...
    99+
    2022-11-12
  • 浅谈Python 中整型对象的存储问题
    在 Python 整型对象所存储的位置是不同的, 有一些是一直存储在某个存储里面, 而其它的, 则在使用时开辟出空间. 说这句话的理由, 可以看看如下代码: a = 5 b = 5 a is b # T...
    99+
    2022-06-04
    浅谈 整型 对象
  • 浅谈python中的面向对象和类的基本语法
    当我发现要写python的面向对象的时候,我是踌躇满面,坐立不安呀。我一直在想:这个坑应该怎么爬?因为python中关于面向对象的内容很多,如果要讲透,最好是用面向对象的思想重新学一遍前面的内容。这个坑是如...
    99+
    2022-06-04
    浅谈 面向对象 语法
  • 二维码是如何被 Python 对象处理的?
    随着移动互联网的普及,二维码已经成为了现代社会中不可或缺的一部分。在 Python 中,我们可以使用第三方库 qrcode 来生成二维码,并对其进行处理。那么,二维码在 Python 中是如何被处理的呢? 一、生成二维码 生成二维码是二维...
    99+
    2023-10-07
    对象 二维码 unix
  • 浅谈React中的浅比较是如何工作的
    React 中浅比较的概念无处不在,它在不同的流程中起着关键的作用,也可以在React组件的多个生命周期中找到。比如,React Hooks中的依赖数组,通过React.memo进行...
    99+
    2022-11-13
  • 浅谈Python中用datetime包进行对时间的一些操作
    1. 计算给出两个时间之间的时间差 import datetime as dt # current time cur_time = dt.datetime.today() # one day pre_t...
    99+
    2022-06-04
    浅谈 中用 操作
  • 浅谈如何在bat文件中调用另一个bat文件
    目录情景一:两个bat文件在同一个目录下情景二:两个bat文件不在同一个目录下情景三:开启一个新的cmd窗口来运行另一个bat文件情景一:两个bat文件在同一个目录下 有时候我们需要...
    99+
    2022-11-13
  • ASP 缓存中的对象如何被索引和检索?
    在 ASP 中,缓存是一种非常重要的机制。它可以帮助我们将一些常用的数据或者对象保存在内存中,以便在后续的请求中可以快速地访问。在 ASP 中,我们可以通过 Cache 对象来进行缓存的相关操作。但是,当我们在缓存中存储了大量的对象时,如...
    99+
    2023-08-11
    缓存 索引 对象
  • 浅谈我是如何用redis做实时订阅推送的
    前阵子开发了公司领劵中心的项目,这个项目是以redis作为关键技术落地的。 先说一下领劵中心的项目吧,这个项目就类似京东app的领劵中心,当然图是截取京东的,公司的就不截了。。。 ...
    99+
    2022-11-11
  • 什么是Python shell对象,如何在Django中使用?
    Python shell对象是Python解释器的一种交互式界面,它允许用户在命令行中输入Python代码并立即执行它们。Python shell对象是Python语言的一个重要组成部分,它可以帮助开发人员快速测试和调试代码。在本文中,我们...
    99+
    2023-06-02
    shell 对象 django
  • Python中如何使用函数对象
    本篇文章为大家展示了Python中如何使用函数对象,内容简明扼要并且容易理解,绝对能使你眼前一亮,通过这篇文章的详细介绍希望你能有所收获。Python是如何工作的?我们将从高层开始了解python的内部工作。你在你的python REPL里...
    99+
    2023-06-17
  • Python中如何使用内置对象
    这期内容当中小编将会给大家带来有关Python中如何使用内置对象,文章内容丰富且以专业的角度为大家分析和叙述,阅读完这篇文章希望大家可以有所收获。以下是引用片段:PyAPI_DATA(PyTypeObject) PyType_Ty...
    99+
    2023-06-17
  • 浅谈MySQL中是如何实现事务提交和回滚的
    目录什么是事务redo logundo log总结一下什么是事务 事务是由数据库中一系列的访问和更新组成的逻辑执行单元 事务的逻辑单元中可以是一条SQL语句,也可以是一段SQL逻辑,...
    99+
    2022-11-13
  • Python中的面向对象编程是如何实现的?
    Python中的面向对象编程是如何实现的?面向对象编程(Object-Oriented Programming,简称OOP)是一种程序设计范式,在Python中也得到了广泛的应用。通过面向对象编程,我们可以将程序中的数据和功能组织为对象,并...
    99+
    2023-10-25
    Python 实现方式 面向对象 (OOP)
  • Python 中的对象是什么?如何在 LeetCode 上使用它们?
    Python 是一门面向对象的编程语言,它的一切都是对象。在 Python 中,一切都被视为对象,包括数字、字符串、函数等等。在本文中,我们将深入探讨 Python 中的对象以及如何在 LeetCode 上使用它们。 Python 中的对...
    99+
    2023-11-13
    对象 leetcode windows
  • mybatisresultmap如何为对象赋值的调用顺序
    目录resultmap 为对象赋值的调用顺序java bean定义如下部分mybatis映射文件如下createResultObject方法的代码如下使用resultMap时需注意的...
    99+
    2022-11-13
  • 数组容器对象在Python中是如何实现的?
    数组是一种常见的数据结构,用于存储一系列相同类型的数据。在Python中,我们可以使用数组容器对象来实现数组。本文将介绍数组容器对象在Python中的实现方式,并提供一些演示代码,帮助读者更好地理解和应用该容器对象。 一、数组容器对象的定义...
    99+
    2023-08-20
    数组 容器 对象
  • Java中的对象是如何在NPM中使用的?
    随着Node.js的流行,越来越多的Java开发者开始将自己的代码转移到这个平台上。在Node.js中,有一个流行的包管理器叫做NPM(Node Package Manager)。那么,在Java中的对象是如何在NPM中使用的呢?本文将为...
    99+
    2023-08-19
    对象 npm apache
  • Python和Git在Windows中如何使用对象?
    在当今时代,Python和Git是两个非常流行的工具,它们可以被用于各种不同的应用场景中。Python是一种高级编程语言,可以用于开发各种类型的应用程序,而Git是一种分布式版本控制系统,可以用于跟踪和管理代码的修改。 在本文中,我们将介...
    99+
    2023-11-13
    git windows 对象
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作