python 类 对象 方法 应用_Python 面向对象编程OOP (五) 写类神器:Dataclass

Dataclasses

大家好,上一期受到了朋友的启发,这一期我主要记录一下我的Dataclasses的学习过程。

之前简单回顾了attrs的用法,这一期来看更简洁的自带写类神器:dataclasses,需要注意,版本要大于等于Python 3.7

官方文档链接:Data Classes

下面直接来看例子:

创建Dataclass

from dataclasses import dataclass

@dataclass

class Position:

name: str

lon: float

lat: float

可以发现,主要起作用的是装饰符@dataclass ,需要注意,如果想要使用dataclass,需要Python 3.7或更高版本

使用dataclass的好处是可以节省书写__init()__等一些常用的实例方法

这里创建一个Position类,用来显示一个地点的位置

name:地点的名字

lon:经度

lat:纬度

新建一个实例来看看:

>>> pos = Position('Oslo', 10.8, 59.9)

>>> print(pos)

Position(name='Oslo', lon=10.8, lat=59.9)

>>> pos.lat

59.9

>>> print(f'{pos.name} is at {pos.lat}°N, {pos.lon}°E')

Oslo is at 59.9°N, 10.8°E

除了这种方法,还要一种类似创建namedtuple的方式也可以:

from dataclasses import make_dataclass

Position = make_dataclass('Position', ['name', 'lat', 'lon'])

默认值

让我们看看如何给类的属性添加默认值:

from dataclasses import dataclass

@dataclass

class Position:

name: str

lon: float = 0.0

lat: float = 0.0

效果和普通的类设定初始值的效果是一样的:

>>> Position('Null Island')

Position(name='Null Island', lon=0.0, lat=0.0)

>>> Position('Greenwich', lat=51.8)

Position(name='Greenwich', lon=0.0, lat=51.8)

>>> Position('Vancouver', -123.1, 49.3)

Position(name='Vancouver', lon=-123.1, lat=49.3)

输入提示

大家可以发现我们的Positon类规定了三个属性的类型:

name:str

lon:float

lat:float

现在如果想要开放限制,允许任意的数据类型,可以这么做:

from dataclasses import dataclass

from typing import Any

@dataclass

class WithoutExplicitTypes:

name: Any

value: Any = 42

这样运行的时候不会报错,哪怕瞎传参:

>>> Position(3.14, 'pi day', 2018)

Position(name=3.14, lon='pi day', lat=2018)

添加一个方法

现在我们想要计算两个地点的距离,可以参考如下公式:

根据这个公式为类添加一个.distance_to()方法

from dataclasses import dataclass

from math import asin, cos, radians, sin, sqrt

@dataclass

class Position:

name: str

lon: float = 0.0

lat: float = 0.0

def distance_to(self, other):

r = 6371 # Earth radius in kilometers

lam_1, lam_2 = radians(self.lon), radians(other.lon)

phi_1, phi_2 = radians(self.lat), radians(other.lat)

h = (sin((phi_2 - phi_1) / 2)**2

+ cos(phi_1) * cos(phi_2) * sin((lam_2 - lam_1) / 2)**2)

return 2 * r * asin(sqrt(h))

实验一下:

>>> oslo = Position('Oslo', 10.8, 59.9)

>>> vancouver = Position('Vancouver', -123.1, 49.3)

>>> oslo.distance_to(vancouver)

7181.7841229421165

更加灵活的应用

目前为止我们已经看到了dataclass的基础用法,现在我们看看根据实际需要,有哪些其他灵活的应用方式。

现在创建两个类,纸牌类和牌库类,纸牌类的属性包括花色和数字,牌库是List类型,包含纸牌类的一些实例

from dataclasses import dataclass

from typing import List

@dataclass

class PlayingCard:

rank: str

suit: str

@dataclass

class Deck:

cards: List[PlayingCard]

现在向牌库添加红桃Q和黑桃A的操作可以这样:

>>> queen_of_hearts = PlayingCard('Q', 'Hearts')

