技术手记

Python 多层 decorator 内获取原始函数参数字典

0. 在 decorator 里获取原始函数的参数值

项目里做了一个通用锁,使用 decorator 来方便的包住某些需要限制并发的函数。因为并发不是函数级别的,而是根据参数来限制,所以需要把参数传到通用锁的 decorator 里,代码大致如下

def lock_decorator(key=None):
    def _lock_func(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            # TODO: get lock_key
            lock_key = kwargs.get(key, '')
            with LockContext(key=lock_key):
                return func(*args, **kwargs)
        return wrapper
    return _lock_func

@lock_decorator(key='uid')
def apply_recharge(uid, amount):
    # ...

考虑到函数调用不一定都是带着参数名的,就是说调用时不一定所有参数都会进 **kwargs,那就需要从 **args 里面按参数名捞参数

怎么能知道原函数的参数名列表,翻各种手册的得知可以用 inspect.getargspec(func) 来搞到,那么上面的 TODO 部分就可以改写如下

            args_name = inspect.getargspec(func)[0]
            key_index = args_name.index(key)
            if len(args) > key_index:
                lock_key = args[key_index]
            else:
                lock_key = kwargs.get(key, '')

自此,一切都很美好

1. 在 decorator 里获取原始函数的调用参数字典

项目里又做了个通用的 Logger,也做成 decorator 往目标函数一套,就可以打印出调用时的入参和结果,大致如下

def log_decorator():
    def _log_func(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            # TODO: get full args
            print('call func [{}] with args [{}] and kwargs [{}]'.format(func.__name__, args, kwargs))
            ret = func(*args, **kwargs)
            print('func [{}] return [{}]'.format(func.__name__, ret))
            return ret
        return wrapper
    return _log_func

@log_decorator()
def apply_recharge(uid, amount):
    # ...

看起来也还好,不过因为函数可能带默认参数,而且也希望看到 **args 到底传到哪个参数上,还是希望把所有参数按 Key-Value 的形式打印出来,跟处理通用锁一样,用 inspect.getargspec(func) 把参数名和默认值都摸出来,再考虑一下可变参数的情况,对上面的 TODO 部分改写如下

            args_name, _, _, func_defaults = inspect.getargspec(func)
            parsed_kwargs = dict()
            # default args
            default_args = dict()
            default_start = len(args_name, func_defaults)
            for idx, d in enumerate(func_defaults):
                default_args[args_name[default_start + idx]] = d
            parsed_kwargs.update(default_args)
            # args with name
            varargs_start = len(args_name)
            for idx, a in enumerate(args[:varargs_start]):
                parsed_kwargs[args_name[idx]] = a
            # varargs
            if len(args) > varargs_start:
                parsed_kwargs['varargs'] = args[varargs_start:]
            # kwargs
            parsed_kwargs.update(kwargs)
            print('call func [{}] with args [{}]'.format(func.__name__, parsed_kwargs))

到这里,还是很美好

2. 多层 decorator 怎么拿到最原始函数的参数表

注意到上面两个例子里,apply_recharge 都只套了一个 decorator,如果两个一起用会发生什么?

根据 PEP318 里对 decorator 的定义

@dec2
@dec1
def func(arg1, arg2, ...):
    pass

等价于

def func(arg1, arg2, ...):
    pass
func = dec2(dec1(func))

这里就出问题了,dec2 拿到的传入函数其实是 dec1 而不是 func。不过在把 lock_decoratorlog_decorator 混用时,不管谁写前面,func.__name__ 都是原始的函数名,说明也还是有神器的地方做了穿透,但是 inspect.getargspec 又拿不到最底层函数的参数表,导致不管谁前谁后,都有问题

注意到每个 decorator 构建的时候都又封了一个 @functools.wraps(func),这个是干嘛的呢?以前都是无脑用,也没想过为啥要包一层这个,去掉会怎样?

去掉这个 @functools.wraps(func) 后,inspect.getargspec 还是一样的只能拿到最近一层的信息,而之前本来可以拿到底层的 func.__name__ 也变成最近一层的函数名了,说明这里做了穿透。那么去看看代码吧

# functools.py

from _functools import partial, reduce

# update_wrapper() and wraps() are tools to help write
# wrapper functions that can handle naive introspection

WRAPPER_ASSIGNMENTS = ('__module__', '__name__', '__doc__')
WRAPPER_UPDATES = ('__dict__',)
def update_wrapper(wrapper,
                   wrapped,
                   assigned = WRAPPER_ASSIGNMENTS,
                   updated = WRAPPER_UPDATES):
    """Update a wrapper function to look like the wrapped function

       wrapper is the function to be updated
       wrapped is the original function
       assigned is a tuple naming the attributes assigned directly
       from the wrapped function to the wrapper function (defaults to
       functools.WRAPPER_ASSIGNMENTS)
       updated is a tuple naming the attributes of the wrapper that
       are updated with the corresponding attribute from the wrapped
       function (defaults to functools.WRAPPER_UPDATES)
    """
    for attr in assigned:
        setattr(wrapper, attr, getattr(wrapped, attr))
    for attr in updated:
        getattr(wrapper, attr).update(getattr(wrapped, attr, {}))
    # Return the wrapper so this can be used as a decorator via partial()
    return wrapper

def wraps(wrapped,
          assigned = WRAPPER_ASSIGNMENTS,
          updated = WRAPPER_UPDATES):
    """Decorator factory to apply update_wrapper() to a wrapper function

       Returns a decorator that invokes update_wrapper() with the decorated
       function as the wrapper argument and the arguments to wraps() as the
       remaining arguments. Default arguments are as for update_wrapper().
       This is a convenience function to simplify applying partial() to
       update_wrapper().
    """
    return partial(update_wrapper, wrapped=wrapped,
                   assigned=assigned, updated=updated)

原来就是这里耍花样了,把底层函数的 ('__module__', '__name__', '__doc__') 都赋给了 decorator 封起来的这一层,欺骗更上层用 __name__ 去判断时就当我是底层

那我也学这个,把 inspect.getargspec 的地方也处理下不就完了,去看看这个地方是怎么拿参数表的

# inspect.py

def getargspec(func):
    """Get the names and default values of a function's arguments.

    A tuple of four things is returned: (args, varargs, varkw, defaults).
    'args' is a list of the argument names (it may contain nested lists).
    'varargs' and 'varkw' are the names of the * and ** arguments or None.
    'defaults' is an n-tuple of the default values of the last n arguments.
    """

    if ismethod(func):
        func = func.im_func
    if not isfunction(func):
        raise TypeError('{!r} is not a Python function'.format(func))
    args, varargs, varkw = getargs(func.func_code)
    return ArgSpec(args, varargs, varkw, func.func_defaults)

看了下用到了 func.func_codefunc.func_defaults,按 Python 官方文档 https://docs.python.org/2/library/inspect.html 的解释,func_code 是运行时的字节码,从这里面捞参数表果然可行,那是不是我把这两个属性也传递上去就行了呢?改用自己的 wraps 如下

WRAPPER_ASSIGNMENTS = functools.WRAPPER_ASSGNMENTS + ('func_code', 'func_defaults')
def my_wraps(wrapped,
             assigned = WRAPPER_ASSIGNMENTS,
             updated = WRAPPER_UPDATES):
    return _functool.partial(functools.update_wrapper, wrapped=wrapped,
                             assigned=assigned, updated=updated)

运行时报错,看了下错误提示,func_code 不可覆盖,这也对,都是运行时的字节码了,这个覆盖掉那包的这层 decorator 到底还有没有自己的逻辑部分

还是自己动手丰衣足食,既然 func_code 不可覆盖,我自己另外弄一个总可以了吧,而且当前需求是拿到参数表和默认参数,那就直接解出来穿透,也懒得最后再解一次。修改 my_wraps 如下

WRAPPER_ASSIGNMENTS = functools.WRAPPER_ASSIGNMENTS + ('__func_args_name__', '__func_default_args__')

def my_wraps(wrapped,
             assigned = WRAPPER_ASSIGNMENTS,
             updated = functools.WRAPPER_UPDATES):
    if getattr(wrapped, '__func_args_name__', None) is None:
        setattr(wrapped, '__func_args_name__', inspect.getargs(wrapped.func_code)[0])
        func_defaults = getattr(wrapped, 'func_defaults') or ()
        default_args = dict()
        default_start = len(wrapped.__func_args_name__) - len(func_defaults)
        for idx, d in enumerate(func_defaults):
            default_args[wrapped.__func_args_name__[default_start + idx]] = d
        setattr(wrapped, '__func_default_args__', default_args)
    return _functools.partial(functools.update_wrapper, wrapped=wrapped,
                              assigned=assigned, updated=updated)

同时在运行时解参数表,也用一个通用函数来实现

def parse_func(func, *args, **kwargs):
    parsed_kwargs = dict()
    # default args
    parsed_kwargs.update(func.__func_default_args__)
    # args with name
    varargs_start = len(func.__func_args_name__)
    for idx, a in enumerate(args[:varargs_start]):
        parsed_kwargs[func.__func_args_name__[idx]] = a
    # varargs
    if len(args) > varargs_start:
        parsed_kwargs['varargs'] = args[varargs_start:]
    # kwargs
    parsed_kwargs.update(kwargs)

    return parsed_kwargs

这样在 lock_decoratorlog_decorator 里,用 my_wraps 来封装处理,同时在里面用 parse_func 来解析参数,就能拿到完整的参数表了

完整的测试代码见 https://gist.github.com/whusnoopy/9081544f7eaf4e9ceeaa9eba46ff28da

cmder 在 Win10 WSL 下粘贴丢字符的解决

Win10 升级到 1803 后,用 cmder 连的 bash,粘贴文本时总丢大量字符,怀疑某些部分被识别成了控制字符或怎样。一开始以为是 WSL 的问题,不过后面交叉验证,发现如果用 cmder 开 Windows 命令行,或直接在 Windows 命令行下运行 bash 就没事。翻了好久,终于在 ConEmu 项目下找到相关讨论,作者最近认可了这个问题并发布了更新:https://github.com/Maximus5/ConEmu/issues/1545#issuecomment-386444227

因为 cmder 就是封装了一下 ConEmu,所以去 https://github.com/Maximus5/ConEmu/releases/tag/v18.05.06 下载最新的 180506 版本 ConEmu,并解压到 cmder 目录的 cmder\vendor\conemu-maximus5 下替换原来的文件就好

WSL 下一些奇怪的路径依赖问题优化

在公司换用 Windows 做开发机,装了 Windows Subsystem for Linux(WSL),也就是那个 Ubuntu,用来跑开发环境

我的代码放在 Windows 的文件系统里,在 WSL 里通过 ln -s /mnt/c/foo ~/foo 的方式映射过去,不过在跑 yarn 装 node modules 的时候,会经常出现路径依赖的错误,大概就是 /mnt/c/xxxxx 这样的路径在计算父目录或子目录时会出问题

另外我跑 Docker,是使用 Docker for Windows 作为宿主,在 WSL 里装 Linux 的 Docker 客户端做控制,跑 docker-compose 总是发现挂载不上开发目录到文件系统,最后看了下是 WSL 默认的 /mnt/c/ 这样的挂载点识别有问题

最后按某些野路子方法,把 WSL 访问宿主机的入口调整为 /c/ 这样就好了

$ sudo mkdir /c
$ sudo mount --bind /mnt/c /c

不过这有个问题是重启后需要重新挂载,之前有按别的一些处理方式写到 /etc/fstab 文件表里,但是 WSL 不支持自动加载,所以按 https://nickjanetakis.com/blog/setting-up-docker-for-windows-and-wsl-to-work-flawlessly 的提示来加到 ~/.bashrc 里或我的 ~/.zshrc 里,并把 /bin/mount 改成所有用户都可启用

$ echo "sudo mount --bind /mnt/c /c" >> ~/.bashrc && source ~/.bashrc
$ sudo echo "yourname ALL=(root) NOPASSWD: /bin/mount" >> /etc/sudoers

注 1:yarn 的问题似乎现在在 /mnt/c/foo 这样的目录结构下工作正常了,不确定是不是 yarn 升级处理了这个问题

隐藏 HP 打印机在 Win10 下无法安装的驱动更新

公司使用的打印机是 HP 的 1536,很经典的一款,但是也有一个很头疼的问题,就是在 Windows 10 下,会总是提示会有驱动升级,名称是 HP driver update for HP LaserJet M1530 MFP Series PCL 6,但是从来都无法安装成功

看这个事情很不爽,但是一直也没有找到很好的解决办法,HP 官方论坛和微软的官方论坛里都没有合理的解决,有提议把驱动卸了就不会出更新提示的,但是,卸载了驱动我还怎么打印

好在今天搜到一个 Win10 系统禁止某一更新 的方法,实测解决,原文请点左边的链接进去,我这里简单重复

  1. 去微软官网下载 wushowhide.diagcab
  2. 运行,在 Hide Update 里勾选不想要的更新,解决

Windows 10 开机自动登录并锁屏

办公室的电脑有时候遇上断电重启后,希望能在通电后自动重启,且启动相关应用并保持在锁屏界面(比如启动 QQ,方便从办公室外网远程桌面)

断电自动重启这个好办,一般主板 BIOS 有设置

自动登录也能找到大把的教程,常见的操作步骤如下

Win+R 运行窗口里输入 netplwiz 并打开
用户 选项卡下去掉 要使用本计算机,用户必须输入用户名和密码 前面的勾
确定,在弹出的自动登录窗口中输入自动登录的用户名和密码
确定,保存

这样可以实现开机后自动登录有密码的账号,但是还是挺危险的,因为人不在电脑前但电脑是登录状态,万一被干点啥就呵呵了,自动屏保也有个触发时间间隔。所以还要设置登录后立即锁屏

一个方式是在启动任务里添加锁屏任务,进入 计算机管理,在 系统工具 > 系统计划任务 > 任务计划程序库 里,右侧操作面板 创建基本任务,更新如下信息

添加任意自己方便记忆的名称和描述
触发器当前用户登录时
操作启动程序
点下一步后多出来的步骤里启动程序或脚本填 %windir%\system32\rundll32.exe,添加参数里填 user32.dll, LockWorkStation
完成

另一种方式是创建一个快捷方式,快捷方式的对象位置写 %windir%\system32\rundll32.exe user32.dll, LockWorkStation,然后把这个快捷方式复制到开始菜单的 启动 里。原理类似,参数也一样,不过笨狗担心这个存在时序和可能被干掉的可能,还是选的上一种

修改 Windows 10 系统信息里制造商信息

在 Windows 的在线设备管理器里看自己账号登录了哪些机器,有一些品牌机是能正常显示型号或序列号的,但是自己装的机器会显示成 To Be Filled by O.E.M.

查了一下有几个解决办法

一是在 $Windows$/System32 目录下添加 oeminfo.iniOEMLOGO.bmp,参考 https://www.neowin.net/forum/topic/79809-how-to-change-to-be-filled-by-oem/?do=findComment&comment=871161

另一个是直接改注册表,参考 http://www.thewindowsclub.com/add-change-oem-information-windows,在注册表 HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\OEMInformation 下新建一个字符串值,名字叫 Manufacturer,值填自己想要的,弄完后在本地计算机属性里就可以看到

目前还不确定是否可以在微软的账号管理里看到这个改变,根据之前经验,改了计算机名什么的都需要过一小段时间才能生效

另如果需要在 dxdiag 里也看到正确的系统制造商和系统型号,参考 https://www.reddit.com/r/techsupport/comments/3f6gyo/upgraded_to_windows_10_and_now_getting_to_be/,在注册表 HKEY_LOCAL_MACHINE\HARDWARE\DESCRIPTION\System\Bios 目录下检查 SystemManufactureSystemProductName 这两个键是否存在,以及值是否跟 BaseBoardManufacturerBaseBoardProduct 一样

wtforms 的 StringField 里设置 default 无效

在项目里使用 wtforms,设置了一个带默认值的 StringField,但提交表单如果没有这项或值是空的,则返回为空,并不会使用 default 里的值

例如定义如下表单

def ListForm(Form):
    status = StringField("status", validators=[Optional()], default="all")

提交一个空请求,返回的 status 是空而不是 all。去跟代码并看了下 GitHub 上的 issue 讨论,按 wtforms 官方的说法,这是故意设计成这样的,参考如下内容

大意是,如果用户设置了一个 Field,那么他就应该有值,不然我们就强行设置为空

但是这个逻辑狗屁不通,因为除了 StringField 其他的类型域就没这个问题,比如 IntegerField 就是可以这么用而且能返回正确的 default

def ListForm(Form):
    status = StringField("status", validators=[Optional()], default="all")
    x = IntegerField("x", validators=[Optional()], default=9)

没办法自己新增了一个 StringFieldWithDefault 的类来解决这个问题(被覆盖的代码可以看上面 GitHub Issue 里的讨论)

class StringFieldWithDefault(StringField):
    def process_formdata(self, valuelist):
        if valuelist:
            self.data = valuelist[0]
        else:
            self.data = self.object_data

而且 wtforms 的数据校验也是谜一般的逻辑,比如设置如下

def ListForm(Form):
    status = StringField("status", validators=[Optional(), AnyOf(["all", "visible"])], default="all")
    x = IntegerField("x", validators=[DataRequired()])

这里会出现两个问题

一是 Optional 如果发现没有值或空,是能通过这个校验的,这里逻辑也没错,但是特喵的 Optional 如果遇到值为空的时候,会把之前的所有错误都清空,并且停止检查后面的 validator。这个迷一般的逻辑会导致如果传的空字段,即没有按正常人预期的拿到 default,也没有去执行 AnyOf 的校验,而是直接通过了。这个官方无解,也不打算修,只说在 3.x 的时候考虑加配置参数,不过看进度上一个版本已经是 2015 年发布的 2.1,而且 GitHub 仓库并不活跃,那么只能呵呵呵呵,然后换自己的 StringFieldWithDefault 吧

二是 DataRequired 不是检查 if field.name in formdata,而是检查 if field.data,这就意味着,如果传了一个 0 的整型参数给 IntegerField,是通不过 DataRequired 的验证的,同理还有空字符串。这个可以改用 InputRequired 来解决,还算是官方给了条活路

推荐一下 Vultr 及记录最近的折腾

Vultr 是一家 VPS 提供商,类似 Linode 和 DigitOcean,知道他比较晚,是在用了 DigitOcean 后。当时换他的主要考虑是有日本机房,DO 的新加坡机房在国内某些网络下访问很快,另外某些网络下就是龟速,Linode 则是因为贵,10 刀起价,虽然配置也好但是用不上

最近 Vultr 把入门款的 VPS 拉低到了 2.5 刀每月,1 CPU 512M RAM 20G SSD 500G BW 对于个人用户来说怎么看都够了,最大的优势还是有日本机房而且不像 Linode 那样有大概率被分到被墙的 IP。(感觉他家最近把入门款从 768RAM 5$ 改成 512RAM 2.5$ 应该是为了面对 Linode 把起价降低到 5 刀后的竞争)

下面是推荐链接,如果你通过这个注册并使用,我可以获得一定的奖励,你没有任何损失

http://www.vultr.com/?ref=6804130

另外,Vultr 目前还在搞充返活动,最高充一百送一百,送的金额 12 个月内有效。如果只是像我一样普通用,充 25 送 25,一年内开 2.5$ 的入门款实际只花 5$,实际上就是拿 25$ 可以用 20 个月。当然如果确定用的长也可以充 50,只不过送的 50 用不完就会过期。也可以前期选 5$ 1G RAM 的版本,不过后面迁服务器还是要折腾下,没啥必要

我自己为了省钱,把 Vultr 之前 5$/mon 的 768RAM 转成了 2.5$/mon 的 512RAM 版,之前的那个版本已经没法新建了,也不支持平滑降级,只能重新建一个 2.5$ 的实例然后人肉迁一把,还好东西不多,之前有记录的情况下半个小时不到就搞定了

另外本站之前使用的社会化评论框工具多说要停止服务了:重要通知: 多说即将关闭,还是希望能有比 WordPress 原生更友好的评论工具,所以换了 Disqus,可能会被墙,但是访问我这边的大部分人应该都是随身自带翻墙的,就无所谓了

不知道是我自己之前改 WordPress 改挂了还是多说这个版本就是有问题,从去年四月某个时间开始多说的评论就没有自动同步回我 WordPress 里,强行导入也在 wp-admin/ajax.php 调用时提示 404。没办法跑多说去导出所有数据为 json,然后在 http://urouge.github.io/migrate-to-disqus/ 提供的工具下转成 Disqus 的 xml 格式并导入到 Disqus。导入时注意 Disqus 并没有一个明确的提示说导入成功或失败,而且 Disqus 管理后台默认显示的是最近三个月的评论,很容易误判是导入没成功又导一次,然后就呵呵哒的有一堆重复评论了,最简单粗暴去除重复的方法反倒是在 Disqus 里把这个站点删除,然后新建一个并导入

至于本地缺失的再怎么导回就再说吧,刚在 Disqus 插件里试了下说 Disqus server return 500,可能回头在 Disqus 里直接导出,然后 WordPress 里直接导入是可行的

奇怪的开发需求们

这是一篇写于 2015 年 4 月的草稿,现在已经忘了当时想说什么了,又不舍得直接删除,看看发出来自己还能不能看懂当时的只言片语

大需求应该是我们自己做了一套独立系统想开个新业务

一开始面对的就是帐号体系和登陆方式, 在有自己的账号体系下如何自制授权。最后用了一些不算 OAuth 的简化版验证来实现了原型快速可用。因为双方都是自己的系统,所以可以加入一个私有的加密用的 salt,然后在已有账号体系里用账号加时间戳加一个私有字段,通过 salt 做个 MD5 或 SHA1,把用户名,时间戳和加密后内容明文发送,在接收方用明文收到的用户名和时间戳加上没有传输但是两边都加入的私有字段一起,通过私有的 salt 算 Hash,对比结果,以及确认时间戳是最近的

然后是白名单需求,因为是对一个已有系统的部分用户开放测试功能,不希望没选上的用户也进入到测试系统里来。这个似乎没有什么特别要考虑的,做一个白名单放到 DB 里持久化,就是看怎么改这个白名单,是要提供一个 Web 界面,还是裸改代码。印象中这个项目还没从裸改代码进化到 Web 管理,就无疾而终了(手动捂脸

最头疼的还是错误处理,就是文档没说的那些事,怎么做好各种防御式编程,才是工作中的精髓所在

简单粗暴的做,大概一两周后,使用 cloc 统计代码如下(用了 CDN 上的 Bootstrap,当时也不怎么会前端,大部分事情都放到后端做,所以前端代码很少)

$ perl ~/Downloads/cloc-1.62.pl . --exclude-dir=env,.git,.idea,logs
      51 text files.
      50 unique files.
       6 files ignored.

http://cloc.sourceforge.net v 1.62  T=0.33 s (137.7 files/s, 6957.6 lines/s)
-------------------------------------------------------------------------------
Language                     files          blank        comment           code
-------------------------------------------------------------------------------
Python                          25            369             52           1078
HTML                            17             43              1            603
CSS                              1              9              0             50
Javascript                       1              9              1             47
Bourne Shell                     1              2              0             10
-------------------------------------------------------------------------------
SUM:                            45            432             54           1788
-------------------------------------------------------------------------------

成长的笔记

毕业这么久,自己做事,带人做事,带团队做事,一路过来有很多感悟,又好像说不清到底成长在哪里,最近看了知乎上一个回答,深有感悟,归纳的挺好

原文在 https://www.zhihu.com/question/50539172/answer/121771903 ,答主 Cat Chen 之前在度厂工作过,似乎有过一面之缘,最近看他的 Blog 和知乎回答,在这类思考上还是走的挺前的,跟从之

回到主题,在 Cat Chen 的知乎回答里,把工作中人的能力分成三块:Technical、Direction、People,用中文对应大概是 技术能力、方向把控能力、沟通能力

其中 技术能力 是个人可控,可以独立成长,并且很容易评估成长程度,同样一件事给一个人完成,有人一天就能完成的很好,有人花一周还弄的磕磕碰碰,前者比后者强,这个是绝大部分人都可以达到的境界

而对于 方向把控能力,我们从小到大接受的教育都比较忽略这一点,更多的时候我们都是在指定了问题的情况下怎么去解决问题,而真的把一个开放问题丢到面前,必须要自己找方向时,就挺难了。大多数人在这个阶段就被淘汰了,或者鸵鸟政策自己看现在自己可以做什么就去做什么,自我安慰只要我一直在努力那必然也是有成果的,殊不知方向错了做的再多都没用,还可能有反作用

在有 技术能力 基础和 方向把控能力 上,可以更好的与 沟通能力 做同步成长。对自己的反馈上,沟通能力好,才可以更事半功倍的提升自己的技术能力,不然有些事别人点拨一下就可以解决的问题,自己可能要花十天半月还不一定摸到门路;沟通能力好,才有机会获得足够的信息输入,且信息来源可靠或有可靠性分析,进而影响方向把控能力。对外而言,不是所有事情都可以单枪匹马搞定的,那就涉及到怎么去拉到其他人来帮自己做事情,除了上级死压下级这类没法反抗的情况,绝大部分时候涉及到怎么让人「信服」自己的事情,信服的基础是你证明你能做好这件事(技术能力),并且你做的这件事对别人有好处(方向把控能力),别人才会心甘情愿的追随或听从你,否则只会出现「唉我这个事很靠谱你们怎么都不理我」或「你们一个个明明有空为啥都不愿意听我讲跟我做事」的情况