Python:__call__和__getattr__组合黑魔法

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

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

具体代码如下:


@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())

神奇之处在哪里呢?在

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

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

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

具体代码中,的确有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

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

其实…; 黑魔法的确就是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)

其实,准确的说,还有一个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()

输出:

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!

看明白了吗?

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

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

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

输出:

get_price : start_date: {start_date} close_date :{close_date}
print -> locals : {'start_date': '2020-01-01', 'close_date': '2021-01-01'}
__call__  -> method :<function get_price at 0x7f50095b2310>!
hello method <function get_price at 0x7f50095b2310> {'start_date': '2020-01-01', 'close_date': '2021-01-01'}!
query ok!

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

CLIENT(get_price,**locals())

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

CLIENT.get_price(**locals())

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

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值