>>> ace_of_spades = PlayingCard('A', 'Spades')

>>> two_cards = Deck([queen_of_hearts, ace_of_spades])

Deck(cards=[PlayingCard(rank='Q', suit='Hearts'),

PlayingCard(rank='A', suit='Spades')])

现在我们可以创建一套完整的扑克牌牌库,注意这里使用了符号表示花色,建议在实际环境中改换为字符串:

RANKS = '2 3 4 5 6 7 8 9 10 J Q K A'.split()

SUITS = '♣ ♢ ♡ ♠'.split()

def make_french_deck():

return [PlayingCard(r, s) for s in SUITS for r in RANKS]

>>> make_french_deck()

[PlayingCard(rank='2', suit='♣'), PlayingCard(rank='3', suit='♣'), ...

PlayingCard(rank='K', suit='♠'), PlayingCard(rank='A', suit='♠')]

理论上,我们可以把这个方法作为Deck类的初始变量,但是根本不行,因为它可变:

from dataclasses import dataclass

from typing import List

@dataclass

class Deck: # Will NOT work

cards: List[PlayingCard] = make_french_deck()

面对这种情况,dataclass的解决方案是使用default_factory函数作为field的参数来指明:

from dataclasses import dataclass, field

from typing import List

@dataclass

class Deck:

cards: List[PlayingCard] = field(default_factory=make_french_deck)

现在就没有任何问题了:

>>> Deck()

Deck(cards=[PlayingCard(rank='2', suit='♣'), PlayingCard(rank='3', suit='♣'), ...

PlayingCard(rank='K', suit='♠'), PlayingCard(rank='A', suit='♠')])

field

现在简单总结一下dataclass中使用field涉及到的关键参数:

default: Default value of the field

default_factory: Function that returns the initial value of the field

init: Use field in .__init__() method? (Default is True.)

repr: Use field in repr of the object? (Default is True.)

compare: Include the field in comparisons? (Default is True.)

hash: Include the field when calculating hash()? (Default is to use the same as for compare.)

metadata: A mapping with information about the field

最后这个metadata有点像前端h5中的那个,就是可以为类的一个属性添加一个额外的描述信息:

from dataclasses import dataclass, field

@dataclass

class Position:

name: str

lon: float = field(default=0.0, metadata={'unit': 'degrees'})

lat: float = field(default=0.0, metadata={'unit': 'degrees'})

可以发现,传递的是一个dict,现在可以使用fields来查看一个属性的附加信息了:

>>> from dataclasses import fields

>>> fields(Position)

(Field(name='name',type=,...,metadata={}),

Field(name='lon',type=,...,metadata={'unit': 'degrees'}),

Field(name='lat',type=,...,metadata={'unit': 'degrees'}))

>>> lat_unit = fields(Position)[2].metadata['unit']

>>> lat_unit

'degrees'

描述类(str,repr)

这里指的就是常用的repr(obj)和str(obj)

先看一下刚才的Deck()描述:

>>> Deck()

