• Python:__call__和__getattr__组合黑魔法


    一、问题的提出
    github上jqdatasdk库中,有一段这样的代码,让人感觉到很神奇。
    在api.py文件

    https://github.com/JoinQuant/jqdatasdk/blob/master/jqdatasdk/api.py
    
    • 1

    具体代码如下:

    
    @assert_auth
    def get_price(security, start_date=None, end_date=None, frequency='daily',
                  fields=None, skip_paused=False, fq='pre', count=None, panel=True, fill_paused=True):
    
        if panel and PandasChecker.check_version():
            panel = False
        security = convert_security(security)
        start_date = to_date_str(start_date)
        end_date = to_date_str(end_date)
        if (not count) and (not start_date):
            start_date = "2015-01-01"
        if count and start_date:
            raise ParamsError("(start_date, count) only one param is required")
        return JQDataClient.instance().get_price(**locals())
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    神奇之处在哪里呢?在

    JQDataClient.instance().get_price(**locals())
    
    • 1

    如果不细看,你感觉很正常呀。不就是对象调用其方法嘛。但是你到client.py文件中:

    https://github.com/JoinQuant/jqdatasdk/blob/master/jqdatasdk/client.py
    
    • 1

    具体代码中,的确有class JQDataClient的定义,但没也没有看到里面有get_price呀

    class JQDataClient(object):
    
        _threading_local = threading.local()
        _auth_params = {}
    
        _default_host = "39.107.190.114"
        _default_port = 7000
    
        request_timeout = 300
        request_attempt_count = 3
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    这是如何一回事?
    难道,对象可以调用任意的函数?不会吧,还有这么随便的。

    其实…; 黑魔法的确就是class JQDataClien里面的两个方法:

    def __call__(self, method, **kwargs):
            err, result = None, None
            for _ in range(self.request_attempt_count):
                try:
                    result = self.query(method, kwargs)
                    break
                except socket_error as ex:
                    if not self._ping_server():
                        self._reset()
                    err = ex
                    time.sleep(0.6)
                except ResponseError as ex:
                    err = ex
    
            if result is None and isinstance(err, Exception):
                raise err
    
            return self.convert_message(result)
    
        def __getattr__(self, method):
            return lambda **kwargs: self(method, **kwargs)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    其实,准确的说,还有一个query()。

    二、细说一下__call__和__getattr__组合黑魔法

    通常我们能单用一个,就已经不常见了,更不用说,把这两个组合起来用了。
    下面,做一下简单的POC测试,来验证一下。

    class Client:
        def __init__(self, host):
           self.host = host
        def query(self, method, params):
            print(f"hello method {method} {params}!")
        def __call__(self, method, **kwargs):
            print(f"__call__  -> method :{method}!")
            result = self.query(method, kwargs)
            print('query ok!')
        def __getattr__(self, method):
            print(f"__getattr__ -> method :{method}")
            return lambda **kwargs: self(method, **kwargs)
    CLIENT = Client("122.122.122.122") 
    def get_price(start_date,close_date):
        print("get_price : start_date: {start_date} close_date :{close_date}")
        print(f"print -> locals : {locals()}")
        CLIENT.get_price(**locals())
    def test():
        start_date = "2020-01-01"
        close_date = "2021-01-01"
        get_price(start_date,close_date)
    test()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    输出:

    get_price : start_date: {start_date} close_date :{close_date}
    print -> locals : {'start_date': '2020-01-01', 'close_date': '2021-01-01'}
    __getattr__ -> method :get_price
    __call__  -> method :get_price!
    hello method get_price {'start_date': '2020-01-01', 'close_date': '2021-01-01'}!
    query ok!
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    看明白了吗?

    三、为什么要这一对组合?

    两者分开用,不是可以?何必多此一举?

    import sys
    sys.path.append("/home/songroom/.local/lib/python3.6/site-packages")
    import random
    import itertools
    import uuid
    from sqlalchemy.orm import scoped_session, sessionmaker
    class Client:
        def __init__(self, host):
           self.host = host
           self.request_id =  itertools.count(
                random.choice(range(0, 1000, 10))
            )
        def query(self, method, params):
            print(f"hello method {method} {params}!")
        def __call__(self, method, **kwargs):
            print(f"__call__  -> method :{method}!")
            result = self.query(method, kwargs)
            print('query ok!')
        def __getattr__(self, method):
            print(f"__getattr__ -> method :{method}")
            return lambda **kwargs: self(method, **kwargs)
    
    CLIENT = Client("122.122.122.122") 
    
    def get_price(start_date,close_date):
        print("get_price : start_date: {start_date} close_date :{close_date}")
        print(f"print -> locals : {locals()}")
        CLIENT(get_price,**locals())  #CLIENT.get_price(**locals())
    def test():
        start_date = "2020-01-01"
        close_date = "2021-01-01"
        get_price(start_date,close_date)
    
    test()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34

    输出:

    get_price : start_date: {start_date} close_date :{close_date}
    print -> locals : {'start_date': '2020-01-01', 'close_date': '2021-01-01'}
    __call__  -> method :!
    hello method  {'start_date': '2020-01-01', 'close_date': '2021-01-01'}!
    query ok!
    
    • 1
    • 2
    • 3
    • 4
    • 5

    的确,__call__本来是可以单玩的,但是这种情况下,只能用以下()调用的形式:

    CLIENT(get_price,**locals())
    
    • 1

    但是,和__getattr__组合后,就不一样了,更加优雅了,可以用“.”号来调用,让人在外感上好象更自然一些:

    CLIENT.get_price(**locals())
    
    • 1

    其实,在内部实际上,_getatrr__把“.”打通,然后再调用()的方法。这就是魔法的全部。

  • 相关阅读:
    el-tree结合el-switch实现状态切换
    VoLTE端到端业务详解 | 应用案例二
    nginx 配置防盗链(了解)
    Redis系列12:Redis 的事务机制
    vue3解决跨域问题!亲测有效
    Selenium打开页面,出现弹窗需要登录账号密码,怎么解决?
    Java进阶——如何查看Java字节码
    负载均衡原理
    哈希表的模拟实现
    Android 集成onenet物联网平台
  • 原文地址:https://blog.csdn.net/wowotuo/article/details/126655156