Deck(cards=[PlayingCard(rank='2', suit='♣'), PlayingCard(rank='3', suit='♣'), PlayingCard(rank='4', suit='♣'), PlayingCard(rank='5', suit='♣')...

有些过长了,让我们用传统的str或者repr来表达,首先从纸牌类PlayingCard开始:

from dataclasses import dataclass

@dataclass

class PlayingCard:

rank: str

suit: str

def __str__(self):

return f'{self.suit}{self.rank}'

>>> ace_of_spades = PlayingCard('A', '♠')

>>> ace_of_spades

PlayingCard(rank='A', suit='♠')

>>> print(ace_of_spades)

♠A

现在看上去好多了,现在再自定义下牌库类Deck的描述:

from dataclasses import dataclass, field

from typing import List

@dataclass

class Deck:

cards: List[PlayingCard] = field(default_factory=make_french_deck)

def __repr__(self):

cards = ', '.join(f'{c!s}' for c in self.cards)

return f'{self.__class__.__name__}({cards})'

这回看上去舒服多了:

>>> Deck()

Deck(♣2, ♣3, ♣4, ♣5, ♣6, ♣7, ♣8, ♣9, ♣10, ♣J, ♣Q, ♣K, ♣A,

♢2, ♢3, ♢4, ♢5, ♢6, ♢7, ♢8, ♢9, ♢10, ♢J, ♢Q, ♢K, ♢A,

♡2, ♡3, ♡4, ♡5, ♡6, ♡7, ♡8, ♡9, ♡10, ♡J, ♡Q, ♡K, ♡A,

♠2, ♠3, ♠4, ♠5, ♠6, ♠7, ♠8, ♠9, ♠10, ♠J, ♠Q, ♠K, ♠A)

不可修改的Dataclass

其实这里和FrozenSet有点像,无非在装饰器中添加了一个参数frozen=True:

from dataclasses import dataclass

@dataclass(frozen=True)

class Position:

name: str

lon: float = 0.0

lat: float = 0.0

在这样一个frozen data class中,我们不能随意赋值:

>>> pos = Position('Oslo', 10.8, 59.9)

>>> pos.name

'Oslo'

>>> pos.name = 'Stockholm'

dataclasses.FrozenInstanceError: cannot assign to field 'name'

继承

使用dataclass的继承也比较简单,和普通类的继承没有太大区别:

from dataclasses import dataclass

@dataclass

class Position:

name: str

lon: float

lat: float

@dataclass

class Capital(Position):

country: str

>>> Capital('Oslo', 10.8, 59.9, 'Norway')

Capital(name='Oslo', lon=10.8, lat=59.9, country='Norway')

这里可以发现,创建Capital类的实例时会自动继承了父类的属性,我们只需要额外加入country这个新属性就可以了

假如父类初始化时有默认值:

from dataclasses import dataclass

@dataclass

class Position:

name: str

lon: float = 0.0

lat: float = 0.0

@dataclass

class Capital(Position):

country: str = 'Unknown'

lat: float = 40.0

简单优化

可以用我之前提到的slot方法进行优化:

from dataclasses import dataclass

@dataclass

class SimplePosition:

name: str

lon: float

lat: float

@dataclass

class SlotPosition:

__slots__ = ['name', 'lon', 'lat']

name: str

lon: float

lat: float

总结

这次我记录了dataclass的基础用法,是参考了别人的文章,如果想要了解更多,还是要到官方文档去看

我的其他原创文章已经放到了Github上,如果感兴趣的朋友可以去看看,链接如下:

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Python是一种简单易学的编程语言,而面向对象编程OOP)正是Python语言中的一大特色。在Python中,我们可以借助类(class)和对象(object)来实现面向对象编程。 首先,我们需要了解类的概念。类是用来描述具有相同属性和方法对象的模板。在Python中,我们可以通过class关键字来定义一个类,然后在类中定义属性和方法。例如: ```python class Dog: def __init__(self, name, age): self.name = name self.age = age def bark(self): print(self.name + " is barking!") ``` 在上面的例子中,我们定义了一个名为Dog的类,该类有一个名为__init__的特殊方法用来初始化对象的属性,还有一个名为bark的方法用来描述狗叫的行为。 接下来,我们需要了解对象的概念。对象是类的实例,通过类创建对象的过程称为实例化。在Python中,我们可以使用类名加括号的形式来创建对象。例如: ```python my_dog = Dog("Buddy", 3) my_dog.bark() ``` 在上面的例子中,我们创建了一个名为my_dog的Dog类对象,并调用了该对象的bark方法。 除了类和对象Python中还有一些面向对象编程的特性,如继承、封装和多态。通过这些特性,我们可以更加灵活地设计和实现程序。 总的来说,通过学习面向对象编程,我们可以更好地组织和管理代码,使得代码更易读、易维护。但是要注意,面向对象编程并不是万能的,需要根据具体情况选择合适的编程范式。希望通过本面向对象编程指南,读者能更加深入地了解Python中的面向对象编程特性,从而提升自己的编程水平。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值